@quatronic/qore 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a70250c5-daf4-5ff3-b490-5556ac377549")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="bf813c0e-4054-532d-9e74-62fbe8673876")}catch(e){}}();
4
4
  import { createRequire as __qoreCreateRequire } from "node:module"; const require = __qoreCreateRequire(import.meta.url);
5
5
  import {
6
6
  addSecretValues,
7
7
  buildScrubContext
8
- } from "./chunk-QYBDINUC.js";
8
+ } from "./chunk-OAU6HJHZ.js";
9
9
 
10
10
  // src/cli.ts
11
11
  import { homedir as homedir2 } from "os";
12
12
  import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
13
13
 
14
14
  // src/cli/io.ts
15
- import { confirm, search, select } from "@inquirer/prompts";
15
+ import { confirm, input, search, select } from "@inquirer/prompts";
16
16
  import { createInterface } from "readline";
17
17
 
18
18
  // src/platform/browser.ts
@@ -51,6 +51,10 @@ var EXIT = {
51
51
  QORE_REPO_ACCESS_PENDING: 5,
52
52
  QORE_GIT_AUTH: 5,
53
53
  QORE_WORKSPACE_CONFLICT: 2,
54
+ QORE_ROOT_NOT_SET: 2,
55
+ QORE_ROOT_INVALID: 2,
56
+ QORE_ROOT_NOT_WRITABLE: 1,
57
+ QORE_PROJECT_NOT_FOUND: 2,
54
58
  QORE_AUTH_UNAVAILABLE: 6,
55
59
  QORE_AUTH_BUSY: 6,
56
60
  QORE_API_UNREACHABLE: 6,
@@ -152,13 +156,21 @@ function processIo(interactive, stderrTTY = Boolean(process.stderr.isTTY)) {
152
156
  throw e;
153
157
  }
154
158
  },
159
+ async input(message, o) {
160
+ try {
161
+ return await input({ message, default: o?.default });
162
+ } catch (e) {
163
+ if (e.name === "ExitPromptError") throw new CancelledError();
164
+ throw e;
165
+ }
166
+ },
155
167
  waitForEnter: createWaitForEnter(process.stdin, process.stderr),
156
168
  openUrl: openBrowser
157
169
  };
158
170
  }
