@quatronic/qore 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,11 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="28c91dc8-dbb9-5c0a-91e3-f368931a4ed3")}catch(e){}}();
4
+ import { createRequire as __qoreCreateRequire } from "node:module"; const require = __qoreCreateRequire(import.meta.url);
5
+ import {
6
+ addSecretValues,
7
+ buildScrubContext
8
+ } from "./chunk-OAU6HJHZ.js";
9
+
3
10
  // src/cli.ts
4
11
  import { homedir as homedir2 } from "os";
5
12
  import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
6
13
 
7
14
  // src/cli/io.ts
8
- import { confirm, search, select } from "@inquirer/prompts";
15
+ import { confirm, input, search, select } from "@inquirer/prompts";
9
16
  import { createInterface } from "readline";
10
17
 
11
18
  // src/platform/browser.ts
@@ -44,6 +51,10 @@ var EXIT = {
44
51
  QORE_REPO_ACCESS_PENDING: 5,
45
52
  QORE_GIT_AUTH: 5,
46
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,
47
58
  QORE_AUTH_UNAVAILABLE: 6,
48
59
  QORE_AUTH_BUSY: 6,
49
60
  QORE_API_UNREACHABLE: 6,
@@ -145,13 +156,21 @@ function processIo(interactive, stderrTTY = Boolean(process.stderr.isTTY)) {
145
156
  throw e;
146
157
  }
147
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
+ },
148
167
  waitForEnter: createWaitForEnter(process.stdin, process.stderr),
149
168
  openUrl: openBrowser
150
169
  };
151
170
  }
