@quatronic/qore 0.1.5 → 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,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a70250c5-daf4-5ff3-b490-5556ac377549")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="28c91dc8-dbb9-5c0a-91e3-f368931a4ed3")}catch(e){}}();
4
4
  import { createRequire as __qoreCreateRequire } from "node:module"; const require = __qoreCreateRequire(import.meta.url);
5
5
  import {
6
6
  addSecretValues,
7
7
  buildScrubContext
8
- } from "./chunk-QYBDINUC.js";
8
+ } from "./chunk-OAU6HJHZ.js";
9
9
 
10
10
  // src/cli.ts
11
11
  import { homedir as homedir2 } from "os";
12
12
  import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
13
13
 
14
14
  // src/cli/io.ts
15
- import { confirm, search, select } from "@inquirer/prompts";
15
+ import { confirm, input, search, select } from "@inquirer/prompts";
16
16
  import { createInterface } from "readline";
17
17
 
18
18
  // src/platform/browser.ts
@@ -51,6 +51,10 @@ var EXIT = {
51
51
  QORE_REPO_ACCESS_PENDING: 5,
52
52
  QORE_GIT_AUTH: 5,
53
53
  QORE_WORKSPACE_CONFLICT: 2,
54
+ QORE_ROOT_NOT_SET: 2,
55
+ QORE_ROOT_INVALID: 2,
56
+ QORE_ROOT_NOT_WRITABLE: 1,
57
+ QORE_PROJECT_NOT_FOUND: 2,
54
58
  QORE_AUTH_UNAVAILABLE: 6,
55
59
  QORE_AUTH_BUSY: 6,
56
60
  QORE_API_UNREACHABLE: 6,
@@ -152,13 +156,21 @@ function processIo(interactive, stderrTTY = Boolean(process.stderr.isTTY)) {
152
156
  throw e;
153
157
  }
154
158
  },
159
+ async input(message, o) {
160
+ try {
161
+ return await input({ message, default: o?.default });
162
+ } catch (e) {
163
+ if (e.name === "ExitPromptError") throw new CancelledError();
164
+ throw e;
165
+ }
166
+ },
155
167
  waitForEnter: createWaitForEnter(process.stdin, process.stderr),
156
168
  openUrl: openBrowser
157
169
  };
158
170
  }
