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