152
- function createWaitForEnter(input, output) {
153
- return (message, signal) => new Promise((resolve5, reject) => {
154
- 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 });
155
174
  const onAbort = () => {
156
175
  rl.close();
157
176
  reject(new CancelledError());
@@ -159,7 +178,7 @@ function createWaitForEnter(input, output) {
159
178
  const done = () => {
160
179
  signal.removeEventListener("abort", onAbort);
161
180
  rl.close();
162
- resolve5();
181
+ resolve6();
163
182
  };
164
183
  signal.addEventListener("abort", onAbort, { once: true });
165
184
  rl.on("SIGINT", () => {
@@ -176,7 +195,7 @@ import { Command, CommanderError } from "commander";
176
195
 
177
196
  // src/core/api/http.ts
178
197
  function abortableFetch(fetch3, signal) {
179
- 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 });
180
199
  }
181
200
  var TLS = /* @__PURE__ */ new Set([
182
201
  "SELF_SIGNED_CERT_IN_CHAIN",
@@ -320,7 +339,7 @@ function renderDoctorJson(io2, report) {
320
339
  return true;
321
340
  }
322
341
  var SYMBOL = { ok: "\u2713", warn: "!", fail: "\u2717" };
323
- var TOOL_IDS = ["qore", "node", "git", "github-ssh", "docker"];
342
+ var TOOL_IDS = ["qore", "node", "git", "github-ssh", "docker", "telemetry"];
324
343
  var ROW_WIDTH = 80;
325
344
  var COL_B = 36;
326
345
  function rawLine(r) {
@@ -435,7 +454,16 @@ function buildProgram(deps2, io2, signal, register, setExitCode) {
435
454
  for (const r of register) r(program, ctx);
436
455
  return program;
437
456
  }
438
- async function run(argv2, deps2, io2, signal, register) {
457
+ function commandPath(action) {
458
+ const names = [];
459
+ for (let c = action; c?.parent; c = c.parent) names.unshift(c.name());
460
+ return names.join(" ");
461
+ }
462
+ function isCrash(e) {
463
+ if (e instanceof QoreError || e instanceof CancelledError || e instanceof CommanderError) return false;
464
+ return !(e instanceof Error && (e.name === "CancelledError" || e.name === "QoreError"));
465
+ }
466
+ async function run(argv2, deps2, io2, signal, register, hooks = {}) {
439
467
  const json2 = jsonFromArgv(argv2);
440
468
  const parseIo = json2 ? { ...io2, err: () => {
441
469
  } } : io2;
@@ -443,8 +471,12 @@ async function run(argv2, deps2, io2, signal, register) {
443
471
  const program = buildProgram(deps2, parseIo, signal, register, (code) => {
444
472
  state.exitCode = code;
445
473
  });
446
- program.hook("preAction", (_program, action) => {
474
+ program.hook("preAction", async (_program, action) => {
447
475
  state.command = action.name();
476
+ try {
477
+ await hooks.onCommand?.(commandPath(action));
478
+ } catch {
479
+ }
448
480
  });
449
481
  try {
450
482
  await program.parseAsync(argv2, { from: "user" });
@@ -456,6 +488,12 @@ async function run(argv2, deps2, io2, signal, register) {
456
488
  if (json2) io2.out(JSON.stringify({ ok: false, error: { code: "QORE_CONTEXT_INVALID", message: e.message } }));
457
489
  return 2;
458
490
  }
491
+ if (isCrash(e)) {
492
+ try {
493
+ hooks.onCrash?.(e);
494
+ } catch {
495
+ }
496
+ }
459
497
  const opts = program.opts();
460
498
  renderError(io2, e, Boolean(opts.json), pc2.isColorSupported && opts.color !== false);
461
499
  if (!opts.json && needsDoctor(e) && state.command !== "doctor") io2.err("Run qore doctor for a full check of your machine.");
@@ -464,7 +502,7 @@ async function run(argv2, deps2, io2, signal, register) {
464
502
  }
465
503
 
466
504
  // src/cli/commands/clone.ts
467
- 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";
468
506
  import { Option } from "commander";
469
507
 
470
508
  // src/core/api/meta.ts
@@ -918,8 +956,8 @@ var ResolutionCache = class {
918
956
  const f = await this.load();
919
957
  const hits = [];
920
958
  for (const [k, e] of Object.entries(f.entries)) {
921
- const sep2 = k.lastIndexOf("|");
922
- if (keys.includes(k.slice(sep2 + 1)) && this.fresh(e)) hits.push({ origin: k.slice(0, sep2), at: new Date(e.resolvedAt).getTime() });
959
+ const sep3 = k.lastIndexOf("|");
960
+ if (keys.includes(k.slice(sep3 + 1)) && this.fresh(e)) hits.push({ origin: k.slice(0, sep3), at: new Date(e.resolvedAt).getTime() });
923
961
  }
924
962
  hits.sort((a, b) => b.at - a.at);
925
963
  return [...new Set(hits.map((h) => h.origin))];
@@ -1225,6 +1263,13 @@ async function syncPresent(deps2, results, actions, pull, ctx) {
1225
1263
  }
1226
1264
  }
1227
1265
 
1266
+ // src/core/projects/registry.ts
1267
+ import { realpath as realpath4 } from "fs/promises";
1268
+
1269
+ // src/core/projects/discover.ts
1270
+ import { readdir, realpath as realpath2 } from "fs/promises";
1271
+ import { join as join6 } from "path";
1272
+
1228
1273
  // src/core/workspace/file.ts
1229
1274
  import { randomBytes as randomBytes2 } from "crypto";
1230
1275
  import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm, writeFile as writeFile2 } from "fs/promises";
@@ -1261,8 +1306,8 @@ async function readAt(dir2) {
1261
1306
  async function readWorkspace(dir2) {
1262
1307
  return readAt(dir2);
1263
1308
  }
1264
- async function findWorkspace(start, home) {
1265
- let dir2 = resolve(start);
1309
+ async function findWorkspace(start2, home) {
1310
+ let dir2 = resolve(start2);
1266
1311
  const stop = resolve(home);
1267
1312
  for (; ; ) {
1268
1313
  const ws = await readAt(dir2);
@@ -1286,17 +1331,225 @@ async function writeWorkspace(root, o) {
1286
1331
  }
1287
1332
  async function workspacePortal(deps2, ws) {
1288
1333
  const portal = resolvePortal(ws.portal, void 0);
1289
- const label2 = portal.builtin ?? portal.apiOrigin;
1334
+ const label3 = portal.builtin ?? portal.apiOrigin;
1290
1335
  const profile = (await deps2.config.read()).profiles[portal.apiOrigin];
1291
1336
  const loggedIn = profile && await new TokenStore(deps2.keychain, profileHash(profile.apiOrigin, profile.issuer)).getRefresh();
1292
1337
  if (!loggedIn) {
1293
- throw new QoreError("QORE_NOT_LOGGED_IN", `This workspace belongs to ${label2}, which you are not logged in to`, {
1294
- hint: `Run \`qore login --portal ${label2}\`.`
1338
+ throw new QoreError("QORE_NOT_LOGGED_IN", `This workspace belongs to ${label3}, which you are not logged in to`, {
1339
+ hint: `Run \`qore login --portal ${label3}\`.`
1295
1340
  });
1296
1341
  }
1297
1342
  return portal;
1298
1343
  }
1299
1344
 
1345
+ // src/core/projects/discover.ts
1346
+ var SKIP = (name) => name.startsWith(".") || name === "node_modules";
1347
+ async function subdirs(dir2) {
1348
+ try {
1349
+ return (await readdir(dir2, { withFileTypes: true })).filter((d) => d.isDirectory() && !SKIP(d.name)).map((d) => d.name);
1350
+ } catch {
1351
+ return [];
1352
+ }
1353
+ }
1354
+ async function scanRoot(root) {
1355
+ const out = [];
1356
+ for (const tenant of (await subdirs(root)).sort()) {
1357
+ for (const app of (await subdirs(join6(root, tenant))).sort()) {
1358
+ const path = join6(root, tenant, app);
1359
+ try {
1360
+ const ws = await readWorkspace(path);
1361
+ if (ws) out.push({ kind: "workspace", path, portal: ws.portal, tenantSlug: tenant, appId: ws.appId, appSlug: app });
1362
+ } catch {
1363
+ }
1364
+ }
1365
+ }
1366
+ return out;
1367
+ }
1368
+ var GONE = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
1369
+ async function stillValid(e) {
1370
+ try {
1371
+ const real = await realpath2(e.path);
1372
+ if (e.kind === "workspace") {
1373
+ const ws = await readWorkspace(real);
1374
+ return ws && ws.appId === e.appId ? real : "gone";
1375
+ }
1376
+ return real;
1377
+ } catch (err) {
1378
+ if (err instanceof CancelledError) throw err;
1379
+ return GONE.has(err.code ?? "") ? "gone" : "unknown";
1380
+ }
1381
+ }
1382
+ async function knownProjects(deps2, o = {}) {
1383
+ const cfg = await deps2.config.read();
1384
+ const root = cfg.projectsRoot ? await realpath2(cfg.projectsRoot).catch(() => cfg.projectsRoot) : void 0;
1385
+ const out = root ? (await scanRoot(root)).map((p) => ({ ...p, source: "root" })) : [];
1386
+ const seen = new Set(out.map((p) => p.path));
1387
+ const stale = [];
1388
+ for (const e of cfg.projects ?? []) {
1389
+ const real = await stillValid(e);
1390
+ if (real === "gone") {
1391
+ stale.push(e.path);
1392
+ continue;
1393
+ }
1394
+ if (real === "unknown") continue;
1395
+ if (seen.has(real)) continue;
1396
+ seen.add(real);
1397
+ out.push({ ...e, path: real, source: "registry" });
1398
+ }
1399
+ if (stale.length > 0) await forget(deps2, stale);
1400
+ return o.portal ? out.filter((p) => p.portal === o.portal) : out;
1401
+ }
1402
+
1403
+ // src/core/projects/root.ts
1404
+ import { randomBytes as randomBytes3 } from "crypto";
1405
+ import { mkdir as mkdir3, realpath as realpath3, rm as rm2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
1406
+ import { dirname as dirname3, isAbsolute, join as join8, parse, relative as relative2, resolve as resolve3, sep } from "path";
1407
+
1408
+ // src/core/context/repoRoot.ts
1409
+ import { stat } from "fs/promises";
1410
+ import { dirname as dirname2, join as join7, resolve as resolve2 } from "path";
1411
+ async function findRepoRoot(start2) {
1412
+ let dir2 = resolve2(start2);
1413
+ for (; ; ) {
1414
+ try {
1415
+ await stat(join7(dir2, ".git"));
1416
+ return dir2;
1417
+ } catch {
1418
+ }
1419
+ const up = dirname2(dir2);
1420
+ if (up === dir2) return null;
1421
+ dir2 = up;
1422
+ }
1423
+ }
1424
+
1425
+ // src/core/projects/root.ts
1426
+ var NOT_WRITABLE = /* @__PURE__ */ new Set(["EACCES", "EPERM", "EROFS"]);
1427
+ function expandPath(deps2, input2) {
1428
+ const s = input2.trim();
1429
+ if (s === "~") return deps2.home;
1430
+ if (s.startsWith("~/") || s.startsWith("~\\")) return trimSeparator(join8(deps2.home, s.slice(2)));
1431
+ return resolve3(deps2.cwd, s);
1432
+ }
1433
+ function trimSeparator(p) {
1434
+ const root = parse(p).root;
1435
+ let end = p.length;
1436
+ while (end > root.length && (p[end - 1] === "/" || p[end - 1] === "\\")) end--;
1437
+ return p.slice(0, end);
1438
+ }
1439
+ async function underAFile(path) {
1440
+ for (let p = dirname3(path); ; p = dirname3(p)) {
1441
+ const s = await stat2(p).catch((e) => e.code === "ENOENT" ? void 0 : null);
1442
+ if (s === null) return false;
1443
+ if (s) return !s.isDirectory();
1444
+ if (dirname3(p) === p) return false;
1445
+ }
1446
+ }
1447
+ function isInside(parent, child) {
1448
+ const rel = relative2(parent, child);
1449
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1450
+ }
1451
+ function scannedBy(root, e) {
1452
+ if (e.kind !== "workspace" || !isInside(root, e.path)) return false;
1453
+ return relative2(root, e.path).split(sep).filter(Boolean).length === 2;
1454
+ }
1455
+ async function writableError(path) {
1456
+ const probe = join8(path, `.qore-write-test-${randomBytes3(6).toString("hex")}`);
1457
+ try {
1458
+ await writeFile3(probe, "");
1459
+ await rm2(probe, { force: true });
1460
+ return null;
1461
+ } catch (e) {
1462
+ return e;
1463
+ }
1464
+ }
1465
+ var invalid2 = (path, why) => new QoreError("QORE_ROOT_INVALID", `${path} cannot be the projects root: ${why}`, { hint: "Pick another folder: qore init --root <path>." });
1466
+ var notWritable = (path, e) => new QoreError("QORE_ROOT_NOT_WRITABLE", `${path} is not writable (${e.code ?? e.message})`, {
1467
+ hint: "Fix its permissions, or pick another folder: qore init --root <path>.",
1468
+ cause: e
1469
+ });
1470
+ async function checkRoot(deps2, input2) {
1471
+ const path = expandPath(deps2, input2);
1472
+ if (dirname3(path) === path) throw invalid2(path, "it is the filesystem root");
1473
+ try {
1474
+ if (!(await stat2(path)).isDirectory()) throw invalid2(path, "it is a file");
1475
+ } catch (e) {
1476
+ if (e instanceof QoreError) throw e;
1477
+ const err = e;
1478
+ if (err.code === "ENOTDIR") throw invalid2(path, "a parent of it is a file");
1479
+ if (NOT_WRITABLE.has(err.code ?? "")) throw notWritable(path, err);
1480
+ if (err.code !== "ENOENT") throw e;
1481
+ if (await underAFile(path)) throw invalid2(path, "a parent of it is a file");
1482
+ }
1483
+ const repo = await findRepoRoot(path);
1484
+ if (await findWorkspace(path, deps2.home) || repo && !isInside(repo, deps2.home)) {
1485
+ throw invalid2(path, "it lies inside a project (an app workspace or a git repository)");
1486
+ }
1487
+ return path;
1488
+ }
1489
+ async function createRoot(path) {
1490
+ try {
1491
+ await mkdir3(path, { recursive: true });
1492
+ } catch (e) {
1493
+ const err2 = e;
1494
+ if (NOT_WRITABLE.has(err2.code ?? "")) throw notWritable(path, err2);
1495
+ if (err2.code === "ENOTDIR" || err2.code === "EEXIST") throw invalid2(path, "a parent of it is a file");
1496
+ throw e;
1497
+ }
1498
+ const err = await writableError(path);
1499
+ if (err) throw notWritable(path, err);
1500
+ return realpath3(path);
1501
+ }
1502
+ async function validateRoot(deps2, input2) {
1503
+ return createRoot(await checkRoot(deps2, input2));
1504
+ }
1505
+
1506
+ // src/core/projects/registry.ts
1507
+ var LOCK = "projects";
1508
+ var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1509
+ async function quietly(fn) {
1510
+ try {
1511
+ await fn();
1512
+ } catch (e) {
1513
+ if (e instanceof CancelledError) throw e;
1514
+ }
1515
+ }
1516
+ async function remember(deps2, e) {
1517
+ await quietly(async () => {
1518
+ const path = await realpath4(e.path);
1519
+ const entry = { ...e, path };
1520
+ const cfg = await deps2.config.read();
1521
+ const root = cfg.projectsRoot ? await realpath4(cfg.projectsRoot).catch(() => cfg.projectsRoot) : void 0;
1522
+ if (root && scannedBy(root, entry)) return;
1523
+ const current = cfg.projects?.find((p) => p.path === path);
1524
+ if (current && same(current, entry)) return;
1525
+ await deps2.withLock(LOCK, () => deps2.config.update((c) => {
1526
+ c.projects = [...(c.projects ?? []).filter((p) => p.path !== path), entry];
1527
+ }).then(() => void 0));
1528
+ });
1529
+ }
1530
+ async function forget(deps2, paths) {
1531
+ const drop = new Set(paths);
1532
+ await quietly(() => deps2.withLock(LOCK, () => deps2.config.update((c) => {
1533
+ if (c.projects) c.projects = c.projects.filter((p) => !drop.has(p.path));
1534
+ }).then(() => void 0)));
1535
+ }
1536
+ async function setRoot(deps2, next) {
1537
+ let registered = 0;
1538
+ await deps2.withLock(LOCK, async () => {
1539
+ const old = (await deps2.config.read()).projectsRoot;
1540
+ const carried = old && old !== next ? await scanRoot(old) : [];
1541
+ const keep = carried.filter((p) => !next || !scannedBy(next, p));
1542
+ registered = keep.length;
1543
+ await deps2.config.update((c) => {
1544
+ c.projectsRoot = next;
1545
+ if (next === void 0) delete c.projectsRoot;
1546
+ const rest = (c.projects ?? []).filter((p) => !keep.some((k) => k.path === p.path));
1547
+ if (rest.length + keep.length > 0) c.projects = [...rest, ...keep];
1548
+ });
1549
+ });
1550
+ return { registered };
1551
+ }
1552
+
1300
1553
  // src/core/workspace/safety.ts
1301
1554
  var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/;
1302
1555
  var WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/;
@@ -1347,9 +1600,9 @@ function firstSentenceLower(hint) {
1347
1600
  return sentence.length ? sentence[0].toLowerCase() + sentence.slice(1) : sentence;
1348
1601
  }
1349
1602
  function failedLine(r) {
1350
- const label2 = FAILURE_LABELS[r.error?.code ?? ""] ?? "failed";
1603
+ const label3 = FAILURE_LABELS[r.error?.code ?? ""] ?? "failed";
1351
1604
  const detail2 = firstSentenceLower(r.error?.hint);
1352
- return `\u2717 ${r.slug} ${label2}${detail2 ? ` \u2014 ${detail2}` : ""}`;
1605
+ return `\u2717 ${r.slug} ${label3}${detail2 ? ` \u2014 ${detail2}` : ""}`;
1353
1606
  }
1354
1607
  function line(r, conflictCount) {
1355
1608
  switch (r.status) {
@@ -1448,85 +1701,164 @@ async function pickApp(deps2, io2, session, tenantFlag) {
1448
1701
  });
1449
1702
  return { tenant: tenant.slug, app };
1450
1703
  }
1704
+ async function cloneApp(ctx, appArg, dirArg, flags) {
1705
+ const { deps: deps2, io: io2 } = ctx;
1706
+ const g = ctx.opts();
1707
+ const cfg = await deps2.config.read();
1708
+ const portal = resolvePortal(g.portal, cfg.defaultProfile);
1709
+ const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
1710
+ const session = await openSession(deps2, portal, "clone", onRequest);
1711
+ await ensureCapability(deps2, portal.apiOrigin, "app.modules");
1712
+ const picked = appArg === void 0 ? await pickApp(deps2, io2, session, flags.tenant) : void 0;
1713
+ const appSlug = picked?.app ?? appArg;
1714
+ const ref = await resolveApp(deps2, io2, session, appSlug, picked?.tenant ?? flags.tenant);
1715
+ const app = await session.api.get(`/api/cli/v1/apps/${encodeURIComponent(ref.appId)}/modules`);
1716
+ const projectsRoot = cfg.projectsRoot;
1717
+ if (dirArg === void 0 && !isSafeSlug(app.app.slug)) throw unsafeSlugError(app.app.slug);
1718
+ if (dirArg === void 0 && projectsRoot && !isSafeSlug(app.app.tenant.slug)) throw unsafeSlugError(app.app.tenant.slug);
1719
+ 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);
1720
+ const dirLabel = dirArg ?? (projectsRoot ? root : app.app.slug);
1721
+ const info = await deps2.git.inspect(root);
1722
+ if (info.exists && !info.empty) {
1723
+ const ws = await readWorkspace(root);
1724
+ if (!ws || ws.appId !== app.app.id || ws.portal !== portal.apiOrigin) {
1725
+ throw new QoreError("QORE_WORKSPACE_CONFLICT", `${root} exists and is not the workspace of ${app.app.slug}`, {
1726
+ hint: `Pick another directory: qore clone ${appSlug} <dir>.`
1727
+ });
1728
+ }
1729
+ }
1730
+ await writeWorkspace(root, { portal: portal.apiOrigin, appId: app.app.id });
1731
+ await remember(deps2, {
1732
+ kind: "workspace",
1733
+ path: root,
1734
+ portal: portal.apiOrigin,
1735
+ tenantSlug: app.app.tenant.slug,
1736
+ appId: app.app.id,
1737
+ appSlug: app.app.slug
1738
+ });
1739
+ const dirs = {};
1740
+ const modulesForPlan = [];
1741
+ const earlyFailures = [];
1742
+ for (const m of app.modules) {
1743
+ if (!isSafeSlug(m.slug)) {
1744
+ earlyFailures.push(asFailure(m.slug, unsafeSlugError(m.slug)));
1745
+ continue;
1746
+ }
1747
+ try {
1748
+ dirs[m.slug] = await deps2.git.inspect(join9(root, m.slug));
1749
+ modulesForPlan.push(m);
1750
+ } catch (e) {
1751
+ earlyFailures.push(asFailure(m.slug, e));
1752
+ }
1753
+ }
1754
+ const { actions } = planWorkspace(root, modulesForPlan, dirs, []);
1755
+ const applied = await applyActions(deps2, { portal, app, actions, protocol: flags.protocol, onResult: () => {
1756
+ } });
1757
+ for (const r of applied) {
1758
+ if (r.status === "cloned" && r.dir && isInside(deps2.cwd, r.dir)) r.dir = relative3(deps2.cwd, r.dir) || ".";
1759
+ }
1760
+ const results = [...earlyFailures, ...applied];
1761
+ const err = reportWorkspaceRun(io2, results, Boolean(g.json));
1762
+ if (!g.json) {
1763
+ io2.out(`\u2713 Wrote ${join9(dirLabel, "qore.workspace.json")}`);
1764
+ const first = results.find((r) => r.status === "cloned" || r.status === "present");
1765
+ if (first && !err) io2.out(`Next: cd ${join9(dirLabel, first.slug)} && qore env pull`);
1766
+ }
1767
+ if (!g.json && dirArg === void 0 && !projectsRoot) io2.err("Tip: run `qore init` to always clone into one projects folder.");
1768
+ if (err) throw err;
1769
+ return { root, appSlug: app.app.slug };
1770
+ }
1451
1771
  function registerClone(program, ctx) {
1452
1772
  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) => {
1773
+ await cloneApp(ctx, appArg, dirArg, flags);
1774
+ });
1775
+ }
1776
+
1777
+ // src/core/telemetry/consent.ts
1778
+ var ON = /* @__PURE__ */ new Set(["1", "true", "on", "yes"]);
1779
+ var DO_NOT_TRACK_EXEMPT = /* @__PURE__ */ new Set(["0", "false"]);
1780
+ var norm = (v) => (v ?? "").trim().toLowerCase();
1781
+ function resolveConsent(env, cfg) {
1782
+ const dnt = norm(env.DO_NOT_TRACK);
1783
+ if (dnt !== "" && !DO_NOT_TRACK_EXEMPT.has(dnt)) return { enabled: false, source: "DO_NOT_TRACK" };
1784
+ const q = norm(env.QORE_TELEMETRY);
1785
+ if (q !== "") return { enabled: ON.has(q), source: "QORE_TELEMETRY" };
1786
+ if (typeof cfg.telemetry?.enabled === "boolean") return { enabled: cfg.telemetry.enabled, source: "config" };
1787
+ return { enabled: true, source: "default" };
1788
+ }
1789
+ function needsNotice(cfg, consent) {
1790
+ return consent.enabled && consent.source === "default" && !cfg.telemetry?.noticeShownAt;
1791
+ }
1792
+ function describeConsent(consent, built) {
1793
+ const state = `crash reports ${consent.enabled ? "on" : "off"} (${consent.source})`;
1794
+ return consent.enabled && !built ? `${state} \xB7 this build has no DSN, nothing is sent` : state;
1795
+ }
1796
+
1797
+ // src/cli/commands/config.ts
1798
+ async function describeRoot(deps2) {
1799
+ const known = await knownProjects(deps2);
1800
+ return {
1801
+ root: (await deps2.config.read()).projectsRoot ?? null,
1802
+ projects: { inRoot: known.filter((p) => p.source === "root").length, registered: known.filter((p) => p.source === "registry").length }
1803
+ };
1804
+ }
1805
+ function registerConfig(program, ctx) {
1806
+ const config = program.command("config").description("Change qore's own settings");
1807
+ config.command("telemetry").description("Show or set crash reporting").argument("[state]", "on or off").action(async (state) => {
1453
1808
  const { deps: deps2, io: io2 } = ctx;
1454
- const g = ctx.opts();
1455
- const cfg = await deps2.config.read();
1456
- const portal = resolvePortal(g.portal, cfg.defaultProfile);
1457
- const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
1458
- const session = await openSession(deps2, portal, "clone", onRequest);
1459
- await ensureCapability(deps2, portal.apiOrigin, "app.modules");
1460
- const picked = appArg === void 0 ? await pickApp(deps2, io2, session, flags.tenant) : void 0;
1461
- const appSlug = picked?.app ?? appArg;
1462
- const ref = await resolveApp(deps2, io2, session, appSlug, picked?.tenant ?? flags.tenant);
1463
- const app = await session.api.get(`/api/cli/v1/apps/${encodeURIComponent(ref.appId)}/modules`);
1464
- if (dirArg === void 0 && !isSafeSlug(app.app.slug)) throw unsafeSlugError(app.app.slug);
1465
- const dirLabel = dirArg ?? app.app.slug;
1466
- const root = resolve2(deps2.cwd, dirLabel);
1467
- const info = await deps2.git.inspect(root);
1468
- if (info.exists && !info.empty) {
1469
- const ws = await readWorkspace(root);
1470
- if (!ws || ws.appId !== app.app.id || ws.portal !== portal.apiOrigin) {
1471
- throw new QoreError("QORE_WORKSPACE_CONFLICT", `${root} exists and is not the workspace of ${app.app.slug}`, {
1472
- hint: `Pick another directory: qore clone ${appSlug} <dir>.`
1473
- });
1474
- }
1809
+ if (state !== void 0 && state !== "on" && state !== "off") {
1810
+ throw new QoreError("QORE_CONTEXT_INVALID", `Unknown telemetry state: ${state}`, { hint: "Use: qore config telemetry on|off" });
1475
1811
  }
1476
- await writeWorkspace(root, { portal: portal.apiOrigin, appId: app.app.id });
1477
- const dirs = {};
1478
- const modulesForPlan = [];
1479
- const earlyFailures = [];
1480
- for (const m of app.modules) {
1481
- if (!isSafeSlug(m.slug)) {
1482
- earlyFailures.push(asFailure(m.slug, unsafeSlugError(m.slug)));
1483
- continue;
1484
- }
1485
- try {
1486
- dirs[m.slug] = await deps2.git.inspect(join6(root, m.slug));
1487
- modulesForPlan.push(m);
1488
- } catch (e) {
1489
- earlyFailures.push(asFailure(m.slug, e));
1490
- }
1812
+ if (state) await deps2.config.update((c) => {
1813
+ c.telemetry = { ...c.telemetry, enabled: state === "on" };
1814
+ });
1815
+ const consent = resolveConsent(deps2.env, await deps2.config.read());
1816
+ const built = deps2.sentryDsn !== "";
1817
+ if (ctx.opts().json) {
1818
+ renderJson(io2, { telemetry: { ...consent, built } });
1819
+ return;
1491
1820
  }
1492
- const { actions } = planWorkspace(root, modulesForPlan, dirs, []);
1493
- const applied = await applyActions(deps2, { portal, app, actions, protocol: flags.protocol, onResult: () => {
1494
- } });
1495
- for (const r of applied) {
1496
- if (r.status === "cloned" && r.dir) r.dir = relative2(deps2.cwd, r.dir) || ".";
1821
+ io2.out(describeConsent(consent, built));
1822
+ if (state && consent.source !== "config") io2.err(`${consent.source} in your environment overrides this setting.`);
1823
+ });
1824
+ 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) => {
1825
+ const { deps: deps2, io: io2 } = ctx;
1826
+ if (path !== void 0 && f.unset) {
1827
+ throw new QoreError("QORE_CONTEXT_INVALID", "Pass a path or --unset, not both", { hint: "qore config root <path> | qore config root --unset" });
1497
1828
  }
1498
- const results = [...earlyFailures, ...applied];
1499
- const err = reportWorkspaceRun(io2, results, Boolean(g.json));
1500
- if (!g.json) {
1501
- io2.out(`\u2713 Wrote ${join6(dirLabel, "qore.workspace.json")}`);
1502
- const first = results.find((r) => r.status === "cloned" || r.status === "present");
1503
- if (first && !err) io2.out(`Next: cd ${join6(dirLabel, first.slug)} && qore env pull`);
1829
+ if (path !== void 0) await setRoot(deps2, await validateRoot(deps2, path));
1830
+ if (f.unset) await setRoot(deps2, void 0);
1831
+ const d = await describeRoot(deps2);
1832
+ if (ctx.opts().json) {
1833
+ renderJson(io2, d);
1834
+ return;
1504
1835
  }
1505
- if (err) throw err;
1836
+ io2.out(d.root ? `Projects root: ${d.root}` : "Projects root: not set \u2014 run qore init");
1837
+ io2.out(`Known projects: ${d.projects.inRoot} in the root, ${d.projects.registered} elsewhere`);
1506
1838
  });
1507
1839
  }
1508
1840
 
1509
1841
  // src/cli/commands/dev.ts
1510
- import { randomBytes as randomBytes5 } from "crypto";
1511
- import { realpath as realpath5 } from "fs/promises";
1842
+ import { randomBytes as randomBytes6 } from "crypto";
1843
+ import { realpath as realpath8 } from "fs/promises";
1512
1844
  import { constants as constants3 } from "os";
1513
- import { dirname as dirname6, join as join18, relative as relative6 } from "path";
1845
+ import { dirname as dirname7, join as join21, relative as relative7 } from "path";
1514
1846
  import { StringDecoder } from "string_decoder";
1515
1847
 
1516
1848
  // src/core/context/repoConfig.ts
1517
1849
  import { readFile as readFile3 } from "fs/promises";
1518
- import { join as join7 } from "path";
1850
+ import { join as join10 } from "path";
1519
1851
  var FORBIDDEN = ["portal", "api", "apiOrigin", "issuer", "tenant", "tenantId", "app", "appId", "module", "moduleId"];
1520
1852
  var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
1521
1853
  var SCRIPT = /^[A-Za-z0-9:_.-]+$/;
1522
- 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 });
1854
+ var invalid3 = (msg, details) => new QoreError("QORE_CONTEXT_INVALID", `.qore.json: ${msg}`, { hint: "Fix .qore.json in the repository root; see the $schema it names.", details });
1523
1855
  function moduleDefaults(type) {
1524
1856
  return type === "frontend" ? { envFile: ".env.local", localSecrets: ["SESSION_COOKIE_SECRET"] } : { envFile: ".env", localSecrets: [] };
1525
1857
  }
1526
1858
  async function readRepoConfig(root) {
1527
1859
  let raw;
1528
1860
  try {
1529
- raw = await readFile3(join7(root, ".qore.json"), "utf8");
1861
+ raw = await readFile3(join10(root, ".qore.json"), "utf8");
1530
1862
  } catch (e) {
1531
1863
  if (e.code === "ENOENT") return {};
1532
1864
  throw e;
@@ -1535,44 +1867,44 @@ async function readRepoConfig(root) {
1535
1867
  try {
1536
1868
  doc = JSON.parse(raw);
1537
1869
  } catch {
1538
- throw invalid2("not valid JSON");
1870
+ throw invalid3("not valid JSON");
1539
1871
  }
1540
- if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw invalid2("must be an object");
1872
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw invalid3("must be an object");
1541
1873
  const v = doc.schemaVersion === void 0 ? 1 : doc.schemaVersion;
1542
- if (!Number.isInteger(v) || v < 1) throw invalid2("schemaVersion must be a positive integer");
1874
+ if (!Number.isInteger(v) || v < 1) throw invalid3("schemaVersion must be a positive integer");
1543
1875
  if (v > 1) {
1544
1876
  throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `.qore.json schemaVersion ${v} is newer than this qore`, { hint: "Run: npm i -g @quatronic/qore" });
1545
1877
  }
1546
- 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 });
1878
+ for (const f of FORBIDDEN) if (f in doc) throw invalid3(`"${f}" is not allowed \u2014 .qore.json never selects a portal or a resource`, { field: f });
1547
1879
  const out = {};
1548
1880
  if (doc.envFile !== void 0) {
1549
- if (typeof doc.envFile !== "string" || !doc.envFile) throw invalid2("envFile must be a non-empty string");
1881
+ if (typeof doc.envFile !== "string" || !doc.envFile) throw invalid3("envFile must be a non-empty string");
1550
1882
  out.envFile = doc.envFile;
1551
1883
  }
1552
1884
  if (doc.localSecrets !== void 0) {
1553
1885
  if (!Array.isArray(doc.localSecrets) || !doc.localSecrets.every((k) => typeof k === "string" && KEY.test(k))) {
1554
- throw invalid2("localSecrets must be a list of env key names");
1886
+ throw invalid3("localSecrets must be a list of env key names");
1555
1887
  }
1556
- if (new Set(doc.localSecrets).size !== doc.localSecrets.length) throw invalid2("localSecrets must not contain duplicates");
1888
+ if (new Set(doc.localSecrets).size !== doc.localSecrets.length) throw invalid3("localSecrets must not contain duplicates");
1557
1889
  out.localSecrets = doc.localSecrets;
1558
1890
  }
1559
1891
  if (doc.dev !== void 0) {
1560
1892
  const d = doc.dev;
1561
- if (!d || typeof d !== "object" || Array.isArray(d)) throw invalid2("dev must be an object");
1893
+ if (!d || typeof d !== "object" || Array.isArray(d)) throw invalid3("dev must be an object");
1562
1894
  const dev = {};
1563
1895
  if (d.install !== void 0) {
1564
- if (typeof d.install !== "boolean") throw invalid2("dev.install must be true or false");
1896
+ if (typeof d.install !== "boolean") throw invalid3("dev.install must be true or false");
1565
1897
  dev.install = d.install;
1566
1898
  }
1567
1899
  if (d.services !== void 0) {
1568
1900
  if (!Array.isArray(d.services) || !d.services.every((s) => typeof s === "string" && SCRIPT.test(s))) {
1569
- throw invalid2("dev.services must be a list of script names");
1901
+ throw invalid3("dev.services must be a list of script names");
1570
1902
  }
1571
- if (new Set(d.services).size !== d.services.length) throw invalid2("dev.services must not contain duplicates");
1903
+ if (new Set(d.services).size !== d.services.length) throw invalid3("dev.services must not contain duplicates");
1572
1904
  dev.services = d.services;
1573
1905
  }
1574
1906
  if (d.script !== void 0) {
1575
- if (typeof d.script !== "string" || !SCRIPT.test(d.script)) throw invalid2("dev.script must be a script name");
1907
+ if (typeof d.script !== "string" || !SCRIPT.test(d.script)) throw invalid3("dev.script must be a script name");
1576
1908
  dev.script = d.script;
1577
1909
  }
1578
1910
  out.dev = dev;
@@ -1580,25 +1912,8 @@ async function readRepoConfig(root) {
1580
1912
  return out;
1581
1913
  }
1582
1914
 
1583
- // src/core/context/repoRoot.ts
1584
- import { stat } from "fs/promises";
1585
- import { dirname as dirname2, join as join8, resolve as resolve3 } from "path";
1586
- async function findRepoRoot(start) {
1587
- let dir2 = resolve3(start);
1588
- for (; ; ) {
1589
- try {
1590
- await stat(join8(dir2, ".git"));
1591
- return dir2;
1592
- } catch {
1593
- }
1594
- const up = dirname2(dir2);
1595
- if (up === dir2) return null;
1596
- dir2 = up;
1597
- }
1598
- }
1599
-
1600
1915
  // src/core/context/resolver.ts
1601
- import { isAbsolute, join as join9, relative as relative3 } from "path";
1916
+ import { isAbsolute as isAbsolute2, join as join11, relative as relative4 } from "path";
1602
1917
  var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1603
1918
  var HINT_FLAG = "cd into a module repository, or pass --module <slug|id>.";
1604
1919
  var RESOLVE = "/api/cli/v1/repos/resolve";
@@ -1706,8 +2021,8 @@ async function byFlag(deps2, o, cache, ws) {
1706
2021
  return { portal, session, module, source: overrides ? { kind: "flag", overrides } : { kind: "flag" }, repoRoot, workspace: ws };
1707
2022
  }
1708
2023
  var isBelow = (root, dir2) => {
1709
- const rel = relative3(root, dir2);
1710
- return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
2024
+ const rel = relative4(root, dir2);
2025
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
1711
2026
  };
1712
2027
  async function inWorkspaceRoot(deps2, ws) {
1713
2028
  if (!ws) return false;
@@ -1811,7 +2126,7 @@ async function anyLoggedIn(deps2, cfg) {
1811
2126
  return false;
1812
2127
  }
1813
2128
  async function resolveModuleContext(deps2, o) {
1814
- const cache = new ResolutionCache(join9(deps2.config.dir, "cache"), deps2.now);
2129
+ const cache = new ResolutionCache(join11(deps2.config.dir, "cache"), deps2.now);
1815
2130
  const ws = await findWorkspace(deps2.cwd, deps2.home);
1816
2131
  if (o.module) return byFlag(deps2, o, cache, ws);
1817
2132
  const cfg = await deps2.config.read();
@@ -1962,15 +2277,17 @@ function exitHint(tail, h) {
1962
2277
  }
1963
2278
 
1964
2279
  // src/core/dev/env.ts
1965
- import { dirname as dirname5, join as join12 } from "path";
2280
+ import { dirname as dirname6, join as join14 } from "path";
1966
2281
 
1967
2282
  // src/core/env/fetch.ts
1968
2283
  async function fetchLocalEnv(deps2, mc, app) {
1969
2284
  await ensureCapability(deps2, mc.portal.apiOrigin, "module.local-env");
1970
2285
  try {
1971
- return await mc.session.api.get(
2286
+ const local = await mc.session.api.get(
1972
2287
  `/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/local-env?app_id=${encodeURIComponent(app.id)}`
1973
2288
  );
2289
+ deps2.registerSecrets(Object.values(local.env));
2290
+ return local;
1974
2291
  } catch (e) {
1975
2292
  if (!(e instanceof QoreError) || e.code !== "QORE_ACCESS_DENIED") throw e;
1976
2293
  if (e.serverCode === "app.access.denied") {
@@ -2062,8 +2379,8 @@ function renderEnv(original, merged) {
2062
2379
  }
2063
2380
 
2064
2381
  // src/core/env/pull.ts
2065
- import { lstat, realpath as realpath2 } from "fs/promises";
2066
- import { basename, dirname as dirname3, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve4, sep } from "path";
2382
+ import { lstat, realpath as realpath5 } from "fs/promises";
2383
+ import { basename, dirname as dirname4, isAbsolute as isAbsolute3, relative as relative5, resolve as resolve5, sep as sep2 } from "path";
2067
2384
  var outside = (target) => new QoreError("QORE_CONTEXT_INVALID", `The env file must be inside the repository (got ${target})`, {
2068
2385
  hint: "Use a path inside the repository for --target or .qore.json envFile."
2069
2386
  });
@@ -2071,29 +2388,29 @@ var symlinked = (target) => new QoreError("QORE_CONTEXT_INVALID", `The env file
2071
2388
  hint: "Use a path inside the repository for --target or .qore.json envFile."
2072
2389
  });
2073
2390
  function inside(root, p) {
2074
- const rel = relative4(root, p);
2075
- return rel !== "" && rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute2(rel);
2391
+ const rel = relative5(root, p);
2392
+ return rel !== "" && rel !== ".." && !rel.startsWith(".." + sep2) && !isAbsolute3(rel);
2076
2393
  }
2077
2394
  async function resolveTarget(root, target, base = root) {
2078
- const realRoot = await realpath2(root);
2079
- const realBase = base === root ? realRoot : await realpath2(base);
2080
- const abs = resolve4(realBase, target);
2081
- if (!isAbsolute2(target) && !inside(realRoot, abs)) throw outside(target);
2395
+ const realRoot = await realpath5(root);
2396
+ const realBase = base === root ? realRoot : await realpath5(base);
2397
+ const abs = resolve5(realBase, target);
2398
+ if (!isAbsolute3(target) && !inside(realRoot, abs)) throw outside(target);
2082
2399
  let parent;
2083
2400
  try {
2084
- parent = await realpath2(dirname3(abs));
2401
+ parent = await realpath5(dirname4(abs));
2085
2402
  } catch (e) {
2086
2403
  if (e.code !== "ENOENT") throw e;
2087
2404
  throw new QoreError("QORE_CONTEXT_INVALID", `The directory for ${target} does not exist`, {
2088
2405
  hint: "Create the parent directory, or point --target / .qore.json envFile at an existing one."
2089
2406
  });
2090
2407
  }
2091
- const final = resolve4(parent, basename(abs));
2408
+ const final = resolve5(parent, basename(abs));
2092
2409
  if (!inside(realRoot, final)) throw outside(target);
2093
2410
  try {
2094
2411
  const st = await lstat(final);
2095
2412
  if (st.isSymbolicLink()) throw symlinked(target);
2096
- const realFinal = await realpath2(final);
2413
+ const realFinal = await realpath5(final);
2097
2414
  if (!inside(realRoot, realFinal)) throw outside(target);
2098
2415
  } catch (e) {
2099
2416
  if (e instanceof QoreError) throw e;
@@ -2129,9 +2446,9 @@ function planPull(o) {
2129
2446
  }
2130
2447
 
2131
2448
  // src/core/env/write.ts
2132
- import { chmod as chmod2, readFile as readFile4, rename as rename3, unlink, writeFile as writeFile3 } from "fs/promises";
2133
- import { randomBytes as randomBytes3 } from "crypto";
2134
- import { basename as basename2, dirname as dirname4, join as join10 } from "path";
2449
+ import { chmod as chmod2, readFile as readFile4, rename as rename3, unlink, writeFile as writeFile4 } from "fs/promises";
2450
+ import { randomBytes as randomBytes4 } from "crypto";
2451
+ import { basename as basename2, dirname as dirname5, join as join12 } from "path";
2135
2452
  async function readOrNull(p) {
2136
2453
  try {
2137
2454
  return await readFile4(p, "utf8");
@@ -2142,10 +2459,10 @@ async function readOrNull(p) {
2142
2459
  }
2143
2460
  async function writeSecurely(target, text2) {
2144
2461
  const base = basename2(target);
2145
- const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes3(6).toString("hex")}.local` : `${base}.qore-${randomBytes3(6).toString("hex")}.tmp`;
2146
- const tmp = join10(dirname4(target), tmpName);
2462
+ const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes4(6).toString("hex")}.local` : `${base}.qore-${randomBytes4(6).toString("hex")}.tmp`;
2463
+ const tmp = join12(dirname5(target), tmpName);
2147
2464
  try {
2148
- await writeFile3(tmp, text2, { mode: 384, flag: "wx" });
2465
+ await writeFile4(tmp, text2, { mode: 384, flag: "wx" });
2149
2466
  await chmod2(tmp, 384);
2150
2467
  await rename3(tmp, target);
2151
2468
  } finally {
@@ -2171,8 +2488,8 @@ async function syncEnvFile(o) {
2171
2488
 
2172
2489
  // src/core/dev/lastStart.ts
2173
2490
  import { createHash as createHash2 } from "crypto";
2174
- import { realpath as realpath3 } from "fs/promises";
2175
- import { join as join11 } from "path";
2491
+ import { realpath as realpath6 } from "fs/promises";
2492
+ import { join as join13 } from "path";
2176
2493
  var NETWORK_REASONS = /* @__PURE__ */ new Set(["connect", "dns", "tls", "timeout"]);
2177
2494
  function isOffline(e) {
2178
2495
  if (!(e instanceof QoreError)) return false;
@@ -2181,14 +2498,14 @@ function isOffline(e) {
2181
2498
  return (e.status ?? 0) >= 500 || NETWORK_REASONS.has(String(e.details?.reason));
2182
2499
  }
2183
2500
  async function stateKey(deps2, root) {
2184
- const real = await realpath3(root).catch(() => root);
2501
+ const real = await realpath6(root).catch(() => root);
2185
2502
  return createHash2("sha256").update(trustKey(real, deps2.platform)).digest("hex").slice(0, 16);
2186
2503
  }
2187
2504
  async function saveLastStart(deps2, root, rec) {
2188
- await writeJsonAtomic(join11(deps2.config.dir, "dev"), `${await stateKey(deps2, root)}.json`, rec);
2505
+ await writeJsonAtomic(join13(deps2.config.dir, "dev"), `${await stateKey(deps2, root)}.json`, rec);
2189
2506
  }
2190
2507
  async function loadLastStart(deps2, root) {
2191
- const rec = await readJson(join11(deps2.config.dir, "dev", `${await stateKey(deps2, root)}.json`)).catch(() => null);
2508
+ const rec = await readJson(join13(deps2.config.dir, "dev", `${await stateKey(deps2, root)}.json`)).catch(() => null);
2192
2509
  return rec && rec.schemaVersion === 1 ? rec : null;
2193
2510
  }
2194
2511
 
@@ -2208,7 +2525,7 @@ async function devEnv(deps2, o) {
2208
2525
  if (o.mc && o.app) {
2209
2526
  try {
2210
2527
  const local = await fetchLocalEnv(deps2, o.mc, o.app);
2211
- const example = await readOrNull(join12(dirname5(target), ".env.example"));
2528
+ const example = await readOrNull(join14(dirname6(target), ".env.example"));
2212
2529
  const w = await syncEnvFile({
2213
2530
  target,
2214
2531
  example,
@@ -2343,7 +2660,7 @@ function providerDown(o) {
2343
2660
 
2344
2661
  // src/core/dev/project.ts
2345
2662
  import { access, readFile as readFile5 } from "fs/promises";
2346
- import { join as join13 } from "path";
2663
+ import { join as join15 } from "path";
2347
2664
  var LEGACY_DEV_SCRIPTS = ["node --env-file=.env --import ./dist/instrumentation.js dist/index.js"];
2348
2665
  var LOCKFILES = [["pnpm-lock.yaml", "pnpm"], ["package-lock.json", "npm"], ["yarn.lock", "yarn"]];
2349
2666
  var exists = (p) => access(p).then(() => true, () => false);
@@ -2351,7 +2668,7 @@ var unsupported = (message, hint, details) => new QoreError("QORE_DEV_UNSUPPORTE
2351
2668
  async function readPackageJson(root) {
2352
2669
  let raw;
2353
2670
  try {
2354
- raw = await readFile5(join13(root, "package.json"), "utf8");
2671
+ raw = await readFile5(join15(root, "package.json"), "utf8");
2355
2672
  } catch {
2356
2673
  throw unsupported("This repository has no package.json", "qore dev starts Node repositories through their package.json scripts.");
2357
2674
  }
@@ -2367,7 +2684,7 @@ async function detectPackageManager(root, pkg) {
2367
2684
  if (!m) throw unsupported(`packageManager "${pkg.packageManager}" is not pnpm, npm or yarn`, "qore dev supports pnpm, npm and yarn.");
2368
2685
  return { name: m[1], version: m[2].split("+")[0] };
2369
2686
  }
2370
- for (const [file, name] of LOCKFILES) if (await exists(join13(root, file))) return { name, version: null };
2687
+ for (const [file, name] of LOCKFILES) if (await exists(join15(root, file))) return { name, version: null };
2371
2688
  throw unsupported("No package manager declared and no lockfile found", 'Add "packageManager": "pnpm@<version>" to package.json.');
2372
2689
  }
2373
2690
  function planRecipe(o) {
@@ -2410,21 +2727,21 @@ function installArgs(pm) {
2410
2727
  }
2411
2728
 
2412
2729
  // src/core/dev/install.ts
2413
- import { stat as stat2 } from "fs/promises";
2414
- import { join as join14 } from "path";
2730
+ import { stat as stat3 } from "fs/promises";
2731
+ import { join as join16 } from "path";
2415
2732
  var MARKERS = {
2416
2733
  pnpm: [".modules.yaml"],
2417
2734
  npm: [".package-lock.json"],
2418
2735
  yarn: [".yarn-state.yml", ".yarn-integrity"]
2419
2736
  };
2420
- var LOCK = { pnpm: "pnpm-lock.yaml", npm: "package-lock.json", yarn: "yarn.lock" };
2421
- var mtime = (p) => stat2(p).then((s) => s.mtimeMs, () => null);
2737
+ var LOCK2 = { pnpm: "pnpm-lock.yaml", npm: "package-lock.json", yarn: "yarn.lock" };
2738
+ var mtime = (p) => stat3(p).then((s) => s.mtimeMs, () => null);
2422
2739
  async function needsInstall(root, pm) {
2423
- if (await mtime(join14(root, "node_modules")) === null) return true;
2740
+ if (await mtime(join16(root, "node_modules")) === null) return true;
2424
2741
  let marker = null;
2425
- for (const m of MARKERS[pm.name]) marker ??= await mtime(join14(root, "node_modules", m));
2742
+ for (const m of MARKERS[pm.name]) marker ??= await mtime(join16(root, "node_modules", m));
2426
2743
  if (marker === null) return true;
2427
- const lock = await mtime(join14(root, LOCK[pm.name]));
2744
+ const lock = await mtime(join16(root, LOCK2[pm.name]));
2428
2745
  return lock !== null && lock > marker;
2429
2746
  }
2430
2747
 
@@ -2740,11 +3057,11 @@ async function checkPort(deps2, o) {
2740
3057
  throw await portInUseError(deps2, { slug: o.slug, port: o.port, key: o.key, runs: o.runs });
2741
3058
  }
2742
3059
  function abortableDelay(signal) {
2743
- return (ms) => new Promise((resolve5) => {
3060
+ return (ms) => new Promise((resolve6) => {
2744
3061
  const done = () => {
2745
3062
  clearTimeout(t);
2746
3063
  signal.removeEventListener("abort", done);
2747
- resolve5();
3064
+ resolve6();
2748
3065
  };
2749
3066
  const t = setTimeout(done, ms);
2750
3067
  signal.addEventListener("abort", done, { once: true });
@@ -2827,11 +3144,11 @@ function hintContext(recipe, pkg, env) {
2827
3144
 
2828
3145
  // src/core/dev/runFile.ts
2829
3146
  import { unlink as unlink2 } from "fs/promises";
2830
- import { join as join15 } from "path";
3147
+ import { join as join17 } from "path";
2831
3148
  async function runFilePath(deps2, root) {
2832
- const dir2 = join15(deps2.config.dir, "run");
3149
+ const dir2 = join17(deps2.config.dir, "run");
2833
3150
  const name = `${await stateKey(deps2, root)}.json`;
2834
- return { dir: dir2, name, path: join15(dir2, name) };
3151
+ return { dir: dir2, name, path: join17(dir2, name) };
2835
3152
  }
2836
3153
  async function writeRunFile(deps2, root, rec) {
2837
3154
  const { dir: dir2, name } = await runFilePath(deps2, root);
@@ -2852,6 +3169,177 @@ async function leftoverRun(deps2, root, o = {}) {
2852
3169
  return null;
2853
3170
  }
2854
3171
 
3172
+ // src/cli/project.ts
3173
+ import { readdir as readdir2, stat as stat4 } from "fs/promises";
3174
+ import { join as join18 } from "path";
3175
+
3176
+ // src/core/projects/match.ts
3177
+ function readings(ref) {
3178
+ const s = ref.split("/").map((x) => x.trim());
3179
+ if (s.some((x) => x === "")) return [];
3180
+ if (s.length === 1) return [{ app: s[0] }];
3181
+ if (s.length === 2) return [{ app: s[0], module: s[1] }, { tenant: s[0], app: s[1] }];
3182
+ if (s.length === 3) return [{ tenant: s[0], app: s[1], module: s[2] }];
3183
+ return [];
3184
+ }
3185
+ function hit(p, w) {
3186
+ if (p.appSlug !== w.app || w.tenant !== void 0 && p.tenantSlug !== w.tenant) return null;
3187
+ if (p.kind === "module") return w.module === p.moduleSlug ? { project: p } : null;
3188
+ return w.module ? { project: p, module: w.module } : { project: p };
3189
+ }
3190
+ function matchProjects(projects, ref) {
3191
+ const out = [];
3192
+ for (const w of readings(ref)) {
3193
+ for (const p of projects) {
3194
+ const m = hit(p, w);
3195
+ if (m) out.push(m);
3196
+ }
3197
+ }
3198
+ return out;
3199
+ }
3200
+
3201
+ // src/cli/project.ts
3202
+ async function loggedInAs(ctx) {
3203
+ const cfg = await ctx.deps.config.read();
3204
+ const portal = resolvePortal(ctx.opts().portal, cfg.defaultProfile);
3205
+ const p = cfg.profiles[portal.apiOrigin];
3206
+ if (!p || !await new TokenStore(ctx.deps.keychain, profileHash(p.apiOrigin, p.issuer)).getRefresh()) return null;
3207
+ return p.email ?? p.sub ?? portal.apiOrigin;
3208
+ }
3209
+ var isDir = async (p) => (await stat4(p).catch(() => null))?.isDirectory() ?? false;
3210
+ var label2 = (p) => `${p.tenantSlug} / ${p.appSlug}${p.kind === "module" ? ` / ${p.moduleSlug}` : ""}${p.source === "registry" ? ` (${p.path})` : ""}`;
3211
+ var candidate = (p) => p.kind === "module" ? { kind: "module", id: p.moduleId, slug: p.moduleSlug, tenant: p.tenantSlug, name: p.path } : { kind: "app", slug: p.appSlug, tenant: p.tenantSlug, name: p.path };
3212
+ var withCwd = (ctx, cwd) => ({ ...ctx, deps: { ...ctx.deps, cwd } });
3213
+ async function inProject(ctx) {
3214
+ if (await findWorkspace(ctx.deps.cwd, ctx.deps.home)) return true;
3215
+ const repo = await findRepoRoot(ctx.deps.cwd);
3216
+ return repo !== null && !isInside(repo, ctx.deps.home);
3217
+ }
3218
+ async function moduleDirs(wsPath) {
3219
+ const out = [];
3220
+ for (const d of await readdir2(wsPath, { withFileTypes: true }).catch(() => [])) {
3221
+ if (d.isDirectory() && !d.name.startsWith(".") && await stat4(join18(wsPath, d.name, ".git")).catch(() => null)) out.push(d.name);
3222
+ }
3223
+ return out.sort();
3224
+ }
3225
+ async function pathFor(ctx, m, needModule) {
3226
+ const p = m.project;
3227
+ if (p.kind === "module") return p.path;
3228
+ if (m.module) {
3229
+ const dir2 = join18(p.path, m.module);
3230
+ if (!await isDir(dir2)) {
3231
+ throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no checkout of module ${m.module} in ${p.path}`, {
3232
+ hint: `Run \`qore sync\` in ${p.path} to clone the app's missing modules.`
3233
+ });
3234
+ }
3235
+ return dir2;
3236
+ }
3237
+ if (!needModule) return p.path;
3238
+ const dirs = await moduleDirs(p.path);
3239
+ if (dirs.length === 1) return join18(p.path, dirs[0]);
3240
+ if (dirs.length === 0) {
3241
+ throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no module checkouts in ${p.path}`, {
3242
+ hint: `Run \`qore sync\` in ${p.path}.`
3243
+ });
3244
+ }
3245
+ if (!ctx.io.interactive) {
3246
+ throw new QoreError("QORE_CONTEXT_REQUIRED", `${p.appSlug} has several modules \u2014 which one?`, {
3247
+ hint: `Pass ${p.appSlug}/<module>.`,
3248
+ candidates: dirs.map((slug) => ({ kind: "module", slug, tenant: p.tenantSlug, name: join18(p.path, slug) }))
3249
+ });
3250
+ }
3251
+ return join18(p.path, await ctx.io.select("Select a module", dirs.map((d) => ({ name: d, value: d }))));
3252
+ }
3253
+ async function liveMatches(matches) {
3254
+ const live = [];
3255
+ for (const m of matches) {
3256
+ if (m.project.kind === "workspace" && m.module && !await isDir(join18(m.project.path, m.module))) continue;
3257
+ live.push(m);
3258
+ }
3259
+ return live.length > 0 ? live : matches;
3260
+ }
3261
+ async function choose(ctx, matches, what, explicit) {
3262
+ if (explicit && matches.length === 1) return matches[0];
3263
+ if (!ctx.io.interactive) {
3264
+ throw new QoreError("QORE_CONTEXT_REQUIRED", what, {
3265
+ hint: "Pass a more specific reference: tenant/app or app/module.",
3266
+ candidates: matches.map((m) => candidate(m.project))
3267
+ });
3268
+ }
3269
+ const i = await ctx.io.select("Select a project", matches.map((m, idx) => ({
3270
+ name: m.module ? `${label2(m.project)} / ${m.module}` : label2(m.project),
3271
+ value: idx
3272
+ })));
3273
+ return matches[i];
3274
+ }
3275
+ async function projectCtx(ctx, o) {
3276
+ const g = ctx.opts();
3277
+ const cfg = await ctx.deps.config.read();
3278
+ const portal = g.portal ? resolvePortal(g.portal, cfg.defaultProfile).apiOrigin : void 0;
3279
+ if (o.ref) {
3280
+ const matches = matchProjects(await knownProjects(ctx.deps, { portal }), o.ref);
3281
+ if (matches.length === 0) {
3282
+ const parts = o.ref.split("/");
3283
+ const app = parts.length === 1 ? o.ref : parts.length === 3 ? parts[1] : "<app>";
3284
+ throw new QoreError("QORE_PROJECT_NOT_FOUND", `No local project "${o.ref}"`, {
3285
+ hint: cfg.projectsRoot ? `Run \`qore clone ${app}\`.` : `Run \`qore init\`, or \`qore clone ${app}\`.`
3286
+ });
3287
+ }
3288
+ const m2 = await choose(ctx, await liveMatches(matches), `"${o.ref}" matches several projects`, true);
3289
+ return relocate(ctx, await pathFor(ctx, m2, o.needModule));
3290
+ }
3291
+ if (o.module || o.app || await inProject(ctx)) return ctx;
3292
+ const known = await knownProjects(ctx.deps, { portal });
3293
+ if (known.length === 0) {
3294
+ if (!cfg.projectsRoot) {
3295
+ if (!await loggedInAs(ctx)) return ctx;
3296
+ throw new QoreError("QORE_ROOT_NOT_SET", "Not inside a project, and qore knows no projects yet", {
3297
+ hint: "Run `qore init` to choose a projects folder, or cd into a project."
3298
+ });
3299
+ }
3300
+ throw new QoreError("QORE_PROJECT_NOT_FOUND", `Not inside a project, and ${cfg.projectsRoot} holds none yet`, {
3301
+ hint: "Run `qore clone <app>`."
3302
+ });
3303
+ }
3304
+ const m = await choose(ctx, known.map((project) => ({ project })), "Not inside a project \u2014 which one?", false);
3305
+ return relocate(ctx, await pathFor(ctx, m, o.needModule));
3306
+ }
3307
+ function relocate(ctx, path) {
3308
+ if (path === ctx.deps.cwd) return ctx;
3309
+ if (!ctx.opts().json) ctx.io.err(`\u2192 in ${path}`);
3310
+ return withCwd(ctx, path);
3311
+ }
3312
+ async function rememberContext(ctx, mc, app) {
3313
+ const tenantSlug = mc.module.tenant.slug;
3314
+ if (mc.workspace && mc.workspace.appId === app.id) {
3315
+ await remember(ctx.deps, {
3316
+ kind: "workspace",
3317
+ path: mc.workspace.root,
3318
+ portal: mc.workspace.portal,
3319
+ tenantSlug,
3320
+ appId: app.id,
3321
+ appSlug: app.slug
3322
+ });
3323
+ return;
3324
+ }
3325
+ if (mc.repoRoot) {
3326
+ await remember(ctx.deps, {
3327
+ kind: "module",
3328
+ path: mc.repoRoot,
3329
+ portal: mc.portal.apiOrigin,
3330
+ tenantSlug,
3331
+ appId: app.id,
3332
+ appSlug: app.slug,
3333
+ moduleId: mc.module.id,
3334
+ moduleSlug: mc.module.slug
3335
+ });
3336
+ }
3337
+ }
3338
+ async function rememberWorkspace(ctx, ws, app) {
3339
+ if (ws.appId !== app.id) return;
3340
+ await remember(ctx.deps, { kind: "workspace", path: ws.root, portal: ws.portal, tenantSlug: app.tenant.slug, appId: app.id, appSlug: app.slug });
3341
+ }
3342
+
2855
3343
  // src/cli/context.ts
2856
3344
  var allOfKind = (c, kind) => !!c && c.length > 0 && c.every((x) => x.kind === kind && typeof x.id === "string");
2857
3345
  async function moduleContextFor(ctx, flags, command) {
@@ -2863,11 +3351,11 @@ async function moduleContextFor(ctx, flags, command) {
2863
3351
  } catch (e) {
2864
3352
  if (!(e instanceof QoreError) || e.code !== "QORE_CONTEXT_REQUIRED" || !ctx.io.interactive) throw e;
2865
3353
  if (allOfKind(e.candidates, "module")) {
2866
- const choose = await ctx.io.select(
3354
+ const choose2 = await ctx.io.select(
2867
3355
  "Select a module",
2868
3356
  e.candidates.map((c) => ({ name: `${c.tenant ? `${c.tenant} / ` : ""}${c.slug} (${c.remote})`, value: c.id }))
2869
3357
  );
2870
- return resolveModuleContext(ctx.deps, { ...base, choose });
3358
+ return resolveModuleContext(ctx.deps, { ...base, choose: choose2 });
2871
3359
  }
2872
3360
  if (allOfKind(e.candidates, "tenant")) {
2873
3361
  const tenant = await ctx.io.select("Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.id })));
@@ -3075,9 +3563,9 @@ async function ensureTrusted(ctx, mc, app, o) {
3075
3563
  }
3076
3564
 
3077
3565
  // src/cli/commands/devWorkspace.ts
3078
- import { randomBytes as randomBytes4 } from "crypto";
3079
- import { realpath as realpath4 } from "fs/promises";
3080
- import { join as join17, relative as relative5 } from "path";
3566
+ import { randomBytes as randomBytes5 } from "crypto";
3567
+ import { realpath as realpath7 } from "fs/promises";
3568
+ import { join as join20, relative as relative6 } from "path";
3081
3569
 
3082
3570
  // src/core/dev/order.ts
3083
3571
  function selectSkips(modules, sel) {
@@ -3182,8 +3670,8 @@ function planDev(modules, sel) {
3182
3670
  }
3183
3671
  const order = sccs.map((_, i) => i).sort((a, b) => minSlug[a].localeCompare(minSlug[b]));
3184
3672
  for (const i of order) visit(i);
3185
- const start = [];
3186
- for (const i of orderedSccIndices) start.push(...sccSlugs[i]);
3673
+ const start2 = [];
3674
+ for (const i of orderedSccIndices) start2.push(...sccSlugs[i]);
3187
3675
  const waitFor = {};
3188
3676
  for (const m of starting) {
3189
3677
  const mySccId = sccOf.get(m.id);
@@ -3198,7 +3686,7 @@ function planDev(modules, sel) {
3198
3686
  missingProviders[m.slug] = [...new Set(missing)].sort();
3199
3687
  }
3200
3688
  }
3201
- return { start, waitFor, cycles, skipped, missingProviders };
3689
+ return { start: start2, waitFor, cycles, skipped, missingProviders };
3202
3690
  }
3203
3691
 
3204
3692
  // src/core/dev/supervisor.ts
@@ -3234,15 +3722,15 @@ function supervise(o) {
3234
3722
  });
3235
3723
  const readiness = /* @__PURE__ */ new Map();
3236
3724
  for (const m of o.modules) {
3237
- let resolve5;
3725
+ let resolve6;
3238
3726
  let settled = false;
3239
3727
  const promise = new Promise((r) => {
3240
- resolve5 = r;
3728
+ resolve6 = r;
3241
3729
  });
3242
3730
  readiness.set(m.slug, { promise, settle: (r) => {
3243
3731
  if (settled) return false;
3244
3732
  settled = true;
3245
- resolve5(r);
3733
+ resolve6(r);
3246
3734
  return true;
3247
3735
  } });
3248
3736
  }
@@ -3306,7 +3794,7 @@ function supervise(o) {
3306
3794
  checkDone();
3307
3795
  });
3308
3796
  };
3309
- const start = async (m) => {
3797
+ const start2 = async (m) => {
3310
3798
  const providers = m.waitFor.filter((p) => readiness.has(p));
3311
3799
  const results = await Promise.all(providers.map((p) => readiness.get(p).promise));
3312
3800
  if (stopCode !== null) return;
@@ -3316,7 +3804,7 @@ function supervise(o) {
3316
3804
  });
3317
3805
  launch(m);
3318
3806
  };
3319
- for (const m of o.modules) void start(m);
3807
+ for (const m of o.modules) void start2(m);
3320
3808
  checkDone();
3321
3809
  return {
3322
3810
  done,
@@ -3333,12 +3821,12 @@ function supervise(o) {
3333
3821
  }
3334
3822
 
3335
3823
  // src/core/workspace/stray.ts
3336
- import { lstat as lstat2, readdir } from "fs/promises";
3337
- import { join as join16 } from "path";
3824
+ import { lstat as lstat2, readdir as readdir3 } from "fs/promises";
3825
+ import { join as join19 } from "path";
3338
3826
  async function strayRepos(root, slugs) {
3339
3827
  let entries;
3340
3828
  try {
3341
- entries = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
3829
+ entries = (await readdir3(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
3342
3830
  } catch {
3343
3831
  return [];
3344
3832
  }
@@ -3346,7 +3834,7 @@ async function strayRepos(root, slugs) {
3346
3834
  for (const name of entries) {
3347
3835
  if (slugs.has(name)) continue;
3348
3836
  try {
3349
- await lstat2(join16(root, name, ".git"));
3837
+ await lstat2(join19(root, name, ".git"));
3350
3838
  names.push(name);
3351
3839
  } catch {
3352
3840
  }
@@ -3386,16 +3874,18 @@ function prefixer(slugs, color) {
3386
3874
  import { constants as constants2 } from "os";
3387
3875
  var childRunning = { current: false };
3388
3876
  function registerRun(program, ctx) {
3389
- 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) => {
3390
- const { deps: deps2 } = ctx;
3877
+ 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) => {
3391
3878
  if (ctx.opts().json) {
3392
3879
  throw new QoreError("QORE_CONTEXT_INVALID", "run has no --json output", {
3393
3880
  hint: "Drop --json; the child's own output passes through."
3394
3881
  });
3395
3882
  }
3396
- const mc = await moduleContextFor(ctx, f, "run");
3397
- const app = await appFor(ctx, mc, f.app);
3398
- await ensureTrusted(ctx, mc, app, { yes: f.yes });
3883
+ const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true });
3884
+ const { deps: deps2 } = c;
3885
+ const mc = await moduleContextFor(c, f, "run");
3886
+ const app = await appFor(c, mc, f.app);
3887
+ await rememberContext(c, mc, app);
3888
+ await ensureTrusted(c, mc, app, { yes: f.yes });
3399
3889
  const local = await fetchLocalEnv(deps2, mc, app);
3400
3890
  childRunning.current = true;
3401
3891
  let exit;
@@ -3429,7 +3919,9 @@ async function workspaceContext(ctx, ws) {
3429
3919
  const session = await openSession(deps2, portal, "dev", requestLog(ctx));
3430
3920
  await ensureCapability(deps2, portal.apiOrigin, "app.modules");
3431
3921
  await ensureCapability(deps2, portal.apiOrigin, "module.local-env");
3432
- return { am: await workspaceAppModules(session, portal, ws), online: { portal, session }, offlineCause: null };
3922
+ const am = await workspaceAppModules(session, portal, ws);
3923
+ await rememberWorkspace(ctx, ws, am.app);
3924
+ return { am, online: { portal, session }, offlineCause: null };
3433
3925
  } catch (e) {
3434
3926
  if (!isOffline(e)) throw e;
3435
3927
  const last = await loadLastStart(deps2, ws.root);
@@ -3504,7 +3996,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3504
3996
  continue;
3505
3997
  }
3506
3998
  try {
3507
- dirs[m.slug] = await deps2.git.inspect(join17(ws.root, m.slug));
3999
+ dirs[m.slug] = await deps2.git.inspect(join20(ws.root, m.slug));
3508
4000
  safe.push(m);
3509
4001
  } catch (e) {
3510
4002
  if (!(e instanceof QoreError) || e.code === "QORE_GIT_UNAVAILABLE") throw e;
@@ -3541,7 +4033,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3541
4033
  const results = await applyActions(deps2, { portal: online.portal, app: am, actions, protocol: cloneProtocol(dirs), onResult: () => {
3542
4034
  } });
3543
4035
  renderResults(io2, results, false);
3544
- for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(join17(ws.root, r.slug));
4036
+ for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(join20(ws.root, r.slug));
3545
4037
  ({ present, plan } = computePlan());
3546
4038
  }
3547
4039
  for (const name of await strayRepos(ws.root, new Set(am.modules.map((m) => m.slug)))) renderFinding(ctx, strayClone(name, am.app.name));
@@ -3649,7 +4141,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3649
4141
  workspace: ws,
3650
4142
  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] }
3651
4143
  });
3652
- const rootReal = await realpath4(ws.root);
4144
+ const rootReal = await realpath7(ws.root);
3653
4145
  const withDocker = dockerRecovery(ctx, p.signal);
3654
4146
  for (const u of units.filter((x) => x.recipe && x.trusted)) {
3655
4147
  const slug = u.m.slug;
@@ -3662,7 +4154,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3662
4154
  type: u.m.type,
3663
4155
  moduleId: u.m.id,
3664
4156
  cfg: u.cfg,
3665
- random: () => randomBytes4(32).toString("hex")
4157
+ random: () => randomBytes5(32).toString("hex")
3666
4158
  });
3667
4159
  } catch (e) {
3668
4160
  if (offlineCause && isOffline(e)) throw offlineCause;
@@ -3672,7 +4164,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3672
4164
  const dev = u.dev;
3673
4165
  if (!dev) continue;
3674
4166
  if (online && dev.offline) {
3675
- warn(`${slug}: the portal is unreachable \u2014 starting from ${relative5(rootReal, dev.envFile)} as last written; values may be stale.`, { module: slug });
4167
+ warn(`${slug}: the portal is unreachable \u2014 starting from ${relative6(rootReal, dev.envFile)} as last written; values may be stale.`, { module: slug });
3676
4168
  }
3677
4169
  if (dev.kept.length) {
3678
4170
  warn(
@@ -3680,7 +4172,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3680
4172
  { module: slug, keys: dev.kept }
3681
4173
  );
3682
4174
  }
3683
- report({ module: slug, step: "env", status: "done", detail: relative5(rootReal, dev.envFile) });
4175
+ report({ module: slug, step: "env", status: "done", detail: relative6(rootReal, dev.envFile) });
3684
4176
  const services = servicesMissing(slug, dev.env, u.recipe, { servicesFlag: f.services, configured: u.cfg.dev?.services !== void 0 });
3685
4177
  if (services) renderFinding(ctx, services);
3686
4178
  try {
@@ -3722,7 +4214,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3722
4214
  if (online && units.every((u) => !u.dev.offline)) {
3723
4215
  const envFile = (m) => {
3724
4216
  const u = units.find((x) => x.m.id === m.id);
3725
- return u ? relative5(rootReal, u.dev.envFile) : join17(m.slug, moduleDefaults(m.type).envFile);
4217
+ return u ? relative6(rootReal, u.dev.envFile) : join20(m.slug, moduleDefaults(m.type).envFile);
3726
4218
  };
3727
4219
  await saveLastStart(deps2, ws.root, {
3728
4220
  schemaVersion: 1,
@@ -3911,11 +4403,17 @@ function makeStderrTee(sink) {
3911
4403
  };
3912
4404
  }
3913
4405
  function registerDev(program, ctx) {
3914
- 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) => {
4406
+ 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) => {
3915
4407
  const waitTimeout = parseWaitTimeout(f.waitTimeout);
3916
- const ws = await workspaceRoot(ctx.deps);
3917
- if (ws && f.module === void 0) return workspaceDev(ctx, ws, f, waitTimeout);
3918
- return moduleDev(ctx, f, waitTimeout);
4408
+ if (project !== void 0 && f.project !== void 0 && project !== f.project) {
4409
+ throw new QoreError("QORE_CONTEXT_INVALID", `Pass the project once: "${project}" and --project "${f.project}" differ`, {
4410
+ hint: "qore dev <project> | qore dev --project <project>"
4411
+ });
4412
+ }
4413
+ const c = await projectCtx(ctx, { ref: project ?? f.project, module: f.module, app: f.app, needModule: false });
4414
+ const ws = await workspaceRoot(c.deps);
4415
+ if (ws && f.module === void 0) return workspaceDev(c, ws, f, waitTimeout);
4416
+ return moduleDev(c, f, waitTimeout);
3919
4417
  });
3920
4418
  }
3921
4419
  function parseWaitTimeout(raw) {
@@ -3955,7 +4453,7 @@ async function providerFindings(ctx, root, mc, app, env) {
3955
4453
  let fix = `qore clone ${app.slug}`;
3956
4454
  if (ws && ws.appId === app.id) fix = cdAndDev(deps2.home, ws.root);
3957
4455
  else if (providers.length === 1) {
3958
- const sibling = join18(dirname6(root), providers[0].slug);
4456
+ const sibling = join21(dirname7(root), providers[0].slug);
3959
4457
  if ((await deps2.git.inspect(sibling)).isRepo) fix = cdAndDev(deps2.home, sibling);
3960
4458
  }
3961
4459
  return providerDown({
@@ -3989,6 +4487,7 @@ async function devContext(ctx, f, root) {
3989
4487
  return { online: null, module: last.module, offlineCause: e };
3990
4488
  }
3991
4489
  const app = await appFor(ctx, mc, f.app);
4490
+ await rememberContext(ctx, mc, app);
3992
4491
  await ensureTrusted(ctx, mc, app, { yes: f.yes });
3993
4492
  return { online: { mc, app }, module: mc.module, offlineCause: null };
3994
4493
  }
@@ -4084,13 +4583,13 @@ async function prepareModule(ctx, f, waitTimeout, p) {
4084
4583
  type: c.module.type,
4085
4584
  moduleId: c.module.id,
4086
4585
  cfg,
4087
- random: () => randomBytes5(32).toString("hex")
4586
+ random: () => randomBytes6(32).toString("hex")
4088
4587
  });
4089
4588
  } catch (e) {
4090
4589
  if (c.offlineCause && isOffline(e)) throw c.offlineCause;
4091
4590
  throw e;
4092
4591
  }
4093
- const envFile = relative6(await realpath5(root), dev.envFile);
4592
+ const envFile = relative7(await realpath8(root), dev.envFile);
4094
4593
  if (dev.offline) io2.err(`! The portal is unreachable \u2014 starting from ${envFile} as last written; values may be stale.`);
4095
4594
  if (dev.kept.length) {
4096
4595
  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`);
@@ -4136,12 +4635,20 @@ async function prepareModule(ctx, f, waitTimeout, p) {
4136
4635
  return { root, slug, recipe, env, port: dev.port, pkg, devEnv: dev.env };
4137
4636
  }
4138
4637
 
4638
+ // src/cli/commands/debugTelemetry.ts
4639
+ function registerDebugTelemetry(program, ctx) {
4640
+ program.command("debug-telemetry", { hidden: true }).description("Send a test crash report").option("--crash", "throw an unexpected error").action((o) => {
4641
+ if (o.crash) throw new Error(`qore debug-telemetry crash in ${ctx.deps.home} token=debug-telemetry-secret`);
4642
+ if (ctx.opts().json) renderJson(ctx.io, { crash: false });
4643
+ });
4644
+ }
4645
+
4139
4646
  // src/cli/commands/doctor.ts
4140
4647
  import pc3 from "picocolors";
4141
4648
 
4142
4649
  // src/core/doctor/checks.ts
4143
- import { realpath as realpath6 } from "fs/promises";
4144
- import { basename as basename3, join as join19 } from "path";
4650
+ import { realpath as realpath9, stat as stat5 } from "fs/promises";
4651
+ import { basename as basename3, join as join22 } from "path";
4145
4652
 
4146
4653
  // src/core/diagnostics/ssh.ts
4147
4654
  var SSH_TEST_ARGS = ["-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "git@github.com"];
@@ -4186,8 +4693,8 @@ var UPGRADE = "npm i -g @quatronic/qore";
4186
4693
  var RANK = { ok: 0, warn: 1, fail: 2 };
4187
4694
  var noop = () => {
4188
4695
  };
4189
- var ok = (label2, details) => ({ status: "ok", label: label2, ...details ? { details } : {} });
4190
- var withError = (status2, label2, e) => ({ status: status2, label: label2, error: toJsonError(e) });
4696
+ var ok = (label3, details) => ({ status: "ok", label: label3, ...details ? { details } : {} });
4697
+ var withError = (status2, label3, e) => ({ status: status2, label: label3, error: toJsonError(e) });
4191
4698
  var isCancel = (e, signal) => e instanceof CancelledError || signal.aborted;
4192
4699
  var portalLabel = (p) => p.builtin ?? p.apiOrigin;
4193
4700
  var ToolTimeout = class extends Error {
@@ -4323,7 +4830,7 @@ function ownerText(e) {
4323
4830
  if (d.owner === "qore") return ` by a previous qore dev (pid ${String(d.pid)})`;
4324
4831
  return "";
4325
4832
  }
4326
- var sameDir = async (a, b) => await realpath6(a).catch(() => a) === await realpath6(b).catch(() => b);
4833
+ var sameDir = async (a, b) => await realpath9(a).catch(() => a) === await realpath9(b).catch(() => b);
4327
4834
  async function moduleBody(deps2, m, runs, signal) {
4328
4835
  const parts = [];
4329
4836
  const part = async (fn) => {
@@ -4421,6 +4928,21 @@ async function resolveRepoModule(deps2, session, portal, meta, root) {
4421
4928
  }
4422
4929
  return session.api.get(`/api/cli/v1/modules/${encodeURIComponent(ids[0])}`);
4423
4930
  }
4931
+ async function checkProjectsRoot(cfg) {
4932
+ const root = cfg?.projectsRoot;
4933
+ if (!root) return ok("projects folder not set (optional \u2014 qore init)");
4934
+ const s = await stat5(root).catch(() => null);
4935
+ if (!s?.isDirectory()) return { status: "warn", label: `projects folder ${root} does not exist \u2014 qore init` };
4936
+ const e = await writableError(root);
4937
+ if (e) return { status: "fail", label: `projects folder ${root} is not writable (${e.code ?? e.message})` };
4938
+ return ok(`projects folder ${root}`);
4939
+ }
4940
+ function checkTelemetry(deps2, cfg) {
4941
+ const built = deps2.sentryDsn !== "";
4942
+ if (!cfg) return ok("crash reports off (config unreadable)", { enabled: false, source: "config", reason: "unreadable", built });
4943
+ const consent = resolveConsent(deps2.env, cfg);
4944
+ return ok(describeConsent(consent, built), { enabled: consent.enabled, source: consent.source, built });
4945
+ }
4424
4946
  async function runDoctor(deps2, o = {}) {
4425
4947
  const signal = o.signal ?? new AbortController().signal;
4426
4948
  const toolMs = o.toolTimeoutMs ?? TOOL_TIMEOUT_MS;
@@ -4440,12 +4962,14 @@ async function runDoctor(deps2, o = {}) {
4440
4962
  return r;
4441
4963
  };
4442
4964
  let cfg;
4965
+ let cfgReadable = true;
4443
4966
  try {
4444
4967
  cfg = await deps2.config.read();
4445
4968
  } catch (e) {
4446
4969
  if (isCancel(e, signal)) throw e;
4447
4970
  await push("config", "tools", () => withError("fail", errorLabel(e), e));
4448
4971
  cfg = { schemaVersion: 1, profiles: {} };
4972
+ cfgReadable = false;
4449
4973
  }
4450
4974
  let ws = null;
4451
4975
  let wsError = null;
@@ -4467,6 +4991,8 @@ async function runDoctor(deps2, o = {}) {
4467
4991
  await push("git", "tools", () => checkGit(deps2, signal, toolMs));
4468
4992
  await push("github-ssh", "tools", () => checkGithubSsh(deps2, signal, toolMs));
4469
4993
  await push("docker", "tools", () => checkDocker(deps2, signal, toolMs));
4994
+ await push("telemetry", "tools", () => checkTelemetry(deps2, cfgReadable ? cfg : null));
4995
+ await push("projects-root", "tools", () => checkProjectsRoot(cfgReadable ? cfg : null));
4470
4996
  const sessions = /* @__PURE__ */ new Map();
4471
4997
  let loggedIn = [];
4472
4998
  try {
@@ -4559,7 +5085,7 @@ async function workspaceModule(deps2, ws, am, m, runs, signal, push, perModule)
4559
5085
  await push(`module:${m.slug}`, "workspace", () => ({ status: "warn", label: "not a safe directory name \u2014 skipped" }), m.slug);
4560
5086
  return;
4561
5087
  }
4562
- const mod = { root: join19(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
5088
+ const mod = { root: join22(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
4563
5089
  let prepared = null;
4564
5090
  await push(`module:${m.slug}`, "workspace", async () => {
4565
5091
  const info = await deps2.git.inspect(mod.root);
@@ -4589,26 +5115,28 @@ function registerDoctor(program, ctx) {
4589
5115
  }
4590
5116
 
4591
5117
  // src/cli/commands/env.ts
4592
- import { randomBytes as randomBytes6 } from "crypto";
4593
- import { chmod as chmod3, realpath as realpath7 } from "fs/promises";
4594
- import { dirname as dirname7, join as join20, relative as relative7 } from "path";
5118
+ import { randomBytes as randomBytes7 } from "crypto";
5119
+ import { chmod as chmod3, realpath as realpath10 } from "fs/promises";
5120
+ import { dirname as dirname8, join as join23, relative as relative8 } from "path";
4595
5121
  var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.length} chars)`;
4596
5122
  function registerEnv(program, ctx) {
4597
5123
  const env = program.command("env").description("Local environment");
4598
- 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) => {
4599
- const { deps: deps2, io: io2 } = ctx;
4600
- const g = ctx.opts();
4601
- const mc = await moduleContextFor(ctx, f, "env pull");
5124
+ env.command("pull").description("Write this module's local secrets into its env file").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "the project to pull for, from anywhere (see qore init)").option("--target <path>", "env file to write (inside the repository)").option("--use-portal", "take the portal's value for keys that differ locally").option("--yes", "trust this repository without asking").action(async (f) => {
5125
+ const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true });
5126
+ const { deps: deps2, io: io2 } = c;
5127
+ const g = c.opts();
5128
+ const mc = await moduleContextFor(c, f, "env pull");
4602
5129
  if (!mc.repoRoot) {
4603
5130
  throw new QoreError("QORE_CONTEXT_REQUIRED", "env pull writes into a module repository", { hint: "cd into the module's repository." });
4604
5131
  }
4605
- const app = await appFor(ctx, mc, f.app);
5132
+ const app = await appFor(c, mc, f.app);
5133
+ await rememberContext(c, mc, app);
4606
5134
  const cfg = await readRepoConfig(mc.repoRoot);
4607
5135
  const defaults = moduleDefaults(mc.module.type);
4608
5136
  const target = f.target ? await resolveTarget(mc.repoRoot, f.target, deps2.cwd) : await resolveTarget(mc.repoRoot, cfg.envFile ?? defaults.envFile);
4609
- await ensureTrusted(ctx, mc, app, { yes: f.yes });
5137
+ await ensureTrusted(c, mc, app, { yes: f.yes });
4610
5138
  const existing = await readOrNull(target);
4611
- const example = existing === null ? await readOrNull(join20(dirname7(target), ".env.example")) : null;
5139
+ const example = existing === null ? await readOrNull(join23(dirname8(target), ".env.example")) : null;
4612
5140
  const local = await fetchLocalEnv(deps2, mc, app);
4613
5141
  const inputs = {
4614
5142
  existing,
@@ -4617,7 +5145,7 @@ function registerEnv(program, ctx) {
4617
5145
  managed: local.managedKeys,
4618
5146
  localSecrets: cfg.localSecrets ?? defaults.localSecrets,
4619
5147
  defaultChoice: f.usePortal ? "use-portal" : "keep",
4620
- random: () => randomBytes6(32).toString("hex")
5148
+ random: () => randomBytes7(32).toString("hex")
4621
5149
  };
4622
5150
  let plan = planPull(inputs);
4623
5151
  if (plan.conflicts.length && io2.interactive && !f.usePortal) {
@@ -4649,7 +5177,7 @@ function registerEnv(program, ctx) {
4649
5177
  } else {
4650
5178
  await writeSecurely(target, plan.text);
4651
5179
  }
4652
- const rel = relative7(await realpath7(mc.repoRoot), target);
5180
+ const rel = relative8(await realpath10(mc.repoRoot), target);
4653
5181
  if (g.json) {
4654
5182
  const names = {
4655
5183
  created: plan.created,
@@ -4678,6 +5206,9 @@ function registerEnv(program, ctx) {
4678
5206
  });
4679
5207
  }
4680
5208
 
5209
+ // src/cli/commands/init.ts
5210
+ import { realpath as realpath11 } from "fs/promises";
5211
+
4681
5212
  // src/core/auth/oidc.ts
4682
5213
  import * as oc from "openid-client";
4683
5214
  var LOGIN_SCOPE = "openid profile email offline_access qore:read qore:local-env";
@@ -4745,9 +5276,9 @@ async function createOidc(opts) {
4745
5276
  throw mapOidcError(e, host);
4746
5277
  }
4747
5278
  },
4748
- async pollDevice(start, signal) {
5279
+ async pollDevice(start2, signal) {
4749
5280
  try {
4750
- const t = await oc.pollDeviceAuthorizationGrant(config, start.handle, void 0, { signal });
5281
+ const t = await oc.pollDeviceAuthorizationGrant(config, start2.handle, void 0, { signal });
4751
5282
  return toTokenSet(t);
4752
5283
  } catch (e) {
4753
5284
  throw mapOidcError(e, host);
@@ -4774,14 +5305,14 @@ async function createOidc(opts) {
4774
5305
  // src/core/auth/login.ts
4775
5306
  async function deviceLogin(opts) {
4776
5307
  await opts.store.getRefresh();
4777
- const start = await opts.oidc.startDevice(LOGIN_SCOPE);
5308
+ const start2 = await opts.oidc.startDevice(LOGIN_SCOPE);
4778
5309
  const stopPoll = new AbortController();
4779
5310
  const settled = new AbortController();
4780
- const poll = opts.oidc.pollDevice(start, AbortSignal.any([opts.signal, stopPoll.signal])).finally(() => settled.abort());
5311
+ const poll = opts.oidc.pollDevice(start2, AbortSignal.any([opts.signal, stopPoll.signal])).finally(() => settled.abort());
4781
5312
  poll.catch(() => {
4782
5313
  });
4783
5314
  try {
4784
- await opts.onCode(start, settled.signal);
5315
+ await opts.onCode(start2, settled.signal);
4785
5316
  } catch (e) {
4786
5317
  stopPoll.abort();
4787
5318
  const late = await poll.catch(() => null);
@@ -4809,78 +5340,201 @@ async function deviceLogin(opts) {
4809
5340
  }
4810
5341
 
4811
5342
  // src/cli/commands/login.ts
5343
+ var NO_TENANTS = "Your account is not a developer in any organisation. Ask an admin to invite you as developer.";
5344
+ async function loginFlow(ctx, cmd) {
5345
+ const { deps: deps2, io: io2, signal } = ctx;
5346
+ const g = ctx.opts();
5347
+ const cfg = await deps2.config.read();
5348
+ const portal = resolvePortal(g.portal, cfg.defaultProfile);
5349
+ const meta = await fetchMeta(deps2.fetch, portal.apiOrigin);
5350
+ const known = cfg.profiles[portal.apiOrigin]?.issuer;
5351
+ const pinned = { ...portal, pinnedIssuer: portal.pinnedIssuer ?? known ?? null };
5352
+ if (!pinned.pinnedIssuer && portal.builtin === null) {
5353
+ io2.err(`! Logging in to ${portal.apiOrigin}, identity provider ${meta.auth.issuer}`);
5354
+ }
5355
+ negotiate(meta, pinned, deps2.version);
5356
+ const issuer = pinned.pinnedIssuer ?? meta.auth.issuer;
5357
+ const hash = profileHash(portal.apiOrigin, issuer);
5358
+ const store = new TokenStore(deps2.keychain, hash);
5359
+ const t = await deviceLogin({
5360
+ oidc: await deps2.oidcFor(issuer),
5361
+ store,
5362
+ now: deps2.now,
5363
+ signal,
5364
+ withLock: deps2.withLock,
5365
+ lockName: hash,
5366
+ onCode: async (s, settled) => {
5367
+ const url = s.verificationUriComplete ?? s.verificationUri;
5368
+ io2.err(`! First copy your one-time code: ${s.userCode}`);
5369
+ if (io2.interactive && cmd.browser) {
5370
+ try {
5371
+ await io2.waitForEnter(` Press Enter to open ${url} (or open it yourself)`, AbortSignal.any([signal, settled]));
5372
+ } catch (e) {
5373
+ if (settled.aborted && !signal.aborted) return;
5374
+ throw e;
5375
+ }
5376
+ if (settled.aborted) return;
5377
+ if (!await io2.openUrl(url)) io2.err(` Open ${url} in a browser`);
5378
+ } else {
5379
+ io2.err(` Open ${url} in a browser`);
5380
+ }
5381
+ }
5382
+ });
5383
+ await deps2.config.update((c) => {
5384
+ c.profiles[portal.apiOrigin] = {
5385
+ ...c.profiles[portal.apiOrigin],
5386
+ apiOrigin: portal.apiOrigin,
5387
+ issuer,
5388
+ sub: t.sub,
5389
+ email: t.email,
5390
+ meta: { fetchedAt: deps2.now().toISOString(), doc: meta }
5391
+ };
5392
+ if (g.portal || !c.defaultProfile) c.defaultProfile = portal.apiOrigin;
5393
+ });
5394
+ const { api } = await openSession(deps2, { ...portal, pinnedIssuer: issuer }, "login", requestLog(ctx));
5395
+ const me = await api.get("/api/cli/v1/me");
5396
+ const prev = cfg.profiles[portal.apiOrigin];
5397
+ let active = prev?.sub === t.sub && prev.activeTenant && me.tenants.some((x) => x.id === prev.activeTenant.id) ? prev.activeTenant : void 0;
5398
+ if (active) {
5399
+ } else if (me.tenants.length === 0) {
5400
+ io2.err(`! ${NO_TENANTS}`);
5401
+ } else if (me.tenants.length === 1) {
5402
+ active = { id: me.tenants[0].id, slug: me.tenants[0].slug };
5403
+ io2.err(`\u2713 Using tenant ${me.tenants[0].name}`);
5404
+ } else if (io2.interactive) {
5405
+ active = await io2.select("Select a tenant", me.tenants.map((x) => ({ name: `${x.name} ${x.role}`, value: { id: x.id, slug: x.slug } })));
5406
+ } else {
5407
+ io2.err(`! You belong to ${me.tenants.length} tenants; choose one with \`qore tenant use <slug>\``);
5408
+ }
5409
+ await deps2.config.update((c) => {
5410
+ c.profiles[portal.apiOrigin].activeTenant = active;
5411
+ });
5412
+ const label3 = portal.builtin ?? portal.apiOrigin;
5413
+ if (g.json) renderJson(io2, { portal: label3, email: me.email, activeTenant: active ?? null });
5414
+ else io2.out(`\u2713 Logged in as ${me.email} (${label3})`);
5415
+ }
4812
5416
  function registerLogin(program, ctx) {
4813
- program.command("login").description("Log in with your Qore account").option("--no-browser", "only print the URL").action(async (cmd) => {
4814
- const { deps: deps2, io: io2, signal } = ctx;
5417
+ program.command("login").description("Log in with your Qore account").option("--no-browser", "only print the URL").action(async (cmd) => loginFlow(ctx, cmd));
5418
+ }
5419
+
5420
+ // src/cli/commands/tenant.ts
5421
+ function registerTenant(program, ctx) {
5422
+ const tenant = program.command("tenant").description("Manage the active tenant");
5423
+ tenant.command("list").description("List tenants you have access to").action(async () => {
5424
+ const { deps: deps2, io: io2 } = ctx;
4815
5425
  const g = ctx.opts();
4816
5426
  const cfg = await deps2.config.read();
4817
5427
  const portal = resolvePortal(g.portal, cfg.defaultProfile);
4818
- const meta = await fetchMeta(deps2.fetch, portal.apiOrigin);
4819
- const known = cfg.profiles[portal.apiOrigin]?.issuer;
4820
- const pinned = { ...portal, pinnedIssuer: portal.pinnedIssuer ?? known ?? null };
4821
- if (!pinned.pinnedIssuer && portal.builtin === null) {
4822
- io2.err(`! Logging in to ${portal.apiOrigin}, identity provider ${meta.auth.issuer}`);
4823
- }
4824
- negotiate(meta, pinned, deps2.version);
4825
- const issuer = pinned.pinnedIssuer ?? meta.auth.issuer;
4826
- const hash = profileHash(portal.apiOrigin, issuer);
4827
- const store = new TokenStore(deps2.keychain, hash);
4828
- const t = await deviceLogin({
4829
- oidc: await deps2.oidcFor(issuer),
4830
- store,
4831
- now: deps2.now,
4832
- signal,
4833
- withLock: deps2.withLock,
4834
- lockName: hash,
4835
- onCode: async (s, settled) => {
4836
- const url = s.verificationUriComplete ?? s.verificationUri;
4837
- io2.err(`! First copy your one-time code: ${s.userCode}`);
4838
- if (io2.interactive && cmd.browser) {
4839
- try {
4840
- await io2.waitForEnter(` Press Enter to open ${url} (or open it yourself)`, AbortSignal.any([signal, settled]));
4841
- } catch (e) {
4842
- if (settled.aborted && !signal.aborted) return;
4843
- throw e;
4844
- }
4845
- if (settled.aborted) return;
4846
- if (!await io2.openUrl(url)) io2.err(` Open ${url} in a browser`);
4847
- } else {
4848
- io2.err(` Open ${url} in a browser`);
4849
- }
4850
- }
4851
- });
4852
- await deps2.config.update((c) => {
4853
- c.profiles[portal.apiOrigin] = {
4854
- ...c.profiles[portal.apiOrigin],
4855
- apiOrigin: portal.apiOrigin,
4856
- issuer,
4857
- sub: t.sub,
4858
- email: t.email,
4859
- meta: { fetchedAt: deps2.now().toISOString(), doc: meta }
4860
- };
4861
- if (g.portal || !c.defaultProfile) c.defaultProfile = portal.apiOrigin;
4862
- });
4863
- const { api } = await openSession(deps2, { ...portal, pinnedIssuer: issuer }, "login", requestLog(ctx));
5428
+ const { profile, api } = await openSession(deps2, portal, "tenant list", requestLog(ctx));
4864
5429
  const me = await api.get("/api/cli/v1/me");
4865
- const prev = cfg.profiles[portal.apiOrigin];
4866
- let active = prev?.sub === t.sub && prev.activeTenant && me.tenants.some((x) => x.id === prev.activeTenant.id) ? prev.activeTenant : void 0;
4867
- if (active) {
4868
- } else if (me.tenants.length === 0) {
4869
- io2.err("! Your account is not a developer in any organisation. Ask an admin to invite you as developer.");
4870
- } else if (me.tenants.length === 1) {
4871
- active = { id: me.tenants[0].id, slug: me.tenants[0].slug };
4872
- io2.err(`\u2713 Using tenant ${me.tenants[0].name}`);
4873
- } else if (io2.interactive) {
4874
- active = await io2.select("Select a tenant", me.tenants.map((x) => ({ name: `${x.name} ${x.role}`, value: { id: x.id, slug: x.slug } })));
4875
- } else {
4876
- io2.err(`! You belong to ${me.tenants.length} tenants; choose one with \`qore tenant use <slug>\``);
5430
+ const activeId = profile.activeTenant?.id;
5431
+ if (g.json) {
5432
+ renderJson(io2, { tenants: me.tenants.map((t) => ({ ...t, active: t.id === activeId })) });
5433
+ return;
4877
5434
  }
4878
- await deps2.config.update((c) => {
4879
- c.profiles[portal.apiOrigin].activeTenant = active;
4880
- });
4881
- const label2 = portal.builtin ?? portal.apiOrigin;
4882
- if (g.json) renderJson(io2, { portal: label2, email: me.email, activeTenant: active ?? null });
4883
- else io2.out(`\u2713 Logged in as ${me.email} (${label2})`);
5435
+ for (const t of me.tenants) {
5436
+ const mark = t.id === activeId ? "*" : " ";
5437
+ io2.out(`${mark} ${t.name} ${t.role}`);
5438
+ }
5439
+ });
5440
+ tenant.command("use").description("Set the active tenant").argument("[slug]", "tenant slug").action(async (slug) => {
5441
+ const chosen = await useTenant(ctx, slug);
5442
+ if (ctx.opts().json) renderJson(ctx.io, { activeTenant: chosen });
5443
+ else ctx.io.out(`\u2713 Using tenant ${chosen.slug}`);
5444
+ });
5445
+ }
5446
+ async function useTenant(ctx, slug, known) {
5447
+ const { deps: deps2, io: io2 } = ctx;
5448
+ const g = ctx.opts();
5449
+ const cfg = await deps2.config.read();
5450
+ const portal = resolvePortal(g.portal, cfg.defaultProfile);
5451
+ const me = known ?? await (await openSession(deps2, portal, "tenant use", requestLog(ctx))).api.get("/api/cli/v1/me");
5452
+ const candidates = me.tenants.map((t) => ({ kind: "tenant", id: t.id, slug: t.slug, name: t.name }));
5453
+ let chosen;
5454
+ if (slug) {
5455
+ const found = me.tenants.find((t) => t.slug === slug);
5456
+ if (!found) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `No tenant "${slug}" for this account`, { candidates });
5457
+ chosen = { id: found.id, slug: found.slug };
5458
+ } else if (io2.interactive) {
5459
+ chosen = await io2.select("Select a tenant", me.tenants.map((t) => ({ name: `${t.name} ${t.role}`, value: { id: t.id, slug: t.slug } })));
5460
+ } else {
5461
+ throw new QoreError("QORE_CONTEXT_REQUIRED", "Which tenant? Pass a slug or run interactively.", { candidates });
5462
+ }
5463
+ await deps2.config.update((c) => {
5464
+ c.profiles[portal.apiOrigin].activeTenant = chosen;
5465
+ });
5466
+ return chosen;
5467
+ }
5468
+
5469
+ // src/cli/commands/init.ts
5470
+ var step = (status2, detail2) => detail2 === void 0 ? { status: status2 } : { status: status2, detail: detail2 };
5471
+ async function rootStep(ctx, f) {
5472
+ const { deps: deps2, io: io2 } = ctx;
5473
+ const current = (await deps2.config.read()).projectsRoot;
5474
+ let input2 = f.root;
5475
+ if (input2 === void 0) {
5476
+ if (!io2.interactive) {
5477
+ if (current) return step("already", current);
5478
+ throw new QoreError("QORE_ROOT_NOT_SET", "No projects folder chosen", { hint: "Pass one: qore init --root <path>." });
5479
+ }
5480
+ input2 = await io2.input("Projects folder", { default: current ?? "~/qore" }) || current || "~/qore";
5481
+ }
5482
+ const wanted = await checkRoot(deps2, input2);
5483
+ const asked = await realpath11(wanted).catch(() => wanted);
5484
+ if (current && asked !== current && io2.interactive && !await io2.confirm(`Change the projects folder from ${current} to ${asked}? Nothing is moved.`, ctx.signal)) {
5485
+ return step("skipped", current);
5486
+ }
5487
+ const next = await createRoot(wanted);
5488
+ if (next === current) return step("already", current);
5489
+ await setRoot(deps2, next);
5490
+ return step("done", next);
5491
+ }
5492
+ async function activeTenant(ctx) {
5493
+ const cfg = await ctx.deps.config.read();
5494
+ return cfg.profiles[resolvePortal(ctx.opts().portal, cfg.defaultProfile).apiOrigin]?.activeTenant?.slug ?? null;
5495
+ }
5496
+ async function tenantStep(ctx) {
5497
+ const cfg = await ctx.deps.config.read();
5498
+ const portal = resolvePortal(ctx.opts().portal, cfg.defaultProfile);
5499
+ const { api } = await openSession(ctx.deps, portal, "init", requestLog(ctx));
5500
+ const me = await api.get("/api/cli/v1/me");
5501
+ if (me.tenants.length === 0) return step("skipped", NO_TENANTS);
5502
+ return step("done", (await useTenant(ctx, me.tenants.length === 1 ? me.tenants[0].slug : void 0, me)).slug);
5503
+ }
5504
+ function registerInit(program, ctx) {
5505
+ 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) => {
5506
+ const { io: io2 } = ctx;
5507
+ const json2 = Boolean(ctx.opts().json);
5508
+ const sub = json2 ? { ...ctx, io: { ...io2, out: io2.err }, opts: () => ({ ...ctx.opts(), json: false }) } : ctx;
5509
+ const steps = {
5510
+ root: await rootStep(ctx, f),
5511
+ login: step("skipped"),
5512
+ tenant: step("skipped"),
5513
+ clone: step("skipped")
5514
+ };
5515
+ const who = await loggedInAs(ctx);
5516
+ if (who) steps.login = step("already", who);
5517
+ else if (io2.interactive) {
5518
+ await loginFlow(sub, { browser: true });
5519
+ steps.login = step("done", await loggedInAs(ctx) ?? void 0);
5520
+ } else steps.login = step("skipped", "run qore login");
5521
+ const tenant = await activeTenant(ctx);
5522
+ if (tenant) steps.tenant = step(steps.login.status === "done" ? "done" : "already", tenant);
5523
+ else if (steps.login.status !== "skipped" && io2.interactive) steps.tenant = await tenantStep(sub);
5524
+ else steps.tenant = step("skipped", "run qore tenant use <slug>");
5525
+ if (f.clone) {
5526
+ steps.clone = step("done", (await cloneApp(sub, f.clone, void 0, { tenant: f.tenant, protocol: "https" })).root);
5527
+ } else if (io2.interactive && steps.login.status !== "skipped" && steps.tenant.status !== "skipped" && await io2.confirm("Clone an app now?", ctx.signal)) {
5528
+ steps.clone = step("done", (await cloneApp(sub, void 0, void 0, { tenant: f.tenant, protocol: "https" })).root);
5529
+ }
5530
+ const projectsRoot = (await ctx.deps.config.read()).projectsRoot ?? null;
5531
+ if (json2) {
5532
+ renderJson(io2, { projectsRoot, steps });
5533
+ return;
5534
+ }
5535
+ const mark = (s) => s.status === "skipped" ? "\u2013" : "\u2713";
5536
+ for (const [name, s] of Object.entries(steps)) io2.out(`${mark(s)} ${name.padEnd(6)} ${s.status}${s.detail ? ` ${s.detail}` : ""}`);
5537
+ if (projectsRoot) io2.out(`Next: qore dev <app> works from any directory.`);
4884
5538
  });
4885
5539
  }
4886
5540
 
@@ -4948,6 +5602,7 @@ async function appStatus(ctx, ws, appFlag) {
4948
5602
  await ensureCapability(deps2, portal.apiOrigin, "app.modules");
4949
5603
  await ensureCapability(deps2, portal.apiOrigin, "module.status");
4950
5604
  const am = await workspaceAppModules(session, portal, ws);
5605
+ await rememberWorkspace(ctx, ws, am.app);
4951
5606
  const { tenant, ...app } = am.app;
4952
5607
  if (appFlag && appFlag !== app.id && appFlag !== app.slug) {
4953
5608
  throw new QoreError("QORE_RESOURCE_NOT_FOUND", `This workspace is for ${app.slug}, not "${appFlag}"`, {
@@ -4975,19 +5630,21 @@ async function appStatus(ctx, ws, appFlag) {
4975
5630
  for (const line2 of table(rows)) io2.out(line2);
4976
5631
  }
4977
5632
  function registerStatus(program, ctx) {
4978
- 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) => {
4979
- const { deps: deps2, io: io2 } = ctx;
4980
- const g = ctx.opts();
5633
+ program.command("status").description("Deployment and health per environment for this module (or, in a workspace root, the app)").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "the app or module, from anywhere (see qore init)").action(async (flags) => {
5634
+ const c = await projectCtx(ctx, { ref: flags.project, module: flags.module, app: flags.app, needModule: false });
5635
+ const { deps: deps2, io: io2 } = c;
5636
+ const g = c.opts();
4981
5637
  if (!flags.module) {
4982
5638
  const ws = await workspaceRoot(deps2);
4983
5639
  if (ws) {
4984
- await appStatus(ctx, ws, flags.app);
5640
+ await appStatus(c, ws, flags.app);
4985
5641
  return;
4986
5642
  }
4987
5643
  }
4988
- const mc = await moduleContextFor(ctx, flags, "status");
5644
+ const mc = await moduleContextFor(c, flags, "status");
4989
5645
  await ensureCapability(deps2, mc.portal.apiOrigin, "module.status");
4990
- const app = await appFor(ctx, mc, flags.app);
5646
+ const app = await appFor(c, mc, flags.app);
5647
+ await rememberContext(c, mc, app);
4991
5648
  if (!g.json) io2.err(contextLine(mc, app));
4992
5649
  const st = await mc.session.api.get(
4993
5650
  `/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/status?app_id=${encodeURIComponent(app.id)}`
@@ -5007,7 +5664,7 @@ function registerStatus(program, ctx) {
5007
5664
  }
5008
5665
 
5009
5666
  // src/cli/commands/sync.ts
5010
- import { join as join21, relative as relative8 } from "path";
5667
+ import { join as join24, relative as relative9 } from "path";
5011
5668
  import { Option as Option2 } from "commander";
5012
5669
  function asFailure2(slug, e) {
5013
5670
  if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
@@ -5039,7 +5696,7 @@ function registerSync(program, ctx) {
5039
5696
  continue;
5040
5697
  }
5041
5698
  try {
5042
- dirs[m.slug] = await deps2.git.inspect(join21(ws.root, m.slug));
5699
+ dirs[m.slug] = await deps2.git.inspect(join24(ws.root, m.slug));
5043
5700
  modulesForPlan.push(m);
5044
5701
  } catch (e) {
5045
5702
  earlyFailures.push(asFailure2(m.slug, e));
@@ -5051,7 +5708,7 @@ function registerSync(program, ctx) {
5051
5708
  } });
5052
5709
  await syncPresent(deps2, applied, actions, Boolean(flags.pull), { app, protocol: flags.protocol });
5053
5710
  for (const r of applied) {
5054
- if (r.status === "cloned" && r.dir) r.dir = relative8(deps2.cwd, r.dir) || ".";
5711
+ if (r.status === "cloned" && r.dir) r.dir = relative9(deps2.cwd, r.dir) || ".";
5055
5712
  }
5056
5713
  const removedResults = removed.map((slug) => ({ slug, status: "removed", reason: "not a module of this app \u2014 left in place" }));
5057
5714
  const results = [...earlyFailures, ...applied, ...removedResults];
@@ -5060,52 +5717,6 @@ function registerSync(program, ctx) {
5060
5717
  });
5061
5718
  }
5062
5719
 
5063
- // src/cli/commands/tenant.ts
5064
- function registerTenant(program, ctx) {
5065
- const tenant = program.command("tenant").description("Manage the active tenant");
5066
- tenant.command("list").description("List tenants you have access to").action(async () => {
5067
- const { deps: deps2, io: io2 } = ctx;
5068
- const g = ctx.opts();
5069
- const cfg = await deps2.config.read();
5070
- const portal = resolvePortal(g.portal, cfg.defaultProfile);
5071
- const { profile, api } = await openSession(deps2, portal, "tenant list", requestLog(ctx));
5072
- const me = await api.get("/api/cli/v1/me");
5073
- const activeId = profile.activeTenant?.id;
5074
- if (g.json) {
5075
- renderJson(io2, { tenants: me.tenants.map((t) => ({ ...t, active: t.id === activeId })) });
5076
- return;
5077
- }
5078
- for (const t of me.tenants) {
5079
- const mark = t.id === activeId ? "*" : " ";
5080
- io2.out(`${mark} ${t.name} ${t.role}`);
5081
- }
5082
- });
5083
- tenant.command("use").description("Set the active tenant").argument("[slug]", "tenant slug").action(async (slug) => {
5084
- const { deps: deps2, io: io2 } = ctx;
5085
- const g = ctx.opts();
5086
- const cfg = await deps2.config.read();
5087
- const portal = resolvePortal(g.portal, cfg.defaultProfile);
5088
- const { api } = await openSession(deps2, portal, "tenant use", requestLog(ctx));
5089
- const me = await api.get("/api/cli/v1/me");
5090
- const candidates = me.tenants.map((t) => ({ kind: "tenant", id: t.id, slug: t.slug, name: t.name }));
5091
- let chosen;
5092
- if (slug) {
5093
- const found = me.tenants.find((t) => t.slug === slug);
5094
- if (!found) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `No tenant "${slug}" for this account`, { candidates });
5095
- chosen = { id: found.id, slug: found.slug };
5096
- } else if (io2.interactive) {
5097
- chosen = await io2.select("Select a tenant", me.tenants.map((t) => ({ name: `${t.name} ${t.role}`, value: { id: t.id, slug: t.slug } })));
5098
- } else {
5099
- throw new QoreError("QORE_CONTEXT_REQUIRED", "Which tenant? Pass a slug or run interactively.", { candidates });
5100
- }
5101
- await deps2.config.update((c) => {
5102
- c.profiles[portal.apiOrigin].activeTenant = chosen;
5103
- });
5104
- if (g.json) renderJson(io2, { activeTenant: chosen });
5105
- else io2.out(`\u2713 Using tenant ${chosen.slug}`);
5106
- });
5107
- }
5108
-
5109
5720
  // src/cli/commands/version.ts
5110
5721
  function registerVersion(program, ctx) {
5111
5722
  program.command("version").description("Show the CLI and portal version").action(async () => {
@@ -5125,11 +5736,11 @@ function registerVersion(program, ctx) {
5125
5736
  return;
5126
5737
  }
5127
5738
  io2.out(`qore ${deps2.version}`);
5128
- const label2 = portal.builtin ?? portal.apiOrigin;
5739
+ const label3 = portal.builtin ?? portal.apiOrigin;
5129
5740
  if (portalInfo) {
5130
- io2.out(`portal ${label2}: api ${portalInfo.apiVersions.join(",")}, min ${portalInfo.minCliVersion}, latest ${portalInfo.latestCliVersion}`);
5741
+ io2.out(`portal ${label3}: api ${portalInfo.apiVersions.join(",")}, min ${portalInfo.minCliVersion}, latest ${portalInfo.latestCliVersion}`);
5131
5742
  } else {
5132
- io2.out(`portal ${label2}: unreachable`);
5743
+ io2.out(`portal ${label3}: unreachable`);
5133
5744
  }
5134
5745
  });
5135
5746
  }
@@ -5143,18 +5754,18 @@ function registerWhoami(program, ctx) {
5143
5754
  const portal = resolvePortal(g.portal, cfg.defaultProfile);
5144
5755
  const { profile, api } = await openSession(deps2, portal, "whoami", requestLog(ctx));
5145
5756
  const me = await api.get("/api/cli/v1/me");
5146
- const activeTenant = profile.activeTenant;
5147
- if (activeTenant && !me.tenants.some((t) => t.id === activeTenant.id)) {
5148
- io2.err(`! You no longer have developer access to ${activeTenant.slug}`);
5757
+ const activeTenant2 = profile.activeTenant;
5758
+ if (activeTenant2 && !me.tenants.some((t) => t.id === activeTenant2.id)) {
5759
+ io2.err(`! You no longer have developer access to ${activeTenant2.slug}`);
5149
5760
  }
5150
- const label2 = portal.builtin ?? portal.apiOrigin;
5761
+ const label3 = portal.builtin ?? portal.apiOrigin;
5151
5762
  if (g.json) {
5152
- renderJson(io2, { portal: label2, activeTenant: activeTenant ?? null, me });
5763
+ renderJson(io2, { portal: label3, activeTenant: activeTenant2 ?? null, me });
5153
5764
  return;
5154
5765
  }
5155
- io2.out(`${me.email} (${label2})`);
5766
+ io2.out(`${me.email} (${label3})`);
5156
5767
  for (const t of me.tenants) {
5157
- const mark = activeTenant?.id === t.id ? "*" : " ";
5768
+ const mark = activeTenant2?.id === t.id ? "*" : " ";
5158
5769
  io2.out(`${mark} ${t.name} ${t.role}`);
5159
5770
  for (const a of t.apps) {
5160
5771
  io2.out(` ${a.slug} ${a.access}`);
@@ -5168,6 +5779,7 @@ function registerWhoami(program, ctx) {
5168
5779
 
5169
5780
  // src/cli/commands/index.ts
5170
5781
  var COMMANDS = [
5782
+ registerInit,
5171
5783
  registerLogin,
5172
5784
  registerLogout,
5173
5785
  registerWhoami,
@@ -5179,9 +5791,111 @@ var COMMANDS = [
5179
5791
  registerClone,
5180
5792
  registerSync,
5181
5793
  registerVersion,
5182
- registerDoctor
5794
+ registerConfig,
5795
+ registerDoctor,
5796
+ registerDebugTelemetry
5183
5797
  ];
5184
5798
 
5799
+ // src/cli/telemetry/notice.ts
5800
+ var NOTICE = [
5801
+ "qore sends anonymous crash reports and a per-run health record to Quatronic, to find and fix bugs.",
5802
+ "Tokens, env values and your home path are removed; no user or tenant is attached.",
5803
+ "Turn it off with `qore config telemetry off`, QORE_TELEMETRY=0 or DO_NOT_TRACK=1. See the Privacy section of the README."
5804
+ ];
5805
+ async function showNoticeOnce(deps2, io2, o) {
5806
+ if (o.json || !io2.interactive || !deps2.sentryDsn) return;
5807
+ try {
5808
+ const cfg = await deps2.config.read();
5809
+ if (!needsNotice(cfg, resolveConsent(deps2.env, cfg))) return;
5810
+ await deps2.config.update((c) => {
5811
+ c.telemetry = { ...c.telemetry, noticeShownAt: deps2.now().toISOString() };
5812
+ });
5813
+ } catch {
5814
+ return;
5815
+ }
5816
+ for (const line2 of NOTICE) io2.err(line2);
5817
+ }
5818
+
5819
+ // src/cli/telemetry/index.ts
5820
+ var CRASH_FLUSH_MS = 2e3;
5821
+ var CLEAN_FLUSH_MS = 200;
5822
+ var loadSentry = async () => (await import("./sentry-GMM4X3FT.js")).createSdk;
5823
+ function createTelemetry(deps2, o) {
5824
+ const load = o.load ?? loadSentry;
5825
+ const flushMs = o.flushMs ?? { crash: CRASH_FLUSH_MS, clean: CLEAN_FLUSH_MS };
5826
+ let sdk = null;
5827
+ let crashed = null;
5828
+ const secrets = [];
5829
+ let scrub = null;
5830
+ return {
5831
+ begin(command) {
5832
+ if (sdk || !deps2.sentryDsn) return;
5833
+ sdk = start(deps2, o.argv, command, load, (ctx) => {
5834
+ scrub = ctx;
5835
+ addSecretValues(ctx, secrets);
5836
+ }).catch(() => null);
5837
+ },
5838
+ crash(e) {
5839
+ crashed ??= { e };
5840
+ },
5841
+ registerSecrets(values) {
5842
+ secrets.push(...values);
5843
+ if (scrub) addSecretValues(scrub, values);
5844
+ },
5845
+ async end(signal) {
5846
+ if (!sdk) return { drained: true };
5847
+ const ms = crashed ? flushMs.crash : flushMs.clean;
5848
+ const work = sdk.then(async (s) => {
5849
+ if (!s) return true;
5850
+ if (crashed) s.crash(crashed.e);
5851
+ return s.end(ms);
5852
+ }).catch(() => false);
5853
+ return { drained: await withDeadline(work, ms, signal) };
5854
+ }
5855
+ };
5856
+ }
5857
+ async function start(deps2, argv2, command, load, onScrub) {
5858
+ const cfg = await deps2.config.read().catch(() => null);
5859
+ if (!cfg) return null;
5860
+ if (!resolveConsent(deps2.env, cfg).enabled) return null;
5861
+ const create = await load();
5862
+ const scrub = buildScrubContext({ home: deps2.home, platform: deps2.platform, env: deps2.env, argv: argv2, commandWords: command.split(" ") });
5863
+ onScrub(scrub);
5864
+ const sdk = create({
5865
+ dsn: deps2.sentryDsn,
5866
+ environment: deps2.sentryEnvironment,
5867
+ release: deps2.version,
5868
+ tags: { command, "cli.version": deps2.version, "node.version": deps2.nodeVersion, os: deps2.platform, ci: deps2.env.CI ? "true" : "false" },
5869
+ scrub
5870
+ });
5871
+ sdk.start();
5872
+ return sdk;
5873
+ }
5874
+ function withDeadline(work, ms, signal) {
5875
+ if (signal?.aborted) return Promise.resolve(false);
5876
+ return new Promise((resolve6) => {
5877
+ const done = (v) => {
5878
+ clearTimeout(timer);
5879
+ signal?.removeEventListener("abort", onAbort);
5880
+ resolve6(v);
5881
+ };
5882
+ const onAbort = () => done(false);
5883
+ const timer = setTimeout(() => done(false), ms);
5884
+ signal?.addEventListener("abort", onAbort, { once: true });
5885
+ work.then(done, () => done(false));
5886
+ });
5887
+ }
5888
+ function telemetryHooks(deps2, io2, t, o) {
5889
+ return {
5890
+ onCommand: async (command) => {
5891
+ if (command === "config telemetry") return;
5892
+ await showNoticeOnce(deps2, io2, o);
5893
+ t.begin(command);
5894
+ },
5895
+ onCrash: (e) => t.crash(e)
5896
+ };
5897
+ }
5898
+
5185
5899
  // src/cli/updateCheck.ts
5186
5900
  var REGISTRY_URL = "https://registry.npmjs.org/@quatronic%2Fqore/latest";
5187
5901
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
@@ -5215,7 +5929,7 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
5215
5929
 
5216
5930
  // src/core/config.ts
5217
5931
  import { readFile as readFile6 } from "fs/promises";
5218
- import { join as join22 } from "path";
5932
+ import { join as join25 } from "path";
5219
5933
  var empty = () => ({ schemaVersion: 1, profiles: {} });
5220
5934
  var ConfigStore = class {
5221
5935
  constructor(dir2) {
@@ -5223,7 +5937,7 @@ var ConfigStore = class {
5223
5937
  }
5224
5938
  dir;
5225
5939
  get file() {
5226
- return join22(this.dir, "config.json");
5940
+ return join25(this.dir, "config.json");
5227
5941
  }
5228
5942
  async read() {
5229
5943
  let raw;
@@ -5254,9 +5968,9 @@ var ConfigStore = class {
5254
5968
 
5255
5969
  // src/platform/git.ts
5256
5970
  import { execFile, execFileSync } from "child_process";
5257
- import { mkdir as mkdir3, readdir as readdir2, stat as stat3 } from "fs/promises";
5971
+ import { mkdir as mkdir4, readdir as readdir4, stat as stat6 } from "fs/promises";
5258
5972
  import { homedir } from "os";
5259
- import { dirname as dirname8, join as join23 } from "path";
5973
+ import { dirname as dirname9, join as join26 } from "path";
5260
5974
  function parseRemotes(stdout) {
5261
5975
  const seen = /* @__PURE__ */ new Set();
5262
5976
  const out = [];
@@ -5323,7 +6037,7 @@ function gitExecOptions(repoDir) {
5323
6037
  return { cwd: homedir(), env, windowsHide: true, maxBuffer: 8 * 1024 * 1024 };
5324
6038
  }
5325
6039
  function runGit(args, repoDir) {
5326
- return new Promise((resolve5, reject) => {
6040
+ return new Promise((resolve6, reject) => {
5327
6041
  execFile("git", args, gitExecOptions(repoDir), (err, stdout, stderr) => {
5328
6042
  if (err && err.code === "ENOENT") {
5329
6043
  reject(new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
@@ -5332,7 +6046,7 @@ function runGit(args, repoDir) {
5332
6046
  return;
5333
6047
  }
5334
6048
  const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
5335
- resolve5({ code, stdout: String(stdout), stderr: String(stderr), err: err ?? void 0 });
6049
+ resolve6({ code, stdout: String(stdout), stderr: String(stderr), err: err ?? void 0 });
5336
6050
  });
5337
6051
  });
5338
6052
  }
@@ -5350,7 +6064,7 @@ async function remotes(repoRoot) {
5350
6064
  return parseRemotes(r.stdout);
5351
6065
  }
5352
6066
  async function clone(url, dir2) {
5353
- await mkdir3(dirname8(dir2), { recursive: true });
6067
+ await mkdir4(dirname9(dir2), { recursive: true });
5354
6068
  const r = await runGit(["clone", "--", url, dir2]);
5355
6069
  return toResult(r, "git clone");
5356
6070
  }
@@ -5370,7 +6084,7 @@ async function status(repoRoot) {
5370
6084
  }
5371
6085
  async function statOrThrow(path) {
5372
6086
  try {
5373
- return await stat3(path);
6087
+ return await stat6(path);
5374
6088
  } catch (err) {
5375
6089
  if (err.code === "ENOENT") return void 0;
5376
6090
  throw new QoreError("QORE_INTERNAL", `could not stat ${path}`, { cause: err });
@@ -5382,12 +6096,12 @@ async function inspect(dir2) {
5382
6096
  if (!st.isDirectory()) return { exists: true, empty: false, isRepo: false, remotes: [] };
5383
6097
  let entries;
5384
6098
  try {
5385
- entries = await readdir2(dir2);
6099
+ entries = await readdir4(dir2);
5386
6100
  } catch (err) {
5387
6101
  throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
5388
6102
  }
5389
6103
  const empty2 = entries.length === 0;
5390
- const isRepo = await statOrThrow(join23(dir2, ".git")) !== void 0;
6104
+ const isRepo = await statOrThrow(join26(dir2, ".git")) !== void 0;
5391
6105
  const dirRemotes = isRepo ? await remotes(dir2) : [];
5392
6106
  return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
5393
6107
  }
@@ -5448,15 +6162,15 @@ var OsKeychain = class {
5448
6162
  };
5449
6163
 
5450
6164
  // src/platform/lock.ts
5451
- import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
5452
- import { join as join24 } from "path";
6165
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
6166
+ import { join as join27 } from "path";
5453
6167
  import lockfile from "proper-lockfile";
5454
6168
  function fileLock(dir2) {
5455
6169
  return async (name, fn) => {
5456
- const locks = join24(dir2, "locks");
5457
- await mkdir4(locks, { recursive: true, mode: 448 });
5458
- const target = join24(locks, `${name}.lock`);
5459
- await writeFile4(target, "", { flag: "a" });
6170
+ const locks = join27(dir2, "locks");
6171
+ await mkdir5(locks, { recursive: true, mode: 448 });
6172
+ const target = join27(locks, `${name}.lock`);
6173
+ await writeFile5(target, "", { flag: "a" });
5460
6174
  let release;
5461
6175
  let compromised;
5462
6176
  try {
@@ -5499,7 +6213,7 @@ var STDERR_DRAIN_MS = 500;
5499
6213
  function makeSpawner(spawnFn, proc) {
5500
6214
  return {
5501
6215
  run(cmd, args, env, o) {
5502
- return new Promise((resolve5, reject) => {
6216
+ return new Promise((resolve6, reject) => {
5503
6217
  let child;
5504
6218
  try {
5505
6219
  child = spawnFn(cmd, args, {
@@ -5533,7 +6247,7 @@ function makeSpawner(spawnFn, proc) {
5533
6247
  cleanup();
5534
6248
  const stderr = child.stderr;
5535
6249
  if (!o?.onStderr || !stderr || stderr.readableEnded) {
5536
- resolve5({ code, signal });
6250
+ resolve6({ code, signal });
5537
6251
  return;
5538
6252
  }
5539
6253
  let settled = false;
@@ -5541,7 +6255,7 @@ function makeSpawner(spawnFn, proc) {
5541
6255
  if (settled) return;
5542
6256
  settled = true;
5543
6257
  clearTimeout(timer);
5544
- resolve5({ code, signal });
6258
+ resolve6({ code, signal });
5545
6259
  };
5546
6260
  const timer = setTimeout(finish, STDERR_DRAIN_MS);
5547
6261
  timer.unref?.();
@@ -5596,13 +6310,13 @@ var systemLauncher = {
5596
6310
  child.stdout?.on("data", (c) => out.push(c));
5597
6311
  child.stderr?.on("data", (c) => err.push(c));
5598
6312
  let closed = false;
5599
- const exited = new Promise((resolve5) => {
5600
- child.once("error", (e) => resolve5({ code: null, signal: null, spawnError: e.code ?? "error" }));
6313
+ const exited = new Promise((resolve6) => {
6314
+ child.once("error", (e) => resolve6({ code: null, signal: null, spawnError: e.code ?? "error" }));
5601
6315
  child.once("close", (code, signal) => {
5602
6316
  closed = true;
5603
6317
  out.flush();
5604
6318
  err.flush();
5605
- resolve5({ code, signal });
6319
+ resolve6({ code, signal });
5606
6320
  });
5607
6321
  });
5608
6322
  return {
@@ -5648,11 +6362,11 @@ var systemLauncher = {
5648
6362
  import { execFile as execFile2 } from "child_process";
5649
6363
  import { connect } from "net";
5650
6364
  function accepts(host, port) {
5651
- return new Promise((resolve5) => {
6365
+ return new Promise((resolve6) => {
5652
6366
  const s = connect({ host, port });
5653
6367
  const done = (v) => {
5654
6368
  s.destroy();
5655
- resolve5(v);
6369
+ resolve6(v);
5656
6370
  };
5657
6371
  s.setTimeout(500, () => done(false));
5658
6372
  s.once("connect", () => done(true));
@@ -5660,12 +6374,12 @@ function accepts(host, port) {
5660
6374
  });
5661
6375
  }
5662
6376
  function run2(cmd, args) {
5663
- return new Promise((resolve5) => {
6377
+ return new Promise((resolve6) => {
5664
6378
  execFile2(
5665
6379
  cmd,
5666
6380
  args,
5667
6381
  { timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
5668
- (e, stdout) => resolve5(e && !stdout ? null : String(stdout))
6382
+ (e, stdout) => resolve6(e && !stdout ? null : String(stdout))
5669
6383
  );
5670
6384
  });
5671
6385
  }
@@ -5728,6 +6442,10 @@ var systemPorts = {
5728
6442
  }
5729
6443
  };
5730
6444
 
6445
+ // src/buildInfo.ts
6446
+ var SENTRY_DSN = true ? "https://50b592efb53dbc1a31cb10d3945c6d52@o4512129906049024.ingest.de.sentry.io/4512174341750864" : "";
6447
+ var SENTRY_ENVIRONMENT = true ? "production" : "development";
6448
+
5731
6449
  // src/platform/proxy.ts
5732
6450
  import { execFile as execFile3 } from "child_process";
5733
6451
 
@@ -5754,12 +6472,12 @@ function parseRegProxy(stdout) {
5754
6472
 
5755
6473
  // src/platform/proxy.ts
5756
6474
  function run3(cmd, args) {
5757
- return new Promise((resolve5) => {
6475
+ return new Promise((resolve6) => {
5758
6476
  execFile3(
5759
6477
  cmd,
5760
6478
  args,
5761
6479
  { timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
5762
- (e, stdout) => resolve5(e && !stdout ? null : String(stdout))
6480
+ (e, stdout) => resolve6(e && !stdout ? null : String(stdout))
5763
6481
  );
5764
6482
  });
5765
6483
  }
@@ -5790,7 +6508,7 @@ function systemProxy(platform = process.platform) {
5790
6508
  }
5791
6509
 
5792
6510
  // src/version.ts
5793
- var VERSION = true ? "0.1.4" : "0.0.0-dev";
6511
+ var VERSION = true ? "0.1.6" : "0.0.0-dev";
5794
6512
 
5795
6513
  // src/cli.ts
5796
6514
  if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
@@ -5833,6 +6551,11 @@ var deps = {
5833
6551
  process.kill(process.pid, signal);
5834
6552
  },
5835
6553
  pid: process.pid,
6554
+ registerSecrets: (values) => {
6555
+ telemetry.registerSecrets(values);
6556
+ },
6557
+ sentryDsn: SENTRY_DSN,
6558
+ sentryEnvironment: SENTRY_ENVIRONMENT,
5836
6559
  systemProxy: () => systemProxy(process.platform)
5837
6560
  };
5838
6561
  var termination = preflightTermination(() => ac.abort(), deps.raise);
@@ -5843,16 +6566,24 @@ var io = processIo(isInteractive({
5843
6566
  noInput: preSeparatorArgv.includes("--no-input"),
5844
6567
  env: process.env
5845
6568
  }));
5846
- process.exitCode = await run(argv, deps, io, ac.signal, COMMANDS);
5847
- await termination.reRaise(async () => {
6569
+ var flushStd = async () => {
5848
6570
  await Promise.all([process.stdout, process.stderr].map((s) => new Promise((r) => {
5849
6571
  s.write("", () => r());
5850
6572
  })));
5851
- });
6573
+ };
6574
+ var telemetry = createTelemetry(deps, { argv });
6575
+ process.exitCode = await run(argv, deps, io, ac.signal, COMMANDS, telemetryHooks(deps, io, telemetry, { json }));
6576
+ var { drained } = await telemetry.end(ac.signal);
6577
+ await termination.reRaise(flushStd);
5852
6578
  if (!ac.signal.aborted) {
5853
6579
  try {
5854
6580
  await maybeNotifyUpdate(deps, io, { json });
5855
6581
  } catch {
5856
6582
  }
5857
6583
  }
5858
- //# sourceMappingURL=cli.js.map
6584
+ if (!drained) {
6585
+ await flushStd();
6586
+ process.exit();
6587
+ }
6588
+ //# sourceMappingURL=cli.js.map
6589
+ //# debugId=28c91dc8-dbb9-5c0a-91e3-f368931a4ed3