159
- function createWaitForEnter(input, output) {
160
- return (message, signal) => new Promise((resolve5, reject) => {
161
- const rl = createInterface({ input, output, terminal: true });
171
+ function createWaitForEnter(input2, output) {
172
+ return (message, signal) => new Promise((resolve6, reject) => {
173
+ const rl = createInterface({ input: input2, output, terminal: true });
162
174
  const onAbort = () => {
163
175
  rl.close();
164
176
  reject(new CancelledError());
@@ -166,7 +178,7 @@ function createWaitForEnter(input, output) {
166
178
  const done = () => {
167
179
  signal.removeEventListener("abort", onAbort);
168
180
  rl.close();
169
- resolve5();
181
+ resolve6();
170
182
  };
171
183
  signal.addEventListener("abort", onAbort, { once: true });
172
184
  rl.on("SIGINT", () => {
@@ -183,7 +195,7 @@ import { Command, CommanderError } from "commander";
183
195
 
184
196
  // src/core/api/http.ts
185
197
  function abortableFetch(fetch3, signal) {
186
- return (input, init) => fetch3(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal });
198
+ return (input2, init) => fetch3(input2, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal });
187
199
  }
188
200
  var TLS = /* @__PURE__ */ new Set([
189
201
  "SELF_SIGNED_CERT_IN_CHAIN",
@@ -490,7 +502,7 @@ async function run(argv2, deps2, io2, signal, register, hooks = {}) {
490
502
  }
491
503
 
492
504
  // src/cli/commands/clone.ts
493
- import { join as join6, relative as relative2, resolve as resolve2 } from "path";
505
+ import { join as join9, relative as relative3, resolve as resolve4 } from "path";
494
506
  import { Option } from "commander";
495
507
 
496
508
  // src/core/api/meta.ts
@@ -944,8 +956,8 @@ var ResolutionCache = class {
944
956
  const f = await this.load();
945
957
  const hits = [];
946
958
  for (const [k, e] of Object.entries(f.entries)) {
947
- const sep2 = k.lastIndexOf("|");
948
- if (keys.includes(k.slice(sep2 + 1)) && this.fresh(e)) hits.push({ origin: k.slice(0, sep2), at: new Date(e.resolvedAt).getTime() });
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() });
949
961
  }
950
962
  hits.sort((a, b) => b.at - a.at);
951
963
  return [...new Set(hits.map((h) => h.origin))];
@@ -1251,6 +1263,13 @@ async function syncPresent(deps2, results, actions, pull, ctx) {
1251
1263
  }
1252
1264
  }
1253
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
+
1254
1273
  // src/core/workspace/file.ts
1255
1274
  import { randomBytes as randomBytes2 } from "crypto";
1256
1275
  import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm, writeFile as writeFile2 } from "fs/promises";
@@ -1312,17 +1331,225 @@ async function writeWorkspace(root, o) {
1312
1331
  }
1313
1332
  async function workspacePortal(deps2, ws) {
1314
1333
  const portal = resolvePortal(ws.portal, void 0);
1315
- const label2 = portal.builtin ?? portal.apiOrigin;
1334
+ const label3 = portal.builtin ?? portal.apiOrigin;
1316
1335
  const profile = (await deps2.config.read()).profiles[portal.apiOrigin];
1317
1336
  const loggedIn = profile && await new TokenStore(deps2.keychain, profileHash(profile.apiOrigin, profile.issuer)).getRefresh();
1318
1337
  if (!loggedIn) {
1319
- throw new QoreError("QORE_NOT_LOGGED_IN", `This workspace belongs to ${label2}, which you are not logged in to`, {
1320
- hint: `Run \`qore login --portal ${label2}\`.`
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}\`.`
1321
1340
  });
1322
1341
  }
1323
1342
  return portal;
1324
1343
  }
1325
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
+
1326
1553
  // src/core/workspace/safety.ts
1327
1554
  var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/;
1328
1555
  var WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/;
@@ -1373,9 +1600,9 @@ function firstSentenceLower(hint) {
1373
1600
  return sentence.length ? sentence[0].toLowerCase() + sentence.slice(1) : sentence;
1374
1601
  }
1375
1602
  function failedLine(r) {
1376
- const label2 = FAILURE_LABELS[r.error?.code ?? ""] ?? "failed";
1603
+ const label3 = FAILURE_LABELS[r.error?.code ?? ""] ?? "failed";
1377
1604
  const detail2 = firstSentenceLower(r.error?.hint);
1378
- return `\u2717 ${r.slug} ${label2}${detail2 ? ` \u2014 ${detail2}` : ""}`;
1605
+ return `\u2717 ${r.slug} ${label3}${detail2 ? ` \u2014 ${detail2}` : ""}`;
1379
1606
  }
1380
1607
  function line(r, conflictCount) {
1381
1608
  switch (r.status) {
@@ -1474,61 +1701,76 @@ async function pickApp(deps2, io2, session, tenantFlag) {
1474
1701
  });
1475
1702
  return { tenant: tenant.slug, app };
1476
1703
  }
1477
- function registerClone(program, ctx) {
1478
- program.command("clone").description("Clone an app's modules into a new workspace directory").argument("[app]", "app slug or id (omit to pick one interactively)").argument("[dir]", "target directory (default: the app's slug)").option("--tenant <slug>", "tenant the app slug belongs to, when it is not your active one").addOption(new Option("--protocol <https|ssh>", "clone URL protocol").choices(["https", "ssh"]).default("https")).action(async (appArg, dirArg, flags) => {
1479
- const { deps: deps2, io: io2 } = ctx;
1480
- const g = ctx.opts();
1481
- const cfg = await deps2.config.read();
1482
- const portal = resolvePortal(g.portal, cfg.defaultProfile);
1483
- const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
1484
- const session = await openSession(deps2, portal, "clone", onRequest);
1485
- await ensureCapability(deps2, portal.apiOrigin, "app.modules");
1486
- const picked = appArg === void 0 ? await pickApp(deps2, io2, session, flags.tenant) : void 0;
1487
- const appSlug = picked?.app ?? appArg;
1488
- const ref = await resolveApp(deps2, io2, session, appSlug, picked?.tenant ?? flags.tenant);
1489
- const app = await session.api.get(`/api/cli/v1/apps/${encodeURIComponent(ref.appId)}/modules`);
1490
- if (dirArg === void 0 && !isSafeSlug(app.app.slug)) throw unsafeSlugError(app.app.slug);
1491
- const dirLabel = dirArg ?? app.app.slug;
1492
- const root = resolve2(deps2.cwd, dirLabel);
1493
- const info = await deps2.git.inspect(root);
1494
- if (info.exists && !info.empty) {
1495
- const ws = await readWorkspace(root);
1496
- if (!ws || ws.appId !== app.app.id || ws.portal !== portal.apiOrigin) {
1497
- throw new QoreError("QORE_WORKSPACE_CONFLICT", `${root} exists and is not the workspace of ${app.app.slug}`, {
1498
- hint: `Pick another directory: qore clone ${appSlug} <dir>.`
1499
- });
1500
- }
1501
- }
1502
- await writeWorkspace(root, { portal: portal.apiOrigin, appId: app.app.id });
1503
- const dirs = {};
1504
- const modulesForPlan = [];
1505
- const earlyFailures = [];
1506
- for (const m of app.modules) {
1507
- if (!isSafeSlug(m.slug)) {
1508
- earlyFailures.push(asFailure(m.slug, unsafeSlugError(m.slug)));
1509
- continue;
1510
- }
1511
- try {
1512
- dirs[m.slug] = await deps2.git.inspect(join6(root, m.slug));
1513
- modulesForPlan.push(m);
1514
- } catch (e) {
1515
- earlyFailures.push(asFailure(m.slug, e));
1516
- }
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
+ });
1517
1728
  }
1518
- const { actions } = planWorkspace(root, modulesForPlan, dirs, []);
1519
- const applied = await applyActions(deps2, { portal, app, actions, protocol: flags.protocol, onResult: () => {
1520
- } });
1521
- for (const r of applied) {
1522
- if (r.status === "cloned" && r.dir) r.dir = relative2(deps2.cwd, r.dir) || ".";
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;
1523
1746
  }
1524
- const results = [...earlyFailures, ...applied];
1525
- const err = reportWorkspaceRun(io2, results, Boolean(g.json));
1526
- if (!g.json) {
1527
- io2.out(`\u2713 Wrote ${join6(dirLabel, "qore.workspace.json")}`);
1528
- const first = results.find((r) => r.status === "cloned" || r.status === "present");
1529
- if (first && !err) io2.out(`Next: cd ${join6(dirLabel, first.slug)} && qore env pull`);
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));
1530
1752
  }
1531
- if (err) throw err;
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
+ }
1771
+ function registerClone(program, ctx) {
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);
1532
1774
  });
1533
1775
  }
1534
1776
 
@@ -1553,6 +1795,13 @@ function describeConsent(consent, built) {
1553
1795
  }
1554
1796
 
1555
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
+ }
1556
1805
  function registerConfig(program, ctx) {
1557
1806
  const config = program.command("config").description("Change qore's own settings");
1558
1807
  config.command("telemetry").description("Show or set crash reporting").argument("[state]", "on or off").action(async (state) => {
@@ -1572,29 +1821,44 @@ function registerConfig(program, ctx) {
1572
1821
  io2.out(describeConsent(consent, built));
1573
1822
  if (state && consent.source !== "config") io2.err(`${consent.source} in your environment overrides this setting.`);
1574
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" });
1828
+ }
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;
1835
+ }
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`);
1838
+ });
1575
1839
  }
1576
1840
 
1577
1841
  // src/cli/commands/dev.ts
1578
- import { randomBytes as randomBytes5 } from "crypto";
1579
- import { realpath as realpath5 } from "fs/promises";
1842
+ import { randomBytes as randomBytes6 } from "crypto";
1843
+ import { realpath as realpath8 } from "fs/promises";
1580
1844
  import { constants as constants3 } from "os";
1581
- 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";
1582
1846
  import { StringDecoder } from "string_decoder";
1583
1847
 
1584
1848
  // src/core/context/repoConfig.ts
1585
1849
  import { readFile as readFile3 } from "fs/promises";
1586
- import { join as join7 } from "path";
1850
+ import { join as join10 } from "path";
1587
1851
  var FORBIDDEN = ["portal", "api", "apiOrigin", "issuer", "tenant", "tenantId", "app", "appId", "module", "moduleId"];
1588
1852
  var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
1589
1853
  var SCRIPT = /^[A-Za-z0-9:_.-]+$/;
1590
- var invalid2 = (msg, details) => new QoreError("QORE_CONTEXT_INVALID", `.qore.json: ${msg}`, { hint: "Fix .qore.json in the repository root; see the $schema it names.", details });
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 });
1591
1855
  function moduleDefaults(type) {
1592
1856
  return type === "frontend" ? { envFile: ".env.local", localSecrets: ["SESSION_COOKIE_SECRET"] } : { envFile: ".env", localSecrets: [] };
1593
1857
  }
1594
1858
  async function readRepoConfig(root) {
1595
1859
  let raw;
1596
1860
  try {
1597
- raw = await readFile3(join7(root, ".qore.json"), "utf8");
1861
+ raw = await readFile3(join10(root, ".qore.json"), "utf8");
1598
1862
  } catch (e) {
1599
1863
  if (e.code === "ENOENT") return {};
1600
1864
  throw e;
@@ -1603,44 +1867,44 @@ async function readRepoConfig(root) {
1603
1867
  try {
1604
1868
  doc = JSON.parse(raw);
1605
1869
  } catch {
1606
- throw invalid2("not valid JSON");
1870
+ throw invalid3("not valid JSON");
1607
1871
  }
1608
- 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");
1609
1873
  const v = doc.schemaVersion === void 0 ? 1 : doc.schemaVersion;
1610
- 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");
1611
1875
  if (v > 1) {
1612
1876
  throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `.qore.json schemaVersion ${v} is newer than this qore`, { hint: "Run: npm i -g @quatronic/qore" });
1613
1877
  }
1614
- for (const f of FORBIDDEN) if (f in doc) throw invalid2(`"${f}" is not allowed \u2014 .qore.json never selects a portal or a resource`, { field: f });
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 });
1615
1879
  const out = {};
1616
1880
  if (doc.envFile !== void 0) {
1617
- 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");
1618
1882
  out.envFile = doc.envFile;
1619
1883
  }
1620
1884
  if (doc.localSecrets !== void 0) {
1621
1885
  if (!Array.isArray(doc.localSecrets) || !doc.localSecrets.every((k) => typeof k === "string" && KEY.test(k))) {
1622
- throw invalid2("localSecrets must be a list of env key names");
1886
+ throw invalid3("localSecrets must be a list of env key names");
1623
1887
  }
1624
- 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");
1625
1889
  out.localSecrets = doc.localSecrets;
1626
1890
  }
1627
1891
  if (doc.dev !== void 0) {
1628
1892
  const d = doc.dev;
1629
- 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");
1630
1894
  const dev = {};
1631
1895
  if (d.install !== void 0) {
1632
- 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");
1633
1897
  dev.install = d.install;
1634
1898
  }
1635
1899
  if (d.services !== void 0) {
1636
1900
  if (!Array.isArray(d.services) || !d.services.every((s) => typeof s === "string" && SCRIPT.test(s))) {
1637
- throw invalid2("dev.services must be a list of script names");
1901
+ throw invalid3("dev.services must be a list of script names");
1638
1902
  }
1639
- 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");
1640
1904
  dev.services = d.services;
1641
1905
  }
1642
1906
  if (d.script !== void 0) {
1643
- 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");
1644
1908
  dev.script = d.script;
1645
1909
  }
1646
1910
  out.dev = dev;
@@ -1648,25 +1912,8 @@ async function readRepoConfig(root) {
1648
1912
  return out;
1649
1913
  }
1650
1914
 
1651
- // src/core/context/repoRoot.ts
1652
- import { stat } from "fs/promises";
1653
- import { dirname as dirname2, join as join8, resolve as resolve3 } from "path";
1654
- async function findRepoRoot(start2) {
1655
- let dir2 = resolve3(start2);
1656
- for (; ; ) {
1657
- try {
1658
- await stat(join8(dir2, ".git"));
1659
- return dir2;
1660
- } catch {
1661
- }
1662
- const up = dirname2(dir2);
1663
- if (up === dir2) return null;
1664
- dir2 = up;
1665
- }
1666
- }
1667
-
1668
1915
  // src/core/context/resolver.ts
1669
- import { isAbsolute, join as join9, relative as relative3 } from "path";
1916
+ import { isAbsolute as isAbsolute2, join as join11, relative as relative4 } from "path";
1670
1917
  var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1671
1918
  var HINT_FLAG = "cd into a module repository, or pass --module <slug|id>.";
1672
1919
  var RESOLVE = "/api/cli/v1/repos/resolve";
@@ -1774,8 +2021,8 @@ async function byFlag(deps2, o, cache, ws) {
1774
2021
  return { portal, session, module, source: overrides ? { kind: "flag", overrides } : { kind: "flag" }, repoRoot, workspace: ws };
1775
2022
  }
1776
2023
  var isBelow = (root, dir2) => {
1777
- const rel = relative3(root, dir2);
1778
- return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
2024
+ const rel = relative4(root, dir2);
2025
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
1779
2026
  };
1780
2027
  async function inWorkspaceRoot(deps2, ws) {
1781
2028
  if (!ws) return false;
@@ -1879,7 +2126,7 @@ async function anyLoggedIn(deps2, cfg) {
1879
2126
  return false;
1880
2127
  }
1881
2128
  async function resolveModuleContext(deps2, o) {
1882
- const cache = new ResolutionCache(join9(deps2.config.dir, "cache"), deps2.now);
2129
+ const cache = new ResolutionCache(join11(deps2.config.dir, "cache"), deps2.now);
1883
2130
  const ws = await findWorkspace(deps2.cwd, deps2.home);
1884
2131
  if (o.module) return byFlag(deps2, o, cache, ws);
1885
2132
  const cfg = await deps2.config.read();
@@ -2030,7 +2277,7 @@ function exitHint(tail, h) {
2030
2277
  }
2031
2278
 
2032
2279
  // src/core/dev/env.ts
2033
- import { dirname as dirname5, join as join12 } from "path";
2280
+ import { dirname as dirname6, join as join14 } from "path";
2034
2281
 
2035
2282
  // src/core/env/fetch.ts
2036
2283
  async function fetchLocalEnv(deps2, mc, app) {
@@ -2132,8 +2379,8 @@ function renderEnv(original, merged) {
2132
2379
  }
2133
2380
 
2134
2381
  // src/core/env/pull.ts
2135
- import { lstat, realpath as realpath2 } from "fs/promises";
2136
- import { basename, dirname as dirname3, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve4, sep } from "path";
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";
2137
2384
  var outside = (target) => new QoreError("QORE_CONTEXT_INVALID", `The env file must be inside the repository (got ${target})`, {
2138
2385
  hint: "Use a path inside the repository for --target or .qore.json envFile."
2139
2386
  });
@@ -2141,29 +2388,29 @@ var symlinked = (target) => new QoreError("QORE_CONTEXT_INVALID", `The env file
2141
2388
  hint: "Use a path inside the repository for --target or .qore.json envFile."
2142
2389
  });