159
- function createWaitForEnter(input, output) {
160
- return (message, signal) => new Promise((resolve5, reject) => {
161
- const rl = createInterface({ input, output, terminal: true });
171
+ function createWaitForEnter(input2, output) {
172
+ return (message, signal) => new Promise((resolve6, reject) => {
173
+ const rl = createInterface({ input: input2, output, terminal: true });
162
174
  const onAbort = () => {
163
175
  rl.close();
164
176
  reject(new CancelledError());
@@ -166,7 +178,7 @@ function createWaitForEnter(input, output) {
166
178
  const done = () => {
167
179
  signal.removeEventListener("abort", onAbort);
168
180
  rl.close();
169
- resolve5();
181
+ resolve6();
170
182
  };
171
183
  signal.addEventListener("abort", onAbort, { once: true });
172
184
  rl.on("SIGINT", () => {
@@ -183,7 +195,7 @@ import { Command, CommanderError } from "commander";
183
195
 
184
196
  // src/core/api/http.ts
185
197
  function abortableFetch(fetch3, signal) {
186
- return (input, init) => fetch3(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal });
198
+ return (input2, init) => fetch3(input2, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal });
187
199
  }
188
200
  var TLS = /* @__PURE__ */ new Set([
189
201
  "SELF_SIGNED_CERT_IN_CHAIN",
@@ -490,7 +502,7 @@ async function run(argv2, deps2, io2, signal, register, hooks = {}) {
490
502
  }
491
503
 
492
504
  // src/cli/commands/clone.ts
493
- import { join as join6, relative as relative2, resolve as resolve2 } from "path";
505
+ import { join as join9, relative as relative3, resolve as resolve4 } from "path";
494
506
  import { Option } from "commander";
495
507
 
496
508
  // src/core/api/meta.ts
@@ -526,8 +538,8 @@ async function fetchMetaWithDate(fetch3, apiOrigin) {
526
538
  }
527
539
  if (!res.ok) throw await problemToError(res);
528
540
  const header = res.headers.get("date");
529
- const ms = header ? Date.parse(header) : NaN;
530
- return { meta: await res.json(), date: Number.isNaN(ms) ? null : new Date(ms) };
541
+ const ms2 = header ? Date.parse(header) : NaN;
542
+ return { meta: await res.json(), date: Number.isNaN(ms2) ? null : new Date(ms2) };
531
543
  }
532
544
  function negotiate(meta, portal, cliVersion) {
533
545
  if (portal.pinnedIssuer && meta.auth.issuer !== portal.pinnedIssuer) {
@@ -666,7 +678,7 @@ var QoreApiClient = class {
666
678
  this.o.onRequest?.(`${method} ${path} \u2192 ${res.status}${rid ? ` (${rid})` : ""}`);
667
679
  return res;
668
680
  }
669
- async request(method, path, body) {
681
+ async request(method, path, body, parse2 = (r) => r.json()) {
670
682
  let res = await this.once(method, path, body, false);
671
683
  if (res.status === 401) {
672
684
  res = await this.once(method, path, body, true);
@@ -688,11 +700,14 @@ var QoreApiClient = class {
688
700
  }
689
701
  }
690
702
  if (!res.ok) throw await problemToError(res);
691
- return await res.json();
703
+ return parse2(res);
692
704
  }
693
705
  get(path) {
694
706
  return this.request("GET", path);
695
707
  }
708
+ getText(path) {
709
+ return this.request("GET", path, void 0, (r) => r.text());
710
+ }
696
711
  post(path, body) {
697
712
  return this.request("POST", path, body);
698
713
  }
@@ -708,13 +723,13 @@ var RefreshCoordinator = class {
708
723
  fresh(expiresAt) {
709
724
  return new Date(expiresAt).getTime() - this.o.now().getTime() > SKEW_MS;
710
725
  }
711
- async rotate(scope) {
726
+ async rotate(scope2) {
712
727
  let stored = await this.o.store.getRefresh();
713
728
  if (!stored) throw new QoreError("QORE_NOT_LOGGED_IN", "Not logged in", { hint: "Run `qore login`." });
714
729
  let t;
715
730
  for (let attempt = 0; ; attempt++) {
716
731
  try {
717
- t = await (await this.o.oidc()).refresh(stored.rt, scope);
732
+ t = await (await this.o.oidc()).refresh(stored.rt, scope2);
718
733
  break;
719
734
  } catch (e) {
720
735
  if (!(e instanceof QoreError && e.code === "QORE_SESSION_EXPIRED")) throw e;
@@ -730,7 +745,7 @@ var RefreshCoordinator = class {
730
745
  throw e;
731
746
  }
732
747
  }
733
- await this.o.store.setRefresh({ rt: t.refreshToken, sub: t.sub, scope: t.scope || (scope ?? stored.scope), obtainedAt: this.o.now().toISOString() });
748
+ await this.o.store.setRefresh({ rt: t.refreshToken, sub: t.sub, scope: t.scope || (scope2 ?? stored.scope), obtainedAt: this.o.now().toISOString() });
734
749
  return t;
735
750
  }
736
751
  async accessToken(opts = {}) {
@@ -944,8 +959,8 @@ var ResolutionCache = class {
944
959
  const f = await this.load();
945
960
  const hits = [];
946
961
  for (const [k, e] of Object.entries(f.entries)) {
947
- const sep2 = k.lastIndexOf("|");
948
- if (keys.includes(k.slice(sep2 + 1)) && this.fresh(e)) hits.push({ origin: k.slice(0, sep2), at: new Date(e.resolvedAt).getTime() });
962
+ const sep3 = k.lastIndexOf("|");
963
+ if (keys.includes(k.slice(sep3 + 1)) && this.fresh(e)) hits.push({ origin: k.slice(0, sep3), at: new Date(e.resolvedAt).getTime() });
949
964
  }
950
965
  hits.sort((a, b) => b.at - a.at);
951
966
  return [...new Set(hits.map((h) => h.origin))];
@@ -1251,6 +1266,13 @@ async function syncPresent(deps2, results, actions, pull, ctx) {
1251
1266
  }
1252
1267
  }
1253
1268
 
1269
+ // src/core/projects/registry.ts
1270
+ import { realpath as realpath4 } from "fs/promises";
1271
+
1272
+ // src/core/projects/discover.ts
1273
+ import { readdir, realpath as realpath2 } from "fs/promises";
1274
+ import { join as join6 } from "path";
1275
+
1254
1276
  // src/core/workspace/file.ts
1255
1277
  import { randomBytes as randomBytes2 } from "crypto";
1256
1278
  import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm, writeFile as writeFile2 } from "fs/promises";
@@ -1312,17 +1334,225 @@ async function writeWorkspace(root, o) {
1312
1334
  }
1313
1335
  async function workspacePortal(deps2, ws) {
1314
1336
  const portal = resolvePortal(ws.portal, void 0);
1315
- const label2 = portal.builtin ?? portal.apiOrigin;
1337
+ const label3 = portal.builtin ?? portal.apiOrigin;
1316
1338
  const profile = (await deps2.config.read()).profiles[portal.apiOrigin];
1317
1339
  const loggedIn = profile && await new TokenStore(deps2.keychain, profileHash(profile.apiOrigin, profile.issuer)).getRefresh();
1318
1340
  if (!loggedIn) {
1319
- throw new QoreError("QORE_NOT_LOGGED_IN", `This workspace belongs to ${label2}, which you are not logged in to`, {
1320
- hint: `Run \`qore login --portal ${label2}\`.`
1341
+ throw new QoreError("QORE_NOT_LOGGED_IN", `This workspace belongs to ${label3}, which you are not logged in to`, {
1342
+ hint: `Run \`qore login --portal ${label3}\`.`
1321
1343
  });
1322
1344
  }
1323
1345
  return portal;
1324
1346
  }
1325
1347
 
1348
+ // src/core/projects/discover.ts
1349
+ var SKIP = (name) => name.startsWith(".") || name === "node_modules";
1350
+ async function subdirs(dir2) {
1351
+ try {
1352
+ return (await readdir(dir2, { withFileTypes: true })).filter((d) => d.isDirectory() && !SKIP(d.name)).map((d) => d.name);
1353
+ } catch {
1354
+ return [];
1355
+ }
1356
+ }
1357
+ async function scanRoot(root) {
1358
+ const out = [];
1359
+ for (const tenant of (await subdirs(root)).sort()) {
1360
+ for (const app of (await subdirs(join6(root, tenant))).sort()) {
1361
+ const path = join6(root, tenant, app);
1362
+ try {
1363
+ const ws = await readWorkspace(path);
1364
+ if (ws) out.push({ kind: "workspace", path, portal: ws.portal, tenantSlug: tenant, appId: ws.appId, appSlug: app });
1365
+ } catch {
1366
+ }
1367
+ }
1368
+ }
1369
+ return out;
1370
+ }
1371
+ var GONE = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
1372
+ async function stillValid(e) {
1373
+ try {
1374
+ const real = await realpath2(e.path);
1375
+ if (e.kind === "workspace") {
1376
+ const ws = await readWorkspace(real);
1377
+ return ws && ws.appId === e.appId ? real : "gone";
1378
+ }
1379
+ return real;
1380
+ } catch (err) {
1381
+ if (err instanceof CancelledError) throw err;
1382
+ return GONE.has(err.code ?? "") ? "gone" : "unknown";
1383
+ }
1384
+ }
1385
+ async function knownProjects(deps2, o = {}) {
1386
+ const cfg = await deps2.config.read();
1387
+ const root = cfg.projectsRoot ? await realpath2(cfg.projectsRoot).catch(() => cfg.projectsRoot) : void 0;
1388
+ const out = root ? (await scanRoot(root)).map((p) => ({ ...p, source: "root" })) : [];
1389
+ const seen = new Set(out.map((p) => p.path));
1390
+ const stale = [];
1391
+ for (const e of cfg.projects ?? []) {
1392
+ const real = await stillValid(e);
1393
+ if (real === "gone") {
1394
+ stale.push(e.path);
1395
+ continue;
1396
+ }
1397
+ if (real === "unknown") continue;
1398
+ if (seen.has(real)) continue;
1399
+ seen.add(real);
1400
+ out.push({ ...e, path: real, source: "registry" });
1401
+ }
1402
+ if (stale.length > 0) await forget(deps2, stale);
1403
+ return o.portal ? out.filter((p) => p.portal === o.portal) : out;
1404
+ }
1405
+
1406
+ // src/core/projects/root.ts
1407
+ import { randomBytes as randomBytes3 } from "crypto";
1408
+ import { mkdir as mkdir3, realpath as realpath3, rm as rm2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
1409
+ import { dirname as dirname3, isAbsolute, join as join8, parse, relative as relative2, resolve as resolve3, sep } from "path";
1410
+
1411
+ // src/core/context/repoRoot.ts
1412
+ import { stat } from "fs/promises";
1413
+ import { dirname as dirname2, join as join7, resolve as resolve2 } from "path";
1414
+ async function findRepoRoot(start2) {
1415
+ let dir2 = resolve2(start2);
1416
+ for (; ; ) {
1417
+ try {
1418
+ await stat(join7(dir2, ".git"));
1419
+ return dir2;
1420
+ } catch {
1421
+ }
1422
+ const up = dirname2(dir2);
1423
+ if (up === dir2) return null;
1424
+ dir2 = up;
1425
+ }
1426
+ }
1427
+
1428
+ // src/core/projects/root.ts
1429
+ var NOT_WRITABLE = /* @__PURE__ */ new Set(["EACCES", "EPERM", "EROFS"]);
1430
+ function expandPath(deps2, input2) {
1431
+ const s = input2.trim();
1432
+ if (s === "~") return deps2.home;
1433
+ if (s.startsWith("~/") || s.startsWith("~\\")) return trimSeparator(join8(deps2.home, s.slice(2)));
1434
+ return resolve3(deps2.cwd, s);
1435
+ }
1436
+ function trimSeparator(p) {
1437
+ const root = parse(p).root;
1438
+ let end = p.length;
1439
+ while (end > root.length && (p[end - 1] === "/" || p[end - 1] === "\\")) end--;
1440
+ return p.slice(0, end);
1441
+ }
1442
+ async function underAFile(path) {
1443
+ for (let p = dirname3(path); ; p = dirname3(p)) {
1444
+ const s = await stat2(p).catch((e) => e.code === "ENOENT" ? void 0 : null);
1445
+ if (s === null) return false;
1446
+ if (s) return !s.isDirectory();
1447
+ if (dirname3(p) === p) return false;
1448
+ }
1449
+ }
1450
+ function isInside(parent, child) {
1451
+ const rel = relative2(parent, child);
1452
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1453
+ }
1454
+ function scannedBy(root, e) {
1455
+ if (e.kind !== "workspace" || !isInside(root, e.path)) return false;
1456
+ return relative2(root, e.path).split(sep).filter(Boolean).length === 2;
1457
+ }
1458
+ async function writableError(path) {
1459
+ const probe = join8(path, `.qore-write-test-${randomBytes3(6).toString("hex")}`);
1460
+ try {
1461
+ await writeFile3(probe, "");
1462
+ await rm2(probe, { force: true });
1463
+ return null;
1464
+ } catch (e) {
1465
+ return e;
1466
+ }
1467
+ }
1468
+ var invalid2 = (path, why) => new QoreError("QORE_ROOT_INVALID", `${path} cannot be the projects root: ${why}`, { hint: "Pick another folder: qore init --root <path>." });
1469
+ var notWritable = (path, e) => new QoreError("QORE_ROOT_NOT_WRITABLE", `${path} is not writable (${e.code ?? e.message})`, {
1470
+ hint: "Fix its permissions, or pick another folder: qore init --root <path>.",
1471
+ cause: e
1472
+ });
1473
+ async function checkRoot(deps2, input2) {
1474
+ const path = expandPath(deps2, input2);
1475
+ if (dirname3(path) === path) throw invalid2(path, "it is the filesystem root");
1476
+ try {
1477
+ if (!(await stat2(path)).isDirectory()) throw invalid2(path, "it is a file");
1478
+ } catch (e) {
1479
+ if (e instanceof QoreError) throw e;
1480
+ const err = e;
1481
+ if (err.code === "ENOTDIR") throw invalid2(path, "a parent of it is a file");
1482
+ if (NOT_WRITABLE.has(err.code ?? "")) throw notWritable(path, err);
1483
+ if (err.code !== "ENOENT") throw e;
1484
+ if (await underAFile(path)) throw invalid2(path, "a parent of it is a file");
1485
+ }
1486
+ const repo = await findRepoRoot(path);
1487
+ if (await findWorkspace(path, deps2.home) || repo && !isInside(repo, deps2.home)) {
1488
+ throw invalid2(path, "it lies inside a project (an app workspace or a git repository)");
1489
+ }
1490
+ return path;
1491
+ }
1492
+ async function createRoot(path) {
1493
+ try {
1494
+ await mkdir3(path, { recursive: true });
1495
+ } catch (e) {
1496
+ const err2 = e;
1497
+ if (NOT_WRITABLE.has(err2.code ?? "")) throw notWritable(path, err2);
1498
+ if (err2.code === "ENOTDIR" || err2.code === "EEXIST") throw invalid2(path, "a parent of it is a file");
1499
+ throw e;
1500
+ }
1501
+ const err = await writableError(path);
1502
+ if (err) throw notWritable(path, err);
1503
+ return realpath3(path);
1504
+ }
1505
+ async function validateRoot(deps2, input2) {
1506
+ return createRoot(await checkRoot(deps2, input2));
1507
+ }
1508
+
1509
+ // src/core/projects/registry.ts
1510
+ var LOCK = "projects";
1511
+ var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1512
+ async function quietly(fn) {
1513
+ try {
1514
+ await fn();
1515
+ } catch (e) {
1516
+ if (e instanceof CancelledError) throw e;
1517
+ }
1518
+ }
1519
+ async function remember(deps2, e) {
1520
+ await quietly(async () => {
1521
+ const path = await realpath4(e.path);
1522
+ const entry = { ...e, path };
1523
+ const cfg = await deps2.config.read();
1524
+ const root = cfg.projectsRoot ? await realpath4(cfg.projectsRoot).catch(() => cfg.projectsRoot) : void 0;
1525
+ if (root && scannedBy(root, entry)) return;
1526
+ const current = cfg.projects?.find((p) => p.path === path);
1527
+ if (current && same(current, entry)) return;
1528
+ await deps2.withLock(LOCK, () => deps2.config.update((c) => {
1529
+ c.projects = [...(c.projects ?? []).filter((p) => p.path !== path), entry];
1530
+ }).then(() => void 0));
1531
+ });
1532
+ }
1533
+ async function forget(deps2, paths) {
1534
+ const drop = new Set(paths);
1535
+ await quietly(() => deps2.withLock(LOCK, () => deps2.config.update((c) => {
1536
+ if (c.projects) c.projects = c.projects.filter((p) => !drop.has(p.path));
1537
+ }).then(() => void 0)));
1538
+ }
1539
+ async function setRoot(deps2, next) {
1540
+ let registered = 0;
1541
+ await deps2.withLock(LOCK, async () => {
1542
+ const old = (await deps2.config.read()).projectsRoot;
1543
+ const carried = old && old !== next ? await scanRoot(old) : [];
1544
+ const keep = carried.filter((p) => !next || !scannedBy(next, p));
1545
+ registered = keep.length;
1546
+ await deps2.config.update((c) => {
1547
+ c.projectsRoot = next;
1548
+ if (next === void 0) delete c.projectsRoot;
1549
+ const rest = (c.projects ?? []).filter((p) => !keep.some((k) => k.path === p.path));
1550
+ if (rest.length + keep.length > 0) c.projects = [...rest, ...keep];
1551
+ });
1552
+ });
1553
+ return { registered };
1554
+ }
1555
+
1326
1556
  // src/core/workspace/safety.ts
1327
1557
  var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/;
1328
1558
  var WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/;
@@ -1373,9 +1603,9 @@ function firstSentenceLower(hint) {
1373
1603
  return sentence.length ? sentence[0].toLowerCase() + sentence.slice(1) : sentence;
1374
1604
  }
1375
1605
  function failedLine(r) {
1376
- const label2 = FAILURE_LABELS[r.error?.code ?? ""] ?? "failed";
1606
+ const label3 = FAILURE_LABELS[r.error?.code ?? ""] ?? "failed";
1377
1607
  const detail2 = firstSentenceLower(r.error?.hint);
1378
- return `\u2717 ${r.slug} ${label2}${detail2 ? ` \u2014 ${detail2}` : ""}`;
1608
+ return `\u2717 ${r.slug} ${label3}${detail2 ? ` \u2014 ${detail2}` : ""}`;
1379
1609
  }
1380
1610
  function line(r, conflictCount) {
1381
1611
  switch (r.status) {
@@ -1474,61 +1704,76 @@ async function pickApp(deps2, io2, session, tenantFlag) {
1474
1704
  });
1475
1705
  return { tenant: tenant.slug, app };
1476
1706
  }
1477
- function registerClone(program, ctx) {
1478
- program.command("clone").description("Clone an app's modules into a new workspace directory").argument("[app]", "app slug or id (omit to pick one interactively)").argument("[dir]", "target directory (default: the app's slug)").option("--tenant <slug>", "tenant the app slug belongs to, when it is not your active one").addOption(new Option("--protocol <https|ssh>", "clone URL protocol").choices(["https", "ssh"]).default("https")).action(async (appArg, dirArg, flags) => {
1479
- const { deps: deps2, io: io2 } = ctx;
1480
- const g = ctx.opts();
1481
- const cfg = await deps2.config.read();
1482
- const portal = resolvePortal(g.portal, cfg.defaultProfile);
1483
- const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
1484
- const session = await openSession(deps2, portal, "clone", onRequest);
1485
- await ensureCapability(deps2, portal.apiOrigin, "app.modules");
1486
- const picked = appArg === void 0 ? await pickApp(deps2, io2, session, flags.tenant) : void 0;
1487
- const appSlug = picked?.app ?? appArg;
1488
- const ref = await resolveApp(deps2, io2, session, appSlug, picked?.tenant ?? flags.tenant);
1489
- const app = await session.api.get(`/api/cli/v1/apps/${encodeURIComponent(ref.appId)}/modules`);
1490
- if (dirArg === void 0 && !isSafeSlug(app.app.slug)) throw unsafeSlugError(app.app.slug);
1491
- const dirLabel = dirArg ?? app.app.slug;
1492
- const root = resolve2(deps2.cwd, dirLabel);
1493
- const info = await deps2.git.inspect(root);
1494
- if (info.exists && !info.empty) {
1495
- const ws = await readWorkspace(root);
1496
- if (!ws || ws.appId !== app.app.id || ws.portal !== portal.apiOrigin) {
1497
- throw new QoreError("QORE_WORKSPACE_CONFLICT", `${root} exists and is not the workspace of ${app.app.slug}`, {
1498
- hint: `Pick another directory: qore clone ${appSlug} <dir>.`
1499
- });
1500
- }
1501
- }
1502
- await writeWorkspace(root, { portal: portal.apiOrigin, appId: app.app.id });
1503
- const dirs = {};
1504
- const modulesForPlan = [];
1505
- const earlyFailures = [];
1506
- for (const m of app.modules) {
1507
- if (!isSafeSlug(m.slug)) {
1508
- earlyFailures.push(asFailure(m.slug, unsafeSlugError(m.slug)));
1509
- continue;
1510
- }
1511
- try {
1512
- dirs[m.slug] = await deps2.git.inspect(join6(root, m.slug));
1513
- modulesForPlan.push(m);
1514
- } catch (e) {
1515
- earlyFailures.push(asFailure(m.slug, e));
1516
- }
1707
+ async function cloneApp(ctx, appArg, dirArg, flags) {
1708
+ const { deps: deps2, io: io2 } = ctx;
1709
+ const g = ctx.opts();
1710
+ const cfg = await deps2.config.read();
1711
+ const portal = resolvePortal(g.portal, cfg.defaultProfile);
1712
+ const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
1713
+ const session = await openSession(deps2, portal, "clone", onRequest);
1714
+ await ensureCapability(deps2, portal.apiOrigin, "app.modules");
1715
+ const picked = appArg === void 0 ? await pickApp(deps2, io2, session, flags.tenant) : void 0;
1716
+ const appSlug = picked?.app ?? appArg;
1717
+ const ref = await resolveApp(deps2, io2, session, appSlug, picked?.tenant ?? flags.tenant);
1718
+ const app = await session.api.get(`/api/cli/v1/apps/${encodeURIComponent(ref.appId)}/modules`);
1719
+ const projectsRoot = cfg.projectsRoot;
1720
+ if (dirArg === void 0 && !isSafeSlug(app.app.slug)) throw unsafeSlugError(app.app.slug);
1721
+ if (dirArg === void 0 && projectsRoot && !isSafeSlug(app.app.tenant.slug)) throw unsafeSlugError(app.app.tenant.slug);
1722
+ const root = dirArg !== void 0 ? resolve4(deps2.cwd, dirArg) : projectsRoot ? join9(projectsRoot, app.app.tenant.slug, app.app.slug) : resolve4(deps2.cwd, app.app.slug);
1723
+ const dirLabel = dirArg ?? (projectsRoot ? root : app.app.slug);
1724
+ const info = await deps2.git.inspect(root);
1725
+ if (info.exists && !info.empty) {
1726
+ const ws = await readWorkspace(root);
1727
+ if (!ws || ws.appId !== app.app.id || ws.portal !== portal.apiOrigin) {
1728
+ throw new QoreError("QORE_WORKSPACE_CONFLICT", `${root} exists and is not the workspace of ${app.app.slug}`, {
1729
+ hint: `Pick another directory: qore clone ${appSlug} <dir>.`
1730
+ });
1517
1731
  }
1518
- const { actions } = planWorkspace(root, modulesForPlan, dirs, []);
1519
- const applied = await applyActions(deps2, { portal, app, actions, protocol: flags.protocol, onResult: () => {
1520
- } });
1521
- for (const r of applied) {
1522
- if (r.status === "cloned" && r.dir) r.dir = relative2(deps2.cwd, r.dir) || ".";
1732
+ }
1733
+ await writeWorkspace(root, { portal: portal.apiOrigin, appId: app.app.id });
1734
+ await remember(deps2, {
1735
+ kind: "workspace",
1736
+ path: root,
1737
+ portal: portal.apiOrigin,
1738
+ tenantSlug: app.app.tenant.slug,
1739
+ appId: app.app.id,
1740
+ appSlug: app.app.slug
1741
+ });
1742
+ const dirs = {};
1743
+ const modulesForPlan = [];
1744
+ const earlyFailures = [];
1745
+ for (const m of app.modules) {
1746
+ if (!isSafeSlug(m.slug)) {
1747
+ earlyFailures.push(asFailure(m.slug, unsafeSlugError(m.slug)));
1748
+ continue;
1523
1749
  }
1524
- const results = [...earlyFailures, ...applied];
1525
- const err = reportWorkspaceRun(io2, results, Boolean(g.json));
1526
- if (!g.json) {
1527
- io2.out(`\u2713 Wrote ${join6(dirLabel, "qore.workspace.json")}`);
1528
- const first = results.find((r) => r.status === "cloned" || r.status === "present");
1529
- if (first && !err) io2.out(`Next: cd ${join6(dirLabel, first.slug)} && qore env pull`);
1750
+ try {
1751
+ dirs[m.slug] = await deps2.git.inspect(join9(root, m.slug));
1752
+ modulesForPlan.push(m);
1753
+ } catch (e) {
1754
+ earlyFailures.push(asFailure(m.slug, e));
1530
1755
  }
1531
- if (err) throw err;
1756
+ }
1757
+ const { actions } = planWorkspace(root, modulesForPlan, dirs, []);
1758
+ const applied = await applyActions(deps2, { portal, app, actions, protocol: flags.protocol, onResult: () => {
1759
+ } });
1760
+ for (const r of applied) {
1761
+ if (r.status === "cloned" && r.dir && isInside(deps2.cwd, r.dir)) r.dir = relative3(deps2.cwd, r.dir) || ".";
1762
+ }
1763
+ const results = [...earlyFailures, ...applied];
1764
+ const err = reportWorkspaceRun(io2, results, Boolean(g.json));
1765
+ if (!g.json) {
1766
+ io2.out(`\u2713 Wrote ${join9(dirLabel, "qore.workspace.json")}`);
1767
+ const first = results.find((r) => r.status === "cloned" || r.status === "present");
1768
+ if (first && !err) io2.out(`Next: cd ${join9(dirLabel, first.slug)} && qore env pull`);
1769
+ }
1770
+ if (!g.json && dirArg === void 0 && !projectsRoot) io2.err("Tip: run `qore init` to always clone into one projects folder.");
1771
+ if (err) throw err;
1772
+ return { root, appSlug: app.app.slug };
1773
+ }
1774
+ function registerClone(program, ctx) {
1775
+ program.command("clone").description("Clone an app's modules into a new workspace directory").argument("[app]", "app slug or id (omit to pick one interactively)").argument("[dir]", "target directory (default: the app's slug)").option("--tenant <slug>", "tenant the app slug belongs to, when it is not your active one").addOption(new Option("--protocol <https|ssh>", "clone URL protocol").choices(["https", "ssh"]).default("https")).action(async (appArg, dirArg, flags) => {
1776
+ await cloneApp(ctx, appArg, dirArg, flags);
1532
1777
  });
1533
1778
  }
1534
1779
 
@@ -1553,6 +1798,13 @@ function describeConsent(consent, built) {
1553
1798
  }
1554
1799
 
1555
1800
  // src/cli/commands/config.ts
1801
+ async function describeRoot(deps2) {
1802
+ const known = await knownProjects(deps2);
1803
+ return {
1804
+ root: (await deps2.config.read()).projectsRoot ?? null,
1805
+ projects: { inRoot: known.filter((p) => p.source === "root").length, registered: known.filter((p) => p.source === "registry").length }
1806
+ };
1807
+ }
1556
1808
  function registerConfig(program, ctx) {
1557
1809
  const config = program.command("config").description("Change qore's own settings");
1558
1810
  config.command("telemetry").description("Show or set crash reporting").argument("[state]", "on or off").action(async (state) => {
@@ -1572,29 +1824,44 @@ function registerConfig(program, ctx) {
1572
1824
  io2.out(describeConsent(consent, built));
1573
1825
  if (state && consent.source !== "config") io2.err(`${consent.source} in your environment overrides this setting.`);
1574
1826
  });
1827
+ config.command("root").description("Show or set the projects folder qore clones into (see qore init)").argument("[path]", "the new projects folder").option("--unset", "clear it; projects already there stay findable").action(async (path, f) => {
1828
+ const { deps: deps2, io: io2 } = ctx;
1829
+ if (path !== void 0 && f.unset) {
1830
+ throw new QoreError("QORE_CONTEXT_INVALID", "Pass a path or --unset, not both", { hint: "qore config root <path> | qore config root --unset" });
1831
+ }
1832
+ if (path !== void 0) await setRoot(deps2, await validateRoot(deps2, path));
1833
+ if (f.unset) await setRoot(deps2, void 0);
1834
+ const d = await describeRoot(deps2);
1835
+ if (ctx.opts().json) {
1836
+ renderJson(io2, d);
1837
+ return;
1838
+ }
1839
+ io2.out(d.root ? `Projects root: ${d.root}` : "Projects root: not set \u2014 run qore init");
1840
+ io2.out(`Known projects: ${d.projects.inRoot} in the root, ${d.projects.registered} elsewhere`);
1841
+ });
1575
1842
  }
1576
1843
 
1577
1844
  // src/cli/commands/dev.ts
1578
- import { randomBytes as randomBytes5 } from "crypto";
1579
- import { realpath as realpath5 } from "fs/promises";
1845
+ import { randomBytes as randomBytes6 } from "crypto";
1846
+ import { realpath as realpath8 } from "fs/promises";
1580
1847
  import { constants as constants3 } from "os";
1581
- import { dirname as dirname6, join as join18, relative as relative6 } from "path";
1848
+ import { dirname as dirname7, join as join21, relative as relative7 } from "path";
1582
1849
  import { StringDecoder } from "string_decoder";
1583
1850
 
1584
1851
  // src/core/context/repoConfig.ts
1585
1852
  import { readFile as readFile3 } from "fs/promises";
1586
- import { join as join7 } from "path";
1853
+ import { join as join10 } from "path";
1587
1854
  var FORBIDDEN = ["portal", "api", "apiOrigin", "issuer", "tenant", "tenantId", "app", "appId", "module", "moduleId"];
1588
1855
  var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
1589
1856
  var SCRIPT = /^[A-Za-z0-9:_.-]+$/;
1590
- 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 });
1857
+ var invalid3 = (msg, details) => new QoreError("QORE_CONTEXT_INVALID", `.qore.json: ${msg}`, { hint: "Fix .qore.json in the repository root; see the $schema it names.", details });
1591
1858
  function moduleDefaults(type) {
1592
1859
  return type === "frontend" ? { envFile: ".env.local", localSecrets: ["SESSION_COOKIE_SECRET"] } : { envFile: ".env", localSecrets: [] };
1593
1860
  }
1594
1861
  async function readRepoConfig(root) {
1595
1862
  let raw;
1596
1863
  try {
1597
- raw = await readFile3(join7(root, ".qore.json"), "utf8");
1864
+ raw = await readFile3(join10(root, ".qore.json"), "utf8");
1598
1865
  } catch (e) {
1599
1866
  if (e.code === "ENOENT") return {};
1600
1867
  throw e;
@@ -1603,44 +1870,44 @@ async function readRepoConfig(root) {
1603
1870
  try {
1604
1871
  doc = JSON.parse(raw);
1605
1872
  } catch {
1606
- throw invalid2("not valid JSON");
1873
+ throw invalid3("not valid JSON");
1607
1874
  }
1608
- if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw invalid2("must be an object");
1875
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw invalid3("must be an object");
1609
1876
  const v = doc.schemaVersion === void 0 ? 1 : doc.schemaVersion;
1610
- if (!Number.isInteger(v) || v < 1) throw invalid2("schemaVersion must be a positive integer");
1877
+ if (!Number.isInteger(v) || v < 1) throw invalid3("schemaVersion must be a positive integer");
1611
1878
  if (v > 1) {
1612
1879
  throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `.qore.json schemaVersion ${v} is newer than this qore`, { hint: "Run: npm i -g @quatronic/qore" });
1613
1880
  }
1614
- for (const f of FORBIDDEN) if (f in doc) throw invalid2(`"${f}" is not allowed \u2014 .qore.json never selects a portal or a resource`, { field: f });
1881
+ for (const f of FORBIDDEN) if (f in doc) throw invalid3(`"${f}" is not allowed \u2014 .qore.json never selects a portal or a resource`, { field: f });
1615
1882
  const out = {};
1616
1883
  if (doc.envFile !== void 0) {
1617
- if (typeof doc.envFile !== "string" || !doc.envFile) throw invalid2("envFile must be a non-empty string");
1884
+ if (typeof doc.envFile !== "string" || !doc.envFile) throw invalid3("envFile must be a non-empty string");
1618
1885
  out.envFile = doc.envFile;
1619
1886
  }
1620
1887
  if (doc.localSecrets !== void 0) {
1621
1888
  if (!Array.isArray(doc.localSecrets) || !doc.localSecrets.every((k) => typeof k === "string" && KEY.test(k))) {
1622
- throw invalid2("localSecrets must be a list of env key names");
1889
+ throw invalid3("localSecrets must be a list of env key names");
1623
1890
  }
1624
- if (new Set(doc.localSecrets).size !== doc.localSecrets.length) throw invalid2("localSecrets must not contain duplicates");
1891
+ if (new Set(doc.localSecrets).size !== doc.localSecrets.length) throw invalid3("localSecrets must not contain duplicates");
1625
1892
  out.localSecrets = doc.localSecrets;
1626
1893
  }
1627
1894
  if (doc.dev !== void 0) {
1628
1895
  const d = doc.dev;
1629
- if (!d || typeof d !== "object" || Array.isArray(d)) throw invalid2("dev must be an object");
1896
+ if (!d || typeof d !== "object" || Array.isArray(d)) throw invalid3("dev must be an object");
1630
1897
  const dev = {};
1631
1898
  if (d.install !== void 0) {
1632
- if (typeof d.install !== "boolean") throw invalid2("dev.install must be true or false");
1899
+ if (typeof d.install !== "boolean") throw invalid3("dev.install must be true or false");
1633
1900
  dev.install = d.install;
1634
1901
  }
1635
1902
  if (d.services !== void 0) {
1636
1903
  if (!Array.isArray(d.services) || !d.services.every((s) => typeof s === "string" && SCRIPT.test(s))) {
1637
- throw invalid2("dev.services must be a list of script names");
1904
+ throw invalid3("dev.services must be a list of script names");
1638
1905
  }
1639
- if (new Set(d.services).size !== d.services.length) throw invalid2("dev.services must not contain duplicates");
1906
+ if (new Set(d.services).size !== d.services.length) throw invalid3("dev.services must not contain duplicates");
1640
1907
  dev.services = d.services;
1641
1908
  }
1642
1909
  if (d.script !== void 0) {
1643
- if (typeof d.script !== "string" || !SCRIPT.test(d.script)) throw invalid2("dev.script must be a script name");
1910
+ if (typeof d.script !== "string" || !SCRIPT.test(d.script)) throw invalid3("dev.script must be a script name");
1644
1911
  dev.script = d.script;
1645
1912
  }
1646
1913
  out.dev = dev;
@@ -1648,25 +1915,8 @@ async function readRepoConfig(root) {
1648
1915
  return out;
1649
1916
  }
1650
1917
 
1651
- // src/core/context/repoRoot.ts
1652
- import { stat } from "fs/promises";
1653
- import { dirname as dirname2, join as join8, resolve as resolve3 } from "path";
1654
- async function findRepoRoot(start2) {
1655
- let dir2 = resolve3(start2);
1656
- for (; ; ) {
1657
- try {
1658
- await stat(join8(dir2, ".git"));
1659
- return dir2;
1660
- } catch {
1661
- }
1662
- const up = dirname2(dir2);
1663
- if (up === dir2) return null;
1664
- dir2 = up;
1665
- }
1666
- }
1667
-
1668
1918
  // src/core/context/resolver.ts
1669
- import { isAbsolute, join as join9, relative as relative3 } from "path";
1919
+ import { isAbsolute as isAbsolute2, join as join11, relative as relative4 } from "path";
1670
1920
  var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1671
1921
  var HINT_FLAG = "cd into a module repository, or pass --module <slug|id>.";
1672
1922
  var RESOLVE = "/api/cli/v1/repos/resolve";
@@ -1774,8 +2024,8 @@ async function byFlag(deps2, o, cache, ws) {
1774
2024
  return { portal, session, module, source: overrides ? { kind: "flag", overrides } : { kind: "flag" }, repoRoot, workspace: ws };
1775
2025
  }
1776
2026
  var isBelow = (root, dir2) => {
1777
- const rel = relative3(root, dir2);
1778
- return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
2027
+ const rel = relative4(root, dir2);
2028
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
1779
2029
  };
1780
2030
  async function inWorkspaceRoot(deps2, ws) {
1781
2031
  if (!ws) return false;
@@ -1878,8 +2128,9 @@ async function anyLoggedIn(deps2, cfg) {
1878
2128
  }
1879
2129
  return false;
1880
2130
  }
2131
+ var isForeignRepo = (e) => e instanceof QoreError && e.code === "QORE_CONTEXT_REQUIRED" && e.details?.foreignRepo === true;
1881
2132
  async function resolveModuleContext(deps2, o) {
1882
- const cache = new ResolutionCache(join9(deps2.config.dir, "cache"), deps2.now);
2133
+ const cache = new ResolutionCache(join11(deps2.config.dir, "cache"), deps2.now);
1883
2134
  const ws = await findWorkspace(deps2.cwd, deps2.home);
1884
2135
  if (o.module) return byFlag(deps2, o, cache, ws);
1885
2136
  const cfg = await deps2.config.read();
@@ -1896,7 +2147,7 @@ async function resolveModuleContext(deps2, o) {
1896
2147
  const said = gitError?.hint;
1897
2148
  throw new QoreError("QORE_CONTEXT_REQUIRED", "This repository has no remote Qore can match", {
1898
2149
  hint: said ? `${HINT_FLAG} git said: ${said}` : HINT_FLAG,
1899
- details: gitError ? { git: said ?? gitError.message } : void 0,
2150
+ details: gitError ? { git: said ?? gitError.message } : { foreignRepo: true },
1900
2151
  cause: gitError
1901
2152
  });
1902
2153
  }
@@ -1925,6 +2176,7 @@ async function resolveModuleContext(deps2, o) {
1925
2176
  }
1926
2177
  const details = { triedProfiles: tried };
1927
2178
  if (skipped.length > 0) details.skippedProfiles = skipped;
2179
+ if (answered.length > 0) details.foreignRepo = true;
1928
2180
  if (answered.length === 0) {
1929
2181
  const cause = skipErrors[0] ?? unsupported2;
1930
2182
  if (cause) throw withDetails(cause, details);
@@ -2030,7 +2282,7 @@ function exitHint(tail, h) {
2030
2282
  }
2031
2283
 
2032
2284
  // src/core/dev/env.ts
2033
- import { dirname as dirname5, join as join12 } from "path";
2285
+ import { dirname as dirname6, join as join14 } from "path";
2034
2286
 
2035
2287
  // src/core/env/fetch.ts
2036
2288
  async function fetchLocalEnv(deps2, mc, app) {
@@ -2132,8 +2384,8 @@ function renderEnv(original, merged) {
2132
2384
  }
2133
2385
 
2134
2386
  // src/core/env/pull.ts
2135
- import { lstat, realpath as realpath2 } from "fs/promises";
2136
- import { basename, dirname as dirname3, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve4, sep } from "path";
2387
+ import { lstat, realpath as realpath5 } from "fs/promises";
2388
+ import { basename, dirname as dirname4, isAbsolute as isAbsolute3, relative as relative5, resolve as resolve5, sep as sep2 } from "path";
2137
2389
  var outside = (target) => new QoreError("QORE_CONTEXT_INVALID", `The env file must be inside the repository (got ${target})`, {
2138
2390
  hint: "Use a path inside the repository for --target or .qore.json envFile."
2139
2391
  });
@@ -2141,29 +2393,29 @@ var symlinked = (target) => new QoreError("QORE_CONTEXT_INVALID", `The env file
2141
2393
  hint: "Use a path inside the repository for --target or .qore.json envFile."
2142
2394
  });
2143
2395
  function inside(root, p) {
2144
- const rel = relative4(root, p);
2145
- return rel !== "" && rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute2(rel);
2396
+ const rel = relative5(root, p);
2397
+ return rel !== "" && rel !== ".." && !rel.startsWith(".." + sep2) && !isAbsolute3(rel);
2146
2398
  }
2147
2399
  async function resolveTarget(root, target, base = root) {
2148
- const realRoot = await realpath2(root);
2149
- const realBase = base === root ? realRoot : await realpath2(base);
2150
- const abs = resolve4(realBase, target);
2151
- if (!isAbsolute2(target) && !inside(realRoot, abs)) throw outside(target);
2400
+ const realRoot = await realpath5(root);
2401
+ const realBase = base === root ? realRoot : await realpath5(base);
2402
+ const abs = resolve5(realBase, target);
2403
+ if (!isAbsolute3(target) && !inside(realRoot, abs)) throw outside(target);
2152
2404
  let parent;
2153
2405
  try {
2154
- parent = await realpath2(dirname3(abs));
2406
+ parent = await realpath5(dirname4(abs));
2155
2407
  } catch (e) {
2156
2408
  if (e.code !== "ENOENT") throw e;
2157
2409
  throw new QoreError("QORE_CONTEXT_INVALID", `The directory for ${target} does not exist`, {
2158
2410
  hint: "Create the parent directory, or point --target / .qore.json envFile at an existing one."
2159
2411
  });
2160
2412
  }
2161
- const final = resolve4(parent, basename(abs));
2413
+ const final = resolve5(parent, basename(abs));
2162
2414
  if (!inside(realRoot, final)) throw outside(target);
2163
2415
  try {
2164
2416
  const st = await lstat(final);
2165
2417
  if (st.isSymbolicLink()) throw symlinked(target);
2166
- const realFinal = await realpath2(final);
2418
+ const realFinal = await realpath5(final);
2167
2419
  if (!inside(realRoot, realFinal)) throw outside(target);
2168
2420
  } catch (e) {
2169
2421
  if (e instanceof QoreError) throw e;
@@ -2199,9 +2451,9 @@ function planPull(o) {
2199
2451
  }
2200
2452
 
2201
2453
  // src/core/env/write.ts
2202
- import { chmod as chmod2, readFile as readFile4, rename as rename3, unlink, writeFile as writeFile3 } from "fs/promises";
2203
- import { randomBytes as randomBytes3 } from "crypto";
2204
- import { basename as basename2, dirname as dirname4, join as join10 } from "path";
2454
+ import { chmod as chmod2, readFile as readFile4, rename as rename3, unlink, writeFile as writeFile4 } from "fs/promises";
2455
+ import { randomBytes as randomBytes4 } from "crypto";
2456
+ import { basename as basename2, dirname as dirname5, join as join12 } from "path";
2205
2457
  async function readOrNull(p) {
2206
2458
  try {
2207
2459
  return await readFile4(p, "utf8");
@@ -2212,10 +2464,10 @@ async function readOrNull(p) {
2212
2464
  }
2213
2465
  async function writeSecurely(target, text2) {
2214
2466
  const base = basename2(target);
2215
- const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes3(6).toString("hex")}.local` : `${base}.qore-${randomBytes3(6).toString("hex")}.tmp`;
2216
- const tmp = join10(dirname4(target), tmpName);
2467
+ const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes4(6).toString("hex")}.local` : `${base}.qore-${randomBytes4(6).toString("hex")}.tmp`;
2468
+ const tmp = join12(dirname5(target), tmpName);
2217
2469
  try {
2218
- await writeFile3(tmp, text2, { mode: 384, flag: "wx" });
2470
+ await writeFile4(tmp, text2, { mode: 384, flag: "wx" });
2219
2471
  await chmod2(tmp, 384);
2220
2472
  await rename3(tmp, target);
2221
2473
  } finally {
@@ -2241,8 +2493,8 @@ async function syncEnvFile(o) {
2241
2493
 
2242
2494
  // src/core/dev/lastStart.ts
2243
2495
  import { createHash as createHash2 } from "crypto";
2244
- import { realpath as realpath3 } from "fs/promises";
2245
- import { join as join11 } from "path";
2496
+ import { realpath as realpath6 } from "fs/promises";
2497
+ import { join as join13 } from "path";
2246
2498
  var NETWORK_REASONS = /* @__PURE__ */ new Set(["connect", "dns", "tls", "timeout"]);
2247
2499
  function isOffline(e) {
2248
2500
  if (!(e instanceof QoreError)) return false;
@@ -2251,14 +2503,14 @@ function isOffline(e) {
2251
2503
  return (e.status ?? 0) >= 500 || NETWORK_REASONS.has(String(e.details?.reason));
2252
2504
  }
2253
2505
  async function stateKey(deps2, root) {
2254
- const real = await realpath3(root).catch(() => root);
2506
+ const real = await realpath6(root).catch(() => root);
2255
2507
  return createHash2("sha256").update(trustKey(real, deps2.platform)).digest("hex").slice(0, 16);
2256
2508
  }
2257
2509
  async function saveLastStart(deps2, root, rec) {
2258
- await writeJsonAtomic(join11(deps2.config.dir, "dev"), `${await stateKey(deps2, root)}.json`, rec);
2510
+ await writeJsonAtomic(join13(deps2.config.dir, "dev"), `${await stateKey(deps2, root)}.json`, rec);
2259
2511
  }
2260
2512
  async function loadLastStart(deps2, root) {
2261
- const rec = await readJson(join11(deps2.config.dir, "dev", `${await stateKey(deps2, root)}.json`)).catch(() => null);
2513
+ const rec = await readJson(join13(deps2.config.dir, "dev", `${await stateKey(deps2, root)}.json`)).catch(() => null);
2262
2514
  return rec && rec.schemaVersion === 1 ? rec : null;
2263
2515
  }
2264
2516
 
@@ -2278,7 +2530,7 @@ async function devEnv(deps2, o) {
2278
2530
  if (o.mc && o.app) {
2279
2531
  try {
2280
2532
  const local = await fetchLocalEnv(deps2, o.mc, o.app);
2281
- const example = await readOrNull(join12(dirname5(target), ".env.example"));
2533
+ const example = await readOrNull(join14(dirname6(target), ".env.example"));
2282
2534
  const w = await syncEnvFile({
2283
2535
  target,
2284
2536
  example,
@@ -2332,7 +2584,7 @@ function notClonedReason(m, am) {
2332
2584
  }
2333
2585
 
2334
2586
  // src/core/dev/findings.ts
2335
- var plural = (n, one, many) => n === 1 ? one : many;
2587
+ var plural = (n, one2, many) => n === 1 ? one2 : many;
2336
2588
  function modulesMissing(o) {
2337
2589
  const actionFor = new Map(o.actions.map((a) => [a.module.slug, a]));
2338
2590
  const findings = [];
@@ -2413,7 +2665,7 @@ function providerDown(o) {
2413
2665
 
2414
2666
  // src/core/dev/project.ts
2415
2667
  import { access, readFile as readFile5 } from "fs/promises";
2416
- import { join as join13 } from "path";
2668
+ import { join as join15 } from "path";
2417
2669
  var LEGACY_DEV_SCRIPTS = ["node --env-file=.env --import ./dist/instrumentation.js dist/index.js"];
2418
2670
  var LOCKFILES = [["pnpm-lock.yaml", "pnpm"], ["package-lock.json", "npm"], ["yarn.lock", "yarn"]];
2419
2671
  var exists = (p) => access(p).then(() => true, () => false);
@@ -2421,7 +2673,7 @@ var unsupported = (message, hint, details) => new QoreError("QORE_DEV_UNSUPPORTE
2421
2673
  async function readPackageJson(root) {
2422
2674
  let raw;
2423
2675
  try {
2424
- raw = await readFile5(join13(root, "package.json"), "utf8");
2676
+ raw = await readFile5(join15(root, "package.json"), "utf8");
2425
2677
  } catch {
2426
2678
  throw unsupported("This repository has no package.json", "qore dev starts Node repositories through their package.json scripts.");
2427
2679
  }
@@ -2437,7 +2689,7 @@ async function detectPackageManager(root, pkg) {
2437
2689
  if (!m) throw unsupported(`packageManager "${pkg.packageManager}" is not pnpm, npm or yarn`, "qore dev supports pnpm, npm and yarn.");
2438
2690
  return { name: m[1], version: m[2].split("+")[0] };
2439
2691
  }
2440
- for (const [file, name] of LOCKFILES) if (await exists(join13(root, file))) return { name, version: null };
2692
+ for (const [file, name] of LOCKFILES) if (await exists(join15(root, file))) return { name, version: null };
2441
2693
  throw unsupported("No package manager declared and no lockfile found", 'Add "packageManager": "pnpm@<version>" to package.json.');
2442
2694
  }
2443
2695
  function planRecipe(o) {
@@ -2480,21 +2732,21 @@ function installArgs(pm) {
2480
2732
  }
2481
2733
 
2482
2734
  // src/core/dev/install.ts
2483
- import { stat as stat2 } from "fs/promises";
2484
- import { join as join14 } from "path";
2735
+ import { stat as stat3 } from "fs/promises";
2736
+ import { join as join16 } from "path";
2485
2737
  var MARKERS = {
2486
2738
  pnpm: [".modules.yaml"],
2487
2739
  npm: [".package-lock.json"],
2488
2740
  yarn: [".yarn-state.yml", ".yarn-integrity"]
2489
2741
  };
2490
- var LOCK = { pnpm: "pnpm-lock.yaml", npm: "package-lock.json", yarn: "yarn.lock" };
2491
- var mtime = (p) => stat2(p).then((s) => s.mtimeMs, () => null);
2742
+ var LOCK2 = { pnpm: "pnpm-lock.yaml", npm: "package-lock.json", yarn: "yarn.lock" };
2743
+ var mtime = (p) => stat3(p).then((s) => s.mtimeMs, () => null);
2492
2744
  async function needsInstall(root, pm) {
2493
- if (await mtime(join14(root, "node_modules")) === null) return true;
2745
+ if (await mtime(join16(root, "node_modules")) === null) return true;
2494
2746
  let marker = null;
2495
- for (const m of MARKERS[pm.name]) marker ??= await mtime(join14(root, "node_modules", m));
2747
+ for (const m of MARKERS[pm.name]) marker ??= await mtime(join16(root, "node_modules", m));
2496
2748
  if (marker === null) return true;
2497
- const lock = await mtime(join14(root, LOCK[pm.name]));
2749
+ const lock = await mtime(join16(root, LOCK2[pm.name]));
2498
2750
  return lock !== null && lock > marker;
2499
2751
  }
2500
2752
 
@@ -2622,7 +2874,7 @@ function nodeVersionError(found, range, slug) {
2622
2874
  // src/core/dev/preflight.ts
2623
2875
  var pmMajor = (v) => v ? v.split(".")[0] : "latest";
2624
2876
  var GRACE_MS = 1e4;
2625
- var secs = (ms) => `${Math.round(ms / 1e3)} s`;
2877
+ var secs = (ms2) => `${Math.round(ms2 / 1e3)} s`;
2626
2878
  async function runTool(deps2, cmd, args, o) {
2627
2879
  if (o.signal.aborted) throw new CancelledError();
2628
2880
  const tail = [];
@@ -2810,13 +3062,13 @@ async function checkPort(deps2, o) {
2810
3062
  throw await portInUseError(deps2, { slug: o.slug, port: o.port, key: o.key, runs: o.runs });
2811
3063
  }
2812
3064
  function abortableDelay(signal) {
2813
- return (ms) => new Promise((resolve5) => {
3065
+ return (ms2) => new Promise((resolve6) => {
2814
3066
  const done = () => {
2815
3067
  clearTimeout(t);
2816
3068
  signal.removeEventListener("abort", done);
2817
- resolve5();
3069
+ resolve6();
2818
3070
  };
2819
- const t = setTimeout(done, ms);
3071
+ const t = setTimeout(done, ms2);
2820
3072
  signal.addEventListener("abort", done, { once: true });
2821
3073
  });
2822
3074
  }
@@ -2897,11 +3149,11 @@ function hintContext(recipe, pkg, env) {
2897
3149
 
2898
3150
  // src/core/dev/runFile.ts
2899
3151
  import { unlink as unlink2 } from "fs/promises";
2900
- import { join as join15 } from "path";
3152
+ import { join as join17 } from "path";
2901
3153
  async function runFilePath(deps2, root) {
2902
- const dir2 = join15(deps2.config.dir, "run");
3154
+ const dir2 = join17(deps2.config.dir, "run");
2903
3155
  const name = `${await stateKey(deps2, root)}.json`;
2904
- return { dir: dir2, name, path: join15(dir2, name) };
3156
+ return { dir: dir2, name, path: join17(dir2, name) };
2905
3157
  }
2906
3158
  async function writeRunFile(deps2, root, rec) {
2907
3159
  const { dir: dir2, name } = await runFilePath(deps2, root);
@@ -2922,8 +3174,197 @@ async function leftoverRun(deps2, root, o = {}) {
2922
3174
  return null;
2923
3175
  }
2924
3176
 
3177
+ // src/cli/project.ts
3178
+ import { readdir as readdir2, stat as stat4 } from "fs/promises";
3179
+ import { join as join18 } from "path";
3180
+
3181
+ // src/core/projects/match.ts
3182
+ function readings(ref) {
3183
+ const s = ref.split("/").map((x) => x.trim());
3184
+ if (s.some((x) => x === "")) return [];
3185
+ if (s.length === 1) return [{ app: s[0] }];
3186
+ if (s.length === 2) return [{ app: s[0], module: s[1] }, { tenant: s[0], app: s[1] }];
3187
+ if (s.length === 3) return [{ tenant: s[0], app: s[1], module: s[2] }];
3188
+ return [];
3189
+ }
3190
+ function hit(p, w) {
3191
+ if (p.appSlug !== w.app || w.tenant !== void 0 && p.tenantSlug !== w.tenant) return null;
3192
+ if (p.kind === "module") return w.module === p.moduleSlug ? { project: p } : null;
3193
+ return w.module ? { project: p, module: w.module } : { project: p };
3194
+ }
3195
+ function matchProjects(projects, ref) {
3196
+ const out = [];
3197
+ for (const w of readings(ref)) {
3198
+ for (const p of projects) {
3199
+ const m = hit(p, w);
3200
+ if (m) out.push(m);
3201
+ }
3202
+ }
3203
+ return out;
3204
+ }
3205
+
3206
+ // src/cli/project.ts
3207
+ async function loggedInAs(ctx) {
3208
+ const cfg = await ctx.deps.config.read();
3209
+ const portal = resolvePortal(ctx.opts().portal, cfg.defaultProfile);
3210
+ const p = cfg.profiles[portal.apiOrigin];
3211
+ if (!p || !await new TokenStore(ctx.deps.keychain, profileHash(p.apiOrigin, p.issuer)).getRefresh()) return null;
3212
+ return p.email ?? p.sub ?? portal.apiOrigin;
3213
+ }
3214
+ var isDir = async (p) => (await stat4(p).catch(() => null))?.isDirectory() ?? false;
3215
+ var label2 = (p) => `${p.tenantSlug} / ${p.appSlug}${p.kind === "module" ? ` / ${p.moduleSlug}` : ""}${p.source === "registry" ? ` (${p.path})` : ""}`;
3216
+ var candidate = (p) => p.kind === "module" ? { kind: "module", id: p.moduleId, slug: p.moduleSlug, tenant: p.tenantSlug, name: p.path } : { kind: "app", slug: p.appSlug, tenant: p.tenantSlug, name: p.path };
3217
+ var withCwd = (ctx, cwd) => ({ ...ctx, deps: { ...ctx.deps, cwd } });
3218
+ async function standingIn(ctx) {
3219
+ if (await findWorkspace(ctx.deps.cwd, ctx.deps.home)) return "workspace";
3220
+ const repo = await findRepoRoot(ctx.deps.cwd);
3221
+ return repo !== null && !isInside(repo, ctx.deps.home) ? { repo } : null;
3222
+ }
3223
+ async function isForeign(ctx, command) {
3224
+ const g = ctx.opts();
3225
+ try {
3226
+ await resolveModuleContext(ctx.deps, { portal: g.portal, command, onRequest: g.verbose ? (l) => ctx.io.err(l) : void 0 });
3227
+ return false;
3228
+ } catch (e) {
3229
+ if (e instanceof CancelledError) throw e;
3230
+ return isForeignRepo(e);
3231
+ }
3232
+ }
3233
+ async function moduleDirs(wsPath) {
3234
+ const out = [];
3235
+ for (const d of await readdir2(wsPath, { withFileTypes: true }).catch(() => [])) {
3236
+ if (d.isDirectory() && !d.name.startsWith(".") && await stat4(join18(wsPath, d.name, ".git")).catch(() => null)) out.push(d.name);
3237
+ }
3238
+ return out.sort();
3239
+ }
3240
+ async function pathFor(ctx, m, needModule) {
3241
+ const p = m.project;
3242
+ if (p.kind === "module") return p.path;
3243
+ if (m.module) {
3244
+ const dir2 = join18(p.path, m.module);
3245
+ if (!await isDir(dir2)) {
3246
+ throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no checkout of module ${m.module} in ${p.path}`, {
3247
+ hint: `Run \`qore sync\` in ${p.path} to clone the app's missing modules.`
3248
+ });
3249
+ }
3250
+ return dir2;
3251
+ }
3252
+ if (!needModule) return p.path;
3253
+ const dirs = await moduleDirs(p.path);
3254
+ if (dirs.length === 1) return join18(p.path, dirs[0]);
3255
+ if (dirs.length === 0) {
3256
+ throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no module checkouts in ${p.path}`, {
3257
+ hint: `Run \`qore sync\` in ${p.path}.`
3258
+ });
3259
+ }
3260
+ if (!ctx.io.interactive) {
3261
+ throw new QoreError("QORE_CONTEXT_REQUIRED", `${p.appSlug} has several modules \u2014 which one?`, {
3262
+ hint: `Pass ${p.appSlug}/<module>.`,
3263
+ candidates: dirs.map((slug) => ({ kind: "module", slug, tenant: p.tenantSlug, name: join18(p.path, slug) }))
3264
+ });
3265
+ }
3266
+ return join18(p.path, await ctx.io.select("Select a module", dirs.map((d) => ({ name: d, value: d }))));
3267
+ }
3268
+ async function liveMatches(matches) {
3269
+ const live = [];
3270
+ for (const m of matches) {
3271
+ if (m.project.kind === "workspace" && m.module && !await isDir(join18(m.project.path, m.module))) continue;
3272
+ live.push(m);
3273
+ }
3274
+ return live.length > 0 ? live : matches;
3275
+ }
3276
+ async function choose(ctx, matches, what, explicit) {
3277
+ if (explicit && matches.length === 1) return matches[0];
3278
+ if (!ctx.io.interactive) {
3279
+ throw new QoreError("QORE_CONTEXT_REQUIRED", what, {
3280
+ hint: "Pass a more specific reference: tenant/app or app/module.",
3281
+ candidates: matches.map((m) => candidate(m.project))
3282
+ });
3283
+ }
3284
+ const i = await ctx.io.select("Select a project", matches.map((m, idx) => ({
3285
+ name: m.module ? `${label2(m.project)} / ${m.module}` : label2(m.project),
3286
+ value: idx
3287
+ })));
3288
+ return matches[i];
3289
+ }
3290
+ async function projectCtx(ctx, o) {
3291
+ const g = ctx.opts();
3292
+ const cfg = await ctx.deps.config.read();
3293
+ const portal = g.portal ? resolvePortal(g.portal, cfg.defaultProfile).apiOrigin : void 0;
3294
+ if (o.ref) {
3295
+ const matches = matchProjects(await knownProjects(ctx.deps, { portal }), o.ref);
3296
+ if (matches.length === 0) {
3297
+ const parts = o.ref.split("/");
3298
+ const app = parts.length === 1 ? o.ref : parts.length === 3 ? parts[1] : "<app>";
3299
+ throw new QoreError("QORE_PROJECT_NOT_FOUND", `No local project "${o.ref}"`, {
3300
+ hint: cfg.projectsRoot ? `Run \`qore clone ${app}\`.` : `Run \`qore init\`, or \`qore clone ${app}\`.`
3301
+ });
3302
+ }
3303
+ const m2 = await choose(ctx, await liveMatches(matches), `"${o.ref}" matches several projects`, true);
3304
+ return relocate(ctx, await pathFor(ctx, m2, o.needModule));
3305
+ }
3306
+ if (o.module || o.app) return ctx;
3307
+ const here = await standingIn(ctx);
3308
+ if (here === "workspace") return ctx;
3309
+ const known = await knownProjects(ctx.deps, { portal });
3310
+ if (here && (known.length === 0 || known.some((p) => isInside(p.path, here.repo)) || !await isForeign(ctx, o.command ?? "project"))) return ctx;
3311
+ if (known.length === 0) {
3312
+ if (!cfg.projectsRoot) {
3313
+ if (!await loggedInAs(ctx)) return ctx;
3314
+ throw new QoreError("QORE_ROOT_NOT_SET", "Not inside a project, and qore knows no projects yet", {
3315
+ hint: "Run `qore init` to choose a projects folder, or cd into a project."
3316
+ });
3317
+ }
3318
+ throw new QoreError("QORE_PROJECT_NOT_FOUND", `Not inside a project, and ${cfg.projectsRoot} holds none yet`, {
3319
+ hint: "Run `qore clone <app>`."
3320
+ });
3321
+ }
3322
+ const what = here ? "This repository is not a Qore module \u2014 which project?" : "Not inside a project \u2014 which one?";
3323
+ const m = await choose(ctx, known.map((project) => ({ project })), what, false);
3324
+ return relocate(ctx, await pathFor(ctx, m, o.needModule));
3325
+ }
3326
+ function relocate(ctx, path) {
3327
+ if (path === ctx.deps.cwd) return ctx;
3328
+ if (!ctx.opts().json) ctx.io.err(`\u2192 in ${path}`);
3329
+ return withCwd(ctx, path);
3330
+ }
3331
+ async function rememberContext(ctx, mc, app) {
3332
+ const tenantSlug = mc.module.tenant.slug;
3333
+ if (mc.workspace && mc.workspace.appId === app.id) {
3334
+ await remember(ctx.deps, {
3335
+ kind: "workspace",
3336
+ path: mc.workspace.root,
3337
+ portal: mc.workspace.portal,
3338
+ tenantSlug,
3339
+ appId: app.id,
3340
+ appSlug: app.slug
3341
+ });
3342
+ return;
3343
+ }
3344
+ if (mc.repoRoot) {
3345
+ await remember(ctx.deps, {
3346
+ kind: "module",
3347
+ path: mc.repoRoot,
3348
+ portal: mc.portal.apiOrigin,
3349
+ tenantSlug,
3350
+ appId: app.id,
3351
+ appSlug: app.slug,
3352
+ moduleId: mc.module.id,
3353
+ moduleSlug: mc.module.slug
3354
+ });
3355
+ }
3356
+ }
3357
+ async function rememberWorkspace(ctx, ws, app) {
3358
+ if (ws.appId !== app.id) return;
3359
+ await remember(ctx.deps, { kind: "workspace", path: ws.root, portal: ws.portal, tenantSlug: app.tenant.slug, appId: app.id, appSlug: app.slug });
3360
+ }
3361
+
2925
3362
  // src/cli/context.ts
2926
3363
  var allOfKind = (c, kind) => !!c && c.length > 0 && c.every((x) => x.kind === kind && typeof x.id === "string");
3364
+ async function pick(ctx, err, message, choices) {
3365
+ if (!ctx.io.interactive) throw err;
3366
+ return ctx.io.select(message, choices);
3367
+ }
2927
3368
  async function moduleContextFor(ctx, flags, command) {
2928
3369
  const g = ctx.opts();
2929
3370
  const onRequest = g.verbose ? (l) => ctx.io.err(l) : void 0;
@@ -2933,14 +3374,16 @@ async function moduleContextFor(ctx, flags, command) {
2933
3374
  } catch (e) {
2934
3375
  if (!(e instanceof QoreError) || e.code !== "QORE_CONTEXT_REQUIRED" || !ctx.io.interactive) throw e;
2935
3376
  if (allOfKind(e.candidates, "module")) {
2936
- const choose = await ctx.io.select(
3377
+ const choose2 = await pick(
3378
+ ctx,
3379
+ e,
2937
3380
  "Select a module",
2938
3381
  e.candidates.map((c) => ({ name: `${c.tenant ? `${c.tenant} / ` : ""}${c.slug} (${c.remote})`, value: c.id }))
2939
3382
  );
2940
- return resolveModuleContext(ctx.deps, { ...base, choose });
3383
+ return resolveModuleContext(ctx.deps, { ...base, choose: choose2 });
2941
3384
  }
2942
3385
  if (allOfKind(e.candidates, "tenant")) {
2943
- const tenant = await ctx.io.select("Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.id })));
3386
+ const tenant = await pick(ctx, e, "Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.id })));
2944
3387
  return resolveModuleContext(ctx.deps, { ...base, tenant });
2945
3388
  }
2946
3389
  throw e;
@@ -2951,7 +3394,7 @@ async function appFor(ctx, mc, app) {
2951
3394
  return pickApp2(mc, app);
2952
3395
  } catch (e) {
2953
3396
  if (!(e instanceof QoreError) || e.code !== "QORE_APP_AMBIGUOUS" || !ctx.io.interactive) throw e;
2954
- const id = await ctx.io.select("Select an app", mc.module.apps.map((a) => ({ name: a.name, value: a.id })));
3397
+ const id = await pick(ctx, e, "Select an app", mc.module.apps.map((a) => ({ name: a.name, value: a.id })));
2955
3398
  return pickApp2(mc, id);
2956
3399
  }
2957
3400
  }
@@ -3145,9 +3588,9 @@ async function ensureTrusted(ctx, mc, app, o) {
3145
3588
  }
3146
3589
 
3147
3590
  // src/cli/commands/devWorkspace.ts
3148
- import { randomBytes as randomBytes4 } from "crypto";
3149
- import { realpath as realpath4 } from "fs/promises";
3150
- import { join as join17, relative as relative5 } from "path";
3591
+ import { randomBytes as randomBytes5 } from "crypto";
3592
+ import { realpath as realpath7 } from "fs/promises";
3593
+ import { join as join20, relative as relative6 } from "path";
3151
3594
 
3152
3595
  // src/core/dev/order.ts
3153
3596
  function selectSkips(modules, sel) {
@@ -3283,8 +3726,8 @@ function exitCode(e) {
3283
3726
  }
3284
3727
  function supervise(o) {
3285
3728
  const now = o.now ?? Date.now;
3286
- const delay = o.delay ?? ((ms) => new Promise((r) => {
3287
- setTimeout(r, ms).unref();
3729
+ const delay = o.delay ?? ((ms2) => new Promise((r) => {
3730
+ setTimeout(r, ms2).unref();
3288
3731
  }));
3289
3732
  const abort = new AbortController();
3290
3733
  const emit = (e) => {
@@ -3304,15 +3747,15 @@ function supervise(o) {
3304
3747
  });
3305
3748
  const readiness = /* @__PURE__ */ new Map();
3306
3749
  for (const m of o.modules) {
3307
- let resolve5;
3750
+ let resolve6;
3308
3751
  let settled = false;
3309
3752
  const promise = new Promise((r) => {
3310
- resolve5 = r;
3753
+ resolve6 = r;
3311
3754
  });
3312
3755
  readiness.set(m.slug, { promise, settle: (r) => {
3313
3756
  if (settled) return false;
3314
3757
  settled = true;
3315
- resolve5(r);
3758
+ resolve6(r);
3316
3759
  return true;
3317
3760
  } });
3318
3761
  }
@@ -3403,12 +3846,12 @@ function supervise(o) {
3403
3846
  }
3404
3847
 
3405
3848
  // src/core/workspace/stray.ts
3406
- import { lstat as lstat2, readdir } from "fs/promises";
3407
- import { join as join16 } from "path";
3849
+ import { lstat as lstat2, readdir as readdir3 } from "fs/promises";
3850
+ import { join as join19 } from "path";
3408
3851
  async function strayRepos(root, slugs) {
3409
3852
  let entries;
3410
3853
  try {
3411
- entries = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
3854
+ entries = (await readdir3(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
3412
3855
  } catch {
3413
3856
  return [];
3414
3857
  }
@@ -3416,7 +3859,7 @@ async function strayRepos(root, slugs) {
3416
3859
  for (const name of entries) {
3417
3860
  if (slugs.has(name)) continue;
3418
3861
  try {
3419
- await lstat2(join16(root, name, ".git"));
3862
+ await lstat2(join19(root, name, ".git"));
3420
3863
  names.push(name);
3421
3864
  } catch {
3422
3865
  }
@@ -3456,16 +3899,18 @@ function prefixer(slugs, color) {
3456
3899
  import { constants as constants2 } from "os";
3457
3900
  var childRunning = { current: false };
3458
3901
  function registerRun(program, ctx) {
3459
- 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) => {
3460
- const { deps: deps2 } = ctx;
3902
+ 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("--project <ref>", "the project whose secrets to use, from anywhere (see qore init)").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) => {
3461
3903
  if (ctx.opts().json) {
3462
3904
  throw new QoreError("QORE_CONTEXT_INVALID", "run has no --json output", {
3463
3905
  hint: "Drop --json; the child's own output passes through."
3464
3906
  });
3465
3907
  }
3466
- const mc = await moduleContextFor(ctx, f, "run");
3467
- const app = await appFor(ctx, mc, f.app);
3468
- await ensureTrusted(ctx, mc, app, { yes: f.yes });
3908
+ const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true, command: "run" });
3909
+ const { deps: deps2 } = c;
3910
+ const mc = await moduleContextFor(c, f, "run");
3911
+ const app = await appFor(c, mc, f.app);
3912
+ await rememberContext(c, mc, app);
3913
+ await ensureTrusted(c, mc, app, { yes: f.yes });
3469
3914
  const local = await fetchLocalEnv(deps2, mc, app);
3470
3915
  childRunning.current = true;
3471
3916
  let exit;
@@ -3499,7 +3944,9 @@ async function workspaceContext(ctx, ws) {
3499
3944
  const session = await openSession(deps2, portal, "dev", requestLog(ctx));
3500
3945
  await ensureCapability(deps2, portal.apiOrigin, "app.modules");
3501
3946
  await ensureCapability(deps2, portal.apiOrigin, "module.local-env");
3502
- return { am: await workspaceAppModules(session, portal, ws), online: { portal, session }, offlineCause: null };
3947
+ const am = await workspaceAppModules(session, portal, ws);
3948
+ await rememberWorkspace(ctx, ws, am.app);
3949
+ return { am, online: { portal, session }, offlineCause: null };
3503
3950
  } catch (e) {
3504
3951
  if (!isOffline(e)) throw e;
3505
3952
  const last = await loadLastStart(deps2, ws.root);
@@ -3574,7 +4021,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3574
4021
  continue;
3575
4022
  }
3576
4023
  try {
3577
- dirs[m.slug] = await deps2.git.inspect(join17(ws.root, m.slug));
4024
+ dirs[m.slug] = await deps2.git.inspect(join20(ws.root, m.slug));
3578
4025
  safe.push(m);
3579
4026
  } catch (e) {
3580
4027
  if (!(e instanceof QoreError) || e.code === "QORE_GIT_UNAVAILABLE") throw e;
@@ -3611,7 +4058,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3611
4058
  const results = await applyActions(deps2, { portal: online.portal, app: am, actions, protocol: cloneProtocol(dirs), onResult: () => {
3612
4059
  } });
3613
4060
  renderResults(io2, results, false);
3614
- for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(join17(ws.root, r.slug));
4061
+ for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(join20(ws.root, r.slug));
3615
4062
  ({ present, plan } = computePlan());
3616
4063
  }
3617
4064
  for (const name of await strayRepos(ws.root, new Set(am.modules.map((m) => m.slug)))) renderFinding(ctx, strayClone(name, am.app.name));
@@ -3719,7 +4166,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3719
4166
  workspace: ws,
3720
4167
  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] }
3721
4168
  });
3722
- const rootReal = await realpath4(ws.root);
4169
+ const rootReal = await realpath7(ws.root);
3723
4170
  const withDocker = dockerRecovery(ctx, p.signal);
3724
4171
  for (const u of units.filter((x) => x.recipe && x.trusted)) {
3725
4172
  const slug = u.m.slug;
@@ -3732,7 +4179,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3732
4179
  type: u.m.type,
3733
4180
  moduleId: u.m.id,
3734
4181
  cfg: u.cfg,
3735
- random: () => randomBytes4(32).toString("hex")
4182
+ random: () => randomBytes5(32).toString("hex")
3736
4183
  });
3737
4184
  } catch (e) {
3738
4185
  if (offlineCause && isOffline(e)) throw offlineCause;
@@ -3742,7 +4189,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3742
4189
  const dev = u.dev;
3743
4190
  if (!dev) continue;
3744
4191
  if (online && dev.offline) {
3745
- warn(`${slug}: the portal is unreachable \u2014 starting from ${relative5(rootReal, dev.envFile)} as last written; values may be stale.`, { module: slug });
4192
+ warn(`${slug}: the portal is unreachable \u2014 starting from ${relative6(rootReal, dev.envFile)} as last written; values may be stale.`, { module: slug });
3746
4193
  }
3747
4194
  if (dev.kept.length) {
3748
4195
  warn(
@@ -3750,7 +4197,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3750
4197
  { module: slug, keys: dev.kept }
3751
4198
  );
3752
4199
  }
3753
- report({ module: slug, step: "env", status: "done", detail: relative5(rootReal, dev.envFile) });
4200
+ report({ module: slug, step: "env", status: "done", detail: relative6(rootReal, dev.envFile) });
3754
4201
  const services = servicesMissing(slug, dev.env, u.recipe, { servicesFlag: f.services, configured: u.cfg.dev?.services !== void 0 });
3755
4202
  if (services) renderFinding(ctx, services);
3756
4203
  try {
@@ -3792,7 +4239,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3792
4239
  if (online && units.every((u) => !u.dev.offline)) {
3793
4240
  const envFile = (m) => {
3794
4241
  const u = units.find((x) => x.m.id === m.id);
3795
- return u ? relative5(rootReal, u.dev.envFile) : join17(m.slug, moduleDefaults(m.type).envFile);
4242
+ return u ? relative6(rootReal, u.dev.envFile) : join20(m.slug, moduleDefaults(m.type).envFile);
3796
4243
  };
3797
4244
  await saveLastStart(deps2, ws.root, {
3798
4245
  schemaVersion: 1,
@@ -3981,11 +4428,17 @@ function makeStderrTee(sink) {
3981
4428
  };
3982
4429
  }
3983
4430
  function registerDev(program, ctx) {
3984
- 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) => {
4431
+ program.command("dev").description("Start this module's dev server \u2014 or, in a workspace root, every module of the app").argument("[project]", "app or app/module to start, from anywhere (see qore init)").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "same as the [project] argument").option("--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 (project, f) => {
3985
4432
  const waitTimeout = parseWaitTimeout(f.waitTimeout);
3986
- const ws = await workspaceRoot(ctx.deps);
3987
- if (ws && f.module === void 0) return workspaceDev(ctx, ws, f, waitTimeout);
3988
- return moduleDev(ctx, f, waitTimeout);
4433
+ if (project !== void 0 && f.project !== void 0 && project !== f.project) {
4434
+ throw new QoreError("QORE_CONTEXT_INVALID", `Pass the project once: "${project}" and --project "${f.project}" differ`, {
4435
+ hint: "qore dev <project> | qore dev --project <project>"
4436
+ });
4437
+ }
4438
+ const c = await projectCtx(ctx, { ref: project ?? f.project, module: f.module, app: f.app, needModule: false, command: "dev" });
4439
+ const ws = await workspaceRoot(c.deps);
4440
+ if (ws && f.module === void 0) return workspaceDev(c, ws, f, waitTimeout);
4441
+ return moduleDev(c, f, waitTimeout);
3989
4442
  });
3990
4443
  }
3991
4444
  function parseWaitTimeout(raw) {
@@ -4025,7 +4478,7 @@ async function providerFindings(ctx, root, mc, app, env) {
4025
4478
  let fix = `qore clone ${app.slug}`;
4026
4479
  if (ws && ws.appId === app.id) fix = cdAndDev(deps2.home, ws.root);
4027
4480
  else if (providers.length === 1) {
4028
- const sibling = join18(dirname6(root), providers[0].slug);
4481
+ const sibling = join21(dirname7(root), providers[0].slug);
4029
4482
  if ((await deps2.git.inspect(sibling)).isRepo) fix = cdAndDev(deps2.home, sibling);
4030
4483
  }
4031
4484
  return providerDown({
@@ -4059,6 +4512,7 @@ async function devContext(ctx, f, root) {
4059
4512
  return { online: null, module: last.module, offlineCause: e };
4060
4513
  }
4061
4514
  const app = await appFor(ctx, mc, f.app);
4515
+ await rememberContext(ctx, mc, app);
4062
4516
  await ensureTrusted(ctx, mc, app, { yes: f.yes });
4063
4517
  return { online: { mc, app }, module: mc.module, offlineCause: null };
4064
4518
  }
@@ -4086,9 +4540,9 @@ async function moduleDev(ctx, f, waitTimeout) {
4086
4540
  childRunning.current = false;
4087
4541
  await removeRunFile(deps2, root);
4088
4542
  }
4089
- const ms = deps2.now().getTime() - startedAt.getTime();
4543
+ const ms2 = deps2.now().getTime() - startedAt.getTime();
4090
4544
  if (exit.code !== 0 && exit.signal === null) {
4091
- io2.err(ms < EARLY_EXIT_MS ? `\u2717 ${slug} exited after ${Math.round(ms / 1e3)} s with code ${exit.code ?? 1} \u2014 its output above says why.` : `\u2717 ${slug} exited with code ${exit.code ?? 1}.`);
4545
+ io2.err(ms2 < EARLY_EXIT_MS ? `\u2717 ${slug} exited after ${Math.round(ms2 / 1e3)} s with code ${exit.code ?? 1} \u2014 its output above says why.` : `\u2717 ${slug} exited with code ${exit.code ?? 1}.`);
4092
4546
  const hint = exitHint(tee.lines(), hintContext(recipe, pkg, devEnv2));
4093
4547
  if (hint) io2.err(` \u2192 ${hint.hint}`);
4094
4548
  }
@@ -4154,13 +4608,13 @@ async function prepareModule(ctx, f, waitTimeout, p) {
4154
4608
  type: c.module.type,
4155
4609
  moduleId: c.module.id,
4156
4610
  cfg,
4157
- random: () => randomBytes5(32).toString("hex")
4611
+ random: () => randomBytes6(32).toString("hex")
4158
4612
  });
4159
4613
  } catch (e) {
4160
4614
  if (c.offlineCause && isOffline(e)) throw c.offlineCause;
4161
4615
  throw e;
4162
4616
  }
4163
- const envFile = relative6(await realpath5(root), dev.envFile);
4617
+ const envFile = relative7(await realpath8(root), dev.envFile);
4164
4618
  if (dev.offline) io2.err(`! The portal is unreachable \u2014 starting from ${envFile} as last written; values may be stale.`);
4165
4619
  if (dev.kept.length) {
4166
4620
  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`);
@@ -4218,8 +4672,8 @@ function registerDebugTelemetry(program, ctx) {
4218
4672
  import pc3 from "picocolors";
4219
4673
 
4220
4674
  // src/core/doctor/checks.ts
4221
- import { realpath as realpath6 } from "fs/promises";
4222
- import { basename as basename3, join as join19 } from "path";
4675
+ import { realpath as realpath9, stat as stat5 } from "fs/promises";
4676
+ import { basename as basename3, join as join22 } from "path";
4223
4677
 
4224
4678
  // src/core/diagnostics/ssh.ts
4225
4679
  var SSH_TEST_ARGS = ["-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "git@github.com"];
@@ -4264,24 +4718,24 @@ var UPGRADE = "npm i -g @quatronic/qore";
4264
4718
  var RANK = { ok: 0, warn: 1, fail: 2 };
4265
4719
  var noop = () => {
4266
4720
  };
4267
- var ok = (label2, details) => ({ status: "ok", label: label2, ...details ? { details } : {} });
4268
- var withError = (status2, label2, e) => ({ status: status2, label: label2, error: toJsonError(e) });
4721
+ var ok = (label3, details) => ({ status: "ok", label: label3, ...details ? { details } : {} });
4722
+ var withError = (status2, label3, e) => ({ status: status2, label: label3, error: toJsonError(e) });
4269
4723
  var isCancel = (e, signal) => e instanceof CancelledError || signal.aborted;
4270
4724
  var portalLabel = (p) => p.builtin ?? p.apiOrigin;
4271
4725
  var ToolTimeout = class extends Error {
4272
- constructor(tool, ms) {
4273
- super(`${tool} did not answer within ${Math.round(ms / 1e3)} s`);
4726
+ constructor(tool, ms2) {
4727
+ super(`${tool} did not answer within ${Math.round(ms2 / 1e3)} s`);
4274
4728
  this.tool = tool;
4275
- this.ms = ms;
4729
+ this.ms = ms2;
4276
4730
  }
4277
4731
  tool;
4278
4732
  ms;
4279
4733
  };
4280
- async function timed(tool, signal, ms, fn) {
4734
+ async function timed(tool, signal, ms2, fn) {
4281
4735
  try {
4282
- return await fn(AbortSignal.any([signal, AbortSignal.timeout(ms)]));
4736
+ return await fn(AbortSignal.any([signal, AbortSignal.timeout(ms2)]));
4283
4737
  } catch (e) {
4284
- if (e instanceof CancelledError && !signal.aborted) throw new ToolTimeout(tool, ms);
4738
+ if (e instanceof CancelledError && !signal.aborted) throw new ToolTimeout(tool, ms2);
4285
4739
  throw e;
4286
4740
  }
4287
4741
  }
@@ -4312,8 +4766,8 @@ function checkOwnNode(deps2) {
4312
4766
  }
4313
4767
  return ok(`Node ${v} (\u226522 required)`);
4314
4768
  }
4315
- async function checkGit(deps2, signal, ms) {
4316
- const res = await timed("git", signal, ms, (s) => runTool(deps2, "git", ["--version"], { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
4769
+ async function checkGit(deps2, signal, ms2) {
4770
+ const res = await timed("git", signal, ms2, (s) => runTool(deps2, "git", ["--version"], { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
4317
4771
  if (res.spawnError === "ENOENT") {
4318
4772
  const e = new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
4319
4773
  hint: "Install git (https://git-scm.com/downloads) and open a new terminal."
@@ -4327,14 +4781,14 @@ async function checkGit(deps2, signal, ms) {
4327
4781
  const m = /git version (\d+)\.(\d+)/.exec(res.tail.join("\n"));
4328
4782
  return ok(m ? `git ${m[1]}.${m[2]}` : "git");
4329
4783
  }
4330
- async function checkGithubSsh(deps2, signal, ms) {
4331
- const res = await timed("ssh", signal, ms, (s) => runTool(deps2, "ssh", SSH_TEST_ARGS, { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
4784
+ async function checkGithubSsh(deps2, signal, ms2) {
4785
+ const res = await timed("ssh", signal, ms2, (s) => runTool(deps2, "ssh", SSH_TEST_ARGS, { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
4332
4786
  const r = classifySshTest(res);
4333
4787
  if (r.ok) return ok(`GitHub SSH ok (${r.user})`, { user: r.user });
4334
4788
  return withError("warn", `GitHub SSH: ${r.error.message} (https still works)`, r.error);
4335
4789
  }
4336
- async function checkDocker(deps2, signal, ms) {
4337
- return timed("docker", signal, ms, async (s) => {
4790
+ async function checkDocker(deps2, signal, ms2) {
4791
+ return timed("docker", signal, ms2, async (s) => {
4338
4792
  const o = { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s };
4339
4793
  const info = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], o);
4340
4794
  const infoErr = classifyDockerInfo(info, deps2.platform);
@@ -4401,7 +4855,7 @@ function ownerText(e) {
4401
4855
  if (d.owner === "qore") return ` by a previous qore dev (pid ${String(d.pid)})`;
4402
4856
  return "";
4403
4857
  }
4404
- var sameDir = async (a, b) => await realpath6(a).catch(() => a) === await realpath6(b).catch(() => b);
4858
+ var sameDir = async (a, b) => await realpath9(a).catch(() => a) === await realpath9(b).catch(() => b);
4405
4859
  async function moduleBody(deps2, m, runs, signal) {
4406
4860
  const parts = [];
4407
4861
  const part = async (fn) => {
@@ -4466,16 +4920,16 @@ function runsAt(deps2, root) {
4466
4920
  return () => p ??= leftoverRun(deps2, root, { prune: false }).then((r) => r ? [r] : []);
4467
4921
  }
4468
4922
  var UNTRUSTED = { status: "warn", label: "not checked (repository not trusted)" };
4469
- async function checkRepoPm(deps2, m, recipe, signal, ms) {
4923
+ async function checkRepoPm(deps2, m, recipe, signal, ms2) {
4470
4924
  if (!await isTrusted(deps2, m.root, m.id)) return UNTRUSTED;
4471
- const found = await timed(recipe.pm.name, signal, ms, (s) => checkPackageManager(deps2, recipe, noop, s));
4925
+ const found = await timed(recipe.pm.name, signal, ms2, (s) => checkPackageManager(deps2, recipe, noop, s));
4472
4926
  const { name, version } = recipe.pm;
4473
4927
  if (found === null) return ok(`${name} (version unknown)`);
4474
4928
  return ok(version ? `${name} ${found} (matches)` : `${name} ${found}`, { found, ...version ? { pinned: version } : {} });
4475
4929
  }
4476
- async function checkRepoNode(deps2, m, recipe, pkg, signal, ms) {
4930
+ async function checkRepoNode(deps2, m, recipe, pkg, signal, ms2) {
4477
4931
  if (!await isTrusted(deps2, m.root, m.id)) return UNTRUSTED;
4478
- const found = await timed("node", signal, ms, (s) => checkNode(deps2, recipe, pkg, m.slug, s));
4932
+ const found = await timed("node", signal, ms2, (s) => checkNode(deps2, recipe, pkg, m.slug, s));
4479
4933
  return ok(`Node ${found ?? "?"} (engines ${pkg.engines.node})`);
4480
4934
  }
4481
4935
  async function loggedInPortals(deps2, profiles) {
@@ -4499,6 +4953,15 @@ async function resolveRepoModule(deps2, session, portal, meta, root) {
4499
4953
  }
4500
4954
  return session.api.get(`/api/cli/v1/modules/${encodeURIComponent(ids[0])}`);
4501
4955
  }
4956
+ async function checkProjectsRoot(cfg) {
4957
+ const root = cfg?.projectsRoot;
4958
+ if (!root) return ok("projects folder not set (optional \u2014 qore init)");
4959
+ const s = await stat5(root).catch(() => null);
4960
+ if (!s?.isDirectory()) return { status: "warn", label: `projects folder ${root} does not exist \u2014 qore init` };
4961
+ const e = await writableError(root);
4962
+ if (e) return { status: "fail", label: `projects folder ${root} is not writable (${e.code ?? e.message})` };
4963
+ return ok(`projects folder ${root}`);
4964
+ }
4502
4965
  function checkTelemetry(deps2, cfg) {
4503
4966
  const built = deps2.sentryDsn !== "";
4504
4967
  if (!cfg) return ok("crash reports off (config unreadable)", { enabled: false, source: "config", reason: "unreadable", built });
@@ -4554,6 +5017,7 @@ async function runDoctor(deps2, o = {}) {
4554
5017
  await push("github-ssh", "tools", () => checkGithubSsh(deps2, signal, toolMs));
4555
5018
  await push("docker", "tools", () => checkDocker(deps2, signal, toolMs));
4556
5019
  await push("telemetry", "tools", () => checkTelemetry(deps2, cfgReadable ? cfg : null));
5020
+ await push("projects-root", "tools", () => checkProjectsRoot(cfgReadable ? cfg : null));
4557
5021
  const sessions = /* @__PURE__ */ new Map();
4558
5022
  let loggedIn = [];
4559
5023
  try {
@@ -4646,7 +5110,7 @@ async function workspaceModule(deps2, ws, am, m, runs, signal, push, perModule)
4646
5110
  await push(`module:${m.slug}`, "workspace", () => ({ status: "warn", label: "not a safe directory name \u2014 skipped" }), m.slug);
4647
5111
  return;
4648
5112
  }
4649
- const mod = { root: join19(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
5113
+ const mod = { root: join22(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
4650
5114
  let prepared = null;
4651
5115
  await push(`module:${m.slug}`, "workspace", async () => {
4652
5116
  const info = await deps2.git.inspect(mod.root);
@@ -4676,26 +5140,28 @@ function registerDoctor(program, ctx) {
4676
5140
  }
4677
5141
 
4678
5142
  // src/cli/commands/env.ts
4679
- import { randomBytes as randomBytes6 } from "crypto";
4680
- import { chmod as chmod3, realpath as realpath7 } from "fs/promises";
4681
- import { dirname as dirname7, join as join20, relative as relative7 } from "path";
5143
+ import { randomBytes as randomBytes7 } from "crypto";
5144
+ import { chmod as chmod3, realpath as realpath10 } from "fs/promises";
5145
+ import { dirname as dirname8, join as join23, relative as relative8 } from "path";
4682
5146
  var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.length} chars)`;
4683
5147
  function registerEnv(program, ctx) {
4684
5148
  const env = program.command("env").description("Local environment");
4685
- 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) => {
4686
- const { deps: deps2, io: io2 } = ctx;
4687
- const g = ctx.opts();
4688
- const mc = await moduleContextFor(ctx, f, "env pull");
5149
+ env.command("pull").description("Write this module's local secrets into its env file").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "the project to pull for, from anywhere (see qore init)").option("--target <path>", "env file to write (inside the repository)").option("--use-portal", "take the portal's value for keys that differ locally").option("--yes", "trust this repository without asking").action(async (f) => {
5150
+ const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true, command: "env pull" });
5151
+ const { deps: deps2, io: io2 } = c;
5152
+ const g = c.opts();
5153
+ const mc = await moduleContextFor(c, f, "env pull");
4689
5154
  if (!mc.repoRoot) {
4690
5155
  throw new QoreError("QORE_CONTEXT_REQUIRED", "env pull writes into a module repository", { hint: "cd into the module's repository." });
4691
5156
  }
4692
- const app = await appFor(ctx, mc, f.app);
5157
+ const app = await appFor(c, mc, f.app);
5158
+ await rememberContext(c, mc, app);
4693
5159
  const cfg = await readRepoConfig(mc.repoRoot);
4694
5160
  const defaults = moduleDefaults(mc.module.type);
4695
5161
  const target = f.target ? await resolveTarget(mc.repoRoot, f.target, deps2.cwd) : await resolveTarget(mc.repoRoot, cfg.envFile ?? defaults.envFile);
4696
- await ensureTrusted(ctx, mc, app, { yes: f.yes });
5162
+ await ensureTrusted(c, mc, app, { yes: f.yes });
4697
5163
  const existing = await readOrNull(target);
4698
- const example = existing === null ? await readOrNull(join20(dirname7(target), ".env.example")) : null;
5164
+ const example = existing === null ? await readOrNull(join23(dirname8(target), ".env.example")) : null;
4699
5165
  const local = await fetchLocalEnv(deps2, mc, app);
4700
5166
  const inputs = {
4701
5167
  existing,
@@ -4704,7 +5170,7 @@ function registerEnv(program, ctx) {
4704
5170
  managed: local.managedKeys,
4705
5171
  localSecrets: cfg.localSecrets ?? defaults.localSecrets,
4706
5172
  defaultChoice: f.usePortal ? "use-portal" : "keep",
4707
- random: () => randomBytes6(32).toString("hex")
5173
+ random: () => randomBytes7(32).toString("hex")
4708
5174
  };
4709
5175
  let plan = planPull(inputs);
4710
5176
  if (plan.conflicts.length && io2.interactive && !f.usePortal) {
@@ -4716,7 +5182,7 @@ function registerEnv(program, ctx) {
4716
5182
  choices[key] = bulk;
4717
5183
  continue;
4718
5184
  }
4719
- const pick = await io2.select(
5185
+ const pick2 = await io2.select(
4720
5186
  `${key} differs (local ${maskLen(current.get(key))}, portal ${maskLen(local.env[key])})`,
4721
5187
  [
4722
5188
  { name: "keep local", value: "keep" },
@@ -4725,9 +5191,9 @@ function registerEnv(program, ctx) {
4725
5191
  { name: "keep local for all", value: "skip" }
4726
5192
  ]
4727
5193
  );
4728
- if (pick === "all") bulk = "use-portal";
4729
- if (pick === "skip") bulk = "keep";
4730
- choices[key] = pick === "portal" || pick === "all" ? "use-portal" : "keep";
5194
+ if (pick2 === "all") bulk = "use-portal";
5195
+ if (pick2 === "skip") bulk = "keep";
5196
+ choices[key] = pick2 === "portal" || pick2 === "all" ? "use-portal" : "keep";
4731
5197
  }
4732
5198
  plan = planPull({ ...inputs, choices });
4733
5199
  }
@@ -4736,7 +5202,7 @@ function registerEnv(program, ctx) {
4736
5202
  } else {
4737
5203
  await writeSecurely(target, plan.text);
4738
5204
  }
4739
- const rel = relative7(await realpath7(mc.repoRoot), target);
5205
+ const rel = relative8(await realpath10(mc.repoRoot), target);
4740
5206
  if (g.json) {
4741
5207
  const names = {
4742
5208
  created: plan.created,
@@ -4765,9 +5231,486 @@ function registerEnv(program, ctx) {
4765
5231
  });
4766
5232
  }
4767
5233
 
5234
+ // src/core/observability/compose.ts
5235
+ function pageWarnings(...pages) {
5236
+ return pages.some((p) => p.ingestPaused) ? ["ingest_paused"] : [];
5237
+ }
5238
+ function scope(q) {
5239
+ const s = { env: q.env };
5240
+ if (q.since !== void 0) s.since = q.since;
5241
+ if (q.start !== void 0) s.start = q.start;
5242
+ if (q.end !== void 0) s.end = q.end;
5243
+ return s;
5244
+ }
5245
+ async function errorDetail(c, id, q, withs) {
5246
+ const w = scope(q);
5247
+ const markdown = await c.issueMarkdown(id, w);
5248
+ const event = await c.issueEvent(id, w);
5249
+ const out = { issue: { markdown, event }, warnings: [] };
5250
+ if (withs.size === 0) return out;
5251
+ if (!event.traceId) {
5252
+ out.warnings.push("no_trace_id");
5253
+ return out;
5254
+ }
5255
+ if (withs.has("trace")) out.trace = await c.trace(event.traceId, w);
5256
+ if (withs.has("logs")) {
5257
+ const logs = await c.logs({ ...w, trace: event.traceId, limit: 100 });
5258
+ out.logs = logs.items;
5259
+ out.warnings.push(...pageWarnings(logs));
5260
+ }
5261
+ return out;
5262
+ }
5263
+ async function traceDetail(c, id, q, withs) {
5264
+ const w = scope(q);
5265
+ const out = { trace: await c.trace(id, w), warnings: [] };
5266
+ const pages = [];
5267
+ if (withs.has("logs")) {
5268
+ const p = await c.logs({ ...w, trace: id, limit: 100 });
5269
+ out.logs = p.items;
5270
+ pages.push(p);
5271
+ }
5272
+ if (withs.has("errors")) {
5273
+ const p = await c.issues({ ...w, trace: id, status: "all" });
5274
+ out.errors = p.items;
5275
+ pages.push(p);
5276
+ }
5277
+ out.warnings.push(...pageWarnings(...pages));
5278
+ return out;
5279
+ }
5280
+
5281
+ // src/core/observability/client.ts
5282
+ var MAX_RETRY_WAIT_S = 30;
5283
+ var NOT_PROVISIONED = "observability_not_provisioned";
5284
+ var wait = (ms2) => new Promise((r) => setTimeout(r, ms2));
5285
+ var ObsClient = class {
5286
+ constructor(api, moduleId, sleep = wait, slug = moduleId) {
5287
+ this.api = api;
5288
+ this.moduleId = moduleId;
5289
+ this.sleep = sleep;
5290
+ this.slug = slug;
5291
+ }
5292
+ api;
5293
+ moduleId;
5294
+ sleep;
5295
+ slug;
5296
+ path(resource, o, extra = {}) {
5297
+ const p = new URLSearchParams();
5298
+ const all = { env: o.env, since: o.since, start: o.start, end: o.end, q: o.q, limit: o.limit, ...extra };
5299
+ for (const [k, v] of Object.entries(all)) if (v !== void 0 && v !== "") p.set(k, String(v));
5300
+ return `/api/cli/v1/modules/${encodeURIComponent(this.moduleId)}/observability/${resource}?${p}`;
5301
+ }
5302
+ async call(fn) {
5303
+ try {
5304
+ return await this.retryOnce(fn);
5305
+ } catch (e) {
5306
+ if (e instanceof QoreError && e.code === "QORE_ACCESS_DENIED" && e.serverCode === "auth.scope.insufficient") {
5307
+ throw new QoreError("QORE_ACCESS_DENIED", "This login predates `qore errors/traces/logs`", {
5308
+ hint: "Run `qore login` again.",
5309
+ status: e.status,
5310
+ requestId: e.requestId,
5311
+ serverCode: e.serverCode
5312
+ });
5313
+ }
5314
+ if (e instanceof QoreError && e.serverCode === NOT_PROVISIONED) {
5315
+ throw new QoreError("QORE_RESOURCE_NOT_FOUND", `Module "${this.slug}" does not report to Sentry yet`, {
5316
+ hint: "Its Sentry project is not provisioned; pick another module with --module.",
5317
+ status: e.status,
5318
+ requestId: e.requestId,
5319
+ serverCode: e.serverCode,
5320
+ details: e.details
5321
+ });
5322
+ }
5323
+ throw e;
5324
+ }
5325
+ }
5326
+ async retryOnce(fn) {
5327
+ try {
5328
+ return await fn();
5329
+ } catch (e) {
5330
+ if (!(e instanceof QoreError) || e.code !== "QORE_RATE_LIMITED") throw e;
5331
+ const secs2 = typeof e.details?.retryAfter === "number" ? e.details.retryAfter : 1;
5332
+ await this.sleep(Math.min(secs2, MAX_RETRY_WAIT_S) * 1e3);
5333
+ return fn();
5334
+ }
5335
+ }
5336
+ issues(o) {
5337
+ return this.call(() => this.api.get(this.path("issues", o, { status: o.status, sort: o.sort, trace: o.trace })));
5338
+ }
5339
+ issueMarkdown(id, o) {
5340
+ return this.call(() => this.api.getText(this.path(`issues/${encodeURIComponent(id)}/markdown`, { env: o.env, since: o.since, start: o.start, end: o.end }, { event: "recommended" })));
5341
+ }
5342
+ issueEvent(id, o) {
5343
+ return this.call(() => this.api.get(this.path(`issues/${encodeURIComponent(id)}/events/recommended`, { env: o.env, since: o.since, start: o.start, end: o.end })));
5344
+ }
5345
+ logs(o) {
5346
+ return this.call(() => this.api.get(this.path("logs", o, { level: o.level, trace: o.trace })));
5347
+ }
5348
+ spans(o) {
5349
+ return this.call(() => this.api.get(this.path("spans", o, { sort: o.sort })));
5350
+ }
5351
+ trace(id, o) {
5352
+ return this.call(() => this.api.get(this.path(`traces/${encodeURIComponent(id)}`, { env: o.env, since: o.since, start: o.start, end: o.end })));
5353
+ }
5354
+ };
5355
+
5356
+ // src/core/observability/query.ts
5357
+ var ROOT_SPAN_FILTER = "is_transaction:true";
5358
+ var DURATION = /^\d{1,6}(ms|s|m)$/;
5359
+ var SINCE = /^(\d{1,4})([mhdw])$/;
5360
+ var SECONDS = { m: 60, h: 3600, d: 86400, w: 604800 };
5361
+ function parseDuration(s) {
5362
+ if (!DURATION.test(s)) throw new QoreError("QORE_CONTEXT_INVALID", `"${s}" is not a duration`, { hint: "Use e.g. 500ms, 1s or 2m." });
5363
+ return s;
5364
+ }
5365
+ function parseSince(s) {
5366
+ const m = SINCE.exec(s);
5367
+ if (!m || Number(m[1]) * SECONDS[m[2]] > 90 * 86400) {
5368
+ throw new QoreError("QORE_CONTEXT_INVALID", `"${s}" is not a time window`, { hint: "Use a count plus m, h, d or w, at most 90 days (e.g. 24h, 7d)." });
5369
+ }
5370
+ return s;
5371
+ }
5372
+ function joinQuery(...parts) {
5373
+ const q = parts.filter((p) => typeof p === "string" && p.length > 0).join(" ");
5374
+ return q || void 0;
5375
+ }
5376
+
5377
+ // src/cli/observability/context.ts
5378
+ var ALL = { kind: "module", slug: "all", name: "All modules" };
5379
+ var modCandidates = (mods, withAll) => [...mods.map((m) => ({ kind: "module", id: m.id, slug: m.slug, name: m.name })), ...withAll ? [ALL] : []];
5380
+ function withRepo(mods) {
5381
+ const r = mods.filter((m) => m.repoUrl !== null);
5382
+ if (r.length === 0) throw new QoreError("QORE_RESOURCE_NOT_FOUND", "No modules with a repository in this app");
5383
+ return r;
5384
+ }
5385
+ function windowOf(f) {
5386
+ if (Boolean(f.start) !== Boolean(f.end)) throw new QoreError("QORE_CONTEXT_INVALID", "--start and --end go together; pass both");
5387
+ if (f.start || f.end) return { start: f.start, end: f.end };
5388
+ return { since: parseSince(f.since ?? "24h") };
5389
+ }
5390
+ var windowText = (w) => w.since ? `last ${w.since}` : `${w.start ?? "\u2026"} \u2013 ${w.end ?? "now"}`;
5391
+ async function chooseModule(ctx, mods, flag, single) {
5392
+ if (flag === "all" && !single) return mods;
5393
+ if (flag && flag !== "all") {
5394
+ const m = mods.find((x) => x.slug === flag || x.id === flag);
5395
+ if (m) return [m];
5396
+ throw new QoreError("QORE_CONTEXT_REQUIRED", `No module "${flag}" in this app`, { candidates: modCandidates(mods, !single) });
5397
+ }
5398
+ const err = new QoreError(
5399
+ "QORE_CONTEXT_REQUIRED",
5400
+ single && flag === "all" ? "A detail view needs one module" : "Which module?",
5401
+ { candidates: modCandidates(mods, !single), hint: "Pass --module <slug>" + (single ? "." : " or --module all.") }
5402
+ );
5403
+ if (flag === "all") throw err;
5404
+ const choice = await pick(
5405
+ ctx,
5406
+ err,
5407
+ "Select a module",
5408
+ [...mods.map((m) => ({ name: m.slug, value: m.id })), ...single ? [] : [{ name: "All modules", value: "all" }]]
5409
+ );
5410
+ return choice === "all" ? mods : mods.filter((m) => m.id === choice);
5411
+ }
5412
+ async function chooseEnv(ctx, api, moduleId, appId, flag) {
5413
+ const st = await api.get(`/api/cli/v1/modules/${encodeURIComponent(moduleId)}/status?app_id=${encodeURIComponent(appId)}`);
5414
+ const names = st.environments.map((e) => e.name);
5415
+ if (names.length === 0) throw new QoreError("QORE_RESOURCE_NOT_FOUND", "No environments yet");
5416
+ const candidates = names.map((n) => ({ kind: "env", slug: n }));
5417
+ if (flag) {
5418
+ if (names.includes(flag)) return flag;
5419
+ throw new QoreError("QORE_CONTEXT_REQUIRED", `Unknown environment "${flag}"`, { candidates, hint: `Pass --env ${names.join("|")}.` });
5420
+ }
5421
+ const err = new QoreError("QORE_CONTEXT_REQUIRED", "Which environment?", { candidates, hint: `Pass --env ${names.join("|")}.` });
5422
+ return pick(ctx, err, "Select an environment", names.map((n) => ({ name: n, value: n })));
5423
+ }
5424
+ async function obsContext(ctx, flags, command, o) {
5425
+ const { deps: deps2, io: io2 } = ctx;
5426
+ const g = ctx.opts();
5427
+ const window = windowOf(flags);
5428
+ const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
5429
+ const ws = flags.module === void 0 || flags.module === "all" ? await workspaceRoot(deps2) : null;
5430
+ let api, tenant, app, mods, base, apiOrigin;
5431
+ if (ws) {
5432
+ const portal = await flagPortal(deps2, g.portal, ws);
5433
+ apiOrigin = portal.apiOrigin;
5434
+ const session = await openSession(deps2, portal, command, onRequest);
5435
+ await ensureCapability(deps2, apiOrigin, "module.observability");
5436
+ await ensureCapability(deps2, apiOrigin, "app.modules");
5437
+ const am = await workspaceAppModules(session, portal, ws);
5438
+ api = session.api;
5439
+ ({ tenant } = am.app);
5440
+ app = { id: am.app.id, slug: am.app.slug, name: am.app.name };
5441
+ mods = await chooseModule(ctx, withRepo(am.modules), flags.module, o.single);
5442
+ base = { portal: portal.builtin ?? portal.apiOrigin, tenant, app, source: WORKSPACE_FILE };
5443
+ } else {
5444
+ const mc = await moduleContextFor(ctx, { module: flags.module === "all" ? void 0 : flags.module }, command);
5445
+ apiOrigin = mc.portal.apiOrigin;
5446
+ await ensureCapability(deps2, apiOrigin, "module.observability");
5447
+ app = await appFor(ctx, mc, flags.app);
5448
+ api = mc.session.api;
5449
+ tenant = mc.module.tenant;
5450
+ if (flags.module === "all") {
5451
+ if (o.single) throw new QoreError(
5452
+ "QORE_CONTEXT_REQUIRED",
5453
+ "A detail view needs one module",
5454
+ { candidates: [{ kind: "module", id: mc.module.id, slug: mc.module.slug, name: mc.module.name }] }
5455
+ );
5456
+ await ensureCapability(deps2, apiOrigin, "app.modules");
5457
+ const am = await api.get(`/api/cli/v1/apps/${encodeURIComponent(app.id)}/modules`);
5458
+ mods = withRepo(am.modules);
5459
+ } else {
5460
+ mods = [mc.module];
5461
+ }
5462
+ base = contextJson(mc, app);
5463
+ delete base.module;
5464
+ }
5465
+ await ensureCapability(deps2, apiOrigin, "module.status");
5466
+ const env = await chooseEnv(ctx, api, mods[0].id, app.id, flags.env);
5467
+ const fanOut2 = mods.length > 1 || flags.module === "all";
5468
+ const targets = mods.map((m) => ({ module: { id: m.id, slug: m.slug, name: m.name, type: m.type, access: m.access }, client: new ObsClient(api, m.id, void 0, m.slug) }));
5469
+ const where = fanOut2 ? "all modules" : mods[0].slug;
5470
+ return {
5471
+ targets,
5472
+ fanOut: fanOut2,
5473
+ env,
5474
+ window,
5475
+ header: `\u2192 ${tenant.name} / ${app.name} / ${where} \xB7 ${env} \xB7 ${windowText(window)}`,
5476
+ json: { ...base, ...fanOut2 ? { modules: targets.map((t) => t.module) } : { module: targets[0].module }, env, window }
5477
+ };
5478
+ }
5479
+
5480
+ // src/cli/format.ts
5481
+ function ago(iso, now) {
5482
+ if (!iso) return "\u2013";
5483
+ const s = Math.max(0, Math.round((now.getTime() - new Date(iso).getTime()) / 1e3));
5484
+ if (s < 60) return "just now";
5485
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
5486
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
5487
+ return `${Math.floor(s / 86400)}d ago`;
5488
+ }
5489
+ var pct = (f) => String(Number((f * 100).toFixed(2)));
5490
+ function uptime(h) {
5491
+ if (!h) return "\u2013";
5492
+ const kind = h.cause && h.kind !== "up" ? `${h.kind}: ${h.cause}` : h.kind;
5493
+ return h.uptime24h === null ? kind : `${kind} (${pct(h.uptime24h)}% 24h)`;
5494
+ }
5495
+ function table(rows) {
5496
+ const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length)));
5497
+ return rows.map((r) => " " + r.map((c, i) => i === r.length - 1 ? c : c.padEnd(widths[i])).join(" "));
5498
+ }
5499
+
5500
+ // src/cli/observability/render.ts
5501
+ var CONTROL = /[\x00-\x08\x0b-\x1f\x7f\x80-\x9f]/g;
5502
+ var clean = (s) => s.replace(CONTROL, "");
5503
+ function fence(content) {
5504
+ const lines = content.map(clean);
5505
+ const longest = Math.max(0, ...lines.flatMap((l) => (l.match(/`+/g) ?? []).map((r) => r.length)));
5506
+ const ticks = "`".repeat(Math.max(3, longest + 1));
5507
+ return [`${ticks}untrusted-data`, ...lines, ticks];
5508
+ }
5509
+ function ms(n) {
5510
+ return n < 1e3 ? `${Math.round(n)}ms` : `${(n / 1e3).toFixed(1)}s`;
5511
+ }
5512
+ var oneLine = (s) => clean((s ?? "").replace(/\s+/g, " ")).trim();
5513
+ function issuesTable(items, now, withModule) {
5514
+ const head = ["ID", "LEVEL", "EVENTS", "USERS", "LAST SEEN", "TITLE"];
5515
+ const rows = items.map((i) => [oneLine(i.shortId), oneLine(i.level), String(i.count), String(i.userCount), ago(i.lastSeen, now), oneLine(i.title)]);
5516
+ return table(withModule ? [["MODULE", ...head], ...rows.map((r, n) => [oneLine(items[n].module.slug), ...r])] : [head, ...rows]);
5517
+ }
5518
+ function spansTable(items, now, withModule) {
5519
+ const head = ["TRACE", "DURATION", "ROOT", "WHEN"];
5520
+ const rows = items.map((s) => [oneLine(s.traceId), ms(s.durationMs), oneLine(s.transaction ?? s.description ?? s.name), ago(s.timestamp, now)]);
5521
+ return table(withModule ? [["MODULE", ...head], ...rows.map((r, n) => [oneLine(items[n].module.slug), ...r])] : [head, ...rows]);
5522
+ }
5523
+ function logLines(items, withModule) {
5524
+ return items.map((l) => [oneLine(l.timestamp), oneLine(l.severity).toUpperCase(), ...withModule ? [oneLine(l.module.slug)] : [], oneLine(l.message)].join(" "));
5525
+ }
5526
+ function spanTree(t) {
5527
+ const all = [];
5528
+ const walk = (n) => {
5529
+ all.push(n);
5530
+ n.children.forEach(walk);
5531
+ };
5532
+ t.roots.forEach(walk);
5533
+ const slowest = all.reduce((a, n) => !a || n.end - n.start > a.end - a.start ? n : a, null);
5534
+ const out = [];
5535
+ const line2 = (n, depth) => {
5536
+ const label3 = [oneLine(n.op), oneLine(n.description ?? n.transaction)].filter(Boolean).join(" ");
5537
+ const errs = n.errors.map((e) => `\u2716 ${oneLine(e.shortId ?? e.eventId)}`).join(" ");
5538
+ out.push([
5539
+ `${" ".repeat(depth)}${label3}`,
5540
+ ms((n.end - n.start) * 1e3),
5541
+ `[${oneLine(n.module.slug)}]`,
5542
+ ...n === slowest ? ["\u25C0 slowest"] : [],
5543
+ ...errs ? [errs] : []
5544
+ ].join(" "));
5545
+ n.children.forEach((c) => line2(c, depth + 1));
5546
+ };
5547
+ t.roots.forEach((r) => line2(r, 0));
5548
+ return out;
5549
+ }
5550
+ var WARN = {
5551
+ ingest_paused: "! This tenant's Sentry quota is reached; new events are not being recorded.",
5552
+ no_trace_id: "! This error has no trace id, so there is no trace or trace logs to show.",
5553
+ sort_unavailable: "! Sorting by duration is not available here; showing the slowest of the most recent matches.",
5554
+ module_not_provisioned: "! Skipped, not reporting to Sentry yet:"
5555
+ };
5556
+ function warningLines(w, skipped = []) {
5557
+ return [...new Set(w)].map((k) => k === "module_not_provisioned" ? `${WARN[k]} ${skipped.join(", ")}` : WARN[k]);
5558
+ }
5559
+
5560
+ // src/cli/observability/flags.ts
5561
+ function addObsFlags(cmd) {
5562
+ return cmd.option("--env <name>", "the environment (required; prompted when interactive)").option("--module <slug|all>", "the module (default: this repository); all = every module of the app").option("--app <slug|id>", "the app, when the module is part of several").option("--since <window>", "time window, e.g. 1h, 24h, 7d (default 24h)").option("--start <iso>", "window start (use with --end, instead of --since)").option("--end <iso>", "window end (use with --start, instead of --since)").option("--query <search>", "Sentry search syntax, appended to the filter").option("--limit <n>", "rows per module, 1-100 (default 25)");
5563
+ }
5564
+ function limitOf(s) {
5565
+ if (s === void 0) return 25;
5566
+ const n = Number(s);
5567
+ if (!Number.isInteger(n) || n < 1 || n > 100) throw new QoreError("QORE_CONTEXT_INVALID", `--limit must be 1-100, not "${s}"`);
5568
+ return n;
5569
+ }
5570
+ function withList(s, allowed) {
5571
+ if (!s) return /* @__PURE__ */ new Set();
5572
+ const parts = s.split(",").map((p) => p.trim()).filter(Boolean);
5573
+ const bad = parts.filter((p) => !allowed.includes(p));
5574
+ if (bad.length) throw new QoreError("QORE_CONTEXT_INVALID", `--with accepts ${allowed.join(", ")}; not ${bad.join(", ")}`);
5575
+ return new Set(parts);
5576
+ }
5577
+ function one(flag, value, allowed) {
5578
+ if (!allowed.includes(value)) {
5579
+ throw new QoreError("QORE_CONTEXT_INVALID", `${flag} must be ${allowed.join("|")}, not "${value}"`);
5580
+ }
5581
+ return value;
5582
+ }
5583
+ async function fanOut(oc2, fn) {
5584
+ const out = { results: [], skipped: [] };
5585
+ let last;
5586
+ for (const target of oc2.targets) {
5587
+ try {
5588
+ out.results.push({ target, result: await fn(target) });
5589
+ } catch (e) {
5590
+ if (oc2.targets.length < 2 || !(e instanceof QoreError) || e.serverCode !== NOT_PROVISIONED) throw e;
5591
+ out.skipped.push({ id: target.module.id, slug: target.module.slug });
5592
+ last = e;
5593
+ }
5594
+ }
5595
+ if (out.results.length === 0 && last) {
5596
+ throw new QoreError("QORE_RESOURCE_NOT_FOUND", "No module of this app reports to Sentry yet", {
5597
+ serverCode: NOT_PROVISIONED,
5598
+ details: { modules: out.skipped.map((m) => m.slug) }
5599
+ });
5600
+ }
5601
+ return out;
5602
+ }
5603
+ var skipWarnings = (f) => f.skipped.length ? ["module_not_provisioned"] : [];
5604
+ var skipJson = (f) => f.skipped.length ? { unprovisionedModules: f.skipped } : {};
5605
+ function printWarnings(io2, w, json2, skipped = []) {
5606
+ if (!json2) for (const l of warningLines(w, skipped.map((m) => m.slug))) io2.err(l);
5607
+ }
5608
+
5609
+ // src/cli/commands/errors.ts
5610
+ var WITHS = ["trace", "logs"];
5611
+ var STATUSES = ["unresolved", "resolved", "all"];
5612
+ var SORTS = ["date", "freq", "new"];
5613
+ var MERGE = {
5614
+ date: (a, b) => b.lastSeen.localeCompare(a.lastSeen),
5615
+ freq: (a, b) => b.count - a.count,
5616
+ new: (a, b) => b.firstSeen.localeCompare(a.firstSeen)
5617
+ };
5618
+ function printDetail(ctx, oc2, d, showIssue) {
5619
+ const { io: io2 } = ctx;
5620
+ printWarnings(io2, d.warnings, false);
5621
+ if (showIssue) for (const l of fence(d.issue.markdown.split("\n"))) io2.out(l);
5622
+ if (d.trace) {
5623
+ io2.out("");
5624
+ io2.out("Trace");
5625
+ for (const l of fence(spanTree(d.trace))) io2.out(l);
5626
+ }
5627
+ if (d.logs) {
5628
+ io2.out("");
5629
+ io2.out(`Logs (${d.logs.length})`);
5630
+ for (const l of fence(logLines(d.logs, oc2.fanOut))) io2.out(l);
5631
+ }
5632
+ }
5633
+ async function interactiveDetail(ctx, oc2, c, id) {
5634
+ const first = await errorDetail(c, id, { env: oc2.env, ...oc2.window }, /* @__PURE__ */ new Set());
5635
+ printDetail(ctx, oc2, first, true);
5636
+ const traceId = first.issue.event.traceId;
5637
+ if (!traceId) return;
5638
+ const scope2 = { env: oc2.env, ...oc2.window };
5639
+ for (; ; ) {
5640
+ const next = await ctx.io.select("Next?", [
5641
+ { name: `Show trace ${oneLine(traceId).slice(0, 8)}`, value: "trace" },
5642
+ { name: "Logs of this trace", value: "logs" },
5643
+ { name: "Done", value: "done" }
5644
+ ]);
5645
+ if (next === "done") return;
5646
+ const extra = { issue: first.issue, warnings: [] };
5647
+ if (next === "trace") extra.trace = await c.trace(traceId, scope2);
5648
+ else {
5649
+ const p = await c.logs({ ...scope2, trace: traceId, limit: 100 });
5650
+ extra.logs = p.items;
5651
+ extra.warnings.push(...pageWarnings(p));
5652
+ }
5653
+ printDetail(ctx, oc2, extra, false);
5654
+ }
5655
+ }
5656
+ function registerErrors(program, ctx) {
5657
+ addObsFlags(program.command("errors [id]").description("Errors of a deployed environment, or one error with its stack trace (requires --env)")).option("--with <list>", `with an id: also show ${WITHS.join(",")}`).option("--status <status>", `${STATUSES.join("|")} (default unresolved)`).option("--sort <sort>", `${SORTS.join("|")} (default date)`).action(async (id, flags) => {
5658
+ const { io: io2, deps: deps2 } = ctx;
5659
+ const g = ctx.opts();
5660
+ const withs = withList(flags.with, WITHS);
5661
+ const status2 = one("--status", flags.status ?? "unresolved", STATUSES);
5662
+ const sort = one("--sort", flags.sort ?? "date", SORTS);
5663
+ const limit = limitOf(flags.limit);
5664
+ const oc2 = await obsContext(ctx, flags, "errors", { single: id !== void 0 });
5665
+ const q = { env: oc2.env, ...oc2.window, q: flags.query, limit };
5666
+ if (id) {
5667
+ const c2 = oc2.targets[0].client;
5668
+ if (g.json) {
5669
+ renderJson(io2, { context: oc2.json, ...await errorDetail(c2, id, q, withs) });
5670
+ return;
5671
+ }
5672
+ io2.err(oc2.header);
5673
+ if (io2.interactive && withs.size === 0) {
5674
+ await interactiveDetail(ctx, oc2, c2, id);
5675
+ return;
5676
+ }
5677
+ printDetail(ctx, oc2, await errorDetail(c2, id, q, withs), true);
5678
+ return;
5679
+ }
5680
+ const f = await fanOut(oc2, (t) => t.client.issues({ ...q, status: status2, sort }));
5681
+ const pages = f.results.map((r) => r.result);
5682
+ const items = pages.flatMap((p) => p.items);
5683
+ if (pages.length > 1) items.sort(MERGE[sort]);
5684
+ const warnings = [...pageWarnings(...pages), ...skipWarnings(f)];
5685
+ if (g.json) {
5686
+ renderJson(io2, { context: oc2.json, items, warnings, ...skipJson(f) });
5687
+ return;
5688
+ }
5689
+ io2.err(oc2.header);
5690
+ printWarnings(io2, warnings, false, f.skipped);
5691
+ if (items.length === 0) {
5692
+ io2.out(`No ${status2 === "all" ? "" : `${status2} `}errors in ${oc2.env} (${windowText(oc2.window)}).`);
5693
+ return;
5694
+ }
5695
+ for (const l of fence(issuesTable(items, deps2.now(), oc2.fanOut))) io2.out(l);
5696
+ if (!io2.interactive) return;
5697
+ const picked = await io2.select(
5698
+ "Which error to open?",
5699
+ [...items.map((i) => ({ name: `${oneLine(i.shortId)} ${oneLine(i.title)}`.slice(0, 100), value: i })), { name: "(none)", value: null }]
5700
+ );
5701
+ if (!picked) return;
5702
+ const c = oc2.targets.find((t) => t.module.id === picked.module.id)?.client;
5703
+ if (!c) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `module ${picked.module.slug} is not among the queried modules`);
5704
+ await interactiveDetail(ctx, oc2, c, picked.shortId);
5705
+ });
5706
+ }
5707
+
5708
+ // src/cli/commands/init.ts
5709
+ import { realpath as realpath11 } from "fs/promises";
5710
+
4768
5711
  // src/core/auth/oidc.ts
4769
5712
  import * as oc from "openid-client";
4770
- var LOGIN_SCOPE = "openid profile email offline_access qore:read qore:local-env";
5713
+ var LOGIN_SCOPE = "openid profile email offline_access qore:read qore:local-env qore:observability";
4771
5714
  function decodeSub(accessToken) {
4772
5715
  const part = accessToken.split(".")[1] ?? "";
4773
5716
  const claims = JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
@@ -4818,9 +5761,9 @@ async function createOidc(opts) {
4818
5761
  throw mapOidcError(e, host);
4819
5762
  }
4820
5763
  return {
4821
- async startDevice(scope) {
5764
+ async startDevice(scope2) {
4822
5765
  try {
4823
- const r = await oc.initiateDeviceAuthorization(config, { scope });
5766
+ const r = await oc.initiateDeviceAuthorization(config, { scope: scope2 });
4824
5767
  return {
4825
5768
  userCode: r.user_code,
4826
5769
  verificationUri: r.verification_uri,
@@ -4840,9 +5783,9 @@ async function createOidc(opts) {
4840
5783
  throw mapOidcError(e, host);
4841
5784
  }
4842
5785
  },
4843
- async refresh(rt, scope) {
5786
+ async refresh(rt, scope2) {
4844
5787
  try {
4845
- const t = await oc.refreshTokenGrant(config, rt, scope ? { scope } : void 0);
5788
+ const t = await oc.refreshTokenGrant(config, rt, scope2 ? { scope: scope2 } : void 0);
4846
5789
  return toTokenSet(t, rt);
4847
5790
  } catch (e) {
4848
5791
  throw mapOidcError(e, host);
@@ -4896,78 +5839,229 @@ async function deviceLogin(opts) {
4896
5839
  }
4897
5840
 
4898
5841
  // src/cli/commands/login.ts
5842
+ var NO_TENANTS = "Your account is not a developer in any organisation. Ask an admin to invite you as developer.";
5843
+ async function loginFlow(ctx, cmd) {
5844
+ const { deps: deps2, io: io2, signal } = ctx;
5845
+ const g = ctx.opts();
5846
+ const cfg = await deps2.config.read();
5847
+ const portal = resolvePortal(g.portal, cfg.defaultProfile);
5848
+ const meta = await fetchMeta(deps2.fetch, portal.apiOrigin);
5849
+ const known = cfg.profiles[portal.apiOrigin]?.issuer;
5850
+ const pinned = { ...portal, pinnedIssuer: portal.pinnedIssuer ?? known ?? null };
5851
+ if (!pinned.pinnedIssuer && portal.builtin === null) {
5852
+ io2.err(`! Logging in to ${portal.apiOrigin}, identity provider ${meta.auth.issuer}`);
5853
+ }
5854
+ negotiate(meta, pinned, deps2.version);
5855
+ const issuer = pinned.pinnedIssuer ?? meta.auth.issuer;
5856
+ const hash = profileHash(portal.apiOrigin, issuer);
5857
+ const store = new TokenStore(deps2.keychain, hash);
5858
+ const t = await deviceLogin({
5859
+ oidc: await deps2.oidcFor(issuer),
5860
+ store,
5861
+ now: deps2.now,
5862
+ signal,
5863
+ withLock: deps2.withLock,
5864
+ lockName: hash,
5865
+ onCode: async (s, settled) => {
5866
+ const url = s.verificationUriComplete ?? s.verificationUri;
5867
+ io2.err(`! First copy your one-time code: ${s.userCode}`);
5868
+ if (io2.interactive && cmd.browser) {
5869
+ try {
5870
+ await io2.waitForEnter(` Press Enter to open ${url} (or open it yourself)`, AbortSignal.any([signal, settled]));
5871
+ } catch (e) {
5872
+ if (settled.aborted && !signal.aborted) return;
5873
+ throw e;
5874
+ }
5875
+ if (settled.aborted) return;
5876
+ if (!await io2.openUrl(url)) io2.err(` Open ${url} in a browser`);
5877
+ } else {
5878
+ io2.err(` Open ${url} in a browser`);
5879
+ }
5880
+ }
5881
+ });
5882
+ await deps2.config.update((c) => {
5883
+ c.profiles[portal.apiOrigin] = {
5884
+ ...c.profiles[portal.apiOrigin],
5885
+ apiOrigin: portal.apiOrigin,
5886
+ issuer,
5887
+ sub: t.sub,
5888
+ email: t.email,
5889
+ meta: { fetchedAt: deps2.now().toISOString(), doc: meta }
5890
+ };
5891
+ if (g.portal || !c.defaultProfile) c.defaultProfile = portal.apiOrigin;
5892
+ });
5893
+ const { api } = await openSession(deps2, { ...portal, pinnedIssuer: issuer }, "login", requestLog(ctx));
5894
+ const me = await api.get("/api/cli/v1/me");
5895
+ const prev = cfg.profiles[portal.apiOrigin];
5896
+ let active = prev?.sub === t.sub && prev.activeTenant && me.tenants.some((x) => x.id === prev.activeTenant.id) ? prev.activeTenant : void 0;
5897
+ if (active) {
5898
+ } else if (me.tenants.length === 0) {
5899
+ io2.err(`! ${NO_TENANTS}`);
5900
+ } else if (me.tenants.length === 1) {
5901
+ active = { id: me.tenants[0].id, slug: me.tenants[0].slug };
5902
+ io2.err(`\u2713 Using tenant ${me.tenants[0].name}`);
5903
+ } else if (io2.interactive) {
5904
+ active = await io2.select("Select a tenant", me.tenants.map((x) => ({ name: `${x.name} ${x.role}`, value: { id: x.id, slug: x.slug } })));
5905
+ } else {
5906
+ io2.err(`! You belong to ${me.tenants.length} tenants; choose one with \`qore tenant use <slug>\``);
5907
+ }
5908
+ await deps2.config.update((c) => {
5909
+ c.profiles[portal.apiOrigin].activeTenant = active;
5910
+ });
5911
+ const label3 = portal.builtin ?? portal.apiOrigin;
5912
+ if (g.json) renderJson(io2, { portal: label3, email: me.email, activeTenant: active ?? null });
5913
+ else io2.out(`\u2713 Logged in as ${me.email} (${label3})`);
5914
+ }
4899
5915
  function registerLogin(program, ctx) {
4900
- program.command("login").description("Log in with your Qore account").option("--no-browser", "only print the URL").action(async (cmd) => {
4901
- const { deps: deps2, io: io2, signal } = ctx;
5916
+ program.command("login").description("Log in with your Qore account").option("--no-browser", "only print the URL").action(async (cmd) => loginFlow(ctx, cmd));
5917
+ }
5918
+
5919
+ // src/cli/commands/tenant.ts
5920
+ function registerTenant(program, ctx) {
5921
+ const tenant = program.command("tenant").description("Manage the active tenant");
5922
+ tenant.command("list").description("List tenants you have access to").action(async () => {
5923
+ const { deps: deps2, io: io2 } = ctx;
4902
5924
  const g = ctx.opts();
4903
5925
  const cfg = await deps2.config.read();
4904
5926
  const portal = resolvePortal(g.portal, cfg.defaultProfile);
4905
- const meta = await fetchMeta(deps2.fetch, portal.apiOrigin);
4906
- const known = cfg.profiles[portal.apiOrigin]?.issuer;
4907
- const pinned = { ...portal, pinnedIssuer: portal.pinnedIssuer ?? known ?? null };
4908
- if (!pinned.pinnedIssuer && portal.builtin === null) {
4909
- io2.err(`! Logging in to ${portal.apiOrigin}, identity provider ${meta.auth.issuer}`);
4910
- }
4911
- negotiate(meta, pinned, deps2.version);
4912
- const issuer = pinned.pinnedIssuer ?? meta.auth.issuer;
4913
- const hash = profileHash(portal.apiOrigin, issuer);
4914
- const store = new TokenStore(deps2.keychain, hash);
4915
- const t = await deviceLogin({
4916
- oidc: await deps2.oidcFor(issuer),
4917
- store,
4918
- now: deps2.now,
4919
- signal,
4920
- withLock: deps2.withLock,
4921
- lockName: hash,
4922
- onCode: async (s, settled) => {
4923
- const url = s.verificationUriComplete ?? s.verificationUri;
4924
- io2.err(`! First copy your one-time code: ${s.userCode}`);
4925
- if (io2.interactive && cmd.browser) {
4926
- try {
4927
- await io2.waitForEnter(` Press Enter to open ${url} (or open it yourself)`, AbortSignal.any([signal, settled]));
4928
- } catch (e) {
4929
- if (settled.aborted && !signal.aborted) return;
4930
- throw e;
4931
- }
4932
- if (settled.aborted) return;
4933
- if (!await io2.openUrl(url)) io2.err(` Open ${url} in a browser`);
4934
- } else {
4935
- io2.err(` Open ${url} in a browser`);
4936
- }
4937
- }
4938
- });
4939
- await deps2.config.update((c) => {
4940
- c.profiles[portal.apiOrigin] = {
4941
- ...c.profiles[portal.apiOrigin],
4942
- apiOrigin: portal.apiOrigin,
4943
- issuer,
4944
- sub: t.sub,
4945
- email: t.email,
4946
- meta: { fetchedAt: deps2.now().toISOString(), doc: meta }
4947
- };
4948
- if (g.portal || !c.defaultProfile) c.defaultProfile = portal.apiOrigin;
4949
- });
4950
- const { api } = await openSession(deps2, { ...portal, pinnedIssuer: issuer }, "login", requestLog(ctx));
5927
+ const { profile, api } = await openSession(deps2, portal, "tenant list", requestLog(ctx));
4951
5928
  const me = await api.get("/api/cli/v1/me");
4952
- const prev = cfg.profiles[portal.apiOrigin];
4953
- let active = prev?.sub === t.sub && prev.activeTenant && me.tenants.some((x) => x.id === prev.activeTenant.id) ? prev.activeTenant : void 0;
4954
- if (active) {
4955
- } else if (me.tenants.length === 0) {
4956
- io2.err("! Your account is not a developer in any organisation. Ask an admin to invite you as developer.");
4957
- } else if (me.tenants.length === 1) {
4958
- active = { id: me.tenants[0].id, slug: me.tenants[0].slug };
4959
- io2.err(`\u2713 Using tenant ${me.tenants[0].name}`);
4960
- } else if (io2.interactive) {
4961
- active = await io2.select("Select a tenant", me.tenants.map((x) => ({ name: `${x.name} ${x.role}`, value: { id: x.id, slug: x.slug } })));
4962
- } else {
4963
- io2.err(`! You belong to ${me.tenants.length} tenants; choose one with \`qore tenant use <slug>\``);
5929
+ const activeId = profile.activeTenant?.id;
5930
+ if (g.json) {
5931
+ renderJson(io2, { tenants: me.tenants.map((t) => ({ ...t, active: t.id === activeId })) });
5932
+ return;
5933
+ }
5934
+ for (const t of me.tenants) {
5935
+ const mark = t.id === activeId ? "*" : " ";
5936
+ io2.out(`${mark} ${t.name} ${t.role}`);
4964
5937
  }
4965
- await deps2.config.update((c) => {
4966
- c.profiles[portal.apiOrigin].activeTenant = active;
4967
- });
4968
- const label2 = portal.builtin ?? portal.apiOrigin;
4969
- if (g.json) renderJson(io2, { portal: label2, email: me.email, activeTenant: active ?? null });
4970
- else io2.out(`\u2713 Logged in as ${me.email} (${label2})`);
5938
+ });
5939
+ tenant.command("use").description("Set the active tenant").argument("[slug]", "tenant slug").action(async (slug) => {
5940
+ const chosen = await useTenant(ctx, slug);
5941
+ if (ctx.opts().json) renderJson(ctx.io, { activeTenant: chosen });
5942
+ else ctx.io.out(`\u2713 Using tenant ${chosen.slug}`);
5943
+ });
5944
+ }
5945
+ async function useTenant(ctx, slug, known) {
5946
+ const { deps: deps2, io: io2 } = ctx;
5947
+ const g = ctx.opts();
5948
+ const cfg = await deps2.config.read();
5949
+ const portal = resolvePortal(g.portal, cfg.defaultProfile);
5950
+ const me = known ?? await (await openSession(deps2, portal, "tenant use", requestLog(ctx))).api.get("/api/cli/v1/me");
5951
+ const candidates = me.tenants.map((t) => ({ kind: "tenant", id: t.id, slug: t.slug, name: t.name }));
5952
+ let chosen;
5953
+ if (slug) {
5954
+ const found = me.tenants.find((t) => t.slug === slug);
5955
+ if (!found) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `No tenant "${slug}" for this account`, { candidates });
5956
+ chosen = { id: found.id, slug: found.slug };
5957
+ } else if (io2.interactive) {
5958
+ chosen = await io2.select("Select a tenant", me.tenants.map((t) => ({ name: `${t.name} ${t.role}`, value: { id: t.id, slug: t.slug } })));
5959
+ } else {
5960
+ throw new QoreError("QORE_CONTEXT_REQUIRED", "Which tenant? Pass a slug or run interactively.", { candidates });
5961
+ }
5962
+ await deps2.config.update((c) => {
5963
+ c.profiles[portal.apiOrigin].activeTenant = chosen;
5964
+ });
5965
+ return chosen;
5966
+ }
5967
+
5968
+ // src/cli/commands/init.ts
5969
+ var step = (status2, detail2) => detail2 === void 0 ? { status: status2 } : { status: status2, detail: detail2 };
5970
+ async function rootStep(ctx, f) {
5971
+ const { deps: deps2, io: io2 } = ctx;
5972
+ const current = (await deps2.config.read()).projectsRoot;
5973
+ let input2 = f.root;
5974
+ if (input2 === void 0) {
5975
+ if (!io2.interactive) {
5976
+ if (current) return step("already", current);
5977
+ throw new QoreError("QORE_ROOT_NOT_SET", "No projects folder chosen", { hint: "Pass one: qore init --root <path>." });
5978
+ }
5979
+ input2 = await io2.input("Projects folder", { default: current ?? "~/qore" }) || current || "~/qore";
5980
+ }
5981
+ const wanted = await checkRoot(deps2, input2);
5982
+ const asked = await realpath11(wanted).catch(() => wanted);
5983
+ if (current && asked !== current && io2.interactive && !await io2.confirm(`Change the projects folder from ${current} to ${asked}? Nothing is moved.`, ctx.signal)) {
5984
+ return step("skipped", current);
5985
+ }
5986
+ const next = await createRoot(wanted);
5987
+ if (next === current) return step("already", current);
5988
+ await setRoot(deps2, next);
5989
+ return step("done", next);
5990
+ }
5991
+ async function activeTenant(ctx) {
5992
+ const cfg = await ctx.deps.config.read();
5993
+ return cfg.profiles[resolvePortal(ctx.opts().portal, cfg.defaultProfile).apiOrigin]?.activeTenant?.slug ?? null;
5994
+ }
5995
+ async function tenantStep(ctx) {
5996
+ const cfg = await ctx.deps.config.read();
5997
+ const portal = resolvePortal(ctx.opts().portal, cfg.defaultProfile);
5998
+ const { api } = await openSession(ctx.deps, portal, "init", requestLog(ctx));
5999
+ const me = await api.get("/api/cli/v1/me");
6000
+ if (me.tenants.length === 0) return step("skipped", NO_TENANTS);
6001
+ return step("done", (await useTenant(ctx, me.tenants.length === 1 ? me.tenants[0].slug : void 0, me)).slug);
6002
+ }
6003
+ function registerInit(program, ctx) {
6004
+ program.command("init").description("Set up qore: a projects folder, login, tenant and a first app").option("--root <path>", "the projects folder (default: ask, ~/qore)").option("--clone <app>", "clone this app into the projects folder").option("--tenant <slug>", "tenant the --clone app belongs to").action(async (f) => {
6005
+ const { io: io2 } = ctx;
6006
+ const json2 = Boolean(ctx.opts().json);
6007
+ const sub = json2 ? { ...ctx, io: { ...io2, out: io2.err }, opts: () => ({ ...ctx.opts(), json: false }) } : ctx;
6008
+ const steps = {
6009
+ root: await rootStep(ctx, f),
6010
+ login: step("skipped"),
6011
+ tenant: step("skipped"),
6012
+ clone: step("skipped")
6013
+ };
6014
+ const who = await loggedInAs(ctx);
6015
+ if (who) steps.login = step("already", who);
6016
+ else if (io2.interactive) {
6017
+ await loginFlow(sub, { browser: true });
6018
+ steps.login = step("done", await loggedInAs(ctx) ?? void 0);
6019
+ } else steps.login = step("skipped", "run qore login");
6020
+ const tenant = await activeTenant(ctx);
6021
+ if (tenant) steps.tenant = step(steps.login.status === "done" ? "done" : "already", tenant);
6022
+ else if (steps.login.status !== "skipped" && io2.interactive) steps.tenant = await tenantStep(sub);
6023
+ else steps.tenant = step("skipped", "run qore tenant use <slug>");
6024
+ if (f.clone) {
6025
+ steps.clone = step("done", (await cloneApp(sub, f.clone, void 0, { tenant: f.tenant, protocol: "https" })).root);
6026
+ } else if (io2.interactive && steps.login.status !== "skipped" && steps.tenant.status !== "skipped" && await io2.confirm("Clone an app now?", ctx.signal)) {
6027
+ steps.clone = step("done", (await cloneApp(sub, void 0, void 0, { tenant: f.tenant, protocol: "https" })).root);
6028
+ }
6029
+ const projectsRoot = (await ctx.deps.config.read()).projectsRoot ?? null;
6030
+ if (json2) {
6031
+ renderJson(io2, { projectsRoot, steps });
6032
+ return;
6033
+ }
6034
+ const mark = (s) => s.status === "skipped" ? "\u2013" : "\u2713";
6035
+ for (const [name, s] of Object.entries(steps)) io2.out(`${mark(s)} ${name.padEnd(6)} ${s.status}${s.detail ? ` ${s.detail}` : ""}`);
6036
+ if (projectsRoot) io2.out(`Next: qore dev <app> works from any directory.`);
6037
+ });
6038
+ }
6039
+
6040
+ // src/cli/commands/logs.ts
6041
+ var LEVELS = ["error", "warn", "info", "debug"];
6042
+ function registerLogs(program, ctx) {
6043
+ addObsFlags(program.command("logs").description("Logs of a deployed environment (requires --env)")).option("--level <level>", `exactly this severity: ${LEVELS.join("|")}`).option("--trace <id>", "only the logs of one trace").action(async (flags) => {
6044
+ const { io: io2 } = ctx;
6045
+ const g = ctx.opts();
6046
+ const limit = limitOf(flags.limit);
6047
+ const level = flags.level === void 0 ? void 0 : one("--level", flags.level, LEVELS);
6048
+ const oc2 = await obsContext(ctx, flags, "logs", { single: false });
6049
+ const q = { env: oc2.env, ...oc2.window, q: flags.query, limit, level, trace: flags.trace };
6050
+ const f = await fanOut(oc2, (t) => t.client.logs(q));
6051
+ const pages = f.results.map((r) => r.result);
6052
+ const items = pages.flatMap((p) => p.items).sort((a, b) => b.timestamp.localeCompare(a.timestamp));
6053
+ const warnings = [...pageWarnings(...pages), ...skipWarnings(f)];
6054
+ if (g.json) {
6055
+ renderJson(io2, { context: oc2.json, items, warnings, ...skipJson(f) });
6056
+ return;
6057
+ }
6058
+ io2.err(oc2.header);
6059
+ printWarnings(io2, warnings, false, f.skipped);
6060
+ if (items.length === 0) {
6061
+ io2.out(`No logs in ${oc2.env} (${windowText(oc2.window)}).`);
6062
+ return;
6063
+ }
6064
+ for (const l of fence(logLines(items, oc2.fanOut))) io2.out(l);
4971
6065
  });
4972
6066
  }
4973
6067
 
@@ -5003,26 +6097,6 @@ function registerLogout(program, ctx) {
5003
6097
  });
5004
6098
  }
5005
6099
 
5006
- // src/cli/format.ts
5007
- function ago(iso, now) {
5008
- if (!iso) return "\u2013";
5009
- const s = Math.max(0, Math.round((now.getTime() - new Date(iso).getTime()) / 1e3));
5010
- if (s < 60) return "just now";
5011
- if (s < 3600) return `${Math.floor(s / 60)}m ago`;
5012
- if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
5013
- return `${Math.floor(s / 86400)}d ago`;
5014
- }
5015
- var pct = (f) => String(Number((f * 100).toFixed(2)));
5016
- function uptime(h) {
5017
- if (!h) return "\u2013";
5018
- const kind = h.cause && h.kind !== "up" ? `${h.kind}: ${h.cause}` : h.kind;
5019
- return h.uptime24h === null ? kind : `${kind} (${pct(h.uptime24h)}% 24h)`;
5020
- }
5021
- function table(rows) {
5022
- const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length)));
5023
- return rows.map((r) => " " + r.map((c, i) => i === r.length - 1 ? c : c.padEnd(widths[i])).join(" "));
5024
- }
5025
-
5026
6100
  // src/cli/commands/status.ts
5027
6101
  var HEAD = ["ENV", "DEPLOY", "COMMIT", "DEPLOYED", "APP HEALTH"];
5028
6102
  var envRow = (e, now) => [e.name, e.paused ? "paused" : e.status, e.commit?.sha ?? "\u2013", ago(e.commit?.deployedAt ?? null, now), uptime(e.health)];
@@ -5035,6 +6109,7 @@ async function appStatus(ctx, ws, appFlag) {
5035
6109
  await ensureCapability(deps2, portal.apiOrigin, "app.modules");
5036
6110
  await ensureCapability(deps2, portal.apiOrigin, "module.status");
5037
6111
  const am = await workspaceAppModules(session, portal, ws);
6112
+ await rememberWorkspace(ctx, ws, am.app);
5038
6113
  const { tenant, ...app } = am.app;
5039
6114
  if (appFlag && appFlag !== app.id && appFlag !== app.slug) {
5040
6115
  throw new QoreError("QORE_RESOURCE_NOT_FOUND", `This workspace is for ${app.slug}, not "${appFlag}"`, {
@@ -5062,19 +6137,21 @@ async function appStatus(ctx, ws, appFlag) {
5062
6137
  for (const line2 of table(rows)) io2.out(line2);
5063
6138
  }
5064
6139
  function registerStatus(program, ctx) {
5065
- program.command("status").description("Deployment and health per environment for this module (or, in a workspace root, the app)").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").action(async (flags) => {
5066
- const { deps: deps2, io: io2 } = ctx;
5067
- const g = ctx.opts();
6140
+ program.command("status").description("Deployment and health per environment for this module (or, in a workspace root, the app)").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "the app or module, from anywhere (see qore init)").action(async (flags) => {
6141
+ const c = await projectCtx(ctx, { ref: flags.project, module: flags.module, app: flags.app, needModule: false, command: "status" });
6142
+ const { deps: deps2, io: io2 } = c;
6143
+ const g = c.opts();
5068
6144
  if (!flags.module) {
5069
6145
  const ws = await workspaceRoot(deps2);
5070
6146
  if (ws) {
5071
- await appStatus(ctx, ws, flags.app);
6147
+ await appStatus(c, ws, flags.app);
5072
6148
  return;
5073
6149
  }
5074
6150
  }
5075
- const mc = await moduleContextFor(ctx, flags, "status");
6151
+ const mc = await moduleContextFor(c, flags, "status");
5076
6152
  await ensureCapability(deps2, mc.portal.apiOrigin, "module.status");
5077
- const app = await appFor(ctx, mc, flags.app);
6153
+ const app = await appFor(c, mc, flags.app);
6154
+ await rememberContext(c, mc, app);
5078
6155
  if (!g.json) io2.err(contextLine(mc, app));
5079
6156
  const st = await mc.session.api.get(
5080
6157
  `/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/status?app_id=${encodeURIComponent(app.id)}`
@@ -5094,7 +6171,7 @@ function registerStatus(program, ctx) {
5094
6171
  }
5095
6172
 
5096
6173
  // src/cli/commands/sync.ts
5097
- import { join as join21, relative as relative8 } from "path";
6174
+ import { join as join24, relative as relative9 } from "path";
5098
6175
  import { Option as Option2 } from "commander";
5099
6176
  function asFailure2(slug, e) {
5100
6177
  if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
@@ -5126,7 +6203,7 @@ function registerSync(program, ctx) {
5126
6203
  continue;
5127
6204
  }
5128
6205
  try {
5129
- dirs[m.slug] = await deps2.git.inspect(join21(ws.root, m.slug));
6206
+ dirs[m.slug] = await deps2.git.inspect(join24(ws.root, m.slug));
5130
6207
  modulesForPlan.push(m);
5131
6208
  } catch (e) {
5132
6209
  earlyFailures.push(asFailure2(m.slug, e));
@@ -5138,7 +6215,7 @@ function registerSync(program, ctx) {
5138
6215
  } });
5139
6216
  await syncPresent(deps2, applied, actions, Boolean(flags.pull), { app, protocol: flags.protocol });
5140
6217
  for (const r of applied) {
5141
- if (r.status === "cloned" && r.dir) r.dir = relative8(deps2.cwd, r.dir) || ".";
6218
+ if (r.status === "cloned" && r.dir) r.dir = relative9(deps2.cwd, r.dir) || ".";
5142
6219
  }
5143
6220
  const removedResults = removed.map((slug) => ({ slug, status: "removed", reason: "not a module of this app \u2014 left in place" }));
5144
6221
  const results = [...earlyFailures, ...applied, ...removedResults];
@@ -5147,49 +6224,106 @@ function registerSync(program, ctx) {
5147
6224
  });
5148
6225
  }
5149
6226
 
5150
- // src/cli/commands/tenant.ts
5151
- function registerTenant(program, ctx) {
5152
- const tenant = program.command("tenant").description("Manage the active tenant");
5153
- tenant.command("list").description("List tenants you have access to").action(async () => {
5154
- const { deps: deps2, io: io2 } = ctx;
6227
+ // src/cli/commands/traces.ts
6228
+ var WITHS2 = ["logs", "errors"];
6229
+ async function spansOf(c, q, sort, warnings) {
6230
+ try {
6231
+ return await c.spans({ ...q, sort });
6232
+ } catch (e) {
6233
+ if (sort === "-duration" && e instanceof QoreError && e.serverCode === "observability.sort_unsupported") {
6234
+ warnings.push("sort_unavailable");
6235
+ return c.spans(q);
6236
+ }
6237
+ throw e;
6238
+ }
6239
+ }
6240
+ function registerTraces(program, ctx) {
6241
+ addObsFlags(program.command("traces [id]").description("Slow or recent requests, or one trace as a span tree (requires --env)")).option("--slower-than <duration>", "only requests slower than this, slowest first (e.g. 500ms, 1s)").option("--with <list>", `with a trace id: also show ${WITHS2.join(",")}`).action(async (id, flags) => {
6242
+ const { io: io2, deps: deps2 } = ctx;
5155
6243
  const g = ctx.opts();
5156
- const cfg = await deps2.config.read();
5157
- const portal = resolvePortal(g.portal, cfg.defaultProfile);
5158
- const { profile, api } = await openSession(deps2, portal, "tenant list", requestLog(ctx));
5159
- const me = await api.get("/api/cli/v1/me");
5160
- const activeId = profile.activeTenant?.id;
6244
+ const withs = withList(flags.with, WITHS2);
6245
+ const slower = flags.slowerThan === void 0 ? void 0 : parseDuration(flags.slowerThan);
6246
+ const limit = limitOf(flags.limit);
6247
+ const oc2 = await obsContext(ctx, flags, "traces", { single: id !== void 0 });
6248
+ const q = { env: oc2.env, ...oc2.window, limit };
6249
+ const scope2 = { env: oc2.env, ...oc2.window };
6250
+ const printLogs = (items2) => {
6251
+ io2.out("");
6252
+ io2.out(`Logs (${items2.length})`);
6253
+ for (const l of fence(logLines(items2, oc2.fanOut))) io2.out(l);
6254
+ };
6255
+ const printErrors = (items2) => {
6256
+ io2.out("");
6257
+ io2.out(`Errors (${items2.length})`);
6258
+ for (const l of fence(issuesTable(items2, deps2.now(), oc2.fanOut))) io2.out(l);
6259
+ };
6260
+ const printTree = (trace2) => {
6261
+ for (const l of fence(spanTree(trace2))) io2.out(l);
6262
+ };
6263
+ const more = async (c2, traceId, w) => {
6264
+ const f2 = { ...scope2, trace: traceId };
6265
+ if (w === "logs") {
6266
+ const p = await c2.logs({ ...f2, limit: 100 });
6267
+ printWarnings(io2, pageWarnings(p), false);
6268
+ printLogs(p.items);
6269
+ } else {
6270
+ const p = await c2.issues({ ...f2, status: "all" });
6271
+ printWarnings(io2, pageWarnings(p), false);
6272
+ printErrors(p.items);
6273
+ }
6274
+ };
6275
+ if (id) {
6276
+ const c2 = oc2.targets[0].client;
6277
+ const b = await traceDetail(c2, id, q, withs);
6278
+ if (g.json) {
6279
+ renderJson(io2, { context: oc2.json, ...b });
6280
+ return;
6281
+ }
6282
+ io2.err(oc2.header);
6283
+ printWarnings(io2, b.warnings, false);
6284
+ printTree(b.trace);
6285
+ if (b.logs) printLogs(b.logs);
6286
+ if (b.errors) printErrors(b.errors);
6287
+ return;
6288
+ }
6289
+ const warnings = [];
6290
+ const listQ = { ...q, q: joinQuery(ROOT_SPAN_FILTER, slower && `span.duration:>${slower}`, flags.query) };
6291
+ const sort = slower ? "-duration" : "-timestamp";
6292
+ const f = await fanOut(oc2, (t) => spansOf(t.client, listQ, sort, warnings));
6293
+ const pages = f.results.map((r) => r.result);
6294
+ warnings.push(...pageWarnings(...pages), ...skipWarnings(f));
6295
+ const items = pages.flatMap((p) => p.items).sort((a, b) => slower ? b.durationMs - a.durationMs : b.timestamp.localeCompare(a.timestamp));
6296
+ const uniq = [...new Set(warnings)];
5161
6297
  if (g.json) {
5162
- renderJson(io2, { tenants: me.tenants.map((t) => ({ ...t, active: t.id === activeId })) });
6298
+ renderJson(io2, { context: oc2.json, items, warnings: uniq, ...skipJson(f) });
5163
6299
  return;
5164
6300
  }
5165
- for (const t of me.tenants) {
5166
- const mark = t.id === activeId ? "*" : " ";
5167
- io2.out(`${mark} ${t.name} ${t.role}`);
6301
+ io2.err(oc2.header);
6302
+ printWarnings(io2, uniq, false, f.skipped);
6303
+ if (items.length === 0) {
6304
+ io2.out(`No ${slower ? `requests slower than ${slower}` : "traces"} in ${oc2.env} (${windowText(oc2.window)}).`);
6305
+ return;
5168
6306
  }
5169
- });
5170
- tenant.command("use").description("Set the active tenant").argument("[slug]", "tenant slug").action(async (slug) => {
5171
- const { deps: deps2, io: io2 } = ctx;
5172
- const g = ctx.opts();
5173
- const cfg = await deps2.config.read();
5174
- const portal = resolvePortal(g.portal, cfg.defaultProfile);
5175
- const { api } = await openSession(deps2, portal, "tenant use", requestLog(ctx));
5176
- const me = await api.get("/api/cli/v1/me");
5177
- const candidates = me.tenants.map((t) => ({ kind: "tenant", id: t.id, slug: t.slug, name: t.name }));
5178
- let chosen;
5179
- if (slug) {
5180
- const found = me.tenants.find((t) => t.slug === slug);
5181
- if (!found) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `No tenant "${slug}" for this account`, { candidates });
5182
- chosen = { id: found.id, slug: found.slug };
5183
- } else if (io2.interactive) {
5184
- chosen = await io2.select("Select a tenant", me.tenants.map((t) => ({ name: `${t.name} ${t.role}`, value: { id: t.id, slug: t.slug } })));
5185
- } else {
5186
- throw new QoreError("QORE_CONTEXT_REQUIRED", "Which tenant? Pass a slug or run interactively.", { candidates });
6307
+ for (const l of fence(spansTable(items, deps2.now(), oc2.fanOut))) io2.out(l);
6308
+ if (!io2.interactive) return;
6309
+ const picked = await io2.select(
6310
+ "Which trace to open?",
6311
+ [...items.map((s) => ({ name: `${oneLine(s.traceId).slice(0, 8)} ${oneLine(s.transaction ?? s.op)}`, value: s })), { name: "(none)", value: null }]
6312
+ );
6313
+ if (!picked) return;
6314
+ const c = oc2.targets.find((t) => t.module.id === picked.module.id)?.client;
6315
+ if (!c) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `module ${picked.module.slug} is not among the queried modules`);
6316
+ const { trace } = await traceDetail(c, picked.traceId, q, /* @__PURE__ */ new Set());
6317
+ printTree(trace);
6318
+ for (; ; ) {
6319
+ const next = await io2.select("Next?", [
6320
+ { name: "Logs of this trace", value: "logs" },
6321
+ { name: "Errors in this trace", value: "errors" },
6322
+ { name: "Done", value: "done" }
6323
+ ]);
6324
+ if (next === "done") return;
6325
+ await more(c, picked.traceId, next);
5187
6326
  }
5188
- await deps2.config.update((c) => {
5189
- c.profiles[portal.apiOrigin].activeTenant = chosen;
5190
- });
5191
- if (g.json) renderJson(io2, { activeTenant: chosen });
5192
- else io2.out(`\u2713 Using tenant ${chosen.slug}`);
5193
6327
  });
5194
6328
  }
5195
6329
 
@@ -5212,11 +6346,11 @@ function registerVersion(program, ctx) {
5212
6346
  return;
5213
6347
  }
5214
6348
  io2.out(`qore ${deps2.version}`);
5215
- const label2 = portal.builtin ?? portal.apiOrigin;
6349
+ const label3 = portal.builtin ?? portal.apiOrigin;
5216
6350
  if (portalInfo) {
5217
- io2.out(`portal ${label2}: api ${portalInfo.apiVersions.join(",")}, min ${portalInfo.minCliVersion}, latest ${portalInfo.latestCliVersion}`);
6351
+ io2.out(`portal ${label3}: api ${portalInfo.apiVersions.join(",")}, min ${portalInfo.minCliVersion}, latest ${portalInfo.latestCliVersion}`);
5218
6352
  } else {
5219
- io2.out(`portal ${label2}: unreachable`);
6353
+ io2.out(`portal ${label3}: unreachable`);
5220
6354
  }
5221
6355
  });
5222
6356
  }
@@ -5230,18 +6364,18 @@ function registerWhoami(program, ctx) {
5230
6364
  const portal = resolvePortal(g.portal, cfg.defaultProfile);
5231
6365
  const { profile, api } = await openSession(deps2, portal, "whoami", requestLog(ctx));
5232
6366
  const me = await api.get("/api/cli/v1/me");
5233
- const activeTenant = profile.activeTenant;
5234
- if (activeTenant && !me.tenants.some((t) => t.id === activeTenant.id)) {
5235
- io2.err(`! You no longer have developer access to ${activeTenant.slug}`);
6367
+ const activeTenant2 = profile.activeTenant;
6368
+ if (activeTenant2 && !me.tenants.some((t) => t.id === activeTenant2.id)) {
6369
+ io2.err(`! You no longer have developer access to ${activeTenant2.slug}`);
5236
6370
  }
5237
- const label2 = portal.builtin ?? portal.apiOrigin;
6371
+ const label3 = portal.builtin ?? portal.apiOrigin;
5238
6372
  if (g.json) {
5239
- renderJson(io2, { portal: label2, activeTenant: activeTenant ?? null, me });
6373
+ renderJson(io2, { portal: label3, activeTenant: activeTenant2 ?? null, me });
5240
6374
  return;
5241
6375
  }
5242
- io2.out(`${me.email} (${label2})`);
6376
+ io2.out(`${me.email} (${label3})`);
5243
6377
  for (const t of me.tenants) {
5244
- const mark = activeTenant?.id === t.id ? "*" : " ";
6378
+ const mark = activeTenant2?.id === t.id ? "*" : " ";
5245
6379
  io2.out(`${mark} ${t.name} ${t.role}`);
5246
6380
  for (const a of t.apps) {
5247
6381
  io2.out(` ${a.slug} ${a.access}`);
@@ -5255,11 +6389,15 @@ function registerWhoami(program, ctx) {
5255
6389
 
5256
6390
  // src/cli/commands/index.ts
5257
6391
  var COMMANDS = [
6392
+ registerInit,
5258
6393
  registerLogin,
5259
6394
  registerLogout,
5260
6395
  registerWhoami,
5261
6396
  registerTenant,
5262
6397
  registerStatus,
6398
+ registerLogs,
6399
+ registerTraces,
6400
+ registerErrors,
5263
6401
  registerEnv,
5264
6402
  registerRun,
5265
6403
  registerDev,
@@ -5294,7 +6432,7 @@ async function showNoticeOnce(deps2, io2, o) {
5294
6432
  // src/cli/telemetry/index.ts
5295
6433
  var CRASH_FLUSH_MS = 2e3;
5296
6434
  var CLEAN_FLUSH_MS = 200;
5297
- var loadSentry = async () => (await import("./sentry-B3MDCAOI.js")).createSdk;
6435
+ var loadSentry = async () => (await import("./sentry-GMM4X3FT.js")).createSdk;
5298
6436
  function createTelemetry(deps2, o) {
5299
6437
  const load = o.load ?? loadSentry;
5300
6438
  const flushMs = o.flushMs ?? { crash: CRASH_FLUSH_MS, clean: CLEAN_FLUSH_MS };
@@ -5319,13 +6457,13 @@ function createTelemetry(deps2, o) {
5319
6457
  },
5320
6458
  async end(signal) {
5321
6459
  if (!sdk) return { drained: true };
5322
- const ms = crashed ? flushMs.crash : flushMs.clean;
6460
+ const ms2 = crashed ? flushMs.crash : flushMs.clean;
5323
6461
  const work = sdk.then(async (s) => {
5324
6462
  if (!s) return true;
5325
6463
  if (crashed) s.crash(crashed.e);
5326
- return s.end(ms);
6464
+ return s.end(ms2);
5327
6465
  }).catch(() => false);
5328
- return { drained: await withDeadline(work, ms, signal) };
6466
+ return { drained: await withDeadline(work, ms2, signal) };
5329
6467
  }
5330
6468
  };
5331
6469
  }
@@ -5346,16 +6484,16 @@ async function start(deps2, argv2, command, load, onScrub) {
5346
6484
  sdk.start();
5347
6485
  return sdk;
5348
6486
  }
5349
- function withDeadline(work, ms, signal) {
6487
+ function withDeadline(work, ms2, signal) {
5350
6488
  if (signal?.aborted) return Promise.resolve(false);
5351
- return new Promise((resolve5) => {
6489
+ return new Promise((resolve6) => {
5352
6490
  const done = (v) => {
5353
6491
  clearTimeout(timer);
5354
6492
  signal?.removeEventListener("abort", onAbort);
5355
- resolve5(v);
6493
+ resolve6(v);
5356
6494
  };
5357
6495
  const onAbort = () => done(false);
5358
- const timer = setTimeout(() => done(false), ms);
6496
+ const timer = setTimeout(() => done(false), ms2);
5359
6497
  signal?.addEventListener("abort", onAbort, { once: true });
5360
6498
  work.then(done, () => done(false));
5361
6499
  });
@@ -5404,7 +6542,7 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
5404
6542
 
5405
6543
  // src/core/config.ts
5406
6544
  import { readFile as readFile6 } from "fs/promises";
5407
- import { join as join22 } from "path";
6545
+ import { join as join25 } from "path";
5408
6546
  var empty = () => ({ schemaVersion: 1, profiles: {} });
5409
6547
  var ConfigStore = class {
5410
6548
  constructor(dir2) {
@@ -5412,7 +6550,7 @@ var ConfigStore = class {
5412
6550
  }
5413
6551
  dir;
5414
6552
  get file() {
5415
- return join22(this.dir, "config.json");
6553
+ return join25(this.dir, "config.json");
5416
6554
  }
5417
6555
  async read() {
5418
6556
  let raw;
@@ -5443,9 +6581,9 @@ var ConfigStore = class {
5443
6581
 
5444
6582
  // src/platform/git.ts
5445
6583
  import { execFile, execFileSync } from "child_process";
5446
- import { mkdir as mkdir3, readdir as readdir2, stat as stat3 } from "fs/promises";
6584
+ import { mkdir as mkdir4, readdir as readdir4, stat as stat6 } from "fs/promises";
5447
6585
  import { homedir } from "os";
5448
- import { dirname as dirname8, join as join23 } from "path";
6586
+ import { dirname as dirname9, join as join26 } from "path";
5449
6587
  function parseRemotes(stdout) {
5450
6588
  const seen = /* @__PURE__ */ new Set();
5451
6589
  const out = [];
@@ -5512,7 +6650,7 @@ function gitExecOptions(repoDir) {
5512
6650
  return { cwd: homedir(), env, windowsHide: true, maxBuffer: 8 * 1024 * 1024 };
5513
6651
  }
5514
6652
  function runGit(args, repoDir) {
5515
- return new Promise((resolve5, reject) => {
6653
+ return new Promise((resolve6, reject) => {
5516
6654
  execFile("git", args, gitExecOptions(repoDir), (err, stdout, stderr) => {
5517
6655
  if (err && err.code === "ENOENT") {
5518
6656
  reject(new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
@@ -5521,7 +6659,7 @@ function runGit(args, repoDir) {
5521
6659
  return;
5522
6660
  }
5523
6661
  const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
5524
- resolve5({ code, stdout: String(stdout), stderr: String(stderr), err: err ?? void 0 });
6662
+ resolve6({ code, stdout: String(stdout), stderr: String(stderr), err: err ?? void 0 });
5525
6663
  });
5526
6664
  });
5527
6665
  }
@@ -5539,7 +6677,7 @@ async function remotes(repoRoot) {
5539
6677
  return parseRemotes(r.stdout);
5540
6678
  }
5541
6679
  async function clone(url, dir2) {
5542
- await mkdir3(dirname8(dir2), { recursive: true });
6680
+ await mkdir4(dirname9(dir2), { recursive: true });
5543
6681
  const r = await runGit(["clone", "--", url, dir2]);
5544
6682
  return toResult(r, "git clone");
5545
6683
  }
@@ -5559,7 +6697,7 @@ async function status(repoRoot) {
5559
6697
  }
5560
6698
  async function statOrThrow(path) {
5561
6699
  try {
5562
- return await stat3(path);
6700
+ return await stat6(path);
5563
6701
  } catch (err) {
5564
6702
  if (err.code === "ENOENT") return void 0;
5565
6703
  throw new QoreError("QORE_INTERNAL", `could not stat ${path}`, { cause: err });
@@ -5571,12 +6709,12 @@ async function inspect(dir2) {
5571
6709
  if (!st.isDirectory()) return { exists: true, empty: false, isRepo: false, remotes: [] };
5572
6710
  let entries;
5573
6711
  try {
5574
- entries = await readdir2(dir2);
6712
+ entries = await readdir4(dir2);
5575
6713
  } catch (err) {
5576
6714
  throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
5577
6715
  }
5578
6716
  const empty2 = entries.length === 0;
5579
- const isRepo = await statOrThrow(join23(dir2, ".git")) !== void 0;
6717
+ const isRepo = await statOrThrow(join26(dir2, ".git")) !== void 0;
5580
6718
  const dirRemotes = isRepo ? await remotes(dir2) : [];
5581
6719
  return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
5582
6720
  }
@@ -5637,15 +6775,15 @@ var OsKeychain = class {
5637
6775
  };
5638
6776
 
5639
6777
  // src/platform/lock.ts
5640
- import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
5641
- import { join as join24 } from "path";
6778
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
6779
+ import { join as join27 } from "path";
5642
6780
  import lockfile from "proper-lockfile";
5643
6781
  function fileLock(dir2) {
5644
6782
  return async (name, fn) => {
5645
- const locks = join24(dir2, "locks");
5646
- await mkdir4(locks, { recursive: true, mode: 448 });
5647
- const target = join24(locks, `${name}.lock`);
5648
- await writeFile4(target, "", { flag: "a" });
6783
+ const locks = join27(dir2, "locks");
6784
+ await mkdir5(locks, { recursive: true, mode: 448 });
6785
+ const target = join27(locks, `${name}.lock`);
6786
+ await writeFile5(target, "", { flag: "a" });
5649
6787
  let release;
5650
6788
  let compromised;
5651
6789
  try {
@@ -5688,7 +6826,7 @@ var STDERR_DRAIN_MS = 500;
5688
6826
  function makeSpawner(spawnFn, proc) {
5689
6827
  return {
5690
6828
  run(cmd, args, env, o) {
5691
- return new Promise((resolve5, reject) => {
6829
+ return new Promise((resolve6, reject) => {
5692
6830
  let child;
5693
6831
  try {
5694
6832
  child = spawnFn(cmd, args, {
@@ -5722,7 +6860,7 @@ function makeSpawner(spawnFn, proc) {
5722
6860
  cleanup();
5723
6861
  const stderr = child.stderr;
5724
6862
  if (!o?.onStderr || !stderr || stderr.readableEnded) {
5725
- resolve5({ code, signal });
6863
+ resolve6({ code, signal });
5726
6864
  return;
5727
6865
  }
5728
6866
  let settled = false;
@@ -5730,7 +6868,7 @@ function makeSpawner(spawnFn, proc) {
5730
6868
  if (settled) return;
5731
6869
  settled = true;
5732
6870
  clearTimeout(timer);
5733
- resolve5({ code, signal });
6871
+ resolve6({ code, signal });
5734
6872
  };
5735
6873
  const timer = setTimeout(finish, STDERR_DRAIN_MS);
5736
6874
  timer.unref?.();
@@ -5785,13 +6923,13 @@ var systemLauncher = {
5785
6923
  child.stdout?.on("data", (c) => out.push(c));
5786
6924
  child.stderr?.on("data", (c) => err.push(c));
5787
6925
  let closed = false;
5788
- const exited = new Promise((resolve5) => {
5789
- child.once("error", (e) => resolve5({ code: null, signal: null, spawnError: e.code ?? "error" }));
6926
+ const exited = new Promise((resolve6) => {
6927
+ child.once("error", (e) => resolve6({ code: null, signal: null, spawnError: e.code ?? "error" }));
5790
6928
  child.once("close", (code, signal) => {
5791
6929
  closed = true;
5792
6930
  out.flush();
5793
6931
  err.flush();
5794
- resolve5({ code, signal });
6932
+ resolve6({ code, signal });
5795
6933
  });
5796
6934
  });
5797
6935
  return {
@@ -5837,11 +6975,11 @@ var systemLauncher = {
5837
6975
  import { execFile as execFile2 } from "child_process";
5838
6976
  import { connect } from "net";
5839
6977
  function accepts(host, port) {
5840
- return new Promise((resolve5) => {
6978
+ return new Promise((resolve6) => {
5841
6979
  const s = connect({ host, port });
5842
6980
  const done = (v) => {
5843
6981
  s.destroy();
5844
- resolve5(v);
6982
+ resolve6(v);
5845
6983
  };
5846
6984
  s.setTimeout(500, () => done(false));
5847
6985
  s.once("connect", () => done(true));
@@ -5849,12 +6987,12 @@ function accepts(host, port) {
5849
6987
  });
5850
6988
  }
5851
6989
  function run2(cmd, args) {
5852
- return new Promise((resolve5) => {
6990
+ return new Promise((resolve6) => {
5853
6991
  execFile2(
5854
6992
  cmd,
5855
6993
  args,
5856
6994
  { timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
5857
- (e, stdout) => resolve5(e && !stdout ? null : String(stdout))
6995
+ (e, stdout) => resolve6(e && !stdout ? null : String(stdout))
5858
6996
  );
5859
6997
  });
5860
6998
  }
@@ -5947,12 +7085,12 @@ function parseRegProxy(stdout) {
5947
7085
 
5948
7086
  // src/platform/proxy.ts
5949
7087
  function run3(cmd, args) {
5950
- return new Promise((resolve5) => {
7088
+ return new Promise((resolve6) => {
5951
7089
  execFile3(
5952
7090
  cmd,
5953
7091
  args,
5954
7092
  { timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
5955
- (e, stdout) => resolve5(e && !stdout ? null : String(stdout))
7093
+ (e, stdout) => resolve6(e && !stdout ? null : String(stdout))
5956
7094
  );
5957
7095
  });
5958
7096
  }
@@ -5983,7 +7121,7 @@ function systemProxy(platform = process.platform) {
5983
7121
  }
5984
7122
 
5985
7123
  // src/version.ts
5986
- var VERSION = true ? "0.1.5" : "0.0.0-dev";
7124
+ var VERSION = true ? "0.1.7" : "0.0.0-dev";
5987
7125
 
5988
7126
  // src/cli.ts
5989
7127
  if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
@@ -6061,4 +7199,4 @@ if (!drained) {
6061
7199
  process.exit();
6062
7200
  }
6063
7201
  //# sourceMappingURL=cli.js.map
6064
- //# debugId=a70250c5-daf4-5ff3-b490-5556ac377549
7202
+ //# debugId=bf813c0e-4054-532d-9e74-62fbe8673876