2143
2390
  function inside(root, p) {
2144
- const rel = relative4(root, p);
2145
- return rel !== "" && rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute2(rel);
2391
+ const rel = relative5(root, p);
2392
+ return rel !== "" && rel !== ".." && !rel.startsWith(".." + sep2) && !isAbsolute3(rel);
2146
2393
  }
2147
2394
  async function resolveTarget(root, target, base = root) {
2148
- const realRoot = await realpath2(root);
2149
- const realBase = base === root ? realRoot : await realpath2(base);
2150
- const abs = resolve4(realBase, target);
2151
- if (!isAbsolute2(target) && !inside(realRoot, abs)) throw outside(target);
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);
2152
2399
  let parent;
2153
2400
  try {
2154
- parent = await realpath2(dirname3(abs));
2401
+ parent = await realpath5(dirname4(abs));
2155
2402
  } catch (e) {
2156
2403
  if (e.code !== "ENOENT") throw e;
2157
2404
  throw new QoreError("QORE_CONTEXT_INVALID", `The directory for ${target} does not exist`, {
2158
2405
  hint: "Create the parent directory, or point --target / .qore.json envFile at an existing one."
2159
2406
  });
2160
2407
  }
2161
- const final = resolve4(parent, basename(abs));
2408
+ const final = resolve5(parent, basename(abs));
2162
2409
  if (!inside(realRoot, final)) throw outside(target);
2163
2410
  try {
2164
2411
  const st = await lstat(final);
2165
2412
  if (st.isSymbolicLink()) throw symlinked(target);
2166
- const realFinal = await realpath2(final);
2413
+ const realFinal = await realpath5(final);
2167
2414
  if (!inside(realRoot, realFinal)) throw outside(target);
2168
2415
  } catch (e) {
2169
2416
  if (e instanceof QoreError) throw e;
@@ -2199,9 +2446,9 @@ function planPull(o) {
2199
2446
  }
2200
2447
 
2201
2448
  // src/core/env/write.ts
2202
- import { chmod as chmod2, readFile as readFile4, rename as rename3, unlink, writeFile as writeFile3 } from "fs/promises";
2203
- import { randomBytes as randomBytes3 } from "crypto";
2204
- import { basename as basename2, dirname as dirname4, join as join10 } from "path";
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";
2205
2452
  async function readOrNull(p) {
2206
2453
  try {
2207
2454
  return await readFile4(p, "utf8");
@@ -2212,10 +2459,10 @@ async function readOrNull(p) {
2212
2459
  }
2213
2460
  async function writeSecurely(target, text2) {
2214
2461
  const base = basename2(target);
2215
- const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes3(6).toString("hex")}.local` : `${base}.qore-${randomBytes3(6).toString("hex")}.tmp`;
2216
- const tmp = join10(dirname4(target), tmpName);
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);
2217
2464
  try {
2218
- await writeFile3(tmp, text2, { mode: 384, flag: "wx" });
2465
+ await writeFile4(tmp, text2, { mode: 384, flag: "wx" });
2219
2466
  await chmod2(tmp, 384);
2220
2467
  await rename3(tmp, target);
2221
2468
  } finally {
@@ -2241,8 +2488,8 @@ async function syncEnvFile(o) {
2241
2488
 
2242
2489
  // src/core/dev/lastStart.ts
2243
2490
  import { createHash as createHash2 } from "crypto";
2244
- import { realpath as realpath3 } from "fs/promises";
2245
- import { join as join11 } from "path";
2491
+ import { realpath as realpath6 } from "fs/promises";
2492
+ import { join as join13 } from "path";
2246
2493
  var NETWORK_REASONS = /* @__PURE__ */ new Set(["connect", "dns", "tls", "timeout"]);
2247
2494
  function isOffline(e) {
2248
2495
  if (!(e instanceof QoreError)) return false;
@@ -2251,14 +2498,14 @@ function isOffline(e) {
2251
2498
  return (e.status ?? 0) >= 500 || NETWORK_REASONS.has(String(e.details?.reason));
2252
2499
  }
2253
2500
  async function stateKey(deps2, root) {
2254
- const real = await realpath3(root).catch(() => root);
2501
+ const real = await realpath6(root).catch(() => root);
2255
2502
  return createHash2("sha256").update(trustKey(real, deps2.platform)).digest("hex").slice(0, 16);
2256
2503
  }
2257
2504
  async function saveLastStart(deps2, root, rec) {
2258
- 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);
2259
2506
  }
2260
2507
  async function loadLastStart(deps2, root) {
2261
- 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);
2262
2509
  return rec && rec.schemaVersion === 1 ? rec : null;
2263
2510
  }
2264
2511
 
@@ -2278,7 +2525,7 @@ async function devEnv(deps2, o) {
2278
2525
  if (o.mc && o.app) {
2279
2526
  try {
2280
2527
  const local = await fetchLocalEnv(deps2, o.mc, o.app);
2281
- const example = await readOrNull(join12(dirname5(target), ".env.example"));
2528
+ const example = await readOrNull(join14(dirname6(target), ".env.example"));
2282
2529
  const w = await syncEnvFile({
2283
2530
  target,
2284
2531
  example,
@@ -2413,7 +2660,7 @@ function providerDown(o) {
2413
2660
 
2414
2661
  // src/core/dev/project.ts
2415
2662
  import { access, readFile as readFile5 } from "fs/promises";
2416
- import { join as join13 } from "path";
2663
+ import { join as join15 } from "path";
2417
2664
  var LEGACY_DEV_SCRIPTS = ["node --env-file=.env --import ./dist/instrumentation.js dist/index.js"];
2418
2665
  var LOCKFILES = [["pnpm-lock.yaml", "pnpm"], ["package-lock.json", "npm"], ["yarn.lock", "yarn"]];
2419
2666
  var exists = (p) => access(p).then(() => true, () => false);
@@ -2421,7 +2668,7 @@ var unsupported = (message, hint, details) => new QoreError("QORE_DEV_UNSUPPORTE
2421
2668
  async function readPackageJson(root) {
2422
2669
  let raw;
2423
2670
  try {
2424
- raw = await readFile5(join13(root, "package.json"), "utf8");
2671
+ raw = await readFile5(join15(root, "package.json"), "utf8");
2425
2672
  } catch {
2426
2673
  throw unsupported("This repository has no package.json", "qore dev starts Node repositories through their package.json scripts.");
2427
2674
  }
@@ -2437,7 +2684,7 @@ async function detectPackageManager(root, pkg) {
2437
2684
  if (!m) throw unsupported(`packageManager "${pkg.packageManager}" is not pnpm, npm or yarn`, "qore dev supports pnpm, npm and yarn.");
2438
2685
  return { name: m[1], version: m[2].split("+")[0] };
2439
2686
  }
2440
- 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 };
2441
2688
  throw unsupported("No package manager declared and no lockfile found", 'Add "packageManager": "pnpm@<version>" to package.json.');
2442
2689
  }
2443
2690
  function planRecipe(o) {
@@ -2480,21 +2727,21 @@ function installArgs(pm) {
2480
2727
  }
2481
2728
 
2482
2729
  // src/core/dev/install.ts
2483
- import { stat as stat2 } from "fs/promises";
2484
- import { join as join14 } from "path";
2730
+ import { stat as stat3 } from "fs/promises";
2731
+ import { join as join16 } from "path";
2485
2732
  var MARKERS = {
2486
2733
  pnpm: [".modules.yaml"],
2487
2734
  npm: [".package-lock.json"],
2488
2735
  yarn: [".yarn-state.yml", ".yarn-integrity"]
2489
2736
  };
2490
- var LOCK = { pnpm: "pnpm-lock.yaml", npm: "package-lock.json", yarn: "yarn.lock" };
2491
- var mtime = (p) => stat2(p).then((s) => s.mtimeMs, () => null);
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);
2492
2739
  async function needsInstall(root, pm) {
2493
- if (await mtime(join14(root, "node_modules")) === null) return true;
2740
+ if (await mtime(join16(root, "node_modules")) === null) return true;
2494
2741
  let marker = null;
2495
- 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));
2496
2743
  if (marker === null) return true;
2497
- const lock = await mtime(join14(root, LOCK[pm.name]));
2744
+ const lock = await mtime(join16(root, LOCK2[pm.name]));
2498
2745
  return lock !== null && lock > marker;
2499
2746
  }
2500
2747
 
@@ -2810,11 +3057,11 @@ async function checkPort(deps2, o) {
2810
3057
  throw await portInUseError(deps2, { slug: o.slug, port: o.port, key: o.key, runs: o.runs });
2811
3058
  }
2812
3059
  function abortableDelay(signal) {
2813
- return (ms) => new Promise((resolve5) => {
3060
+ return (ms) => new Promise((resolve6) => {
2814
3061
  const done = () => {
2815
3062
  clearTimeout(t);
2816
3063
  signal.removeEventListener("abort", done);
2817
- resolve5();
3064
+ resolve6();
2818
3065
  };
2819
3066
  const t = setTimeout(done, ms);
2820
3067
  signal.addEventListener("abort", done, { once: true });
@@ -2897,11 +3144,11 @@ function hintContext(recipe, pkg, env) {
2897
3144
 
2898
3145
  // src/core/dev/runFile.ts
2899
3146
  import { unlink as unlink2 } from "fs/promises";
2900
- import { join as join15 } from "path";
3147
+ import { join as join17 } from "path";
2901
3148
  async function runFilePath(deps2, root) {
2902
- const dir2 = join15(deps2.config.dir, "run");
3149
+ const dir2 = join17(deps2.config.dir, "run");
2903
3150
  const name = `${await stateKey(deps2, root)}.json`;
2904
- return { dir: dir2, name, path: join15(dir2, name) };
3151
+ return { dir: dir2, name, path: join17(dir2, name) };
2905
3152
  }
2906
3153
  async function writeRunFile(deps2, root, rec) {
2907
3154
  const { dir: dir2, name } = await runFilePath(deps2, root);
@@ -2922,6 +3169,177 @@ async function leftoverRun(deps2, root, o = {}) {
2922
3169
  return null;
2923
3170
  }
2924
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
+
2925
3343
  // src/cli/context.ts
2926
3344
  var allOfKind = (c, kind) => !!c && c.length > 0 && c.every((x) => x.kind === kind && typeof x.id === "string");
2927
3345
  async function moduleContextFor(ctx, flags, command) {
@@ -2933,11 +3351,11 @@ async function moduleContextFor(ctx, flags, command) {
2933
3351
  } catch (e) {
2934
3352
  if (!(e instanceof QoreError) || e.code !== "QORE_CONTEXT_REQUIRED" || !ctx.io.interactive) throw e;
2935
3353
  if (allOfKind(e.candidates, "module")) {
2936
- const choose = await ctx.io.select(
3354
+ const choose2 = await ctx.io.select(
2937
3355
  "Select a module",
2938
3356
  e.candidates.map((c) => ({ name: `${c.tenant ? `${c.tenant} / ` : ""}${c.slug} (${c.remote})`, value: c.id }))
2939
3357
  );
2940
- return resolveModuleContext(ctx.deps, { ...base, choose });
3358
+ return resolveModuleContext(ctx.deps, { ...base, choose: choose2 });
2941
3359
  }
2942
3360
  if (allOfKind(e.candidates, "tenant")) {
2943
3361
  const tenant = await ctx.io.select("Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.id })));
@@ -3145,9 +3563,9 @@ async function ensureTrusted(ctx, mc, app, o) {
3145
3563
  }
3146
3564
 
3147
3565
  // src/cli/commands/devWorkspace.ts
3148
- import { randomBytes as randomBytes4 } from "crypto";
3149
- import { realpath as realpath4 } from "fs/promises";
3150
- import { join as join17, relative as relative5 } from "path";
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";
3151
3569
 
3152
3570
  // src/core/dev/order.ts
3153
3571
  function selectSkips(modules, sel) {
@@ -3304,15 +3722,15 @@ function supervise(o) {
3304
3722
  });
3305
3723
  const readiness = /* @__PURE__ */ new Map();
3306
3724
  for (const m of o.modules) {
3307
- let resolve5;
3725
+ let resolve6;
3308
3726
  let settled = false;
3309
3727
  const promise = new Promise((r) => {
3310
- resolve5 = r;
3728
+ resolve6 = r;
3311
3729
  });
3312
3730
  readiness.set(m.slug, { promise, settle: (r) => {
3313
3731
  if (settled) return false;
3314
3732
  settled = true;
3315
- resolve5(r);
3733
+ resolve6(r);
3316
3734
  return true;
3317
3735
  } });
3318
3736
  }
@@ -3403,12 +3821,12 @@ function supervise(o) {
3403
3821
  }
3404
3822
 
3405
3823
  // src/core/workspace/stray.ts
3406
- import { lstat as lstat2, readdir } from "fs/promises";
3407
- import { join as join16 } from "path";
3824
+ import { lstat as lstat2, readdir as readdir3 } from "fs/promises";
3825
+ import { join as join19 } from "path";
3408
3826
  async function strayRepos(root, slugs) {
3409
3827
  let entries;
3410
3828
  try {
3411
- 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);
3412
3830
  } catch {
3413
3831
  return [];
3414
3832
  }
@@ -3416,7 +3834,7 @@ async function strayRepos(root, slugs) {
3416
3834
  for (const name of entries) {
3417
3835
  if (slugs.has(name)) continue;
3418
3836
  try {
3419
- await lstat2(join16(root, name, ".git"));
3837
+ await lstat2(join19(root, name, ".git"));
3420
3838
  names.push(name);
3421
3839
  } catch {
3422
3840
  }
@@ -3456,16 +3874,18 @@ function prefixer(slugs, color) {
3456
3874
  import { constants as constants2 } from "os";
3457
3875
  var childRunning = { current: false };
3458
3876
  function registerRun(program, ctx) {
3459
- program.command("run").description("Run a command with this module's local secrets in its environment").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--yes", "trust this repository without asking").argument("<command>", "the command to run (after --)").argument("[args...]", "its arguments, passed verbatim").action(async (cmd, args, f) => {
3460
- const { deps: deps2 } = ctx;
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) => {
3461
3878
  if (ctx.opts().json) {
3462
3879
  throw new QoreError("QORE_CONTEXT_INVALID", "run has no --json output", {
3463
3880
  hint: "Drop --json; the child's own output passes through."
3464
3881
  });
3465
3882
  }
3466
- const mc = await moduleContextFor(ctx, f, "run");
3467
- const app = await appFor(ctx, mc, f.app);
3468
- await ensureTrusted(ctx, mc, app, { yes: f.yes });
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 });
3469
3889
  const local = await fetchLocalEnv(deps2, mc, app);
3470
3890
  childRunning.current = true;
3471
3891
  let exit;
@@ -3499,7 +3919,9 @@ async function workspaceContext(ctx, ws) {
3499
3919
  const session = await openSession(deps2, portal, "dev", requestLog(ctx));
3500
3920
  await ensureCapability(deps2, portal.apiOrigin, "app.modules");
3501
3921
  await ensureCapability(deps2, portal.apiOrigin, "module.local-env");
3502
- 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 };
3503
3925
  } catch (e) {
3504
3926
  if (!isOffline(e)) throw e;
3505
3927
  const last = await loadLastStart(deps2, ws.root);
@@ -3574,7 +3996,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3574
3996
  continue;
3575
3997
  }
3576
3998
  try {
3577
- 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));
3578
4000
  safe.push(m);
3579
4001
  } catch (e) {
3580
4002
  if (!(e instanceof QoreError) || e.code === "QORE_GIT_UNAVAILABLE") throw e;
@@ -3611,7 +4033,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3611
4033
  const results = await applyActions(deps2, { portal: online.portal, app: am, actions, protocol: cloneProtocol(dirs), onResult: () => {
3612
4034
  } });
3613
4035
  renderResults(io2, results, false);
3614
- for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(join17(ws.root, r.slug));
4036
+ for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(join20(ws.root, r.slug));
3615
4037
  ({ present, plan } = computePlan());
3616
4038
  }
3617
4039
  for (const name of await strayRepos(ws.root, new Set(am.modules.map((m) => m.slug)))) renderFinding(ctx, strayClone(name, am.app.name));
@@ -3719,7 +4141,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3719
4141
  workspace: ws,
3720
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] }
3721
4143
  });
3722
- const rootReal = await realpath4(ws.root);
4144
+ const rootReal = await realpath7(ws.root);
3723
4145
  const withDocker = dockerRecovery(ctx, p.signal);
3724
4146
  for (const u of units.filter((x) => x.recipe && x.trusted)) {
3725
4147
  const slug = u.m.slug;
@@ -3732,7 +4154,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3732
4154
  type: u.m.type,
3733
4155
  moduleId: u.m.id,
3734
4156
  cfg: u.cfg,
3735
- random: () => randomBytes4(32).toString("hex")
4157
+ random: () => randomBytes5(32).toString("hex")
3736
4158
  });
3737
4159
  } catch (e) {
3738
4160
  if (offlineCause && isOffline(e)) throw offlineCause;
@@ -3742,7 +4164,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3742
4164
  const dev = u.dev;
3743
4165
  if (!dev) continue;
3744
4166
  if (online && dev.offline) {
3745
- warn(`${slug}: the portal is unreachable \u2014 starting from ${relative5(rootReal, dev.envFile)} as last written; values may be stale.`, { module: slug });
4167
+ warn(`${slug}: the portal is unreachable \u2014 starting from ${relative6(rootReal, dev.envFile)} as last written; values may be stale.`, { module: slug });
3746
4168
  }
3747
4169
  if (dev.kept.length) {
3748
4170
  warn(
@@ -3750,7 +4172,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3750
4172
  { module: slug, keys: dev.kept }
3751
4173
  );
3752
4174
  }
3753
- 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) });
3754
4176
  const services = servicesMissing(slug, dev.env, u.recipe, { servicesFlag: f.services, configured: u.cfg.dev?.services !== void 0 });
3755
4177
  if (services) renderFinding(ctx, services);
3756
4178
  try {
@@ -3792,7 +4214,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3792
4214
  if (online && units.every((u) => !u.dev.offline)) {
3793
4215
  const envFile = (m) => {
3794
4216
  const u = units.find((x) => x.m.id === m.id);
3795
- return u ? relative5(rootReal, u.dev.envFile) : join17(m.slug, moduleDefaults(m.type).envFile);
4217
+ return u ? relative6(rootReal, u.dev.envFile) : join20(m.slug, moduleDefaults(m.type).envFile);
3796
4218
  };
3797
4219
  await saveLastStart(deps2, ws.root, {
3798
4220
  schemaVersion: 1,
@@ -3981,11 +4403,17 @@ function makeStderrTee(sink) {
3981
4403
  };
3982
4404
  }
3983
4405
  function registerDev(program, ctx) {
3984
- program.command("dev").description("Start this module's dev server \u2014 or, in a workspace root, every module of the app").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--only <slugs>", "workspace: start only these modules (comma-separated)").option("--skip <slugs>", "workspace: start all but these modules (comma-separated)").option("--no-install", "do not run the package manager's install").option("--no-services", "do not start local services (db:up)").option("--kill-others", "workspace: the first module to exit stops the rest").option("--wait-timeout <s>", "workspace: seconds to wait for a provider's port", "60").option("--yes", "trust the repositories without asking").action(async (f) => {
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) => {
3985
4407
  const waitTimeout = parseWaitTimeout(f.waitTimeout);
3986
- const ws = await workspaceRoot(ctx.deps);
3987
- if (ws && f.module === void 0) return workspaceDev(ctx, ws, f, waitTimeout);
3988
- return moduleDev(ctx, f, waitTimeout);
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);
3989
4417
  });
3990
4418
  }
3991
4419
  function parseWaitTimeout(raw) {
@@ -4025,7 +4453,7 @@ async function providerFindings(ctx, root, mc, app, env) {
4025
4453
  let fix = `qore clone ${app.slug}`;
4026
4454
  if (ws && ws.appId === app.id) fix = cdAndDev(deps2.home, ws.root);
4027
4455
  else if (providers.length === 1) {
4028
- const sibling = join18(dirname6(root), providers[0].slug);
4456
+ const sibling = join21(dirname7(root), providers[0].slug);
4029
4457
  if ((await deps2.git.inspect(sibling)).isRepo) fix = cdAndDev(deps2.home, sibling);
4030
4458
  }
4031
4459
  return providerDown({
@@ -4059,6 +4487,7 @@ async function devContext(ctx, f, root) {
4059
4487
  return { online: null, module: last.module, offlineCause: e };
4060
4488
  }
4061
4489
  const app = await appFor(ctx, mc, f.app);
4490
+ await rememberContext(ctx, mc, app);
4062
4491
  await ensureTrusted(ctx, mc, app, { yes: f.yes });
4063
4492
  return { online: { mc, app }, module: mc.module, offlineCause: null };
4064
4493
  }
@@ -4154,13 +4583,13 @@ async function prepareModule(ctx, f, waitTimeout, p) {
4154
4583
  type: c.module.type,
4155
4584
  moduleId: c.module.id,
4156
4585
  cfg,
4157
- random: () => randomBytes5(32).toString("hex")
4586
+ random: () => randomBytes6(32).toString("hex")
4158
4587
  });
4159
4588
  } catch (e) {
4160
4589
  if (c.offlineCause && isOffline(e)) throw c.offlineCause;
4161
4590
  throw e;
4162
4591
  }
4163
- const envFile = relative6(await realpath5(root), dev.envFile);
4592
+ const envFile = relative7(await realpath8(root), dev.envFile);
4164
4593
  if (dev.offline) io2.err(`! The portal is unreachable \u2014 starting from ${envFile} as last written; values may be stale.`);
4165
4594
  if (dev.kept.length) {
4166
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`);
@@ -4218,8 +4647,8 @@ function registerDebugTelemetry(program, ctx) {
4218
4647
  import pc3 from "picocolors";
4219
4648
 
4220
4649
  // src/core/doctor/checks.ts
4221
- import { realpath as realpath6 } from "fs/promises";
4222
- 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";
4223
4652
 
4224
4653
  // src/core/diagnostics/ssh.ts
4225
4654
  var SSH_TEST_ARGS = ["-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "git@github.com"];
@@ -4264,8 +4693,8 @@ var UPGRADE = "npm i -g @quatronic/qore";
4264
4693
  var RANK = { ok: 0, warn: 1, fail: 2 };
4265
4694
  var noop = () => {
4266
4695
  };
4267
- var ok = (label2, details) => ({ status: "ok", label: label2, ...details ? { details } : {} });
4268
- 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) });
4269
4698
  var isCancel = (e, signal) => e instanceof CancelledError || signal.aborted;
4270
4699
  var portalLabel = (p) => p.builtin ?? p.apiOrigin;
4271
4700
  var ToolTimeout = class extends Error {
@@ -4401,7 +4830,7 @@ function ownerText(e) {
4401
4830
  if (d.owner === "qore") return ` by a previous qore dev (pid ${String(d.pid)})`;
4402
4831
  return "";
4403
4832
  }
4404
- 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);
4405
4834
  async function moduleBody(deps2, m, runs, signal) {
4406
4835
  const parts = [];
4407
4836
  const part = async (fn) => {
@@ -4499,6 +4928,15 @@ async function resolveRepoModule(deps2, session, portal, meta, root) {
4499
4928
  }
4500
4929
  return session.api.get(`/api/cli/v1/modules/${encodeURIComponent(ids[0])}`);
4501
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
+ }
4502
4940
  function checkTelemetry(deps2, cfg) {
4503
4941
  const built = deps2.sentryDsn !== "";
4504
4942
  if (!cfg) return ok("crash reports off (config unreadable)", { enabled: false, source: "config", reason: "unreadable", built });
@@ -4554,6 +4992,7 @@ async function runDoctor(deps2, o = {}) {
4554
4992
  await push("github-ssh", "tools", () => checkGithubSsh(deps2, signal, toolMs));
4555
4993
  await push("docker", "tools", () => checkDocker(deps2, signal, toolMs));
4556
4994
  await push("telemetry", "tools", () => checkTelemetry(deps2, cfgReadable ? cfg : null));
4995
+ await push("projects-root", "tools", () => checkProjectsRoot(cfgReadable ? cfg : null));
4557
4996
  const sessions = /* @__PURE__ */ new Map();
4558
4997
  let loggedIn = [];
4559
4998
  try {
@@ -4646,7 +5085,7 @@ async function workspaceModule(deps2, ws, am, m, runs, signal, push, perModule)
4646
5085
  await push(`module:${m.slug}`, "workspace", () => ({ status: "warn", label: "not a safe directory name \u2014 skipped" }), m.slug);
4647
5086
  return;
4648
5087
  }
4649
- 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 };
4650
5089
  let prepared = null;
4651
5090
  await push(`module:${m.slug}`, "workspace", async () => {
4652
5091
  const info = await deps2.git.inspect(mod.root);
@@ -4676,26 +5115,28 @@ function registerDoctor(program, ctx) {
4676
5115
  }
4677
5116
 
4678
5117
  // src/cli/commands/env.ts
4679
- import { randomBytes as randomBytes6 } from "crypto";
4680
- import { chmod as chmod3, realpath as realpath7 } from "fs/promises";
4681
- import { dirname as dirname7, join as join20, relative as relative7 } from "path";
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";
4682
5121
  var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.length} chars)`;
4683
5122
  function registerEnv(program, ctx) {
4684
5123
  const env = program.command("env").description("Local environment");
4685
- env.command("pull").description("Write this module's local secrets into its env file").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--target <path>", "env file to write (inside the repository)").option("--use-portal", "take the portal's value for keys that differ locally").option("--yes", "trust this repository without asking").action(async (f) => {
4686
- const { deps: deps2, io: io2 } = ctx;
4687
- const g = ctx.opts();
4688
- const mc = await moduleContextFor(ctx, f, "env pull");
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");
4689
5129
  if (!mc.repoRoot) {
4690
5130
  throw new QoreError("QORE_CONTEXT_REQUIRED", "env pull writes into a module repository", { hint: "cd into the module's repository." });
4691
5131
  }
4692
- const app = await appFor(ctx, mc, f.app);
5132
+ const app = await appFor(c, mc, f.app);
5133
+ await rememberContext(c, mc, app);
4693
5134
  const cfg = await readRepoConfig(mc.repoRoot);
4694
5135
  const defaults = moduleDefaults(mc.module.type);
4695
5136
  const target = f.target ? await resolveTarget(mc.repoRoot, f.target, deps2.cwd) : await resolveTarget(mc.repoRoot, cfg.envFile ?? defaults.envFile);
4696
- await ensureTrusted(ctx, mc, app, { yes: f.yes });
5137
+ await ensureTrusted(c, mc, app, { yes: f.yes });
4697
5138
  const existing = await readOrNull(target);
4698
- 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;
4699
5140
  const local = await fetchLocalEnv(deps2, mc, app);
4700
5141
  const inputs = {
4701
5142
  existing,
@@ -4704,7 +5145,7 @@ function registerEnv(program, ctx) {
4704
5145
  managed: local.managedKeys,
4705
5146
  localSecrets: cfg.localSecrets ?? defaults.localSecrets,
4706
5147
  defaultChoice: f.usePortal ? "use-portal" : "keep",
4707
- random: () => randomBytes6(32).toString("hex")
5148
+ random: () => randomBytes7(32).toString("hex")
4708
5149
  };
4709
5150
  let plan = planPull(inputs);
4710
5151
  if (plan.conflicts.length && io2.interactive && !f.usePortal) {
@@ -4736,7 +5177,7 @@ function registerEnv(program, ctx) {
4736
5177
  } else {
4737
5178
  await writeSecurely(target, plan.text);
4738
5179
  }
4739
- const rel = relative7(await realpath7(mc.repoRoot), target);
5180
+ const rel = relative8(await realpath10(mc.repoRoot), target);
4740
5181
  if (g.json) {
4741
5182
  const names = {
4742
5183
  created: plan.created,
@@ -4765,6 +5206,9 @@ function registerEnv(program, ctx) {
4765
5206
  });
4766
5207
  }
4767
5208
 
5209
+ // src/cli/commands/init.ts
5210
+ import { realpath as realpath11 } from "fs/promises";
5211
+
4768
5212
  // src/core/auth/oidc.ts
4769
5213
  import * as oc from "openid-client";
4770
5214
  var LOGIN_SCOPE = "openid profile email offline_access qore:read qore:local-env";
@@ -4896,78 +5340,201 @@ async function deviceLogin(opts) {
4896
5340
  }
4897
5341
 
4898
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
+ }
4899
5416
  function registerLogin(program, ctx) {
4900
- program.command("login").description("Log in with your Qore account").option("--no-browser", "only print the URL").action(async (cmd) => {
4901
- const { deps: deps2, io: io2, signal } = ctx;
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;
4902
5425
  const g = ctx.opts();
4903
5426
  const cfg = await deps2.config.read();
4904
5427
  const portal = resolvePortal(g.portal, cfg.defaultProfile);
4905
- const meta = await fetchMeta(deps2.fetch, portal.apiOrigin);
4906
- const known = cfg.profiles[portal.apiOrigin]?.issuer;
4907
- const pinned = { ...portal, pinnedIssuer: portal.pinnedIssuer ?? known ?? null };
4908
- if (!pinned.pinnedIssuer && portal.builtin === null) {
4909
- io2.err(`! Logging in to ${portal.apiOrigin}, identity provider ${meta.auth.issuer}`);
4910
- }
4911
- negotiate(meta, pinned, deps2.version);
4912
- const issuer = pinned.pinnedIssuer ?? meta.auth.issuer;
4913
- const hash = profileHash(portal.apiOrigin, issuer);
4914
- const store = new TokenStore(deps2.keychain, hash);
4915
- const t = await deviceLogin({
4916
- oidc: await deps2.oidcFor(issuer),
4917
- store,
4918
- now: deps2.now,
4919
- signal,
4920
- withLock: deps2.withLock,
4921
- lockName: hash,
4922
- onCode: async (s, settled) => {
4923
- const url = s.verificationUriComplete ?? s.verificationUri;
4924
- io2.err(`! First copy your one-time code: ${s.userCode}`);
4925
- if (io2.interactive && cmd.browser) {
4926
- try {
4927
- await io2.waitForEnter(` Press Enter to open ${url} (or open it yourself)`, AbortSignal.any([signal, settled]));
4928
- } catch (e) {
4929
- if (settled.aborted && !signal.aborted) return;
4930
- throw e;
4931
- }
4932
- if (settled.aborted) return;
4933
- if (!await io2.openUrl(url)) io2.err(` Open ${url} in a browser`);
4934
- } else {
4935
- io2.err(` Open ${url} in a browser`);
4936
- }
4937
- }
4938
- });
4939
- await deps2.config.update((c) => {
4940
- c.profiles[portal.apiOrigin] = {
4941
- ...c.profiles[portal.apiOrigin],
4942
- apiOrigin: portal.apiOrigin,
4943
- issuer,
4944
- sub: t.sub,
4945
- email: t.email,
4946
- meta: { fetchedAt: deps2.now().toISOString(), doc: meta }
4947
- };
4948
- if (g.portal || !c.defaultProfile) c.defaultProfile = portal.apiOrigin;
4949
- });
4950
- const { api } = await openSession(deps2, { ...portal, pinnedIssuer: issuer }, "login", requestLog(ctx));
5428
+ const { profile, api } = await openSession(deps2, portal, "tenant list", requestLog(ctx));
4951
5429
  const me = await api.get("/api/cli/v1/me");
4952
- const prev = cfg.profiles[portal.apiOrigin];
4953
- let active = prev?.sub === t.sub && prev.activeTenant && me.tenants.some((x) => x.id === prev.activeTenant.id) ? prev.activeTenant : void 0;
4954
- if (active) {
4955
- } else if (me.tenants.length === 0) {
4956
- io2.err("! Your account is not a developer in any organisation. Ask an admin to invite you as developer.");
4957
- } else if (me.tenants.length === 1) {
4958
- active = { id: me.tenants[0].id, slug: me.tenants[0].slug };
4959
- io2.err(`\u2713 Using tenant ${me.tenants[0].name}`);
4960
- } else if (io2.interactive) {
4961
- active = await io2.select("Select a tenant", me.tenants.map((x) => ({ name: `${x.name} ${x.role}`, value: { id: x.id, slug: x.slug } })));
4962
- } else {
4963
- io2.err(`! You belong to ${me.tenants.length} tenants; choose one with \`qore tenant use <slug>\``);
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;
4964
5434
  }
4965
- await deps2.config.update((c) => {
4966
- c.profiles[portal.apiOrigin].activeTenant = active;
4967
- });
4968
- const label2 = portal.builtin ?? portal.apiOrigin;
4969
- if (g.json) renderJson(io2, { portal: label2, email: me.email, activeTenant: active ?? null });
4970
- else io2.out(`\u2713 Logged in as ${me.email} (${label2})`);
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.`);
4971
5538
  });
4972
5539
  }
4973
5540
 
@@ -5035,6 +5602,7 @@ async function appStatus(ctx, ws, appFlag) {
5035
5602
  await ensureCapability(deps2, portal.apiOrigin, "app.modules");
5036
5603
  await ensureCapability(deps2, portal.apiOrigin, "module.status");
5037
5604
  const am = await workspaceAppModules(session, portal, ws);
5605
+ await rememberWorkspace(ctx, ws, am.app);
5038
5606
  const { tenant, ...app } = am.app;
5039
5607
  if (appFlag && appFlag !== app.id && appFlag !== app.slug) {
5040
5608
  throw new QoreError("QORE_RESOURCE_NOT_FOUND", `This workspace is for ${app.slug}, not "${appFlag}"`, {
@@ -5062,19 +5630,21 @@ async function appStatus(ctx, ws, appFlag) {
5062
5630
  for (const line2 of table(rows)) io2.out(line2);
5063
5631
  }
5064
5632
  function registerStatus(program, ctx) {
5065
- program.command("status").description("Deployment and health per environment for this module (or, in a workspace root, the app)").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").action(async (flags) => {
5066
- const { deps: deps2, io: io2 } = ctx;
5067
- const g = ctx.opts();
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();
5068
5637
  if (!flags.module) {
5069
5638
  const ws = await workspaceRoot(deps2);
5070
5639
  if (ws) {
5071
- await appStatus(ctx, ws, flags.app);
5640
+ await appStatus(c, ws, flags.app);
5072
5641
  return;
5073
5642
  }
5074
5643
  }
5075
- const mc = await moduleContextFor(ctx, flags, "status");
5644
+ const mc = await moduleContextFor(c, flags, "status");
5076
5645
  await ensureCapability(deps2, mc.portal.apiOrigin, "module.status");
5077
- const app = await appFor(ctx, mc, flags.app);
5646
+ const app = await appFor(c, mc, flags.app);
5647
+ await rememberContext(c, mc, app);
5078
5648
  if (!g.json) io2.err(contextLine(mc, app));
5079
5649
  const st = await mc.session.api.get(
5080
5650
  `/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/status?app_id=${encodeURIComponent(app.id)}`
@@ -5094,7 +5664,7 @@ function registerStatus(program, ctx) {
5094
5664
  }
5095
5665
 
5096
5666
  // src/cli/commands/sync.ts
5097
- import { join as join21, relative as relative8 } from "path";
5667
+ import { join as join24, relative as relative9 } from "path";
5098
5668
  import { Option as Option2 } from "commander";
5099
5669
  function asFailure2(slug, e) {
5100
5670
  if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
@@ -5126,7 +5696,7 @@ function registerSync(program, ctx) {
5126
5696
  continue;
5127
5697
  }
5128
5698
  try {
5129
- 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));
5130
5700
  modulesForPlan.push(m);
5131
5701
  } catch (e) {
5132
5702
  earlyFailures.push(asFailure2(m.slug, e));
@@ -5138,7 +5708,7 @@ function registerSync(program, ctx) {
5138
5708
  } });
5139
5709
  await syncPresent(deps2, applied, actions, Boolean(flags.pull), { app, protocol: flags.protocol });
5140
5710
  for (const r of applied) {
5141
- 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) || ".";
5142
5712
  }
5143
5713
  const removedResults = removed.map((slug) => ({ slug, status: "removed", reason: "not a module of this app \u2014 left in place" }));
5144
5714
  const results = [...earlyFailures, ...applied, ...removedResults];
@@ -5147,52 +5717,6 @@ function registerSync(program, ctx) {
5147
5717
  });
5148
5718
  }
5149
5719
 
5150
- // src/cli/commands/tenant.ts
5151
- function registerTenant(program, ctx) {
5152
- const tenant = program.command("tenant").description("Manage the active tenant");
5153
- tenant.command("list").description("List tenants you have access to").action(async () => {
5154
- const { deps: deps2, io: io2 } = ctx;
5155
- const g = ctx.opts();
5156
- const cfg = await deps2.config.read();
5157
- const portal = resolvePortal(g.portal, cfg.defaultProfile);
5158
- const { profile, api } = await openSession(deps2, portal, "tenant list", requestLog(ctx));
5159
- const me = await api.get("/api/cli/v1/me");
5160
- const activeId = profile.activeTenant?.id;
5161
- if (g.json) {
5162
- renderJson(io2, { tenants: me.tenants.map((t) => ({ ...t, active: t.id === activeId })) });
5163
- return;
5164
- }
5165
- for (const t of me.tenants) {
5166
- const mark = t.id === activeId ? "*" : " ";
5167
- io2.out(`${mark} ${t.name} ${t.role}`);
5168
- }
5169
- });
5170
- tenant.command("use").description("Set the active tenant").argument("[slug]", "tenant slug").action(async (slug) => {
5171
- const { deps: deps2, io: io2 } = ctx;
5172
- const g = ctx.opts();
5173
- const cfg = await deps2.config.read();
5174
- const portal = resolvePortal(g.portal, cfg.defaultProfile);
5175
- const { api } = await openSession(deps2, portal, "tenant use", requestLog(ctx));
5176
- const me = await api.get("/api/cli/v1/me");
5177
- const candidates = me.tenants.map((t) => ({ kind: "tenant", id: t.id, slug: t.slug, name: t.name }));
5178
- let chosen;
5179
- if (slug) {
5180
- const found = me.tenants.find((t) => t.slug === slug);
5181
- if (!found) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `No tenant "${slug}" for this account`, { candidates });
5182
- chosen = { id: found.id, slug: found.slug };
5183
- } else if (io2.interactive) {
5184
- chosen = await io2.select("Select a tenant", me.tenants.map((t) => ({ name: `${t.name} ${t.role}`, value: { id: t.id, slug: t.slug } })));
5185
- } else {
5186
- throw new QoreError("QORE_CONTEXT_REQUIRED", "Which tenant? Pass a slug or run interactively.", { candidates });
5187
- }
5188
- await deps2.config.update((c) => {
5189
- c.profiles[portal.apiOrigin].activeTenant = chosen;
5190
- });
5191
- if (g.json) renderJson(io2, { activeTenant: chosen });
5192
- else io2.out(`\u2713 Using tenant ${chosen.slug}`);
5193
- });
5194
- }
5195
-
5196
5720
  // src/cli/commands/version.ts
5197
5721
  function registerVersion(program, ctx) {
5198
5722
  program.command("version").description("Show the CLI and portal version").action(async () => {
@@ -5212,11 +5736,11 @@ function registerVersion(program, ctx) {
5212
5736
  return;
5213
5737
  }
5214
5738
  io2.out(`qore ${deps2.version}`);
5215
- const label2 = portal.builtin ?? portal.apiOrigin;
5739
+ const label3 = portal.builtin ?? portal.apiOrigin;
5216
5740
  if (portalInfo) {
5217
- 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}`);
5218
5742
  } else {
5219
- io2.out(`portal ${label2}: unreachable`);
5743
+ io2.out(`portal ${label3}: unreachable`);
5220
5744
  }
5221
5745
  });
5222
5746
  }
@@ -5230,18 +5754,18 @@ function registerWhoami(program, ctx) {
5230
5754
  const portal = resolvePortal(g.portal, cfg.defaultProfile);
5231
5755
  const { profile, api } = await openSession(deps2, portal, "whoami", requestLog(ctx));
5232
5756
  const me = await api.get("/api/cli/v1/me");
5233
- const activeTenant = profile.activeTenant;
5234
- if (activeTenant && !me.tenants.some((t) => t.id === activeTenant.id)) {
5235
- io2.err(`! You no longer have developer access to ${activeTenant.slug}`);
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}`);
5236
5760
  }
5237
- const label2 = portal.builtin ?? portal.apiOrigin;
5761
+ const label3 = portal.builtin ?? portal.apiOrigin;
5238
5762
  if (g.json) {
5239
- renderJson(io2, { portal: label2, activeTenant: activeTenant ?? null, me });
5763
+ renderJson(io2, { portal: label3, activeTenant: activeTenant2 ?? null, me });
5240
5764
  return;
5241
5765
  }
5242
- io2.out(`${me.email} (${label2})`);
5766
+ io2.out(`${me.email} (${label3})`);
5243
5767
  for (const t of me.tenants) {
5244
- const mark = activeTenant?.id === t.id ? "*" : " ";
5768
+ const mark = activeTenant2?.id === t.id ? "*" : " ";
5245
5769
  io2.out(`${mark} ${t.name} ${t.role}`);
5246
5770
  for (const a of t.apps) {
5247
5771
  io2.out(` ${a.slug} ${a.access}`);
@@ -5255,6 +5779,7 @@ function registerWhoami(program, ctx) {
5255
5779
 
5256
5780
  // src/cli/commands/index.ts
5257
5781
  var COMMANDS = [
5782
+ registerInit,
5258
5783
  registerLogin,
5259
5784
  registerLogout,
5260
5785
  registerWhoami,
@@ -5294,7 +5819,7 @@ async function showNoticeOnce(deps2, io2, o) {
5294
5819
  // src/cli/telemetry/index.ts
5295
5820
  var CRASH_FLUSH_MS = 2e3;
5296
5821
  var CLEAN_FLUSH_MS = 200;
5297
- var loadSentry = async () => (await import("./sentry-B3MDCAOI.js")).createSdk;
5822
+ var loadSentry = async () => (await import("./sentry-GMM4X3FT.js")).createSdk;
5298
5823
  function createTelemetry(deps2, o) {
5299
5824
  const load = o.load ?? loadSentry;
5300
5825
  const flushMs = o.flushMs ?? { crash: CRASH_FLUSH_MS, clean: CLEAN_FLUSH_MS };
@@ -5348,11 +5873,11 @@ async function start(deps2, argv2, command, load, onScrub) {
5348
5873
  }
5349
5874
  function withDeadline(work, ms, signal) {
5350
5875
  if (signal?.aborted) return Promise.resolve(false);
5351
- return new Promise((resolve5) => {
5876
+ return new Promise((resolve6) => {
5352
5877
  const done = (v) => {
5353
5878
  clearTimeout(timer);
5354
5879
  signal?.removeEventListener("abort", onAbort);
5355
- resolve5(v);
5880
+ resolve6(v);
5356
5881
  };
5357
5882
  const onAbort = () => done(false);
5358
5883
  const timer = setTimeout(() => done(false), ms);
@@ -5404,7 +5929,7 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
5404
5929
 
5405
5930
  // src/core/config.ts
5406
5931
  import { readFile as readFile6 } from "fs/promises";
5407
- import { join as join22 } from "path";
5932
+ import { join as join25 } from "path";
5408
5933
  var empty = () => ({ schemaVersion: 1, profiles: {} });
5409
5934
  var ConfigStore = class {
5410
5935
  constructor(dir2) {
@@ -5412,7 +5937,7 @@ var ConfigStore = class {
5412
5937
  }
5413
5938
  dir;
5414
5939
  get file() {
5415
- return join22(this.dir, "config.json");
5940
+ return join25(this.dir, "config.json");
5416
5941
  }
5417
5942
  async read() {
5418
5943
  let raw;
@@ -5443,9 +5968,9 @@ var ConfigStore = class {
5443
5968
 
5444
5969
  // src/platform/git.ts
5445
5970
  import { execFile, execFileSync } from "child_process";
5446
- 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";
5447
5972
  import { homedir } from "os";
5448
- import { dirname as dirname8, join as join23 } from "path";
5973
+ import { dirname as dirname9, join as join26 } from "path";
5449
5974
  function parseRemotes(stdout) {
5450
5975
  const seen = /* @__PURE__ */ new Set();
5451
5976
  const out = [];
@@ -5512,7 +6037,7 @@ function gitExecOptions(repoDir) {
5512
6037
  return { cwd: homedir(), env, windowsHide: true, maxBuffer: 8 * 1024 * 1024 };
5513
6038
  }
5514
6039
  function runGit(args, repoDir) {
5515
- return new Promise((resolve5, reject) => {
6040
+ return new Promise((resolve6, reject) => {
5516
6041
  execFile("git", args, gitExecOptions(repoDir), (err, stdout, stderr) => {
5517
6042
  if (err && err.code === "ENOENT") {
5518
6043
  reject(new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
@@ -5521,7 +6046,7 @@ function runGit(args, repoDir) {
5521
6046
  return;
5522
6047
  }
5523
6048
  const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
5524
- 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 });
5525
6050
  });
5526
6051
  });
5527
6052
  }
@@ -5539,7 +6064,7 @@ async function remotes(repoRoot) {
5539
6064
  return parseRemotes(r.stdout);
5540
6065
  }
5541
6066
  async function clone(url, dir2) {
5542
- await mkdir3(dirname8(dir2), { recursive: true });
6067
+ await mkdir4(dirname9(dir2), { recursive: true });
5543
6068
  const r = await runGit(["clone", "--", url, dir2]);
5544
6069
  return toResult(r, "git clone");
5545
6070
  }
@@ -5559,7 +6084,7 @@ async function status(repoRoot) {
5559
6084
  }
5560
6085
  async function statOrThrow(path) {
5561
6086
  try {
5562
- return await stat3(path);
6087
+ return await stat6(path);
5563
6088
  } catch (err) {
5564
6089
  if (err.code === "ENOENT") return void 0;
5565
6090
  throw new QoreError("QORE_INTERNAL", `could not stat ${path}`, { cause: err });
@@ -5571,12 +6096,12 @@ async function inspect(dir2) {
5571
6096
  if (!st.isDirectory()) return { exists: true, empty: false, isRepo: false, remotes: [] };
5572
6097
  let entries;
5573
6098
  try {
5574
- entries = await readdir2(dir2);
6099
+ entries = await readdir4(dir2);
5575
6100
  } catch (err) {
5576
6101
  throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
5577
6102
  }
5578
6103
  const empty2 = entries.length === 0;
5579
- const isRepo = await statOrThrow(join23(dir2, ".git")) !== void 0;
6104
+ const isRepo = await statOrThrow(join26(dir2, ".git")) !== void 0;
5580
6105
  const dirRemotes = isRepo ? await remotes(dir2) : [];
5581
6106
  return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
5582
6107
  }
@@ -5637,15 +6162,15 @@ var OsKeychain = class {
5637
6162
  };
5638
6163
 
5639
6164
  // src/platform/lock.ts
5640
- import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
5641
- import { join as join24 } from "path";
6165
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
6166
+ import { join as join27 } from "path";
5642
6167
  import lockfile from "proper-lockfile";
5643
6168
  function fileLock(dir2) {
5644
6169
  return async (name, fn) => {
5645
- const locks = join24(dir2, "locks");
5646
- await mkdir4(locks, { recursive: true, mode: 448 });
5647
- const target = join24(locks, `${name}.lock`);
5648
- await writeFile4(target, "", { flag: "a" });
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" });
5649
6174
  let release;
5650
6175
  let compromised;
5651
6176
  try {
@@ -5688,7 +6213,7 @@ var STDERR_DRAIN_MS = 500;
5688
6213
  function makeSpawner(spawnFn, proc) {
5689
6214
  return {
5690
6215
  run(cmd, args, env, o) {
5691
- return new Promise((resolve5, reject) => {
6216
+ return new Promise((resolve6, reject) => {
5692
6217
  let child;
5693
6218
  try {
5694
6219
  child = spawnFn(cmd, args, {
@@ -5722,7 +6247,7 @@ function makeSpawner(spawnFn, proc) {
5722
6247
  cleanup();
5723
6248
  const stderr = child.stderr;
5724
6249
  if (!o?.onStderr || !stderr || stderr.readableEnded) {
5725
- resolve5({ code, signal });
6250
+ resolve6({ code, signal });
5726
6251
  return;
5727
6252
  }
5728
6253
  let settled = false;
@@ -5730,7 +6255,7 @@ function makeSpawner(spawnFn, proc) {
5730
6255
  if (settled) return;
5731
6256
  settled = true;
5732
6257
  clearTimeout(timer);
5733
- resolve5({ code, signal });
6258
+ resolve6({ code, signal });
5734
6259
  };
5735
6260
  const timer = setTimeout(finish, STDERR_DRAIN_MS);
5736
6261
  timer.unref?.();
@@ -5785,13 +6310,13 @@ var systemLauncher = {
5785
6310
  child.stdout?.on("data", (c) => out.push(c));
5786
6311
  child.stderr?.on("data", (c) => err.push(c));
5787
6312
  let closed = false;
5788
- const exited = new Promise((resolve5) => {
5789
- 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" }));
5790
6315
  child.once("close", (code, signal) => {
5791
6316
  closed = true;
5792
6317
  out.flush();
5793
6318
  err.flush();
5794
- resolve5({ code, signal });
6319
+ resolve6({ code, signal });
5795
6320
  });
5796
6321
  });
5797
6322
  return {
@@ -5837,11 +6362,11 @@ var systemLauncher = {
5837
6362
  import { execFile as execFile2 } from "child_process";
5838
6363
  import { connect } from "net";
5839
6364
  function accepts(host, port) {
5840
- return new Promise((resolve5) => {
6365
+ return new Promise((resolve6) => {
5841
6366
  const s = connect({ host, port });
5842
6367
  const done = (v) => {
5843
6368
  s.destroy();
5844
- resolve5(v);
6369
+ resolve6(v);
5845
6370
  };
5846
6371
  s.setTimeout(500, () => done(false));
5847
6372
  s.once("connect", () => done(true));
@@ -5849,12 +6374,12 @@ function accepts(host, port) {
5849
6374
  });
5850
6375
  }
5851
6376
  function run2(cmd, args) {
5852
- return new Promise((resolve5) => {
6377
+ return new Promise((resolve6) => {
5853
6378
  execFile2(
5854
6379
  cmd,
5855
6380
  args,
5856
6381
  { timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
5857
- (e, stdout) => resolve5(e && !stdout ? null : String(stdout))
6382
+ (e, stdout) => resolve6(e && !stdout ? null : String(stdout))
5858
6383
  );
5859
6384
  });
5860
6385
  }
@@ -5947,12 +6472,12 @@ function parseRegProxy(stdout) {
5947
6472
 
5948
6473
  // src/platform/proxy.ts
5949
6474
  function run3(cmd, args) {
5950
- return new Promise((resolve5) => {
6475
+ return new Promise((resolve6) => {
5951
6476
  execFile3(
5952
6477
  cmd,
5953
6478
  args,
5954
6479
  { timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
5955
- (e, stdout) => resolve5(e && !stdout ? null : String(stdout))
6480
+ (e, stdout) => resolve6(e && !stdout ? null : String(stdout))
5956
6481
  );
5957
6482
  });
5958
6483
  }
@@ -5983,7 +6508,7 @@ function systemProxy(platform = process.platform) {
5983
6508
  }
5984
6509
 
5985
6510
  // src/version.ts
5986
- var VERSION = true ? "0.1.5" : "0.0.0-dev";
6511
+ var VERSION = true ? "0.1.6" : "0.0.0-dev";
5987
6512
 
5988
6513
  // src/cli.ts
5989
6514
  if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
@@ -6061,4 +6586,4 @@ if (!drained) {
6061
6586
  process.exit();
6062
6587
  }
6063
6588
  //# sourceMappingURL=cli.js.map
6064
- //# debugId=a70250c5-daf4-5ff3-b490-5556ac377549
6589
+ //# debugId=28c91dc8-dbb9-5c0a-91e3-f368931a4ed3