@quatronic/qore 0.1.6 → 0.1.8
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 +81 -2
- package/dist/cli.js +1990 -746
- package/package.json +23 -4
- package/viewer/dist/assets/elk.bundled-C8gcDXx2.js +24 -0
- package/viewer/dist/assets/index-BRgYtLRN.css +1 -0
- package/viewer/dist/assets/index-VDGZO23h.js +15 -0
- package/viewer/dist/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
- package/viewer/dist/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
- package/viewer/dist/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
- package/viewer/dist/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
- package/viewer/dist/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
- package/viewer/dist/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
- package/viewer/dist/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
- package/viewer/dist/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
- package/viewer/dist/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
- package/viewer/dist/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
- package/viewer/dist/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
- package/viewer/dist/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
- package/viewer/dist/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
- package/viewer/dist/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
- package/viewer/dist/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
- package/viewer/dist/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
- package/viewer/dist/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
- package/viewer/dist/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
- package/viewer/dist/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
- package/viewer/dist/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
- package/viewer/dist/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
- package/viewer/dist/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
- package/viewer/dist/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
- package/viewer/dist/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
- package/viewer/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/viewer/dist/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/viewer/dist/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/viewer/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/viewer/dist/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/viewer/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/viewer/dist/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
- package/viewer/dist/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
- package/viewer/dist/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
- package/viewer/dist/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
- package/viewer/dist/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
- package/viewer/dist/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
- package/viewer/dist/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
- package/viewer/dist/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
- package/viewer/dist/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
- package/viewer/dist/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
- package/viewer/dist/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
- package/viewer/dist/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
- package/viewer/dist/index.html +13 -0
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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]="c4bda056-b13b-52b5-831f-2afd3f6c6867")}catch(e){}}();
|
|
4
4
|
import { createRequire as __qoreCreateRequire } from "node:module"; const require = __qoreCreateRequire(import.meta.url);
|
|
5
5
|
import {
|
|
6
6
|
addSecretValues,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
|
|
10
10
|
// src/cli.ts
|
|
11
11
|
import { homedir as homedir2 } from "os";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
12
13
|
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
|
|
13
14
|
|
|
14
15
|
// src/cli/io.ts
|
|
@@ -169,7 +170,7 @@ function processIo(interactive, stderrTTY = Boolean(process.stderr.isTTY)) {
|
|
|
169
170
|
};
|
|
170
171
|
}
|
|
171
172
|
function createWaitForEnter(input2, output) {
|
|
172
|
-
return (message, signal) => new Promise((
|
|
173
|
+
return (message, signal) => new Promise((resolve7, reject) => {
|
|
173
174
|
const rl = createInterface({ input: input2, output, terminal: true });
|
|
174
175
|
const onAbort = () => {
|
|
175
176
|
rl.close();
|
|
@@ -178,7 +179,7 @@ function createWaitForEnter(input2, output) {
|
|
|
178
179
|
const done = () => {
|
|
179
180
|
signal.removeEventListener("abort", onAbort);
|
|
180
181
|
rl.close();
|
|
181
|
-
|
|
182
|
+
resolve7();
|
|
182
183
|
};
|
|
183
184
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
184
185
|
rl.on("SIGINT", () => {
|
|
@@ -382,16 +383,16 @@ function groupByModule(results) {
|
|
|
382
383
|
const order = [];
|
|
383
384
|
const groups = /* @__PURE__ */ new Map();
|
|
384
385
|
for (const r of results) {
|
|
385
|
-
const
|
|
386
|
-
if (!groups.has(
|
|
387
|
-
groups.set(
|
|
388
|
-
order.push(
|
|
386
|
+
const key2 = r.module ?? r.id;
|
|
387
|
+
if (!groups.has(key2)) {
|
|
388
|
+
groups.set(key2, []);
|
|
389
|
+
order.push(key2);
|
|
389
390
|
}
|
|
390
|
-
groups.get(
|
|
391
|
+
groups.get(key2).push(r);
|
|
391
392
|
}
|
|
392
393
|
const isSub = (r) => r.id.startsWith("pm:") || r.id.startsWith("node:");
|
|
393
|
-
return order.flatMap((
|
|
394
|
-
const g = groups.get(
|
|
394
|
+
return order.flatMap((key2) => {
|
|
395
|
+
const g = groups.get(key2);
|
|
395
396
|
return [...g.filter((r) => !isSub(r)), ...g.filter(isSub)];
|
|
396
397
|
});
|
|
397
398
|
}
|
|
@@ -538,8 +539,8 @@ async function fetchMetaWithDate(fetch3, apiOrigin) {
|
|
|
538
539
|
}
|
|
539
540
|
if (!res.ok) throw await problemToError(res);
|
|
540
541
|
const header = res.headers.get("date");
|
|
541
|
-
const
|
|
542
|
-
return { meta: await res.json(), date: Number.isNaN(
|
|
542
|
+
const ms2 = header ? Date.parse(header) : NaN;
|
|
543
|
+
return { meta: await res.json(), date: Number.isNaN(ms2) ? null : new Date(ms2) };
|
|
543
544
|
}
|
|
544
545
|
function negotiate(meta, portal, cliVersion) {
|
|
545
546
|
if (portal.pinnedIssuer && meta.auth.issuer !== portal.pinnedIssuer) {
|
|
@@ -678,7 +679,7 @@ var QoreApiClient = class {
|
|
|
678
679
|
this.o.onRequest?.(`${method} ${path} \u2192 ${res.status}${rid ? ` (${rid})` : ""}`);
|
|
679
680
|
return res;
|
|
680
681
|
}
|
|
681
|
-
async request(method, path, body) {
|
|
682
|
+
async request(method, path, body, parse2 = (r) => r.json()) {
|
|
682
683
|
let res = await this.once(method, path, body, false);
|
|
683
684
|
if (res.status === 401) {
|
|
684
685
|
res = await this.once(method, path, body, true);
|
|
@@ -700,11 +701,14 @@ var QoreApiClient = class {
|
|
|
700
701
|
}
|
|
701
702
|
}
|
|
702
703
|
if (!res.ok) throw await problemToError(res);
|
|
703
|
-
return
|
|
704
|
+
return parse2(res);
|
|
704
705
|
}
|
|
705
706
|
get(path) {
|
|
706
707
|
return this.request("GET", path);
|
|
707
708
|
}
|
|
709
|
+
getText(path) {
|
|
710
|
+
return this.request("GET", path, void 0, (r) => r.text());
|
|
711
|
+
}
|
|
708
712
|
post(path, body) {
|
|
709
713
|
return this.request("POST", path, body);
|
|
710
714
|
}
|
|
@@ -720,13 +724,13 @@ var RefreshCoordinator = class {
|
|
|
720
724
|
fresh(expiresAt) {
|
|
721
725
|
return new Date(expiresAt).getTime() - this.o.now().getTime() > SKEW_MS;
|
|
722
726
|
}
|
|
723
|
-
async rotate(
|
|
727
|
+
async rotate(scope2) {
|
|
724
728
|
let stored = await this.o.store.getRefresh();
|
|
725
729
|
if (!stored) throw new QoreError("QORE_NOT_LOGGED_IN", "Not logged in", { hint: "Run `qore login`." });
|
|
726
730
|
let t;
|
|
727
731
|
for (let attempt = 0; ; attempt++) {
|
|
728
732
|
try {
|
|
729
|
-
t = await (await this.o.oidc()).refresh(stored.rt,
|
|
733
|
+
t = await (await this.o.oidc()).refresh(stored.rt, scope2);
|
|
730
734
|
break;
|
|
731
735
|
} catch (e) {
|
|
732
736
|
if (!(e instanceof QoreError && e.code === "QORE_SESSION_EXPIRED")) throw e;
|
|
@@ -742,7 +746,7 @@ var RefreshCoordinator = class {
|
|
|
742
746
|
throw e;
|
|
743
747
|
}
|
|
744
748
|
}
|
|
745
|
-
await this.o.store.setRefresh({ rt: t.refreshToken, sub: t.sub, scope: t.scope || (
|
|
749
|
+
await this.o.store.setRefresh({ rt: t.refreshToken, sub: t.sub, scope: t.scope || (scope2 ?? stored.scope), obtainedAt: this.o.now().toISOString() });
|
|
746
750
|
return t;
|
|
747
751
|
}
|
|
748
752
|
async accessToken(opts = {}) {
|
|
@@ -927,23 +931,23 @@ var ResolutionCache = class {
|
|
|
927
931
|
fresh(e) {
|
|
928
932
|
return !!e && this.now().getTime() - new Date(e.resolvedAt).getTime() < TTL_MS2;
|
|
929
933
|
}
|
|
930
|
-
async get(origin,
|
|
931
|
-
const e = (await this.load()).entries[`${origin}|${
|
|
934
|
+
async get(origin, key2) {
|
|
935
|
+
const e = (await this.load()).entries[`${origin}|${key2}`];
|
|
932
936
|
return this.fresh(e) ? e.moduleId : null;
|
|
933
937
|
}
|
|
934
938
|
/** Never throws: a read-only config dir, a full disk or a Windows EPERM on rename
|
|
935
939
|
* (another qore holding the file) must not fail a command that already resolved. */
|
|
936
|
-
async put(origin,
|
|
940
|
+
async put(origin, key2, moduleId) {
|
|
937
941
|
const f = await this.load();
|
|
938
|
-
f.entries[`${origin}|${
|
|
942
|
+
f.entries[`${origin}|${key2}`] = { moduleId, resolvedAt: this.now().toISOString() };
|
|
939
943
|
for (const [k, e] of Object.entries(f.entries)) if (!this.fresh(e)) delete f.entries[k];
|
|
940
944
|
await this.save(f);
|
|
941
945
|
}
|
|
942
946
|
/** Never throws, like `put`. */
|
|
943
|
-
async drop(origin,
|
|
947
|
+
async drop(origin, key2) {
|
|
944
948
|
const f = await this.load();
|
|
945
|
-
if (!(`${origin}|${
|
|
946
|
-
delete f.entries[`${origin}|${
|
|
949
|
+
if (!(`${origin}|${key2}` in f.entries)) return;
|
|
950
|
+
delete f.entries[`${origin}|${key2}`];
|
|
947
951
|
await this.save(f);
|
|
948
952
|
}
|
|
949
953
|
async save(f) {
|
|
@@ -956,8 +960,8 @@ var ResolutionCache = class {
|
|
|
956
960
|
const f = await this.load();
|
|
957
961
|
const hits = [];
|
|
958
962
|
for (const [k, e] of Object.entries(f.entries)) {
|
|
959
|
-
const
|
|
960
|
-
if (keys.includes(k.slice(
|
|
963
|
+
const sep4 = k.lastIndexOf("|");
|
|
964
|
+
if (keys.includes(k.slice(sep4 + 1)) && this.fresh(e)) hits.push({ origin: k.slice(0, sep4), at: new Date(e.resolvedAt).getTime() });
|
|
961
965
|
}
|
|
962
966
|
hits.sort((a, b) => b.at - a.at);
|
|
963
967
|
return [...new Set(hits.map((h) => h.origin))];
|
|
@@ -1020,12 +1024,12 @@ async function isTrusted(deps2, root, moduleId) {
|
|
|
1020
1024
|
return (cfg.trustedRepos?.[await keyOf(deps2, root)] ?? []).includes(moduleId);
|
|
1021
1025
|
}
|
|
1022
1026
|
async function trust(deps2, root, moduleId) {
|
|
1023
|
-
const
|
|
1027
|
+
const key2 = await keyOf(deps2, root);
|
|
1024
1028
|
await deps2.config.update((c) => {
|
|
1025
1029
|
const repos = c.trustedRepos ??= {};
|
|
1026
|
-
const list = new Set(repos[
|
|
1030
|
+
const list = new Set(repos[key2] ?? []);
|
|
1027
1031
|
list.add(moduleId);
|
|
1028
|
-
repos[
|
|
1032
|
+
repos[key2] = [...list];
|
|
1029
1033
|
});
|
|
1030
1034
|
}
|
|
1031
1035
|
|
|
@@ -1186,8 +1190,8 @@ async function applyActions(deps2, o) {
|
|
|
1186
1190
|
const r = await deps2.git.clone(url, action.dir);
|
|
1187
1191
|
if (r.ok) {
|
|
1188
1192
|
await trust(deps2, action.dir, action.module.id);
|
|
1189
|
-
const
|
|
1190
|
-
if (
|
|
1193
|
+
const key2 = repoKey(action.module.repoUrl);
|
|
1194
|
+
if (key2) await cache.put(o.portal.apiOrigin, key2, action.module.id);
|
|
1191
1195
|
result = { slug: action.module.slug, status: "cloned", dir: action.dir };
|
|
1192
1196
|
} else {
|
|
1193
1197
|
const err = classifyCloneFailure(r.stderr, action.module, o.app, o.protocol);
|
|
@@ -1432,9 +1436,9 @@ function expandPath(deps2, input2) {
|
|
|
1432
1436
|
}
|
|
1433
1437
|
function trimSeparator(p) {
|
|
1434
1438
|
const root = parse(p).root;
|
|
1435
|
-
let
|
|
1436
|
-
while (
|
|
1437
|
-
return p.slice(0,
|
|
1439
|
+
let end2 = p.length;
|
|
1440
|
+
while (end2 > root.length && (p[end2 - 1] === "/" || p[end2 - 1] === "\\")) end2--;
|
|
1441
|
+
return p.slice(0, end2);
|
|
1438
1442
|
}
|
|
1439
1443
|
async function underAFile(path) {
|
|
1440
1444
|
for (let p = dirname3(path); ; p = dirname3(p)) {
|
|
@@ -1838,82 +1842,13 @@ function registerConfig(program, ctx) {
|
|
|
1838
1842
|
});
|
|
1839
1843
|
}
|
|
1840
1844
|
|
|
1841
|
-
// src/cli/commands/
|
|
1842
|
-
import {
|
|
1843
|
-
import {
|
|
1844
|
-
import {
|
|
1845
|
-
import { dirname as dirname7, join as join21, relative as relative7 } from "path";
|
|
1846
|
-
import { StringDecoder } from "string_decoder";
|
|
1847
|
-
|
|
1848
|
-
// src/core/context/repoConfig.ts
|
|
1849
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
1850
|
-
import { join as join10 } from "path";
|
|
1851
|
-
var FORBIDDEN = ["portal", "api", "apiOrigin", "issuer", "tenant", "tenantId", "app", "appId", "module", "moduleId"];
|
|
1852
|
-
var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1853
|
-
var SCRIPT = /^[A-Za-z0-9:_.-]+$/;
|
|
1854
|
-
var invalid3 = (msg, details) => new QoreError("QORE_CONTEXT_INVALID", `.qore.json: ${msg}`, { hint: "Fix .qore.json in the repository root; see the $schema it names.", details });
|
|
1855
|
-
function moduleDefaults(type) {
|
|
1856
|
-
return type === "frontend" ? { envFile: ".env.local", localSecrets: ["SESSION_COOKIE_SECRET"] } : { envFile: ".env", localSecrets: [] };
|
|
1857
|
-
}
|
|
1858
|
-
async function readRepoConfig(root) {
|
|
1859
|
-
let raw;
|
|
1860
|
-
try {
|
|
1861
|
-
raw = await readFile3(join10(root, ".qore.json"), "utf8");
|
|
1862
|
-
} catch (e) {
|
|
1863
|
-
if (e.code === "ENOENT") return {};
|
|
1864
|
-
throw e;
|
|
1865
|
-
}
|
|
1866
|
-
let doc;
|
|
1867
|
-
try {
|
|
1868
|
-
doc = JSON.parse(raw);
|
|
1869
|
-
} catch {
|
|
1870
|
-
throw invalid3("not valid JSON");
|
|
1871
|
-
}
|
|
1872
|
-
if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw invalid3("must be an object");
|
|
1873
|
-
const v = doc.schemaVersion === void 0 ? 1 : doc.schemaVersion;
|
|
1874
|
-
if (!Number.isInteger(v) || v < 1) throw invalid3("schemaVersion must be a positive integer");
|
|
1875
|
-
if (v > 1) {
|
|
1876
|
-
throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `.qore.json schemaVersion ${v} is newer than this qore`, { hint: "Run: npm i -g @quatronic/qore" });
|
|
1877
|
-
}
|
|
1878
|
-
for (const f of FORBIDDEN) if (f in doc) throw invalid3(`"${f}" is not allowed \u2014 .qore.json never selects a portal or a resource`, { field: f });
|
|
1879
|
-
const out = {};
|
|
1880
|
-
if (doc.envFile !== void 0) {
|
|
1881
|
-
if (typeof doc.envFile !== "string" || !doc.envFile) throw invalid3("envFile must be a non-empty string");
|
|
1882
|
-
out.envFile = doc.envFile;
|
|
1883
|
-
}
|
|
1884
|
-
if (doc.localSecrets !== void 0) {
|
|
1885
|
-
if (!Array.isArray(doc.localSecrets) || !doc.localSecrets.every((k) => typeof k === "string" && KEY.test(k))) {
|
|
1886
|
-
throw invalid3("localSecrets must be a list of env key names");
|
|
1887
|
-
}
|
|
1888
|
-
if (new Set(doc.localSecrets).size !== doc.localSecrets.length) throw invalid3("localSecrets must not contain duplicates");
|
|
1889
|
-
out.localSecrets = doc.localSecrets;
|
|
1890
|
-
}
|
|
1891
|
-
if (doc.dev !== void 0) {
|
|
1892
|
-
const d = doc.dev;
|
|
1893
|
-
if (!d || typeof d !== "object" || Array.isArray(d)) throw invalid3("dev must be an object");
|
|
1894
|
-
const dev = {};
|
|
1895
|
-
if (d.install !== void 0) {
|
|
1896
|
-
if (typeof d.install !== "boolean") throw invalid3("dev.install must be true or false");
|
|
1897
|
-
dev.install = d.install;
|
|
1898
|
-
}
|
|
1899
|
-
if (d.services !== void 0) {
|
|
1900
|
-
if (!Array.isArray(d.services) || !d.services.every((s) => typeof s === "string" && SCRIPT.test(s))) {
|
|
1901
|
-
throw invalid3("dev.services must be a list of script names");
|
|
1902
|
-
}
|
|
1903
|
-
if (new Set(d.services).size !== d.services.length) throw invalid3("dev.services must not contain duplicates");
|
|
1904
|
-
dev.services = d.services;
|
|
1905
|
-
}
|
|
1906
|
-
if (d.script !== void 0) {
|
|
1907
|
-
if (typeof d.script !== "string" || !SCRIPT.test(d.script)) throw invalid3("dev.script must be a script name");
|
|
1908
|
-
dev.script = d.script;
|
|
1909
|
-
}
|
|
1910
|
-
out.dev = dev;
|
|
1911
|
-
}
|
|
1912
|
-
return out;
|
|
1913
|
-
}
|
|
1845
|
+
// src/cli/commands/db.ts
|
|
1846
|
+
import { stat as stat4 } from "fs/promises";
|
|
1847
|
+
import { basename as basename3, join as join15 } from "path";
|
|
1848
|
+
import { InvalidArgumentError } from "commander";
|
|
1914
1849
|
|
|
1915
1850
|
// src/core/context/resolver.ts
|
|
1916
|
-
import { isAbsolute as isAbsolute2, join as
|
|
1851
|
+
import { isAbsolute as isAbsolute2, join as join10, relative as relative4 } from "path";
|
|
1917
1852
|
var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1918
1853
|
var HINT_FLAG = "cd into a module repository, or pass --module <slug|id>.";
|
|
1919
1854
|
var RESOLVE = "/api/cli/v1/repos/resolve";
|
|
@@ -1951,10 +1886,10 @@ async function flagOverrides(deps2, session, portal, cache, moduleId) {
|
|
|
1951
1886
|
const keyed = (await repoRemotes(deps2))?.keyed ?? [];
|
|
1952
1887
|
const derived = /* @__PURE__ */ new Map();
|
|
1953
1888
|
const uncached = [];
|
|
1954
|
-
for (const
|
|
1955
|
-
const id = await cache.get(portal.apiOrigin,
|
|
1956
|
-
if (id) derived.set(
|
|
1957
|
-
else uncached.push(
|
|
1889
|
+
for (const key2 of new Set(keyed.map((r) => r.key))) {
|
|
1890
|
+
const id = await cache.get(portal.apiOrigin, key2);
|
|
1891
|
+
if (id) derived.set(key2, /* @__PURE__ */ new Set([id]));
|
|
1892
|
+
else uncached.push(key2);
|
|
1958
1893
|
}
|
|
1959
1894
|
if (uncached.length > 0) {
|
|
1960
1895
|
try {
|
|
@@ -2125,8 +2060,9 @@ async function anyLoggedIn(deps2, cfg) {
|
|
|
2125
2060
|
}
|
|
2126
2061
|
return false;
|
|
2127
2062
|
}
|
|
2063
|
+
var isForeignRepo = (e) => e instanceof QoreError && e.code === "QORE_CONTEXT_REQUIRED" && e.details?.foreignRepo === true;
|
|
2128
2064
|
async function resolveModuleContext(deps2, o) {
|
|
2129
|
-
const cache = new ResolutionCache(
|
|
2065
|
+
const cache = new ResolutionCache(join10(deps2.config.dir, "cache"), deps2.now);
|
|
2130
2066
|
const ws = await findWorkspace(deps2.cwd, deps2.home);
|
|
2131
2067
|
if (o.module) return byFlag(deps2, o, cache, ws);
|
|
2132
2068
|
const cfg = await deps2.config.read();
|
|
@@ -2143,7 +2079,7 @@ async function resolveModuleContext(deps2, o) {
|
|
|
2143
2079
|
const said = gitError?.hint;
|
|
2144
2080
|
throw new QoreError("QORE_CONTEXT_REQUIRED", "This repository has no remote Qore can match", {
|
|
2145
2081
|
hint: said ? `${HINT_FLAG} git said: ${said}` : HINT_FLAG,
|
|
2146
|
-
details: gitError ? { git: said ?? gitError.message } :
|
|
2082
|
+
details: gitError ? { git: said ?? gitError.message } : { foreignRepo: true },
|
|
2147
2083
|
cause: gitError
|
|
2148
2084
|
});
|
|
2149
2085
|
}
|
|
@@ -2172,6 +2108,7 @@ async function resolveModuleContext(deps2, o) {
|
|
|
2172
2108
|
}
|
|
2173
2109
|
const details = { triedProfiles: tried };
|
|
2174
2110
|
if (skipped.length > 0) details.skippedProfiles = skipped;
|
|
2111
|
+
if (answered.length > 0) details.foreignRepo = true;
|
|
2175
2112
|
if (answered.length === 0) {
|
|
2176
2113
|
const cause = skipErrors[0] ?? unsupported2;
|
|
2177
2114
|
if (cause) throw withDetails(cause, details);
|
|
@@ -2202,183 +2139,457 @@ function pickApp2(mc, app) {
|
|
|
2202
2139
|
throw new QoreError("QORE_APP_AMBIGUOUS", `${mc.module.slug} is part of several apps`, { hint: "Pass --app <slug>.", candidates: appCandidates(apps) });
|
|
2203
2140
|
}
|
|
2204
2141
|
|
|
2205
|
-
// src/core/
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
return
|
|
2212
|
-
}
|
|
2213
|
-
if (/ZodError:/.test(text2)) {
|
|
2214
|
-
const keys = [...text2.matchAll(/"path":\s*\[\s*"([^"]+)"/g)].map((m) => m[1]);
|
|
2215
|
-
if (keys.length > 0) {
|
|
2216
|
-
return {
|
|
2217
|
-
reason: "invalid-env",
|
|
2218
|
-
keys,
|
|
2219
|
-
hint: `Missing or invalid environment variables: ${keys.join(", ")}. Set them in the portal's local secrets (then qore env pull), or in your env file.`
|
|
2220
|
-
};
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
const addrInUse = text2.match(/listen EADDRINUSE: address already in use (?:\S*:)?(\d+)/);
|
|
2224
|
-
if (addrInUse) {
|
|
2225
|
-
const port = Number(addrInUse[1]);
|
|
2226
|
-
return {
|
|
2227
|
-
reason: "port-in-use",
|
|
2228
|
-
port,
|
|
2229
|
-
hint: `Port ${port} was taken while the server started. Run qore dev again; if it repeats, lsof -iTCP:${port} -sTCP:LISTEN names the holder.`
|
|
2230
|
-
};
|
|
2142
|
+
// src/core/erd/dbErrors.ts
|
|
2143
|
+
function dbPort(url) {
|
|
2144
|
+
try {
|
|
2145
|
+
const u = new URL(url);
|
|
2146
|
+
return u.port ? Number(u.port) : 5432;
|
|
2147
|
+
} catch {
|
|
2148
|
+
return null;
|
|
2231
2149
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
const
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
return {
|
|
2240
|
-
reason: "conn-refused",
|
|
2241
|
-
port,
|
|
2242
|
-
hint: `The database on :${port} is not accepting connections (ECONNREFUSED). ` + (up ? `Fix: ${up} (docker compose ps shows its state)` : "docker compose ps shows its state.")
|
|
2243
|
-
};
|
|
2244
|
-
}
|
|
2245
|
-
if (role?.kind === "provider") {
|
|
2246
|
-
return {
|
|
2247
|
-
reason: "conn-refused",
|
|
2248
|
-
port,
|
|
2249
|
-
hint: `${role.key} points at :${port}, where nothing listens (ECONNREFUSED). Start that module \u2014 qore dev in the workspace root starts providers first.`
|
|
2250
|
-
};
|
|
2251
|
-
}
|
|
2252
|
-
return { reason: "conn-refused", port, hint: `Nothing listens on :${port} (ECONNREFUSED) \u2014 a service this module calls is not running.` };
|
|
2150
|
+
}
|
|
2151
|
+
function connectionOf(url) {
|
|
2152
|
+
try {
|
|
2153
|
+
const u = new URL(url);
|
|
2154
|
+
return { host: u.hostname, port: u.port ? Number(u.port) : 5432, database: decodeURIComponent(u.pathname.replace(/^\//, "")) };
|
|
2155
|
+
} catch {
|
|
2156
|
+
return { host: "", port: 0, database: "" };
|
|
2253
2157
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
}
|
|
2158
|
+
}
|
|
2159
|
+
function classifyDbError(e, url) {
|
|
2160
|
+
const { code, message } = e;
|
|
2161
|
+
const port = dbPort(url);
|
|
2162
|
+
const where = port === null ? "The database" : `The database on :${port}`;
|
|
2163
|
+
if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT" || /timeout expired/i.test(message ?? "")) {
|
|
2164
|
+
return new QoreError("QORE_SERVICE_UNHEALTHY", `${where} is not accepting connections`, {
|
|
2165
|
+
hint: "Start it with `qore dev` (or `pnpm db:up`), then run qore db erd again.",
|
|
2166
|
+
details: { reason: "conn-refused", port }
|
|
2167
|
+
});
|
|
2261
2168
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
hint: `The database schema is behind the code (relation ${relation[1]} does not exist). Migrations run when the app starts; if they failed above, that error is the cause.` + (reset ? ` To start clean: ${reset} (wipes local data).` : "")
|
|
2268
|
-
};
|
|
2169
|
+
if (code === "28P01" || code === "28000") {
|
|
2170
|
+
return new QoreError("QORE_SERVICE_UNHEALTHY", `${where} rejected the credentials in DATABASE_URL`, {
|
|
2171
|
+
hint: "The local database was created with other credentials than the portal now provides. `pnpm db:reset` recreates it \u2014 this deletes local data.",
|
|
2172
|
+
details: { reason: "db-auth", port }
|
|
2173
|
+
});
|
|
2269
2174
|
}
|
|
2270
|
-
if (
|
|
2271
|
-
return {
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
};
|
|
2175
|
+
if (code === "3D000") {
|
|
2176
|
+
return new QoreError("QORE_SERVICE_UNHEALTHY", "The database named in DATABASE_URL does not exist", {
|
|
2177
|
+
hint: "Start the app once with `qore dev` so its migrations create it, or run `pnpm db:reset`.",
|
|
2178
|
+
details: { reason: "db-schema", port }
|
|
2179
|
+
});
|
|
2275
2180
|
}
|
|
2276
|
-
return
|
|
2181
|
+
return new QoreError("QORE_SERVICE_UNHEALTHY", `Could not read the database: ${message ?? String(e)}`, {
|
|
2182
|
+
hint: "Check DATABASE_URL in the env file, then run qore db erd again.",
|
|
2183
|
+
details: { port }
|
|
2184
|
+
});
|
|
2277
2185
|
}
|
|
2278
2186
|
|
|
2279
|
-
// src/core/
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
return local;
|
|
2291
|
-
} catch (e) {
|
|
2292
|
-
if (!(e instanceof QoreError) || e.code !== "QORE_ACCESS_DENIED") throw e;
|
|
2293
|
-
if (e.serverCode === "app.access.denied") {
|
|
2294
|
-
throw withDetails(
|
|
2295
|
-
e,
|
|
2296
|
-
{ required: "write", have: mc.module.access },
|
|
2297
|
-
`You have ${mc.module.access} on ${mc.module.slug}; local secrets need write. Ask a maintainer of the app's dev team for write access.`
|
|
2298
|
-
);
|
|
2187
|
+
// src/core/erd/app.ts
|
|
2188
|
+
function orderModules(modules) {
|
|
2189
|
+
const bySlug = new Map(modules.map((m) => [m.slug, m]));
|
|
2190
|
+
const out = [];
|
|
2191
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2192
|
+
const visit = (m, trail) => {
|
|
2193
|
+
if (seen.has(m.slug) || trail.has(m.slug)) return;
|
|
2194
|
+
trail.add(m.slug);
|
|
2195
|
+
for (const dep of m.dependsOn) {
|
|
2196
|
+
const d = bySlug.get(dep);
|
|
2197
|
+
if (d) visit(d, trail);
|
|
2299
2198
|
}
|
|
2300
|
-
|
|
2301
|
-
|
|
2199
|
+
seen.add(m.slug);
|
|
2200
|
+
out.push(m);
|
|
2201
|
+
};
|
|
2202
|
+
for (const m of modules) visit(m, /* @__PURE__ */ new Set());
|
|
2203
|
+
return out;
|
|
2204
|
+
}
|
|
2205
|
+
function groupDatabases(modules) {
|
|
2206
|
+
const groups = [];
|
|
2207
|
+
for (const m of modules) {
|
|
2208
|
+
if (!m.url) continue;
|
|
2209
|
+
const c = connectionOf(m.url);
|
|
2210
|
+
const same2 = groups.find((g) => g.connection.host === c.host && g.connection.port === c.port && g.connection.database === c.database);
|
|
2211
|
+
if (same2) {
|
|
2212
|
+
same2.modules.push(m.slug);
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
groups.push({ key: m.slug, modules: [m.slug], url: m.url, connection: c });
|
|
2302
2216
|
}
|
|
2217
|
+
return groups;
|
|
2303
2218
|
}
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2219
|
+
var REF = /(?:^|\s)->\s*(?:([A-Za-z0-9_-]+):)?([A-Za-z_][\w]*)\.([A-Za-z_][\w]*)\.([A-Za-z_][\w]*)/;
|
|
2220
|
+
function parseRef(comment) {
|
|
2221
|
+
const m = comment ? REF.exec(comment) : null;
|
|
2222
|
+
return m ? { module: m[1] ?? null, schema: m[2], table: m[3], column: m[4] } : null;
|
|
2223
|
+
}
|
|
2224
|
+
function findEnd(dbs, ref, from) {
|
|
2225
|
+
const has = (d) => d.model?.tables.some((t) => t.schema === ref.schema && t.name === ref.table && t.columns.some((c) => c.name === ref.column)) ?? false;
|
|
2226
|
+
const candidates = ref.module ? dbs.filter((d) => d.modules.includes(ref.module)) : [...dbs.filter((d) => d.key !== from), ...dbs.filter((d) => d.key === from)];
|
|
2227
|
+
const hit2 = candidates.find(has);
|
|
2228
|
+
return hit2 ? { database: hit2.key, schema: ref.schema, table: ref.table, column: ref.column } : null;
|
|
2229
|
+
}
|
|
2230
|
+
function resolveRefs(dbs) {
|
|
2231
|
+
const refs = [];
|
|
2232
|
+
for (const d of dbs) for (const t of d.model?.tables ?? []) for (const c of t.columns) {
|
|
2233
|
+
const ref = parseRef(c.comment);
|
|
2234
|
+
const to = ref && findEnd(dbs, ref, d.key);
|
|
2235
|
+
if (to) refs.push({ from: { database: d.key, schema: t.schema, table: t.name, column: c.name }, to });
|
|
2311
2236
|
}
|
|
2312
|
-
return
|
|
2237
|
+
return refs;
|
|
2313
2238
|
}
|
|
2314
|
-
function
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2239
|
+
function combineModels(dbs) {
|
|
2240
|
+
return {
|
|
2241
|
+
tables: dbs.flatMap((d) => (d.model?.tables ?? []).map((t) => ({ ...t, database: d.key }))),
|
|
2242
|
+
enums: dbs.flatMap((d) => (d.model?.enums ?? []).map((e) => ({ ...e, database: d.key }))),
|
|
2243
|
+
databases: dbs.map((d) => ({
|
|
2244
|
+
key: d.key,
|
|
2245
|
+
modules: d.modules,
|
|
2246
|
+
connection: d.connection,
|
|
2247
|
+
...d.error ? { error: d.error } : {},
|
|
2248
|
+
...d.migrations ? { migrations: d.migrations } : {}
|
|
2249
|
+
})),
|
|
2250
|
+
refs: resolveRefs(dbs)
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// src/core/erd/introspect.ts
|
|
2255
|
+
var USER_SCHEMAS = `n.nspname not in ('pg_catalog', 'information_schema') and n.nspname not like 'pg_toast%' and n.nspname not like 'pg_temp%'`;
|
|
2256
|
+
var TABLE_KIND = `c.relkind in ('r', 'p') and not c.relispartition`;
|
|
2257
|
+
var TABLES = `
|
|
2258
|
+
select n.nspname as schema, c.relname as name, obj_description(c.oid, 'pg_class') as comment
|
|
2259
|
+
from pg_class c join pg_namespace n on n.oid = c.relnamespace
|
|
2260
|
+
where ${TABLE_KIND} and ${USER_SCHEMAS}
|
|
2261
|
+
order by 1, 2`;
|
|
2262
|
+
var COLUMNS = `
|
|
2263
|
+
select n.nspname as schema, c.relname as table, a.attname as name,
|
|
2264
|
+
format_type(a.atttypid, a.atttypmod) as type, a.attnotnull as not_null,
|
|
2265
|
+
pg_get_expr(d.adbin, d.adrelid) as "default", col_description(c.oid, a.attnum) as comment
|
|
2266
|
+
from pg_attribute a
|
|
2267
|
+
join pg_class c on c.oid = a.attrelid
|
|
2268
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
2269
|
+
left join pg_attrdef d on d.adrelid = a.attrelid and d.adnum = a.attnum
|
|
2270
|
+
where ${TABLE_KIND} and a.attnum > 0 and not a.attisdropped and ${USER_SCHEMAS}
|
|
2271
|
+
order by n.nspname, c.relname, a.attnum`;
|
|
2272
|
+
var CONSTRAINTS = `
|
|
2273
|
+
select n.nspname as schema, c.relname as table, con.conname as name, con.contype::text as type,
|
|
2274
|
+
(select array_agg(a.attname::text order by k.ord)
|
|
2275
|
+
from unnest(con.conkey) with ordinality as k(attnum, ord)
|
|
2276
|
+
join pg_attribute a on a.attrelid = con.conrelid and a.attnum = k.attnum) as columns,
|
|
2277
|
+
fn.nspname as target_schema, fc.relname as target_table,
|
|
2278
|
+
(select array_agg(a.attname::text order by k.ord)
|
|
2279
|
+
from unnest(con.confkey) with ordinality as k(attnum, ord)
|
|
2280
|
+
join pg_attribute a on a.attrelid = con.confrelid and a.attnum = k.attnum) as target_columns,
|
|
2281
|
+
con.confupdtype::text as on_update, con.confdeltype::text as on_delete,
|
|
2282
|
+
pg_get_constraintdef(con.oid) as definition
|
|
2283
|
+
from pg_constraint con
|
|
2284
|
+
join pg_class c on c.oid = con.conrelid
|
|
2285
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
2286
|
+
left join pg_class fc on fc.oid = con.confrelid
|
|
2287
|
+
left join pg_namespace fn on fn.oid = fc.relnamespace
|
|
2288
|
+
where con.contype in ('p', 'u', 'f', 'c') and not c.relispartition and ${USER_SCHEMAS}
|
|
2289
|
+
order by n.nspname, c.relname, con.conname`;
|
|
2290
|
+
var INDEXES = `
|
|
2291
|
+
select n.nspname as schema, c.relname as table, ic.relname as name, i.indisunique as "unique", am.amname as method,
|
|
2292
|
+
(select array_agg(pg_get_indexdef(i.indexrelid, k.n, true) order by k.n)
|
|
2293
|
+
from generate_series(1, i.indnkeyatts) as k(n)) as columns
|
|
2294
|
+
from pg_index i
|
|
2295
|
+
join pg_class c on c.oid = i.indrelid
|
|
2296
|
+
join pg_class ic on ic.oid = i.indexrelid
|
|
2297
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
2298
|
+
join pg_am am on am.oid = ic.relam
|
|
2299
|
+
where ${TABLE_KIND} and not i.indisprimary
|
|
2300
|
+
and not exists (select 1 from pg_constraint x where x.conindid = i.indexrelid and x.contype in ('p', 'u'))
|
|
2301
|
+
and ${USER_SCHEMAS}
|
|
2302
|
+
order by n.nspname, c.relname, ic.relname`;
|
|
2303
|
+
var ENUMS = `
|
|
2304
|
+
select n.nspname as schema, t.typname as name, array_agg(e.enumlabel::text order by e.enumsortorder) as "values"
|
|
2305
|
+
from pg_type t
|
|
2306
|
+
join pg_namespace n on n.oid = t.typnamespace
|
|
2307
|
+
join pg_enum e on e.enumtypid = t.oid
|
|
2308
|
+
where t.typtype = 'e' and ${USER_SCHEMAS}
|
|
2309
|
+
group by 1, 2 order by 1, 2`;
|
|
2310
|
+
var FK_ACTION = { a: "NO_ACTION", r: "RESTRICT", c: "CASCADE", n: "SET_NULL", d: "SET_DEFAULT" };
|
|
2311
|
+
var str = (v) => String(v);
|
|
2312
|
+
var strOrNull = (v) => v === null || v === void 0 ? null : String(v);
|
|
2313
|
+
function strArray(v) {
|
|
2314
|
+
if (Array.isArray(v)) return v.map(str);
|
|
2315
|
+
if (typeof v === "string" && v.startsWith("{") && v.endsWith("}")) {
|
|
2316
|
+
return v.length === 2 ? [] : v.slice(1, -1).split(",").map((s) => s.replace(/^"|"$/g, ""));
|
|
2319
2317
|
}
|
|
2320
|
-
return
|
|
2318
|
+
return [];
|
|
2321
2319
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2320
|
+
var key = (r) => `${str(r.schema)}.${str(r.table)}`;
|
|
2321
|
+
var checkExpression = (definition) => definition.replace(/^CHECK \((.*)\)$/s, "$1");
|
|
2322
|
+
async function introspect(query) {
|
|
2323
|
+
const rows = [];
|
|
2324
|
+
for (const sql of [TABLES, COLUMNS, CONSTRAINTS, INDEXES, ENUMS]) rows.push((await query(sql)).rows);
|
|
2325
|
+
const [tables, columns, constraints, indexes, enums] = rows;
|
|
2326
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2327
|
+
for (const r of tables) {
|
|
2328
|
+
const t = {
|
|
2329
|
+
schema: str(r.schema),
|
|
2330
|
+
name: str(r.name),
|
|
2331
|
+
comment: strOrNull(r.comment),
|
|
2332
|
+
columns: [],
|
|
2333
|
+
primaryKey: null,
|
|
2334
|
+
uniques: [],
|
|
2335
|
+
foreignKeys: [],
|
|
2336
|
+
checks: [],
|
|
2337
|
+
indexes: []
|
|
2338
|
+
};
|
|
2339
|
+
byKey.set(`${t.schema}.${t.name}`, t);
|
|
2340
|
+
}
|
|
2341
|
+
for (const r of columns) {
|
|
2342
|
+
byKey.get(key(r))?.columns.push({
|
|
2343
|
+
name: str(r.name),
|
|
2344
|
+
type: str(r.type),
|
|
2345
|
+
notNull: Boolean(r.not_null),
|
|
2346
|
+
default: strOrNull(r.default),
|
|
2347
|
+
comment: strOrNull(r.comment)
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
for (const r of constraints) {
|
|
2351
|
+
const t = byKey.get(key(r));
|
|
2352
|
+
if (!t) continue;
|
|
2353
|
+
const name = str(r.name);
|
|
2354
|
+
const cols = strArray(r.columns);
|
|
2355
|
+
switch (str(r.type)) {
|
|
2356
|
+
case "p":
|
|
2357
|
+
t.primaryKey = { name, columns: cols };
|
|
2358
|
+
break;
|
|
2359
|
+
case "u":
|
|
2360
|
+
t.uniques.push({ name, columns: cols });
|
|
2361
|
+
break;
|
|
2362
|
+
case "c":
|
|
2363
|
+
t.checks.push({ name, expression: checkExpression(str(r.definition)) });
|
|
2364
|
+
break;
|
|
2365
|
+
case "f":
|
|
2366
|
+
t.foreignKeys.push({
|
|
2367
|
+
name,
|
|
2368
|
+
columns: cols,
|
|
2369
|
+
targetSchema: str(r.target_schema),
|
|
2370
|
+
targetTable: str(r.target_table),
|
|
2371
|
+
targetColumns: strArray(r.target_columns),
|
|
2372
|
+
onUpdate: FK_ACTION[str(r.on_update)] ?? "NO_ACTION",
|
|
2373
|
+
onDelete: FK_ACTION[str(r.on_delete)] ?? "NO_ACTION"
|
|
2374
|
+
});
|
|
2375
|
+
break;
|
|
2351
2376
|
}
|
|
2352
|
-
conflicts.push(key);
|
|
2353
|
-
const choice = choices[key] ?? defaultChoice;
|
|
2354
|
-
merged.set(key, choice === "use-portal" ? portalValue : localValue);
|
|
2355
2377
|
}
|
|
2356
|
-
|
|
2378
|
+
for (const r of indexes) {
|
|
2379
|
+
byKey.get(key(r))?.indexes.push({ name: str(r.name), unique: Boolean(r.unique), columns: strArray(r.columns), method: str(r.method) });
|
|
2380
|
+
}
|
|
2381
|
+
return {
|
|
2382
|
+
tables: [...byKey.values()],
|
|
2383
|
+
enums: enums.map((r) => ({ schema: str(r.schema), name: str(r.name), values: strArray(r.values) }))
|
|
2384
|
+
};
|
|
2357
2385
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
const appended = [];
|
|
2370
|
-
for (const [key, value] of merged) {
|
|
2371
|
-
if (!seen.has(key)) appended.push(`${key}=${value}`);
|
|
2386
|
+
|
|
2387
|
+
// src/core/erd/migrations.ts
|
|
2388
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2389
|
+
import { join as join11 } from "path";
|
|
2390
|
+
var JOURNAL = join11("src", "db", "migrations", "meta", "_journal.json");
|
|
2391
|
+
async function readJournal(repoRoot) {
|
|
2392
|
+
let text2;
|
|
2393
|
+
try {
|
|
2394
|
+
text2 = await readFile3(join11(repoRoot, JOURNAL), "utf8");
|
|
2395
|
+
} catch {
|
|
2396
|
+
return null;
|
|
2372
2397
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2398
|
+
try {
|
|
2399
|
+
return JSON.parse(text2).entries ?? [];
|
|
2400
|
+
} catch {
|
|
2401
|
+
return null;
|
|
2377
2402
|
}
|
|
2378
|
-
|
|
2403
|
+
}
|
|
2404
|
+
async function appliedMigrations(query) {
|
|
2405
|
+
const { rows: [ledger] } = await query(`select to_regclass('drizzle.__drizzle_migrations') is not null as present`);
|
|
2406
|
+
if (!ledger?.present) return [];
|
|
2407
|
+
const { rows } = await query(`select created_at::text as at from drizzle.__drizzle_migrations`);
|
|
2408
|
+
return rows.map((r) => Number(r.at));
|
|
2409
|
+
}
|
|
2410
|
+
function compareMigrations(journal, applied) {
|
|
2411
|
+
const latest = applied.reduce((a, b) => Math.max(a, b), 0);
|
|
2412
|
+
const done = new Set(applied);
|
|
2413
|
+
const known = new Set(journal.map((e) => e.when));
|
|
2414
|
+
const missing = journal.filter((e) => !done.has(e.when));
|
|
2415
|
+
return {
|
|
2416
|
+
pending: missing.filter((e) => e.when > latest).map((e) => e.tag),
|
|
2417
|
+
skipped: missing.filter((e) => e.when <= latest).map((e) => e.tag),
|
|
2418
|
+
unknown: applied.filter((a) => !known.has(a)).length
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
async function checkMigrations(repoRoot, query) {
|
|
2422
|
+
const journal = repoRoot ? await readJournal(repoRoot) : null;
|
|
2423
|
+
return journal ? compareMigrations(journal, await appliedMigrations(query)) : void 0;
|
|
2379
2424
|
}
|
|
2380
2425
|
|
|
2381
|
-
// src/core/
|
|
2426
|
+
// src/core/context/repoConfig.ts
|
|
2427
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
2428
|
+
import { join as join12 } from "path";
|
|
2429
|
+
var FORBIDDEN = ["portal", "api", "apiOrigin", "issuer", "tenant", "tenantId", "app", "appId", "module", "moduleId"];
|
|
2430
|
+
var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2431
|
+
var SCRIPT = /^[A-Za-z0-9:_.-]+$/;
|
|
2432
|
+
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 });
|
|
2433
|
+
function moduleDefaults(type) {
|
|
2434
|
+
return type === "frontend" ? { envFile: ".env.local", localSecrets: ["SESSION_COOKIE_SECRET"] } : { envFile: ".env", localSecrets: [] };
|
|
2435
|
+
}
|
|
2436
|
+
async function readRepoConfig(root) {
|
|
2437
|
+
let raw;
|
|
2438
|
+
try {
|
|
2439
|
+
raw = await readFile4(join12(root, ".qore.json"), "utf8");
|
|
2440
|
+
} catch (e) {
|
|
2441
|
+
if (e.code === "ENOENT") return {};
|
|
2442
|
+
throw e;
|
|
2443
|
+
}
|
|
2444
|
+
let doc;
|
|
2445
|
+
try {
|
|
2446
|
+
doc = JSON.parse(raw);
|
|
2447
|
+
} catch {
|
|
2448
|
+
throw invalid3("not valid JSON");
|
|
2449
|
+
}
|
|
2450
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw invalid3("must be an object");
|
|
2451
|
+
const v = doc.schemaVersion === void 0 ? 1 : doc.schemaVersion;
|
|
2452
|
+
if (!Number.isInteger(v) || v < 1) throw invalid3("schemaVersion must be a positive integer");
|
|
2453
|
+
if (v > 1) {
|
|
2454
|
+
throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `.qore.json schemaVersion ${v} is newer than this qore`, { hint: "Run: npm i -g @quatronic/qore" });
|
|
2455
|
+
}
|
|
2456
|
+
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 });
|
|
2457
|
+
const out = {};
|
|
2458
|
+
if (doc.envFile !== void 0) {
|
|
2459
|
+
if (typeof doc.envFile !== "string" || !doc.envFile) throw invalid3("envFile must be a non-empty string");
|
|
2460
|
+
out.envFile = doc.envFile;
|
|
2461
|
+
}
|
|
2462
|
+
if (doc.localSecrets !== void 0) {
|
|
2463
|
+
if (!Array.isArray(doc.localSecrets) || !doc.localSecrets.every((k) => typeof k === "string" && KEY.test(k))) {
|
|
2464
|
+
throw invalid3("localSecrets must be a list of env key names");
|
|
2465
|
+
}
|
|
2466
|
+
if (new Set(doc.localSecrets).size !== doc.localSecrets.length) throw invalid3("localSecrets must not contain duplicates");
|
|
2467
|
+
out.localSecrets = doc.localSecrets;
|
|
2468
|
+
}
|
|
2469
|
+
if (doc.dev !== void 0) {
|
|
2470
|
+
const d = doc.dev;
|
|
2471
|
+
if (!d || typeof d !== "object" || Array.isArray(d)) throw invalid3("dev must be an object");
|
|
2472
|
+
const dev = {};
|
|
2473
|
+
if (d.install !== void 0) {
|
|
2474
|
+
if (typeof d.install !== "boolean") throw invalid3("dev.install must be true or false");
|
|
2475
|
+
dev.install = d.install;
|
|
2476
|
+
}
|
|
2477
|
+
if (d.services !== void 0) {
|
|
2478
|
+
if (!Array.isArray(d.services) || !d.services.every((s) => typeof s === "string" && SCRIPT.test(s))) {
|
|
2479
|
+
throw invalid3("dev.services must be a list of script names");
|
|
2480
|
+
}
|
|
2481
|
+
if (new Set(d.services).size !== d.services.length) throw invalid3("dev.services must not contain duplicates");
|
|
2482
|
+
dev.services = d.services;
|
|
2483
|
+
}
|
|
2484
|
+
if (d.script !== void 0) {
|
|
2485
|
+
if (typeof d.script !== "string" || !SCRIPT.test(d.script)) throw invalid3("dev.script must be a script name");
|
|
2486
|
+
dev.script = d.script;
|
|
2487
|
+
}
|
|
2488
|
+
out.dev = dev;
|
|
2489
|
+
}
|
|
2490
|
+
return out;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
// src/core/env/fetch.ts
|
|
2494
|
+
async function fetchLocalEnv(deps2, mc, app) {
|
|
2495
|
+
await ensureCapability(deps2, mc.portal.apiOrigin, "module.local-env");
|
|
2496
|
+
try {
|
|
2497
|
+
const local = await mc.session.api.get(
|
|
2498
|
+
`/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/local-env?app_id=${encodeURIComponent(app.id)}`
|
|
2499
|
+
);
|
|
2500
|
+
deps2.registerSecrets(Object.values(local.env));
|
|
2501
|
+
return local;
|
|
2502
|
+
} catch (e) {
|
|
2503
|
+
if (!(e instanceof QoreError) || e.code !== "QORE_ACCESS_DENIED") throw e;
|
|
2504
|
+
if (e.serverCode === "app.access.denied") {
|
|
2505
|
+
throw withDetails(
|
|
2506
|
+
e,
|
|
2507
|
+
{ required: "write", have: mc.module.access },
|
|
2508
|
+
`You have ${mc.module.access} on ${mc.module.slug}; local secrets need write. Ask a maintainer of the app's dev team for write access.`
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2511
|
+
if (e.serverCode === "auth.scope.insufficient") throw withDetails(e, {}, "Run `qore login` again.");
|
|
2512
|
+
throw e;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// src/core/env/merge.ts
|
|
2517
|
+
function parseEnv(contents) {
|
|
2518
|
+
const map = /* @__PURE__ */ new Map();
|
|
2519
|
+
for (const line2 of contents.split("\n")) {
|
|
2520
|
+
const m = line2.replace(/\r$/, "").match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
2521
|
+
if (m?.[1] !== void 0 && m[2] !== void 0) map.set(m[1], m[2]);
|
|
2522
|
+
}
|
|
2523
|
+
return map;
|
|
2524
|
+
}
|
|
2525
|
+
function isManagedKey(key2, managed) {
|
|
2526
|
+
for (const pattern of managed) {
|
|
2527
|
+
if (pattern.endsWith("*")) {
|
|
2528
|
+
if (key2.startsWith(pattern.slice(0, -1))) return true;
|
|
2529
|
+
} else if (key2 === pattern) return true;
|
|
2530
|
+
}
|
|
2531
|
+
return false;
|
|
2532
|
+
}
|
|
2533
|
+
function planEnvUpdate({
|
|
2534
|
+
current,
|
|
2535
|
+
fetched,
|
|
2536
|
+
choices = {},
|
|
2537
|
+
defaultChoice = "keep",
|
|
2538
|
+
managed = []
|
|
2539
|
+
}) {
|
|
2540
|
+
const merged = new Map(current);
|
|
2541
|
+
const conflicts = [];
|
|
2542
|
+
const added = [];
|
|
2543
|
+
const unchanged = [];
|
|
2544
|
+
const overwritten = [];
|
|
2545
|
+
for (const [key2, portalValue] of Object.entries(fetched)) {
|
|
2546
|
+
const hasLocal = current.has(key2);
|
|
2547
|
+
const localValue = current.get(key2);
|
|
2548
|
+
const localEmpty = !hasLocal || localValue === "" || localValue === '""' || localValue === "''";
|
|
2549
|
+
if (localEmpty) {
|
|
2550
|
+
merged.set(key2, portalValue);
|
|
2551
|
+
added.push(key2);
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
if (localValue === portalValue) {
|
|
2555
|
+
unchanged.push(key2);
|
|
2556
|
+
continue;
|
|
2557
|
+
}
|
|
2558
|
+
if (isManagedKey(key2, managed)) {
|
|
2559
|
+
merged.set(key2, portalValue);
|
|
2560
|
+
overwritten.push(key2);
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
conflicts.push(key2);
|
|
2564
|
+
const choice = choices[key2] ?? defaultChoice;
|
|
2565
|
+
merged.set(key2, choice === "use-portal" ? portalValue : localValue);
|
|
2566
|
+
}
|
|
2567
|
+
return { merged, conflicts, added, unchanged, overwritten };
|
|
2568
|
+
}
|
|
2569
|
+
function renderEnv(original, merged) {
|
|
2570
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2571
|
+
const lines = original.split("\n");
|
|
2572
|
+
const out = lines.map((line2) => {
|
|
2573
|
+
const m = line2.replace(/\r$/, "").match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
2574
|
+
if (!m?.[1]) return line2;
|
|
2575
|
+
const key2 = m[1];
|
|
2576
|
+
if (!merged.has(key2)) return line2;
|
|
2577
|
+
seen.add(key2);
|
|
2578
|
+
return `${key2}=${merged.get(key2) ?? ""}`;
|
|
2579
|
+
});
|
|
2580
|
+
const appended = [];
|
|
2581
|
+
for (const [key2, value] of merged) {
|
|
2582
|
+
if (!seen.has(key2)) appended.push(`${key2}=${value}`);
|
|
2583
|
+
}
|
|
2584
|
+
let text2 = out.join("\n");
|
|
2585
|
+
if (appended.length) {
|
|
2586
|
+
if (text2.length && !text2.endsWith("\n")) text2 += "\n";
|
|
2587
|
+
text2 += appended.join("\n") + "\n";
|
|
2588
|
+
}
|
|
2589
|
+
return text2;
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
// src/core/env/pull.ts
|
|
2382
2593
|
import { lstat, realpath as realpath5 } from "fs/promises";
|
|
2383
2594
|
import { basename, dirname as dirname4, isAbsolute as isAbsolute3, relative as relative5, resolve as resolve5, sep as sep2 } from "path";
|
|
2384
2595
|
var outside = (target) => new QoreError("QORE_CONTEXT_INVALID", `The env file must be inside the repository (got ${target})`, {
|
|
@@ -2424,11 +2635,11 @@ function planPull(o) {
|
|
|
2424
2635
|
const r = planEnvUpdate({ current, fetched: o.fetched, choices: o.choices ?? {}, defaultChoice: o.defaultChoice, managed: o.managed });
|
|
2425
2636
|
const choiceOf = (k) => o.choices?.[k] ?? o.defaultChoice;
|
|
2426
2637
|
const generated = [];
|
|
2427
|
-
for (const
|
|
2428
|
-
const v = r.merged.get(
|
|
2638
|
+
for (const key2 of o.localSecrets) {
|
|
2639
|
+
const v = r.merged.get(key2);
|
|
2429
2640
|
if (v === void 0 || v === "" || v === '""' || v === "''") {
|
|
2430
|
-
r.merged.set(
|
|
2431
|
-
generated.push(
|
|
2641
|
+
r.merged.set(key2, o.random());
|
|
2642
|
+
generated.push(key2);
|
|
2432
2643
|
}
|
|
2433
2644
|
}
|
|
2434
2645
|
return {
|
|
@@ -2446,50 +2657,720 @@ function planPull(o) {
|
|
|
2446
2657
|
}
|
|
2447
2658
|
|
|
2448
2659
|
// src/core/env/write.ts
|
|
2449
|
-
import { chmod as chmod2, readFile as
|
|
2660
|
+
import { chmod as chmod2, readFile as readFile5, rename as rename3, unlink, writeFile as writeFile4 } from "fs/promises";
|
|
2450
2661
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
2451
|
-
import { basename as basename2, dirname as dirname5, join as
|
|
2662
|
+
import { basename as basename2, dirname as dirname5, join as join13 } from "path";
|
|
2452
2663
|
async function readOrNull(p) {
|
|
2453
2664
|
try {
|
|
2454
|
-
return await
|
|
2665
|
+
return await readFile5(p, "utf8");
|
|
2455
2666
|
} catch (e) {
|
|
2456
2667
|
if (e.code === "ENOENT") return null;
|
|
2457
2668
|
throw e;
|
|
2458
2669
|
}
|
|
2459
|
-
}
|
|
2460
|
-
async function writeSecurely(target, text2) {
|
|
2461
|
-
const base = basename2(target);
|
|
2462
|
-
const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes4(6).toString("hex")}.local` : `${base}.qore-${randomBytes4(6).toString("hex")}.tmp`;
|
|
2463
|
-
const tmp =
|
|
2464
|
-
try {
|
|
2465
|
-
await writeFile4(tmp, text2, { mode: 384, flag: "wx" });
|
|
2466
|
-
await chmod2(tmp, 384);
|
|
2467
|
-
await rename3(tmp, target);
|
|
2468
|
-
} finally {
|
|
2469
|
-
await unlink(tmp).catch(() => {
|
|
2470
|
-
});
|
|
2670
|
+
}
|
|
2671
|
+
async function writeSecurely(target, text2) {
|
|
2672
|
+
const base = basename2(target);
|
|
2673
|
+
const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes4(6).toString("hex")}.local` : `${base}.qore-${randomBytes4(6).toString("hex")}.tmp`;
|
|
2674
|
+
const tmp = join13(dirname5(target), tmpName);
|
|
2675
|
+
try {
|
|
2676
|
+
await writeFile4(tmp, text2, { mode: 384, flag: "wx" });
|
|
2677
|
+
await chmod2(tmp, 384);
|
|
2678
|
+
await rename3(tmp, target);
|
|
2679
|
+
} finally {
|
|
2680
|
+
await unlink(tmp).catch(() => {
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
async function syncEnvFile(o) {
|
|
2685
|
+
const existing = await readOrNull(o.target);
|
|
2686
|
+
const plan = planPull({
|
|
2687
|
+
existing,
|
|
2688
|
+
example: existing === null ? o.example : null,
|
|
2689
|
+
fetched: o.fetched,
|
|
2690
|
+
managed: o.managed,
|
|
2691
|
+
localSecrets: o.localSecrets,
|
|
2692
|
+
defaultChoice: "keep",
|
|
2693
|
+
random: o.random
|
|
2694
|
+
});
|
|
2695
|
+
if (existing !== null && plan.text === existing) await chmod2(o.target, 384);
|
|
2696
|
+
else await writeSecurely(o.target, plan.text);
|
|
2697
|
+
return { created: plan.created, kept: plan.kept };
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
// src/core/erd/source.ts
|
|
2701
|
+
function noDatabase(slug) {
|
|
2702
|
+
return new QoreError("QORE_CONTEXT_UNSUPPORTED", `${slug} has no database`, {
|
|
2703
|
+
hint: "qore db erd shows a module's own Postgres (DATABASE_URL); this module declares none."
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
async function resolveDatabaseUrl(deps2, mc, app) {
|
|
2707
|
+
if (mc.repoRoot) {
|
|
2708
|
+
const cfg = await readRepoConfig(mc.repoRoot);
|
|
2709
|
+
const envFile = await resolveTarget(mc.repoRoot, cfg.envFile ?? moduleDefaults(mc.module.type).envFile);
|
|
2710
|
+
const text2 = await readOrNull(envFile);
|
|
2711
|
+
const url2 = text2 === null ? void 0 : parseEnv(text2).get("DATABASE_URL")?.trim().replace(/^["']|["']$/g, "");
|
|
2712
|
+
if (url2) {
|
|
2713
|
+
deps2.registerSecrets([url2]);
|
|
2714
|
+
return { url: url2, source: "env-file", envFile };
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
const local = await fetchLocalEnv(deps2, mc, app);
|
|
2718
|
+
const url = local.env.DATABASE_URL?.trim();
|
|
2719
|
+
if (!url) throw noDatabase(mc.module.slug);
|
|
2720
|
+
return { url, source: "portal", envFile: null };
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
// src/platform/httpServe.ts
|
|
2724
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2725
|
+
import { createServer } from "http";
|
|
2726
|
+
import { extname, resolve as resolve6, sep as sep3 } from "path";
|
|
2727
|
+
var TYPES = {
|
|
2728
|
+
".html": "text/html; charset=utf-8",
|
|
2729
|
+
".js": "text/javascript; charset=utf-8",
|
|
2730
|
+
".css": "text/css; charset=utf-8",
|
|
2731
|
+
".json": "application/json",
|
|
2732
|
+
".ico": "image/x-icon",
|
|
2733
|
+
".png": "image/png",
|
|
2734
|
+
".svg": "image/svg+xml",
|
|
2735
|
+
".woff": "font/woff",
|
|
2736
|
+
".woff2": "font/woff2"
|
|
2737
|
+
};
|
|
2738
|
+
async function serveErd(o) {
|
|
2739
|
+
const root = resolve6(o.dir);
|
|
2740
|
+
let port = 0;
|
|
2741
|
+
const server = createServer((req, res) => {
|
|
2742
|
+
void handle(req, res);
|
|
2743
|
+
});
|
|
2744
|
+
await new Promise((ok2, fail) => {
|
|
2745
|
+
server.once("error", fail);
|
|
2746
|
+
server.listen(o.port, "127.0.0.1", () => {
|
|
2747
|
+
server.off("error", fail);
|
|
2748
|
+
ok2();
|
|
2749
|
+
});
|
|
2750
|
+
});
|
|
2751
|
+
port = server.address().port;
|
|
2752
|
+
async function handle(req, res) {
|
|
2753
|
+
try {
|
|
2754
|
+
await route(req, res);
|
|
2755
|
+
} catch (e) {
|
|
2756
|
+
if (res.headersSent) res.destroy();
|
|
2757
|
+
else end(res, e instanceof TypeError ? 400 : 500);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
async function route(req, res) {
|
|
2761
|
+
const host = req.headers.host;
|
|
2762
|
+
if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`) return end(res, 403);
|
|
2763
|
+
if (req.method !== "GET" && req.method !== "HEAD") return end(res, 405);
|
|
2764
|
+
const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
2765
|
+
const produce = o.routes[path];
|
|
2766
|
+
if (produce) {
|
|
2767
|
+
try {
|
|
2768
|
+
const body = JSON.stringify(await produce());
|
|
2769
|
+
res.writeHead(200, { "content-type": TYPES[".json"], "cache-control": "no-store" });
|
|
2770
|
+
res.end(body);
|
|
2771
|
+
return;
|
|
2772
|
+
} catch (e) {
|
|
2773
|
+
try {
|
|
2774
|
+
o.onError(e);
|
|
2775
|
+
} catch {
|
|
2776
|
+
}
|
|
2777
|
+
res.writeHead(500, { "content-type": TYPES[".json"] });
|
|
2778
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
const file = resolve6(root, `.${path === "/" ? "/index.html" : path}`);
|
|
2783
|
+
if (file !== root && !file.startsWith(root + sep3)) return end(res, 404);
|
|
2784
|
+
try {
|
|
2785
|
+
const body = await readFile6(file);
|
|
2786
|
+
res.writeHead(200, { "content-type": TYPES[extname(file)] ?? "application/octet-stream" });
|
|
2787
|
+
res.end(body);
|
|
2788
|
+
} catch {
|
|
2789
|
+
end(res, 404);
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
return {
|
|
2793
|
+
port,
|
|
2794
|
+
close: () => new Promise((ok2) => {
|
|
2795
|
+
server.closeAllConnections();
|
|
2796
|
+
server.close(() => ok2());
|
|
2797
|
+
})
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
function end(res, status2) {
|
|
2801
|
+
res.writeHead(status2);
|
|
2802
|
+
res.end();
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// src/cli/context.ts
|
|
2806
|
+
var allOfKind = (c, kind) => !!c && c.length > 0 && c.every((x) => x.kind === kind && typeof x.id === "string");
|
|
2807
|
+
async function pick(ctx, err, message, choices) {
|
|
2808
|
+
if (!ctx.io.interactive) throw err;
|
|
2809
|
+
return ctx.io.select(message, choices);
|
|
2810
|
+
}
|
|
2811
|
+
async function moduleContextFor(ctx, flags, command) {
|
|
2812
|
+
const g = ctx.opts();
|
|
2813
|
+
const onRequest = g.verbose ? (l) => ctx.io.err(l) : void 0;
|
|
2814
|
+
const base = { portal: g.portal, module: flags.module, command, onRequest };
|
|
2815
|
+
try {
|
|
2816
|
+
return await resolveModuleContext(ctx.deps, base);
|
|
2817
|
+
} catch (e) {
|
|
2818
|
+
if (!(e instanceof QoreError) || e.code !== "QORE_CONTEXT_REQUIRED" || !ctx.io.interactive) throw e;
|
|
2819
|
+
if (allOfKind(e.candidates, "module")) {
|
|
2820
|
+
const choose2 = await pick(
|
|
2821
|
+
ctx,
|
|
2822
|
+
e,
|
|
2823
|
+
"Select a module",
|
|
2824
|
+
e.candidates.map((c) => ({ name: `${c.tenant ? `${c.tenant} / ` : ""}${c.slug} (${c.remote})`, value: c.id }))
|
|
2825
|
+
);
|
|
2826
|
+
return resolveModuleContext(ctx.deps, { ...base, choose: choose2 });
|
|
2827
|
+
}
|
|
2828
|
+
if (allOfKind(e.candidates, "tenant")) {
|
|
2829
|
+
const tenant = await pick(ctx, e, "Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.id })));
|
|
2830
|
+
return resolveModuleContext(ctx.deps, { ...base, tenant });
|
|
2831
|
+
}
|
|
2832
|
+
throw e;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
async function appFor(ctx, mc, app) {
|
|
2836
|
+
try {
|
|
2837
|
+
return pickApp2(mc, app);
|
|
2838
|
+
} catch (e) {
|
|
2839
|
+
if (!(e instanceof QoreError) || e.code !== "QORE_APP_AMBIGUOUS" || !ctx.io.interactive) throw e;
|
|
2840
|
+
const id = await pick(ctx, e, "Select an app", mc.module.apps.map((a) => ({ name: a.name, value: a.id })));
|
|
2841
|
+
return pickApp2(mc, id);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
function sourceText(mc) {
|
|
2845
|
+
if (mc.source.kind === "remote") return mc.source.remote;
|
|
2846
|
+
if (mc.source.kind === "workspace") return WORKSPACE_FILE;
|
|
2847
|
+
return mc.source.overrides ? `--module, overrides ${mc.source.overrides}` : "--module";
|
|
2848
|
+
}
|
|
2849
|
+
function contextLine(mc, app) {
|
|
2850
|
+
const parts = [mc.module.tenant.name, ...app ? [app.name] : [], mc.module.slug];
|
|
2851
|
+
return `\u2192 ${parts.join(" / ")} (${sourceText(mc)})`;
|
|
2852
|
+
}
|
|
2853
|
+
function contextJson(mc, app) {
|
|
2854
|
+
const { id, slug, name, type, access: access2 } = mc.module;
|
|
2855
|
+
return {
|
|
2856
|
+
portal: mc.portal.builtin ?? mc.portal.apiOrigin,
|
|
2857
|
+
tenant: mc.module.tenant,
|
|
2858
|
+
app: app ?? null,
|
|
2859
|
+
module: { id, slug, name, type, access: access2 },
|
|
2860
|
+
source: mc.source.kind === "remote" ? mc.source.remote : mc.source.kind === "workspace" ? WORKSPACE_FILE : "--module",
|
|
2861
|
+
...mc.source.kind === "flag" && mc.source.overrides ? { overrides: mc.source.overrides } : {}
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// src/cli/project.ts
|
|
2866
|
+
import { readdir as readdir2, stat as stat3 } from "fs/promises";
|
|
2867
|
+
import { join as join14 } from "path";
|
|
2868
|
+
|
|
2869
|
+
// src/core/projects/match.ts
|
|
2870
|
+
function readings(ref) {
|
|
2871
|
+
const s = ref.split("/").map((x) => x.trim());
|
|
2872
|
+
if (s.some((x) => x === "")) return [];
|
|
2873
|
+
if (s.length === 1) return [{ app: s[0] }];
|
|
2874
|
+
if (s.length === 2) return [{ app: s[0], module: s[1] }, { tenant: s[0], app: s[1] }];
|
|
2875
|
+
if (s.length === 3) return [{ tenant: s[0], app: s[1], module: s[2] }];
|
|
2876
|
+
return [];
|
|
2877
|
+
}
|
|
2878
|
+
function hit(p, w) {
|
|
2879
|
+
if (p.appSlug !== w.app || w.tenant !== void 0 && p.tenantSlug !== w.tenant) return null;
|
|
2880
|
+
if (p.kind === "module") return w.module === p.moduleSlug ? { project: p } : null;
|
|
2881
|
+
return w.module ? { project: p, module: w.module } : { project: p };
|
|
2882
|
+
}
|
|
2883
|
+
function matchProjects(projects, ref) {
|
|
2884
|
+
const out = [];
|
|
2885
|
+
for (const w of readings(ref)) {
|
|
2886
|
+
for (const p of projects) {
|
|
2887
|
+
const m = hit(p, w);
|
|
2888
|
+
if (m) out.push(m);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
return out;
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
// src/cli/project.ts
|
|
2895
|
+
async function loggedInAs(ctx) {
|
|
2896
|
+
const cfg = await ctx.deps.config.read();
|
|
2897
|
+
const portal = resolvePortal(ctx.opts().portal, cfg.defaultProfile);
|
|
2898
|
+
const p = cfg.profiles[portal.apiOrigin];
|
|
2899
|
+
if (!p || !await new TokenStore(ctx.deps.keychain, profileHash(p.apiOrigin, p.issuer)).getRefresh()) return null;
|
|
2900
|
+
return p.email ?? p.sub ?? portal.apiOrigin;
|
|
2901
|
+
}
|
|
2902
|
+
var isDir = async (p) => (await stat3(p).catch(() => null))?.isDirectory() ?? false;
|
|
2903
|
+
var label2 = (p) => `${p.tenantSlug} / ${p.appSlug}${p.kind === "module" ? ` / ${p.moduleSlug}` : ""}${p.source === "registry" ? ` (${p.path})` : ""}`;
|
|
2904
|
+
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 };
|
|
2905
|
+
var withCwd = (ctx, cwd) => ({ ...ctx, deps: { ...ctx.deps, cwd } });
|
|
2906
|
+
async function standingIn(ctx) {
|
|
2907
|
+
if (await findWorkspace(ctx.deps.cwd, ctx.deps.home)) return "workspace";
|
|
2908
|
+
const repo = await findRepoRoot(ctx.deps.cwd);
|
|
2909
|
+
return repo !== null && !isInside(repo, ctx.deps.home) ? { repo } : null;
|
|
2910
|
+
}
|
|
2911
|
+
async function isForeign(ctx, command) {
|
|
2912
|
+
const g = ctx.opts();
|
|
2913
|
+
try {
|
|
2914
|
+
await resolveModuleContext(ctx.deps, { portal: g.portal, command, onRequest: g.verbose ? (l) => ctx.io.err(l) : void 0 });
|
|
2915
|
+
return false;
|
|
2916
|
+
} catch (e) {
|
|
2917
|
+
if (e instanceof CancelledError) throw e;
|
|
2918
|
+
return isForeignRepo(e);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
async function moduleDirs(wsPath) {
|
|
2922
|
+
const out = [];
|
|
2923
|
+
for (const d of await readdir2(wsPath, { withFileTypes: true }).catch(() => [])) {
|
|
2924
|
+
if (d.isDirectory() && !d.name.startsWith(".") && await stat3(join14(wsPath, d.name, ".git")).catch(() => null)) out.push(d.name);
|
|
2925
|
+
}
|
|
2926
|
+
return out.sort();
|
|
2927
|
+
}
|
|
2928
|
+
async function pathFor(ctx, m, needModule) {
|
|
2929
|
+
const p = m.project;
|
|
2930
|
+
if (p.kind === "module") return p.path;
|
|
2931
|
+
if (m.module) {
|
|
2932
|
+
const dir2 = join14(p.path, m.module);
|
|
2933
|
+
if (!await isDir(dir2)) {
|
|
2934
|
+
throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no checkout of module ${m.module} in ${p.path}`, {
|
|
2935
|
+
hint: `Run \`qore sync\` in ${p.path} to clone the app's missing modules.`
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
return dir2;
|
|
2939
|
+
}
|
|
2940
|
+
if (!needModule) return p.path;
|
|
2941
|
+
const dirs = await moduleDirs(p.path);
|
|
2942
|
+
if (dirs.length === 1) return join14(p.path, dirs[0]);
|
|
2943
|
+
if (dirs.length === 0) {
|
|
2944
|
+
throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no module checkouts in ${p.path}`, {
|
|
2945
|
+
hint: `Run \`qore sync\` in ${p.path}.`
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
if (!ctx.io.interactive) {
|
|
2949
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", `${p.appSlug} has several modules \u2014 which one?`, {
|
|
2950
|
+
hint: `Pass ${p.appSlug}/<module>.`,
|
|
2951
|
+
candidates: dirs.map((slug) => ({ kind: "module", slug, tenant: p.tenantSlug, name: join14(p.path, slug) }))
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
return join14(p.path, await ctx.io.select("Select a module", dirs.map((d) => ({ name: d, value: d }))));
|
|
2955
|
+
}
|
|
2956
|
+
async function liveMatches(matches) {
|
|
2957
|
+
const live = [];
|
|
2958
|
+
for (const m of matches) {
|
|
2959
|
+
if (m.project.kind === "workspace" && m.module && !await isDir(join14(m.project.path, m.module))) continue;
|
|
2960
|
+
live.push(m);
|
|
2961
|
+
}
|
|
2962
|
+
return live.length > 0 ? live : matches;
|
|
2963
|
+
}
|
|
2964
|
+
async function choose(ctx, matches, what, explicit) {
|
|
2965
|
+
if (explicit && matches.length === 1) return matches[0];
|
|
2966
|
+
if (!ctx.io.interactive) {
|
|
2967
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", what, {
|
|
2968
|
+
hint: "Pass a more specific reference: tenant/app or app/module.",
|
|
2969
|
+
candidates: matches.map((m) => candidate(m.project))
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
const i = await ctx.io.select("Select a project", matches.map((m, idx) => ({
|
|
2973
|
+
name: m.module ? `${label2(m.project)} / ${m.module}` : label2(m.project),
|
|
2974
|
+
value: idx
|
|
2975
|
+
})));
|
|
2976
|
+
return matches[i];
|
|
2977
|
+
}
|
|
2978
|
+
async function projectCtx(ctx, o) {
|
|
2979
|
+
const g = ctx.opts();
|
|
2980
|
+
const cfg = await ctx.deps.config.read();
|
|
2981
|
+
const portal = g.portal ? resolvePortal(g.portal, cfg.defaultProfile).apiOrigin : void 0;
|
|
2982
|
+
if (o.ref) {
|
|
2983
|
+
const matches = matchProjects(await knownProjects(ctx.deps, { portal }), o.ref);
|
|
2984
|
+
if (matches.length === 0) {
|
|
2985
|
+
const parts = o.ref.split("/");
|
|
2986
|
+
const app = parts.length === 1 ? o.ref : parts.length === 3 ? parts[1] : "<app>";
|
|
2987
|
+
throw new QoreError("QORE_PROJECT_NOT_FOUND", `No local project "${o.ref}"`, {
|
|
2988
|
+
hint: cfg.projectsRoot ? `Run \`qore clone ${app}\`.` : `Run \`qore init\`, or \`qore clone ${app}\`.`
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
const m2 = await choose(ctx, await liveMatches(matches), `"${o.ref}" matches several projects`, true);
|
|
2992
|
+
return relocate(ctx, await pathFor(ctx, m2, o.needModule));
|
|
2993
|
+
}
|
|
2994
|
+
if (o.module || o.app) return ctx;
|
|
2995
|
+
const here = await standingIn(ctx);
|
|
2996
|
+
if (here === "workspace") return ctx;
|
|
2997
|
+
const known = await knownProjects(ctx.deps, { portal });
|
|
2998
|
+
if (here && (known.length === 0 || known.some((p) => isInside(p.path, here.repo)) || !await isForeign(ctx, o.command ?? "project"))) return ctx;
|
|
2999
|
+
if (known.length === 0) {
|
|
3000
|
+
if (!cfg.projectsRoot) {
|
|
3001
|
+
if (!await loggedInAs(ctx)) return ctx;
|
|
3002
|
+
throw new QoreError("QORE_ROOT_NOT_SET", "Not inside a project, and qore knows no projects yet", {
|
|
3003
|
+
hint: "Run `qore init` to choose a projects folder, or cd into a project."
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
3006
|
+
throw new QoreError("QORE_PROJECT_NOT_FOUND", `Not inside a project, and ${cfg.projectsRoot} holds none yet`, {
|
|
3007
|
+
hint: "Run `qore clone <app>`."
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
const what = here ? "This repository is not a Qore module \u2014 which project?" : "Not inside a project \u2014 which one?";
|
|
3011
|
+
const m = await choose(ctx, known.map((project) => ({ project })), what, false);
|
|
3012
|
+
return relocate(ctx, await pathFor(ctx, m, o.needModule));
|
|
3013
|
+
}
|
|
3014
|
+
function relocate(ctx, path) {
|
|
3015
|
+
if (path === ctx.deps.cwd) return ctx;
|
|
3016
|
+
if (!ctx.opts().json) ctx.io.err(`\u2192 in ${path}`);
|
|
3017
|
+
return withCwd(ctx, path);
|
|
3018
|
+
}
|
|
3019
|
+
async function rememberContext(ctx, mc, app) {
|
|
3020
|
+
const tenantSlug = mc.module.tenant.slug;
|
|
3021
|
+
if (mc.workspace && mc.workspace.appId === app.id) {
|
|
3022
|
+
await remember(ctx.deps, {
|
|
3023
|
+
kind: "workspace",
|
|
3024
|
+
path: mc.workspace.root,
|
|
3025
|
+
portal: mc.workspace.portal,
|
|
3026
|
+
tenantSlug,
|
|
3027
|
+
appId: app.id,
|
|
3028
|
+
appSlug: app.slug
|
|
3029
|
+
});
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
if (mc.repoRoot) {
|
|
3033
|
+
await remember(ctx.deps, {
|
|
3034
|
+
kind: "module",
|
|
3035
|
+
path: mc.repoRoot,
|
|
3036
|
+
portal: mc.portal.apiOrigin,
|
|
3037
|
+
tenantSlug,
|
|
3038
|
+
appId: app.id,
|
|
3039
|
+
appSlug: app.slug,
|
|
3040
|
+
moduleId: mc.module.id,
|
|
3041
|
+
moduleSlug: mc.module.slug
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
async function rememberWorkspace(ctx, ws, app) {
|
|
3046
|
+
if (ws.appId !== app.id) return;
|
|
3047
|
+
await remember(ctx.deps, { kind: "workspace", path: ws.root, portal: ws.portal, tenantSlug: app.tenant.slug, appId: app.id, appSlug: app.slug });
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
// src/cli/trustPrompt.ts
|
|
3051
|
+
async function ensureTrusted(ctx, mc, app, o) {
|
|
3052
|
+
const root = mc.repoRoot ?? ctx.deps.cwd;
|
|
3053
|
+
if (!ctx.opts().json) ctx.io.err(contextLine(mc, app));
|
|
3054
|
+
if (await isTrusted(ctx.deps, root, mc.module.id)) return;
|
|
3055
|
+
if (o.yes) {
|
|
3056
|
+
await trust(ctx.deps, root, mc.module.id);
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
if (!ctx.io.interactive) {
|
|
3060
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", `This repository is not trusted for ${mc.module.slug} yet`, {
|
|
3061
|
+
hint: "Run interactively once, or pass --yes.",
|
|
3062
|
+
details: { reason: "untrusted", repository: root }
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
if (mc.source.kind === "remote") {
|
|
3066
|
+
const key2 = repoKey(mc.source.url);
|
|
3067
|
+
ctx.io.err(` from remote ${mc.source.remote}: ${key2 ?? "(unparsable remote URL)"}`);
|
|
3068
|
+
}
|
|
3069
|
+
if (!await ctx.io.confirm("Pull local secrets for this module into this repository?")) throw new CancelledError();
|
|
3070
|
+
await trust(ctx.deps, root, mc.module.id);
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// src/cli/commands/db.ts
|
|
3074
|
+
var isDir2 = async (p) => stat4(p).then((s) => s.isDirectory(), () => false);
|
|
3075
|
+
function parsePort(raw) {
|
|
3076
|
+
const n = /^\d+$/.test(raw) ? Number(raw) : NaN;
|
|
3077
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) throw new InvalidArgumentError("a port is a number from 1 to 65535");
|
|
3078
|
+
return n;
|
|
3079
|
+
}
|
|
3080
|
+
var aborted = (signal) => signal.aborted ? Promise.resolve() : new Promise((r) => signal.addEventListener("abort", () => r(), { once: true }));
|
|
3081
|
+
var plural = (n) => `${n} migration${n === 1 ? "" : "s"}`;
|
|
3082
|
+
function migrationLines(m, who) {
|
|
3083
|
+
if (!m) return [];
|
|
3084
|
+
const lines = [];
|
|
3085
|
+
if (m.pending.length) lines.push(`${who} lacks ${plural(m.pending.length)} of this checkout (${m.pending.join(", ")}); they apply on the next start of \`qore dev\`.`);
|
|
3086
|
+
if (m.skipped.length) lines.push(`${who} lacks ${plural(m.skipped.length)} older than its latest (${m.skipped.join(", ")}); Drizzle never applies those, so reset it with \`pnpm db:reset\`.`);
|
|
3087
|
+
if (m.unknown) lines.push(`${who} has ${plural(m.unknown)} this checkout does not know; applied from another branch?`);
|
|
3088
|
+
return lines;
|
|
3089
|
+
}
|
|
3090
|
+
var portInUse = (port) => new QoreError("QORE_PORT_IN_USE", `Port ${port} is already in use`, { hint: "Pick another --port, or leave it out to get a free one." });
|
|
3091
|
+
function registerDb(program, ctx) {
|
|
3092
|
+
const db = program.command("db").description("Work with the local databases of a module or an app");
|
|
3093
|
+
db.command("erd").description("Show this module's data model in the browser \u2014 or, in a workspace root, the whole app's").argument("[project]", "app or app/module to show, 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("--yes", "trust this repository without asking").option("--port <n>", "listen on this port (default: a free one)", parsePort).option("--no-open", "print the URL, do not open a browser").action(async (project, f) => {
|
|
3094
|
+
if (project !== void 0 && f.project !== void 0 && project !== f.project) {
|
|
3095
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `Pass the project once: "${project}" and --project "${f.project}" differ`, {
|
|
3096
|
+
hint: "qore db erd <project> | qore db erd --project <project>"
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3099
|
+
await erd(ctx, { ...f, project: project ?? f.project });
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
async function erd(outer, f) {
|
|
3103
|
+
const json2 = Boolean(outer.opts().json);
|
|
3104
|
+
if (json2 && (f.port !== void 0 || !f.open)) {
|
|
3105
|
+
throw new QoreError("QORE_CONTEXT_INVALID", "--json writes the model and starts no server", {
|
|
3106
|
+
hint: "Drop --port and --no-open, or drop --json."
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
if (f.port !== void 0 && await outer.deps.ports.inUse(f.port)) throw portInUse(f.port);
|
|
3110
|
+
const ctx = await projectCtx(outer, { ref: f.project, module: f.module, app: f.app, needModule: false, command: "db erd" });
|
|
3111
|
+
const { deps: deps2, io: io2 } = ctx;
|
|
3112
|
+
if (!f.module) {
|
|
3113
|
+
const chosen = Boolean(f.project) || ctx !== outer || Boolean(f.app);
|
|
3114
|
+
const ws = await workspaceRoot(deps2) ?? (chosen ? null : await wholeAppWanted(ctx));
|
|
3115
|
+
if (ws) {
|
|
3116
|
+
await erdApp(ctx, f, ws);
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
const mc = await moduleContextFor(ctx, f, "db erd");
|
|
3121
|
+
const app = await appFor(ctx, mc, f.app);
|
|
3122
|
+
await ensureTrusted(ctx, mc, app, { yes: f.yes });
|
|
3123
|
+
const src = await resolveDatabaseUrl(deps2, mc, app);
|
|
3124
|
+
const model = async () => {
|
|
3125
|
+
const conn = await deps2.postgres(src.url, { timeoutMs: 3e3 });
|
|
3126
|
+
try {
|
|
3127
|
+
const m = await introspect(conn.query);
|
|
3128
|
+
const migrations = await checkMigrations(mc.repoRoot, conn.query);
|
|
3129
|
+
return migrations ? { ...m, migrations } : m;
|
|
3130
|
+
} finally {
|
|
3131
|
+
await conn.close().catch(() => {
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
};
|
|
3135
|
+
const fresh = () => model().catch((e) => {
|
|
3136
|
+
throw classifyDbError(e, src.url);
|
|
3137
|
+
});
|
|
3138
|
+
const first = await model().catch((e) => {
|
|
3139
|
+
throw ctx.signal.aborted ? new CancelledError() : classifyDbError(e, src.url);
|
|
3140
|
+
});
|
|
3141
|
+
if (json2) {
|
|
3142
|
+
renderJson(io2, { context: contextJson(mc, app), source: src.source, model: first });
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
if (first.tables.length === 0) io2.err("No tables yet \u2014 have the migrations run? Start the app once with `qore dev`.");
|
|
3146
|
+
for (const line2 of migrationLines(first.migrations, "The database")) io2.err(line2);
|
|
3147
|
+
if (ctx.signal.aborted) throw new CancelledError();
|
|
3148
|
+
const meta = { context: contextJson(mc, app), connection: connectionOf(src.url) };
|
|
3149
|
+
const server = await serveErd({
|
|
3150
|
+
dir: deps2.viewerDir,
|
|
3151
|
+
port: f.port ?? 0,
|
|
3152
|
+
routes: { "/model.json": fresh, "/meta.json": async () => meta },
|
|
3153
|
+
onError: (e) => {
|
|
3154
|
+
if (!ctx.signal.aborted) io2.err(`Could not refresh the model: ${e instanceof Error ? e.message : String(e)}`);
|
|
3155
|
+
}
|
|
3156
|
+
}).catch((e) => {
|
|
3157
|
+
const code = e.code;
|
|
3158
|
+
throw f.port !== void 0 && (code === "EADDRINUSE" || code === "EACCES") ? portInUse(f.port) : e;
|
|
3159
|
+
});
|
|
3160
|
+
await serve(ctx, f, server);
|
|
3161
|
+
}
|
|
3162
|
+
async function wholeAppWanted(ctx) {
|
|
3163
|
+
if (!ctx.io.interactive) return null;
|
|
3164
|
+
const ws = await findWorkspace(ctx.deps.cwd, ctx.deps.home);
|
|
3165
|
+
const repo = ws && await findRepoRoot(ctx.deps.cwd);
|
|
3166
|
+
if (!ws || !repo || repo === ws.root || !isInside(ws.root, repo)) return null;
|
|
3167
|
+
const pick2 = await ctx.io.select("Which data model?", [
|
|
3168
|
+
{ name: "the whole app", value: "app" },
|
|
3169
|
+
{ name: `only ${basename3(repo)}`, value: "module" }
|
|
3170
|
+
]);
|
|
3171
|
+
return pick2 === "app" ? ws : null;
|
|
3172
|
+
}
|
|
3173
|
+
async function serve(ctx, f, server) {
|
|
3174
|
+
try {
|
|
3175
|
+
const url = `http://127.0.0.1:${server.port}`;
|
|
3176
|
+
ctx.io.err(`\u2192 ${url} (Ctrl+C stops)`);
|
|
3177
|
+
if (f.open) await ctx.io.openUrl(url);
|
|
3178
|
+
await aborted(ctx.signal);
|
|
3179
|
+
throw new CancelledError();
|
|
3180
|
+
} finally {
|
|
3181
|
+
await server.close();
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
async function erdApp(ctx, f, ws) {
|
|
3185
|
+
const { deps: deps2, io: io2 } = ctx;
|
|
3186
|
+
const g = ctx.opts();
|
|
3187
|
+
const json2 = Boolean(g.json);
|
|
3188
|
+
const portal = await flagPortal(deps2, g.portal, ws);
|
|
3189
|
+
const session = await openSession(deps2, portal, "db erd", g.verbose ? (l) => io2.err(l) : void 0);
|
|
3190
|
+
await ensureCapability(deps2, portal.apiOrigin, "app.modules");
|
|
3191
|
+
const am = await workspaceAppModules(session, portal, ws);
|
|
3192
|
+
const { tenant, ...app } = am.app;
|
|
3193
|
+
if (f.app && f.app !== app.id && f.app !== app.slug) {
|
|
3194
|
+
throw new QoreError("QORE_RESOURCE_NOT_FOUND", `This workspace is for ${app.slug}, not "${f.app}"`, { hint: "Pass --module <slug> --app <slug>." });
|
|
3195
|
+
}
|
|
3196
|
+
const modules = [];
|
|
3197
|
+
const sources = /* @__PURE__ */ new Map();
|
|
3198
|
+
const roots = /* @__PURE__ */ new Map();
|
|
3199
|
+
for (const m of orderModules(am.modules)) {
|
|
3200
|
+
const dir2 = join15(ws.root, m.slug);
|
|
3201
|
+
const repoRoot = isSafeSlug(m.slug) && await isDir2(dir2) ? dir2 : null;
|
|
3202
|
+
const mc = { portal, session, module: { ...m, tenant, apps: [app] }, source: { kind: "workspace" }, repoRoot, workspace: ws };
|
|
3203
|
+
roots.set(m.slug, repoRoot);
|
|
3204
|
+
if (repoRoot) await ensureTrusted(ctx, mc, app, { yes: f.yes });
|
|
3205
|
+
try {
|
|
3206
|
+
const src = await resolveDatabaseUrl(deps2, mc, app);
|
|
3207
|
+
modules.push({ slug: m.slug, dependsOn: m.dependsOn, url: src.url });
|
|
3208
|
+
sources.set(m.slug, src.source);
|
|
3209
|
+
} catch (e) {
|
|
3210
|
+
if (e instanceof QoreError && e.code === "QORE_CONTEXT_UNSUPPORTED") {
|
|
3211
|
+
modules.push({ slug: m.slug, dependsOn: m.dependsOn, url: null });
|
|
3212
|
+
continue;
|
|
3213
|
+
}
|
|
3214
|
+
if (!json2 && e instanceof QoreError && e.code === "QORE_ACCESS_DENIED") {
|
|
3215
|
+
io2.err(`${m.slug} is left out: ${e.message}${e.hint ? `
|
|
3216
|
+
${e.hint}` : ""}`);
|
|
3217
|
+
modules.push({ slug: m.slug, dependsOn: m.dependsOn, url: null });
|
|
3218
|
+
continue;
|
|
3219
|
+
}
|
|
3220
|
+
throw e;
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
const groups = groupDatabases(modules);
|
|
3224
|
+
if (groups.length === 0) {
|
|
3225
|
+
throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `${app.slug} has no module with a database`, {
|
|
3226
|
+
hint: "qore db erd shows the Postgres of an app's modules; none of them declares a DATABASE_URL."
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
for (const gdb of groups) deps2.registerSecrets([gdb.url]);
|
|
3230
|
+
if (!json2) for (const { modules: mods, connection: c } of groups) {
|
|
3231
|
+
if (mods.length > 1) io2.err(`${mods.join(" and ")} use the same database (${c.host}:${c.port}/${c.database}); each module normally has its own \u2014 check their DATABASE_URL.`);
|
|
3232
|
+
}
|
|
3233
|
+
const readAll = (strict) => Promise.all(groups.map(async (gdb) => {
|
|
3234
|
+
try {
|
|
3235
|
+
const conn = await deps2.postgres(gdb.url, { timeoutMs: 3e3 });
|
|
3236
|
+
try {
|
|
3237
|
+
const model = await introspect(conn.query);
|
|
3238
|
+
const migrations = gdb.modules.length === 1 ? await checkMigrations(roots.get(gdb.modules[0]) ?? null, conn.query) : void 0;
|
|
3239
|
+
return { ...gdb, model, ...migrations ? { migrations } : {} };
|
|
3240
|
+
} finally {
|
|
3241
|
+
await conn.close().catch(() => {
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
} catch (e) {
|
|
3245
|
+
const err = classifyDbError(e, gdb.url);
|
|
3246
|
+
if (strict) throw err;
|
|
3247
|
+
return { ...gdb, model: null, error: err.message };
|
|
3248
|
+
}
|
|
3249
|
+
}));
|
|
3250
|
+
const first = await readAll(json2).catch((e) => {
|
|
3251
|
+
throw ctx.signal.aborted ? new CancelledError() : e;
|
|
3252
|
+
});
|
|
3253
|
+
if (first.every((d) => d.model === null)) throw new QoreError("QORE_SERVICE_UNHEALTHY", `No database of ${app.slug} is reachable`, { hint: first.map((d) => `${d.key}: ${d.error}`).join("\n") });
|
|
3254
|
+
const context = {
|
|
3255
|
+
portal: portal.builtin ?? portal.apiOrigin,
|
|
3256
|
+
tenant,
|
|
3257
|
+
app,
|
|
3258
|
+
module: null,
|
|
3259
|
+
source: WORKSPACE_FILE,
|
|
3260
|
+
modules: am.modules.map(({ id, slug, name, type }) => ({ id, slug, name, type, database: groups.find((gdb) => gdb.modules.includes(slug))?.key ?? null }))
|
|
3261
|
+
};
|
|
3262
|
+
const databases = groups.map(({ key: key2, modules: mods, connection }) => ({ key: key2, modules: mods, connection, source: sources.get(mods[0]) }));
|
|
3263
|
+
if (json2) {
|
|
3264
|
+
renderJson(io2, { context, databases, model: combineModels(first) });
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
for (const d of first) if (d.error) io2.err(`${d.key} (${d.modules.join(", ")}): ${d.error}`);
|
|
3268
|
+
for (const d of first) for (const line2 of migrationLines(d.migrations, `The database of ${d.modules.join(", ")}`)) io2.err(line2);
|
|
3269
|
+
if (first.every((d) => !d.model || d.model.tables.length === 0)) io2.err("No tables yet \u2014 have the migrations run? Start the app once with `qore dev`.");
|
|
3270
|
+
if (ctx.signal.aborted) throw new CancelledError();
|
|
3271
|
+
const meta = { context, connection: null, databases: databases.map(({ key: key2, modules: mods, connection }) => ({ key: key2, modules: mods, connection })) };
|
|
3272
|
+
const server = await serveErd({
|
|
3273
|
+
dir: deps2.viewerDir,
|
|
3274
|
+
port: f.port ?? 0,
|
|
3275
|
+
routes: { "/model.json": () => readAll(false).then(combineModels), "/meta.json": async () => meta },
|
|
3276
|
+
onError: (e) => {
|
|
3277
|
+
if (!ctx.signal.aborted) io2.err(`Could not refresh the model: ${e instanceof Error ? e.message : String(e)}`);
|
|
3278
|
+
}
|
|
3279
|
+
}).catch((e) => {
|
|
3280
|
+
const code = e.code;
|
|
3281
|
+
throw f.port !== void 0 && (code === "EADDRINUSE" || code === "EACCES") ? portInUse(f.port) : e;
|
|
3282
|
+
});
|
|
3283
|
+
await serve(ctx, f, server);
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
// src/cli/commands/dev.ts
|
|
3287
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
3288
|
+
import { realpath as realpath8 } from "fs/promises";
|
|
3289
|
+
import { constants as constants3 } from "os";
|
|
3290
|
+
import { dirname as dirname7, join as join23, relative as relative7 } from "path";
|
|
3291
|
+
import { StringDecoder } from "string_decoder";
|
|
3292
|
+
|
|
3293
|
+
// src/core/diagnostics/node.ts
|
|
3294
|
+
var KEEP_LINES = 50;
|
|
3295
|
+
function exitHint(tail, h) {
|
|
3296
|
+
const text2 = tail.join("\n");
|
|
3297
|
+
const missing = text2.match(/Cannot find (?:module|package) '([^']+)'/);
|
|
3298
|
+
if (missing) {
|
|
3299
|
+
return { reason: "missing-module", hint: `A dependency is missing (${missing[1]}). Run \`${h.installCommand}\`, then qore dev again.` };
|
|
3300
|
+
}
|
|
3301
|
+
if (/ZodError:/.test(text2)) {
|
|
3302
|
+
const keys = [...text2.matchAll(/"path":\s*\[\s*"([^"]+)"/g)].map((m) => m[1]);
|
|
3303
|
+
if (keys.length > 0) {
|
|
3304
|
+
return {
|
|
3305
|
+
reason: "invalid-env",
|
|
3306
|
+
keys,
|
|
3307
|
+
hint: `Missing or invalid environment variables: ${keys.join(", ")}. Set them in the portal's local secrets (then qore env pull), or in your env file.`
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
const addrInUse = text2.match(/listen EADDRINUSE: address already in use (?:\S*:)?(\d+)/);
|
|
3312
|
+
if (addrInUse) {
|
|
3313
|
+
const port = Number(addrInUse[1]);
|
|
3314
|
+
return {
|
|
3315
|
+
reason: "port-in-use",
|
|
3316
|
+
port,
|
|
3317
|
+
hint: `Port ${port} was taken while the server started. Run qore dev again; if it repeats, lsof -iTCP:${port} -sTCP:LISTEN names the holder.`
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
if (/ECONNREFUSED/.test(text2)) {
|
|
3321
|
+
const m = text2.match(/ECONNREFUSED\s+\S*:(\d+)/) ?? text2.match(/code: 'ECONNREFUSED'[^}]*?port: (\d+)/);
|
|
3322
|
+
if (!m) return { reason: "conn-refused", hint: "A connection was refused (ECONNREFUSED) \u2014 a service this module calls is not running." };
|
|
3323
|
+
const port = Number(m[1]);
|
|
3324
|
+
const role = h.ports.get(port);
|
|
3325
|
+
if (role?.kind === "database") {
|
|
3326
|
+
const up = h.runCommand("db:up");
|
|
3327
|
+
return {
|
|
3328
|
+
reason: "conn-refused",
|
|
3329
|
+
port,
|
|
3330
|
+
hint: `The database on :${port} is not accepting connections (ECONNREFUSED). ` + (up ? `Fix: ${up} (docker compose ps shows its state)` : "docker compose ps shows its state.")
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
if (role?.kind === "provider") {
|
|
3334
|
+
return {
|
|
3335
|
+
reason: "conn-refused",
|
|
3336
|
+
port,
|
|
3337
|
+
hint: `${role.key} points at :${port}, where nothing listens (ECONNREFUSED). Start that module \u2014 qore dev in the workspace root starts providers first.`
|
|
3338
|
+
};
|
|
3339
|
+
}
|
|
3340
|
+
return { reason: "conn-refused", port, hint: `Nothing listens on :${port} (ECONNREFUSED) \u2014 a service this module calls is not running.` };
|
|
3341
|
+
}
|
|
3342
|
+
const auth = text2.match(/password authentication failed for user "([^"]+)"/);
|
|
3343
|
+
if (auth) {
|
|
3344
|
+
const reset = h.runCommand("db:reset");
|
|
3345
|
+
return {
|
|
3346
|
+
reason: "db-auth",
|
|
3347
|
+
hint: `The local database was created with other credentials than the portal now provides (user ${auth[1]}).` + (reset ? ` Fix: ${reset} \u2014 this wipes local data.` : "")
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
const relation = text2.match(/relation "([^"]+)" does not exist/);
|
|
3351
|
+
if (relation) {
|
|
3352
|
+
const reset = h.runCommand("db:reset");
|
|
3353
|
+
return {
|
|
3354
|
+
reason: "db-schema",
|
|
3355
|
+
hint: `The database schema is behind the code (relation ${relation[1]} does not exist). Migrations run when the app starts; if they failed above, that error is the cause.` + (reset ? ` To start clean: ${reset} (wipes local data).` : "")
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
if (/ENOSPC: System limit for number of file watchers reached/.test(text2)) {
|
|
3359
|
+
return {
|
|
3360
|
+
reason: "watcher-limit",
|
|
3361
|
+
hint: "Linux ran out of file watchers. Raise the limit: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"
|
|
3362
|
+
};
|
|
2471
3363
|
}
|
|
2472
|
-
|
|
2473
|
-
async function syncEnvFile(o) {
|
|
2474
|
-
const existing = await readOrNull(o.target);
|
|
2475
|
-
const plan = planPull({
|
|
2476
|
-
existing,
|
|
2477
|
-
example: existing === null ? o.example : null,
|
|
2478
|
-
fetched: o.fetched,
|
|
2479
|
-
managed: o.managed,
|
|
2480
|
-
localSecrets: o.localSecrets,
|
|
2481
|
-
defaultChoice: "keep",
|
|
2482
|
-
random: o.random
|
|
2483
|
-
});
|
|
2484
|
-
if (existing !== null && plan.text === existing) await chmod2(o.target, 384);
|
|
2485
|
-
else await writeSecurely(o.target, plan.text);
|
|
2486
|
-
return { created: plan.created, kept: plan.kept };
|
|
3364
|
+
return null;
|
|
2487
3365
|
}
|
|
2488
3366
|
|
|
3367
|
+
// src/core/dev/env.ts
|
|
3368
|
+
import { dirname as dirname6, join as join17 } from "path";
|
|
3369
|
+
|
|
2489
3370
|
// src/core/dev/lastStart.ts
|
|
2490
3371
|
import { createHash as createHash2 } from "crypto";
|
|
2491
3372
|
import { realpath as realpath6 } from "fs/promises";
|
|
2492
|
-
import { join as
|
|
3373
|
+
import { join as join16 } from "path";
|
|
2493
3374
|
var NETWORK_REASONS = /* @__PURE__ */ new Set(["connect", "dns", "tls", "timeout"]);
|
|
2494
3375
|
function isOffline(e) {
|
|
2495
3376
|
if (!(e instanceof QoreError)) return false;
|
|
@@ -2502,16 +3383,16 @@ async function stateKey(deps2, root) {
|
|
|
2502
3383
|
return createHash2("sha256").update(trustKey(real, deps2.platform)).digest("hex").slice(0, 16);
|
|
2503
3384
|
}
|
|
2504
3385
|
async function saveLastStart(deps2, root, rec) {
|
|
2505
|
-
await writeJsonAtomic(
|
|
3386
|
+
await writeJsonAtomic(join16(deps2.config.dir, "dev"), `${await stateKey(deps2, root)}.json`, rec);
|
|
2506
3387
|
}
|
|
2507
3388
|
async function loadLastStart(deps2, root) {
|
|
2508
|
-
const rec = await readJson(
|
|
3389
|
+
const rec = await readJson(join16(deps2.config.dir, "dev", `${await stateKey(deps2, root)}.json`)).catch(() => null);
|
|
2509
3390
|
return rec && rec.schemaVersion === 1 ? rec : null;
|
|
2510
3391
|
}
|
|
2511
3392
|
|
|
2512
3393
|
// src/core/dev/env.ts
|
|
2513
|
-
function envPort(env,
|
|
2514
|
-
const raw = env[
|
|
3394
|
+
function envPort(env, key2) {
|
|
3395
|
+
const raw = env[key2];
|
|
2515
3396
|
if (raw === void 0) return null;
|
|
2516
3397
|
const v = raw.trim().replace(/^["']|["']$/g, "");
|
|
2517
3398
|
if (!/^\d+$/.test(v)) return null;
|
|
@@ -2525,7 +3406,7 @@ async function devEnv(deps2, o) {
|
|
|
2525
3406
|
if (o.mc && o.app) {
|
|
2526
3407
|
try {
|
|
2527
3408
|
const local = await fetchLocalEnv(deps2, o.mc, o.app);
|
|
2528
|
-
const example = await readOrNull(
|
|
3409
|
+
const example = await readOrNull(join17(dirname6(target), ".env.example"));
|
|
2529
3410
|
const w = await syncEnvFile({
|
|
2530
3411
|
target,
|
|
2531
3412
|
example,
|
|
@@ -2579,7 +3460,7 @@ function notClonedReason(m, am) {
|
|
|
2579
3460
|
}
|
|
2580
3461
|
|
|
2581
3462
|
// src/core/dev/findings.ts
|
|
2582
|
-
var
|
|
3463
|
+
var plural2 = (n, one2, many) => n === 1 ? one2 : many;
|
|
2583
3464
|
function modulesMissing(o) {
|
|
2584
3465
|
const actionFor = new Map(o.actions.map((a) => [a.module.slug, a]));
|
|
2585
3466
|
const findings = [];
|
|
@@ -2612,8 +3493,8 @@ function modulesMissing(o) {
|
|
|
2612
3493
|
findings.push({
|
|
2613
3494
|
code: "dev.modules-missing",
|
|
2614
3495
|
severity: "warn",
|
|
2615
|
-
message: `${fixable.length} ${
|
|
2616
|
-
why: deps2.length > 0 ? `${deps2.join("; ")}.` : `${
|
|
3496
|
+
message: `${fixable.length} ${plural2(fixable.length, "module of this app is", "modules of this app are")} not cloned: ${slugs.join(", ")}`,
|
|
3497
|
+
why: deps2.length > 0 ? `${deps2.join("; ")}.` : `${plural2(fixable.length, "It", "They")} will not run in this session.`,
|
|
2617
3498
|
modules: slugs,
|
|
2618
3499
|
action: o.am ? { command: "qore sync", fix: "sync" } : { command: "qore sync" }
|
|
2619
3500
|
});
|
|
@@ -2629,7 +3510,7 @@ function repoBehind(slug, s, o) {
|
|
|
2629
3510
|
code: "dev.repo-behind",
|
|
2630
3511
|
severity: "warn",
|
|
2631
3512
|
module: slug,
|
|
2632
|
-
message: `${slug} is ${s.behind} ${
|
|
3513
|
+
message: `${slug} is ${s.behind} ${plural2(s.behind, "commit", "commits")} behind ${s.upstreamRef ?? "its upstream"} (as of your last fetch).`,
|
|
2633
3514
|
action: { command: o.workspace ? "qore sync --pull" : "git pull" }
|
|
2634
3515
|
};
|
|
2635
3516
|
}
|
|
@@ -2652,15 +3533,15 @@ function providerDown(o) {
|
|
|
2652
3533
|
severity: "warn",
|
|
2653
3534
|
module: o.slug,
|
|
2654
3535
|
modules: o.providers,
|
|
2655
|
-
message: single ? `${o.providers[0]} is not running \u2014 ${o.keys[0]} points at :${o.ports.get(o.keys[0])}, where nothing listens.` : `${o.keys.join(", ")} ${
|
|
3536
|
+
message: single ? `${o.providers[0]} is not running \u2014 ${o.keys[0]} points at :${o.ports.get(o.keys[0])}, where nothing listens.` : `${o.keys.join(", ")} ${plural2(o.keys.length, "points", "point")} at ports nothing listens on (depends on: ${o.providers.join(", ")}).`,
|
|
2656
3537
|
why: single ? `${o.slug} will start, but calls to ${o.providers[0]} will fail. Qore does not fall back to staging.` : `${o.slug} will start, but calls through ${o.keys.join(", ")} will fail. Qore does not fall back to staging.`,
|
|
2657
3538
|
action: { command: o.fix }
|
|
2658
3539
|
};
|
|
2659
3540
|
}
|
|
2660
3541
|
|
|
2661
3542
|
// src/core/dev/project.ts
|
|
2662
|
-
import { access, readFile as
|
|
2663
|
-
import { join as
|
|
3543
|
+
import { access, readFile as readFile7 } from "fs/promises";
|
|
3544
|
+
import { join as join18 } from "path";
|
|
2664
3545
|
var LEGACY_DEV_SCRIPTS = ["node --env-file=.env --import ./dist/instrumentation.js dist/index.js"];
|
|
2665
3546
|
var LOCKFILES = [["pnpm-lock.yaml", "pnpm"], ["package-lock.json", "npm"], ["yarn.lock", "yarn"]];
|
|
2666
3547
|
var exists = (p) => access(p).then(() => true, () => false);
|
|
@@ -2668,7 +3549,7 @@ var unsupported = (message, hint, details) => new QoreError("QORE_DEV_UNSUPPORTE
|
|
|
2668
3549
|
async function readPackageJson(root) {
|
|
2669
3550
|
let raw;
|
|
2670
3551
|
try {
|
|
2671
|
-
raw = await
|
|
3552
|
+
raw = await readFile7(join18(root, "package.json"), "utf8");
|
|
2672
3553
|
} catch {
|
|
2673
3554
|
throw unsupported("This repository has no package.json", "qore dev starts Node repositories through their package.json scripts.");
|
|
2674
3555
|
}
|
|
@@ -2684,7 +3565,7 @@ async function detectPackageManager(root, pkg) {
|
|
|
2684
3565
|
if (!m) throw unsupported(`packageManager "${pkg.packageManager}" is not pnpm, npm or yarn`, "qore dev supports pnpm, npm and yarn.");
|
|
2685
3566
|
return { name: m[1], version: m[2].split("+")[0] };
|
|
2686
3567
|
}
|
|
2687
|
-
for (const [file, name] of LOCKFILES) if (await exists(
|
|
3568
|
+
for (const [file, name] of LOCKFILES) if (await exists(join18(root, file))) return { name, version: null };
|
|
2688
3569
|
throw unsupported("No package manager declared and no lockfile found", 'Add "packageManager": "pnpm@<version>" to package.json.');
|
|
2689
3570
|
}
|
|
2690
3571
|
function planRecipe(o) {
|
|
@@ -2727,21 +3608,21 @@ function installArgs(pm) {
|
|
|
2727
3608
|
}
|
|
2728
3609
|
|
|
2729
3610
|
// src/core/dev/install.ts
|
|
2730
|
-
import { stat as
|
|
2731
|
-
import { join as
|
|
3611
|
+
import { stat as stat5 } from "fs/promises";
|
|
3612
|
+
import { join as join19 } from "path";
|
|
2732
3613
|
var MARKERS = {
|
|
2733
3614
|
pnpm: [".modules.yaml"],
|
|
2734
3615
|
npm: [".package-lock.json"],
|
|
2735
3616
|
yarn: [".yarn-state.yml", ".yarn-integrity"]
|
|
2736
3617
|
};
|
|
2737
3618
|
var LOCK2 = { pnpm: "pnpm-lock.yaml", npm: "package-lock.json", yarn: "yarn.lock" };
|
|
2738
|
-
var mtime = (p) =>
|
|
3619
|
+
var mtime = (p) => stat5(p).then((s) => s.mtimeMs, () => null);
|
|
2739
3620
|
async function needsInstall(root, pm) {
|
|
2740
|
-
if (await mtime(
|
|
3621
|
+
if (await mtime(join19(root, "node_modules")) === null) return true;
|
|
2741
3622
|
let marker = null;
|
|
2742
|
-
for (const m of MARKERS[pm.name]) marker ??= await mtime(
|
|
3623
|
+
for (const m of MARKERS[pm.name]) marker ??= await mtime(join19(root, "node_modules", m));
|
|
2743
3624
|
if (marker === null) return true;
|
|
2744
|
-
const lock = await mtime(
|
|
3625
|
+
const lock = await mtime(join19(root, LOCK2[pm.name]));
|
|
2745
3626
|
return lock !== null && lock > marker;
|
|
2746
3627
|
}
|
|
2747
3628
|
|
|
@@ -2869,7 +3750,7 @@ function nodeVersionError(found, range, slug) {
|
|
|
2869
3750
|
// src/core/dev/preflight.ts
|
|
2870
3751
|
var pmMajor = (v) => v ? v.split(".")[0] : "latest";
|
|
2871
3752
|
var GRACE_MS = 1e4;
|
|
2872
|
-
var secs = (
|
|
3753
|
+
var secs = (ms2) => `${Math.round(ms2 / 1e3)} s`;
|
|
2873
3754
|
async function runTool(deps2, cmd, args, o) {
|
|
2874
3755
|
if (o.signal.aborted) throw new CancelledError();
|
|
2875
3756
|
const tail = [];
|
|
@@ -3006,12 +3887,12 @@ async function composeFailure(deps2, r, slug, env, signal, message, command, std
|
|
|
3006
3887
|
details: { command, stderr, logs: logs.tail.slice(-20) }
|
|
3007
3888
|
});
|
|
3008
3889
|
}
|
|
3009
|
-
async function startServices(deps2, r, slug, env,
|
|
3890
|
+
async function startServices(deps2, r, slug, env, dbPort2, waitMs, signal, onLine, report) {
|
|
3010
3891
|
if (r.services.length === 0) {
|
|
3011
3892
|
report({ module: slug, step: "services", status: "skip" });
|
|
3012
3893
|
return;
|
|
3013
3894
|
}
|
|
3014
|
-
report({ module: slug, step: "services", status: "start", ...
|
|
3895
|
+
report({ module: slug, step: "services", status: "start", ...dbPort2 !== null ? { detail: `:${dbPort2}` } : {} });
|
|
3015
3896
|
const began = deps2.now().getTime();
|
|
3016
3897
|
if (r.dockerServices) {
|
|
3017
3898
|
const res = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], { cwd: r.root, env, onLine, signal });
|
|
@@ -3026,14 +3907,14 @@ async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine
|
|
|
3026
3907
|
const { cmd, args } = runArgs(r.pm, script);
|
|
3027
3908
|
const res = await runTool(deps2, cmd, args, { cwd: r.root, env, onLine, signal });
|
|
3028
3909
|
if (res.code !== 0) {
|
|
3029
|
-
if (
|
|
3030
|
-
throw await portInUseError(deps2, { slug, port:
|
|
3910
|
+
if (dbPort2 !== null && /port is already allocated|address already in use/i.test(res.tail.join("\n"))) {
|
|
3911
|
+
throw await portInUseError(deps2, { slug, port: dbPort2, key: "DB_PORT" });
|
|
3031
3912
|
}
|
|
3032
3913
|
throw await composeFailure(deps2, r, slug, env, signal, `${script} failed in ${slug}`, `${cmd} ${args.join(" ")}`, res.tail);
|
|
3033
3914
|
}
|
|
3034
3915
|
}
|
|
3035
|
-
if (
|
|
3036
|
-
const ok2 = await deps2.ports.waitFor(
|
|
3916
|
+
if (dbPort2 !== null) {
|
|
3917
|
+
const ok2 = await deps2.ports.waitFor(dbPort2, waitMs, signal);
|
|
3037
3918
|
if (signal.aborted) throw new CancelledError();
|
|
3038
3919
|
if (!ok2) {
|
|
3039
3920
|
throw await composeFailure(
|
|
@@ -3042,14 +3923,14 @@ async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine
|
|
|
3042
3923
|
slug,
|
|
3043
3924
|
env,
|
|
3044
3925
|
signal,
|
|
3045
|
-
`The database for ${slug} did not accept connections on port ${
|
|
3926
|
+
`The database for ${slug} did not accept connections on port ${dbPort2} within ${waitMs / 1e3} s`,
|
|
3046
3927
|
void 0,
|
|
3047
3928
|
[]
|
|
3048
3929
|
);
|
|
3049
3930
|
}
|
|
3050
3931
|
}
|
|
3051
3932
|
const took = secs(deps2.now().getTime() - began);
|
|
3052
|
-
report({ module: slug, step: "services", status: "done", detail:
|
|
3933
|
+
report({ module: slug, step: "services", status: "done", detail: dbPort2 !== null ? `:${dbPort2} (${took})` : `(${took})` });
|
|
3053
3934
|
}
|
|
3054
3935
|
async function checkPort(deps2, o) {
|
|
3055
3936
|
if (o.port === null) return;
|
|
@@ -3057,13 +3938,13 @@ async function checkPort(deps2, o) {
|
|
|
3057
3938
|
throw await portInUseError(deps2, { slug: o.slug, port: o.port, key: o.key, runs: o.runs });
|
|
3058
3939
|
}
|
|
3059
3940
|
function abortableDelay(signal) {
|
|
3060
|
-
return (
|
|
3941
|
+
return (ms2) => new Promise((resolve7) => {
|
|
3061
3942
|
const done = () => {
|
|
3062
3943
|
clearTimeout(t);
|
|
3063
3944
|
signal.removeEventListener("abort", done);
|
|
3064
|
-
|
|
3945
|
+
resolve7();
|
|
3065
3946
|
};
|
|
3066
|
-
const t = setTimeout(done,
|
|
3947
|
+
const t = setTimeout(done, ms2);
|
|
3067
3948
|
signal.addEventListener("abort", done, { once: true });
|
|
3068
3949
|
});
|
|
3069
3950
|
}
|
|
@@ -3108,17 +3989,17 @@ async function leftoverOwnsPort(deps2, run4, port) {
|
|
|
3108
3989
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
3109
3990
|
function connPorts(env) {
|
|
3110
3991
|
const out = [];
|
|
3111
|
-
for (const
|
|
3112
|
-
const m = /^CONN_(.+)_URL$/.exec(
|
|
3992
|
+
for (const key2 of Object.keys(env).sort()) {
|
|
3993
|
+
const m = /^CONN_(.+)_URL$/.exec(key2);
|
|
3113
3994
|
if (!m) continue;
|
|
3114
3995
|
let url;
|
|
3115
3996
|
try {
|
|
3116
|
-
url = new URL(env[
|
|
3997
|
+
url = new URL(env[key2].trim().replace(/^["']|["']$/g, ""));
|
|
3117
3998
|
} catch {
|
|
3118
3999
|
continue;
|
|
3119
4000
|
}
|
|
3120
4001
|
const port = Number(url.port);
|
|
3121
|
-
if (port > 0 && LOCAL_HOSTS.has(url.hostname)) out.push({ key, alias: m[1], port });
|
|
4002
|
+
if (port > 0 && LOCAL_HOSTS.has(url.hostname)) out.push({ key: key2, alias: m[1], port });
|
|
3122
4003
|
}
|
|
3123
4004
|
return out;
|
|
3124
4005
|
}
|
|
@@ -3139,259 +4020,34 @@ function hintContext(recipe, pkg, env) {
|
|
|
3139
4020
|
return [r.cmd, ...r.args].join(" ");
|
|
3140
4021
|
},
|
|
3141
4022
|
ports
|
|
3142
|
-
};
|
|
3143
|
-
}
|
|
3144
|
-
|
|
3145
|
-
// src/core/dev/runFile.ts
|
|
3146
|
-
import { unlink as unlink2 } from "fs/promises";
|
|
3147
|
-
import { join as join17 } from "path";
|
|
3148
|
-
async function runFilePath(deps2, root) {
|
|
3149
|
-
const dir2 = join17(deps2.config.dir, "run");
|
|
3150
|
-
const name = `${await stateKey(deps2, root)}.json`;
|
|
3151
|
-
return { dir: dir2, name, path: join17(dir2, name) };
|
|
3152
|
-
}
|
|
3153
|
-
async function writeRunFile(deps2, root, rec) {
|
|
3154
|
-
const { dir: dir2, name } = await runFilePath(deps2, root);
|
|
3155
|
-
await writeJsonAtomic(dir2, name, rec);
|
|
3156
|
-
}
|
|
3157
|
-
async function removeRunFile(deps2, root) {
|
|
3158
|
-
const { path } = await runFilePath(deps2, root);
|
|
3159
|
-
await unlink2(path).catch(() => {
|
|
3160
|
-
});
|
|
3161
|
-
}
|
|
3162
|
-
async function leftoverRun(deps2, root, o = {}) {
|
|
3163
|
-
const { path } = await runFilePath(deps2, root);
|
|
3164
|
-
const rec = await readJson(path).catch(() => null);
|
|
3165
|
-
if (!rec || rec.schemaVersion !== 1) return null;
|
|
3166
|
-
if (deps2.launch.isAlive(rec.qorePid)) return rec;
|
|
3167
|
-
if (o.prune !== false) await unlink2(path).catch(() => {
|
|
3168
|
-
});
|
|
3169
|
-
return null;
|
|
3170
|
-
}
|
|
3171
|
-
|
|
3172
|
-
// src/cli/project.ts
|
|
3173
|
-
import { readdir as readdir2, stat as stat4 } from "fs/promises";
|
|
3174
|
-
import { join as join18 } from "path";
|
|
3175
|
-
|
|
3176
|
-
// src/core/projects/match.ts
|
|
3177
|
-
function readings(ref) {
|
|
3178
|
-
const s = ref.split("/").map((x) => x.trim());
|
|
3179
|
-
if (s.some((x) => x === "")) return [];
|
|
3180
|
-
if (s.length === 1) return [{ app: s[0] }];
|
|
3181
|
-
if (s.length === 2) return [{ app: s[0], module: s[1] }, { tenant: s[0], app: s[1] }];
|
|
3182
|
-
if (s.length === 3) return [{ tenant: s[0], app: s[1], module: s[2] }];
|
|
3183
|
-
return [];
|
|
3184
|
-
}
|
|
3185
|
-
function hit(p, w) {
|
|
3186
|
-
if (p.appSlug !== w.app || w.tenant !== void 0 && p.tenantSlug !== w.tenant) return null;
|
|
3187
|
-
if (p.kind === "module") return w.module === p.moduleSlug ? { project: p } : null;
|
|
3188
|
-
return w.module ? { project: p, module: w.module } : { project: p };
|
|
3189
|
-
}
|
|
3190
|
-
function matchProjects(projects, ref) {
|
|
3191
|
-
const out = [];
|
|
3192
|
-
for (const w of readings(ref)) {
|
|
3193
|
-
for (const p of projects) {
|
|
3194
|
-
const m = hit(p, w);
|
|
3195
|
-
if (m) out.push(m);
|
|
3196
|
-
}
|
|
3197
|
-
}
|
|
3198
|
-
return out;
|
|
3199
|
-
}
|
|
3200
|
-
|
|
3201
|
-
// src/cli/project.ts
|
|
3202
|
-
async function loggedInAs(ctx) {
|
|
3203
|
-
const cfg = await ctx.deps.config.read();
|
|
3204
|
-
const portal = resolvePortal(ctx.opts().portal, cfg.defaultProfile);
|
|
3205
|
-
const p = cfg.profiles[portal.apiOrigin];
|
|
3206
|
-
if (!p || !await new TokenStore(ctx.deps.keychain, profileHash(p.apiOrigin, p.issuer)).getRefresh()) return null;
|
|
3207
|
-
return p.email ?? p.sub ?? portal.apiOrigin;
|
|
3208
|
-
}
|
|
3209
|
-
var isDir = async (p) => (await stat4(p).catch(() => null))?.isDirectory() ?? false;
|
|
3210
|
-
var label2 = (p) => `${p.tenantSlug} / ${p.appSlug}${p.kind === "module" ? ` / ${p.moduleSlug}` : ""}${p.source === "registry" ? ` (${p.path})` : ""}`;
|
|
3211
|
-
var candidate = (p) => p.kind === "module" ? { kind: "module", id: p.moduleId, slug: p.moduleSlug, tenant: p.tenantSlug, name: p.path } : { kind: "app", slug: p.appSlug, tenant: p.tenantSlug, name: p.path };
|
|
3212
|
-
var withCwd = (ctx, cwd) => ({ ...ctx, deps: { ...ctx.deps, cwd } });
|
|
3213
|
-
async function inProject(ctx) {
|
|
3214
|
-
if (await findWorkspace(ctx.deps.cwd, ctx.deps.home)) return true;
|
|
3215
|
-
const repo = await findRepoRoot(ctx.deps.cwd);
|
|
3216
|
-
return repo !== null && !isInside(repo, ctx.deps.home);
|
|
3217
|
-
}
|
|
3218
|
-
async function moduleDirs(wsPath) {
|
|
3219
|
-
const out = [];
|
|
3220
|
-
for (const d of await readdir2(wsPath, { withFileTypes: true }).catch(() => [])) {
|
|
3221
|
-
if (d.isDirectory() && !d.name.startsWith(".") && await stat4(join18(wsPath, d.name, ".git")).catch(() => null)) out.push(d.name);
|
|
3222
|
-
}
|
|
3223
|
-
return out.sort();
|
|
3224
|
-
}
|
|
3225
|
-
async function pathFor(ctx, m, needModule) {
|
|
3226
|
-
const p = m.project;
|
|
3227
|
-
if (p.kind === "module") return p.path;
|
|
3228
|
-
if (m.module) {
|
|
3229
|
-
const dir2 = join18(p.path, m.module);
|
|
3230
|
-
if (!await isDir(dir2)) {
|
|
3231
|
-
throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no checkout of module ${m.module} in ${p.path}`, {
|
|
3232
|
-
hint: `Run \`qore sync\` in ${p.path} to clone the app's missing modules.`
|
|
3233
|
-
});
|
|
3234
|
-
}
|
|
3235
|
-
return dir2;
|
|
3236
|
-
}
|
|
3237
|
-
if (!needModule) return p.path;
|
|
3238
|
-
const dirs = await moduleDirs(p.path);
|
|
3239
|
-
if (dirs.length === 1) return join18(p.path, dirs[0]);
|
|
3240
|
-
if (dirs.length === 0) {
|
|
3241
|
-
throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no module checkouts in ${p.path}`, {
|
|
3242
|
-
hint: `Run \`qore sync\` in ${p.path}.`
|
|
3243
|
-
});
|
|
3244
|
-
}
|
|
3245
|
-
if (!ctx.io.interactive) {
|
|
3246
|
-
throw new QoreError("QORE_CONTEXT_REQUIRED", `${p.appSlug} has several modules \u2014 which one?`, {
|
|
3247
|
-
hint: `Pass ${p.appSlug}/<module>.`,
|
|
3248
|
-
candidates: dirs.map((slug) => ({ kind: "module", slug, tenant: p.tenantSlug, name: join18(p.path, slug) }))
|
|
3249
|
-
});
|
|
3250
|
-
}
|
|
3251
|
-
return join18(p.path, await ctx.io.select("Select a module", dirs.map((d) => ({ name: d, value: d }))));
|
|
3252
|
-
}
|
|
3253
|
-
async function liveMatches(matches) {
|
|
3254
|
-
const live = [];
|
|
3255
|
-
for (const m of matches) {
|
|
3256
|
-
if (m.project.kind === "workspace" && m.module && !await isDir(join18(m.project.path, m.module))) continue;
|
|
3257
|
-
live.push(m);
|
|
3258
|
-
}
|
|
3259
|
-
return live.length > 0 ? live : matches;
|
|
3260
|
-
}
|
|
3261
|
-
async function choose(ctx, matches, what, explicit) {
|
|
3262
|
-
if (explicit && matches.length === 1) return matches[0];
|
|
3263
|
-
if (!ctx.io.interactive) {
|
|
3264
|
-
throw new QoreError("QORE_CONTEXT_REQUIRED", what, {
|
|
3265
|
-
hint: "Pass a more specific reference: tenant/app or app/module.",
|
|
3266
|
-
candidates: matches.map((m) => candidate(m.project))
|
|
3267
|
-
});
|
|
3268
|
-
}
|
|
3269
|
-
const i = await ctx.io.select("Select a project", matches.map((m, idx) => ({
|
|
3270
|
-
name: m.module ? `${label2(m.project)} / ${m.module}` : label2(m.project),
|
|
3271
|
-
value: idx
|
|
3272
|
-
})));
|
|
3273
|
-
return matches[i];
|
|
3274
|
-
}
|
|
3275
|
-
async function projectCtx(ctx, o) {
|
|
3276
|
-
const g = ctx.opts();
|
|
3277
|
-
const cfg = await ctx.deps.config.read();
|
|
3278
|
-
const portal = g.portal ? resolvePortal(g.portal, cfg.defaultProfile).apiOrigin : void 0;
|
|
3279
|
-
if (o.ref) {
|
|
3280
|
-
const matches = matchProjects(await knownProjects(ctx.deps, { portal }), o.ref);
|
|
3281
|
-
if (matches.length === 0) {
|
|
3282
|
-
const parts = o.ref.split("/");
|
|
3283
|
-
const app = parts.length === 1 ? o.ref : parts.length === 3 ? parts[1] : "<app>";
|
|
3284
|
-
throw new QoreError("QORE_PROJECT_NOT_FOUND", `No local project "${o.ref}"`, {
|
|
3285
|
-
hint: cfg.projectsRoot ? `Run \`qore clone ${app}\`.` : `Run \`qore init\`, or \`qore clone ${app}\`.`
|
|
3286
|
-
});
|
|
3287
|
-
}
|
|
3288
|
-
const m2 = await choose(ctx, await liveMatches(matches), `"${o.ref}" matches several projects`, true);
|
|
3289
|
-
return relocate(ctx, await pathFor(ctx, m2, o.needModule));
|
|
3290
|
-
}
|
|
3291
|
-
if (o.module || o.app || await inProject(ctx)) return ctx;
|
|
3292
|
-
const known = await knownProjects(ctx.deps, { portal });
|
|
3293
|
-
if (known.length === 0) {
|
|
3294
|
-
if (!cfg.projectsRoot) {
|
|
3295
|
-
if (!await loggedInAs(ctx)) return ctx;
|
|
3296
|
-
throw new QoreError("QORE_ROOT_NOT_SET", "Not inside a project, and qore knows no projects yet", {
|
|
3297
|
-
hint: "Run `qore init` to choose a projects folder, or cd into a project."
|
|
3298
|
-
});
|
|
3299
|
-
}
|
|
3300
|
-
throw new QoreError("QORE_PROJECT_NOT_FOUND", `Not inside a project, and ${cfg.projectsRoot} holds none yet`, {
|
|
3301
|
-
hint: "Run `qore clone <app>`."
|
|
3302
|
-
});
|
|
3303
|
-
}
|
|
3304
|
-
const m = await choose(ctx, known.map((project) => ({ project })), "Not inside a project \u2014 which one?", false);
|
|
3305
|
-
return relocate(ctx, await pathFor(ctx, m, o.needModule));
|
|
3306
|
-
}
|
|
3307
|
-
function relocate(ctx, path) {
|
|
3308
|
-
if (path === ctx.deps.cwd) return ctx;
|
|
3309
|
-
if (!ctx.opts().json) ctx.io.err(`\u2192 in ${path}`);
|
|
3310
|
-
return withCwd(ctx, path);
|
|
3311
|
-
}
|
|
3312
|
-
async function rememberContext(ctx, mc, app) {
|
|
3313
|
-
const tenantSlug = mc.module.tenant.slug;
|
|
3314
|
-
if (mc.workspace && mc.workspace.appId === app.id) {
|
|
3315
|
-
await remember(ctx.deps, {
|
|
3316
|
-
kind: "workspace",
|
|
3317
|
-
path: mc.workspace.root,
|
|
3318
|
-
portal: mc.workspace.portal,
|
|
3319
|
-
tenantSlug,
|
|
3320
|
-
appId: app.id,
|
|
3321
|
-
appSlug: app.slug
|
|
3322
|
-
});
|
|
3323
|
-
return;
|
|
3324
|
-
}
|
|
3325
|
-
if (mc.repoRoot) {
|
|
3326
|
-
await remember(ctx.deps, {
|
|
3327
|
-
kind: "module",
|
|
3328
|
-
path: mc.repoRoot,
|
|
3329
|
-
portal: mc.portal.apiOrigin,
|
|
3330
|
-
tenantSlug,
|
|
3331
|
-
appId: app.id,
|
|
3332
|
-
appSlug: app.slug,
|
|
3333
|
-
moduleId: mc.module.id,
|
|
3334
|
-
moduleSlug: mc.module.slug
|
|
3335
|
-
});
|
|
3336
|
-
}
|
|
3337
|
-
}
|
|
3338
|
-
async function rememberWorkspace(ctx, ws, app) {
|
|
3339
|
-
if (ws.appId !== app.id) return;
|
|
3340
|
-
await remember(ctx.deps, { kind: "workspace", path: ws.root, portal: ws.portal, tenantSlug: app.tenant.slug, appId: app.id, appSlug: app.slug });
|
|
3341
|
-
}
|
|
3342
|
-
|
|
3343
|
-
// src/cli/context.ts
|
|
3344
|
-
var allOfKind = (c, kind) => !!c && c.length > 0 && c.every((x) => x.kind === kind && typeof x.id === "string");
|
|
3345
|
-
async function moduleContextFor(ctx, flags, command) {
|
|
3346
|
-
const g = ctx.opts();
|
|
3347
|
-
const onRequest = g.verbose ? (l) => ctx.io.err(l) : void 0;
|
|
3348
|
-
const base = { portal: g.portal, module: flags.module, command, onRequest };
|
|
3349
|
-
try {
|
|
3350
|
-
return await resolveModuleContext(ctx.deps, base);
|
|
3351
|
-
} catch (e) {
|
|
3352
|
-
if (!(e instanceof QoreError) || e.code !== "QORE_CONTEXT_REQUIRED" || !ctx.io.interactive) throw e;
|
|
3353
|
-
if (allOfKind(e.candidates, "module")) {
|
|
3354
|
-
const choose2 = await ctx.io.select(
|
|
3355
|
-
"Select a module",
|
|
3356
|
-
e.candidates.map((c) => ({ name: `${c.tenant ? `${c.tenant} / ` : ""}${c.slug} (${c.remote})`, value: c.id }))
|
|
3357
|
-
);
|
|
3358
|
-
return resolveModuleContext(ctx.deps, { ...base, choose: choose2 });
|
|
3359
|
-
}
|
|
3360
|
-
if (allOfKind(e.candidates, "tenant")) {
|
|
3361
|
-
const tenant = await ctx.io.select("Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.id })));
|
|
3362
|
-
return resolveModuleContext(ctx.deps, { ...base, tenant });
|
|
3363
|
-
}
|
|
3364
|
-
throw e;
|
|
3365
|
-
}
|
|
3366
|
-
}
|
|
3367
|
-
async function appFor(ctx, mc, app) {
|
|
3368
|
-
try {
|
|
3369
|
-
return pickApp2(mc, app);
|
|
3370
|
-
} catch (e) {
|
|
3371
|
-
if (!(e instanceof QoreError) || e.code !== "QORE_APP_AMBIGUOUS" || !ctx.io.interactive) throw e;
|
|
3372
|
-
const id = await ctx.io.select("Select an app", mc.module.apps.map((a) => ({ name: a.name, value: a.id })));
|
|
3373
|
-
return pickApp2(mc, id);
|
|
3374
|
-
}
|
|
4023
|
+
};
|
|
3375
4024
|
}
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
4025
|
+
|
|
4026
|
+
// src/core/dev/runFile.ts
|
|
4027
|
+
import { unlink as unlink2 } from "fs/promises";
|
|
4028
|
+
import { join as join20 } from "path";
|
|
4029
|
+
async function runFilePath(deps2, root) {
|
|
4030
|
+
const dir2 = join20(deps2.config.dir, "run");
|
|
4031
|
+
const name = `${await stateKey(deps2, root)}.json`;
|
|
4032
|
+
return { dir: dir2, name, path: join20(dir2, name) };
|
|
3380
4033
|
}
|
|
3381
|
-
function
|
|
3382
|
-
const
|
|
3383
|
-
|
|
4034
|
+
async function writeRunFile(deps2, root, rec) {
|
|
4035
|
+
const { dir: dir2, name } = await runFilePath(deps2, root);
|
|
4036
|
+
await writeJsonAtomic(dir2, name, rec);
|
|
3384
4037
|
}
|
|
3385
|
-
function
|
|
3386
|
-
const {
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
4038
|
+
async function removeRunFile(deps2, root) {
|
|
4039
|
+
const { path } = await runFilePath(deps2, root);
|
|
4040
|
+
await unlink2(path).catch(() => {
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
async function leftoverRun(deps2, root, o = {}) {
|
|
4044
|
+
const { path } = await runFilePath(deps2, root);
|
|
4045
|
+
const rec = await readJson(path).catch(() => null);
|
|
4046
|
+
if (!rec || rec.schemaVersion !== 1) return null;
|
|
4047
|
+
if (deps2.launch.isAlive(rec.qorePid)) return rec;
|
|
4048
|
+
if (o.prune !== false) await unlink2(path).catch(() => {
|
|
4049
|
+
});
|
|
4050
|
+
return null;
|
|
3395
4051
|
}
|
|
3396
4052
|
|
|
3397
4053
|
// src/cli/findings.ts
|
|
@@ -3539,33 +4195,10 @@ async function inPreflight(signals, outer, fn) {
|
|
|
3539
4195
|
}
|
|
3540
4196
|
}
|
|
3541
4197
|
|
|
3542
|
-
// src/cli/trustPrompt.ts
|
|
3543
|
-
async function ensureTrusted(ctx, mc, app, o) {
|
|
3544
|
-
const root = mc.repoRoot ?? ctx.deps.cwd;
|
|
3545
|
-
if (!ctx.opts().json) ctx.io.err(contextLine(mc, app));
|
|
3546
|
-
if (await isTrusted(ctx.deps, root, mc.module.id)) return;
|
|
3547
|
-
if (o.yes) {
|
|
3548
|
-
await trust(ctx.deps, root, mc.module.id);
|
|
3549
|
-
return;
|
|
3550
|
-
}
|
|
3551
|
-
if (!ctx.io.interactive) {
|
|
3552
|
-
throw new QoreError("QORE_CONTEXT_REQUIRED", `This repository is not trusted for ${mc.module.slug} yet`, {
|
|
3553
|
-
hint: "Run interactively once, or pass --yes.",
|
|
3554
|
-
details: { reason: "untrusted", repository: root }
|
|
3555
|
-
});
|
|
3556
|
-
}
|
|
3557
|
-
if (mc.source.kind === "remote") {
|
|
3558
|
-
const key = repoKey(mc.source.url);
|
|
3559
|
-
ctx.io.err(` from remote ${mc.source.remote}: ${key ?? "(unparsable remote URL)"}`);
|
|
3560
|
-
}
|
|
3561
|
-
if (!await ctx.io.confirm("Pull local secrets for this module into this repository?")) throw new CancelledError();
|
|
3562
|
-
await trust(ctx.deps, root, mc.module.id);
|
|
3563
|
-
}
|
|
3564
|
-
|
|
3565
4198
|
// src/cli/commands/devWorkspace.ts
|
|
3566
4199
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
3567
4200
|
import { realpath as realpath7 } from "fs/promises";
|
|
3568
|
-
import { join as
|
|
4201
|
+
import { join as join22, relative as relative6 } from "path";
|
|
3569
4202
|
|
|
3570
4203
|
// src/core/dev/order.ts
|
|
3571
4204
|
function selectSkips(modules, sel) {
|
|
@@ -3701,8 +4334,8 @@ function exitCode(e) {
|
|
|
3701
4334
|
}
|
|
3702
4335
|
function supervise(o) {
|
|
3703
4336
|
const now = o.now ?? Date.now;
|
|
3704
|
-
const delay = o.delay ?? ((
|
|
3705
|
-
setTimeout(r,
|
|
4337
|
+
const delay = o.delay ?? ((ms2) => new Promise((r) => {
|
|
4338
|
+
setTimeout(r, ms2).unref();
|
|
3706
4339
|
}));
|
|
3707
4340
|
const abort = new AbortController();
|
|
3708
4341
|
const emit = (e) => {
|
|
@@ -3722,15 +4355,15 @@ function supervise(o) {
|
|
|
3722
4355
|
});
|
|
3723
4356
|
const readiness = /* @__PURE__ */ new Map();
|
|
3724
4357
|
for (const m of o.modules) {
|
|
3725
|
-
let
|
|
4358
|
+
let resolve7;
|
|
3726
4359
|
let settled = false;
|
|
3727
4360
|
const promise = new Promise((r) => {
|
|
3728
|
-
|
|
4361
|
+
resolve7 = r;
|
|
3729
4362
|
});
|
|
3730
4363
|
readiness.set(m.slug, { promise, settle: (r) => {
|
|
3731
4364
|
if (settled) return false;
|
|
3732
4365
|
settled = true;
|
|
3733
|
-
|
|
4366
|
+
resolve7(r);
|
|
3734
4367
|
return true;
|
|
3735
4368
|
} });
|
|
3736
4369
|
}
|
|
@@ -3822,7 +4455,7 @@ function supervise(o) {
|
|
|
3822
4455
|
|
|
3823
4456
|
// src/core/workspace/stray.ts
|
|
3824
4457
|
import { lstat as lstat2, readdir as readdir3 } from "fs/promises";
|
|
3825
|
-
import { join as
|
|
4458
|
+
import { join as join21 } from "path";
|
|
3826
4459
|
async function strayRepos(root, slugs) {
|
|
3827
4460
|
let entries;
|
|
3828
4461
|
try {
|
|
@@ -3834,7 +4467,7 @@ async function strayRepos(root, slugs) {
|
|
|
3834
4467
|
for (const name of entries) {
|
|
3835
4468
|
if (slugs.has(name)) continue;
|
|
3836
4469
|
try {
|
|
3837
|
-
await lstat2(
|
|
4470
|
+
await lstat2(join21(root, name, ".git"));
|
|
3838
4471
|
names.push(name);
|
|
3839
4472
|
} catch {
|
|
3840
4473
|
}
|
|
@@ -3880,7 +4513,7 @@ function registerRun(program, ctx) {
|
|
|
3880
4513
|
hint: "Drop --json; the child's own output passes through."
|
|
3881
4514
|
});
|
|
3882
4515
|
}
|
|
3883
|
-
const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true });
|
|
4516
|
+
const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true, command: "run" });
|
|
3884
4517
|
const { deps: deps2 } = c;
|
|
3885
4518
|
const mc = await moduleContextFor(c, f, "run");
|
|
3886
4519
|
const app = await appFor(c, mc, f.app);
|
|
@@ -3905,12 +4538,12 @@ function registerRun(program, ctx) {
|
|
|
3905
4538
|
|
|
3906
4539
|
// src/cli/commands/devWorkspace.ts
|
|
3907
4540
|
var INSTALL_PARALLELISM = 4;
|
|
3908
|
-
var
|
|
4541
|
+
var str2 = (v) => typeof v === "string" && v.length > 0;
|
|
3909
4542
|
var slugList = (raw) => raw?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3910
4543
|
function usableLast(last, appId) {
|
|
3911
4544
|
if (!last || last.kind !== "workspace") return false;
|
|
3912
4545
|
const a = last.app;
|
|
3913
|
-
return
|
|
4546
|
+
return str2(last.savedAt) && !!a && a.id === appId && str2(a.slug) && str2(a.name) && !!a.tenant && str2(a.tenant.name) && Array.isArray(last.modules) && last.modules.every((m) => !!m && str2(m.id) && str2(m.slug) && str2(m.type) && str2(m.access) && Array.isArray(m.dependsOn));
|
|
3914
4547
|
}
|
|
3915
4548
|
async function workspaceContext(ctx, ws) {
|
|
3916
4549
|
const { deps: deps2 } = ctx;
|
|
@@ -3996,7 +4629,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3996
4629
|
continue;
|
|
3997
4630
|
}
|
|
3998
4631
|
try {
|
|
3999
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
4632
|
+
dirs[m.slug] = await deps2.git.inspect(join22(ws.root, m.slug));
|
|
4000
4633
|
safe.push(m);
|
|
4001
4634
|
} catch (e) {
|
|
4002
4635
|
if (!(e instanceof QoreError) || e.code === "QORE_GIT_UNAVAILABLE") throw e;
|
|
@@ -4033,7 +4666,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
4033
4666
|
const results = await applyActions(deps2, { portal: online.portal, app: am, actions, protocol: cloneProtocol(dirs), onResult: () => {
|
|
4034
4667
|
} });
|
|
4035
4668
|
renderResults(io2, results, false);
|
|
4036
|
-
for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(
|
|
4669
|
+
for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(join22(ws.root, r.slug));
|
|
4037
4670
|
({ present, plan } = computePlan());
|
|
4038
4671
|
}
|
|
4039
4672
|
for (const name of await strayRepos(ws.root, new Set(am.modules.map((m) => m.slug)))) renderFinding(ctx, strayClone(name, am.app.name));
|
|
@@ -4214,7 +4847,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
4214
4847
|
if (online && units.every((u) => !u.dev.offline)) {
|
|
4215
4848
|
const envFile = (m) => {
|
|
4216
4849
|
const u = units.find((x) => x.m.id === m.id);
|
|
4217
|
-
return u ? relative6(rootReal, u.dev.envFile) :
|
|
4850
|
+
return u ? relative6(rootReal, u.dev.envFile) : join22(m.slug, moduleDefaults(m.type).envFile);
|
|
4218
4851
|
};
|
|
4219
4852
|
await saveLastStart(deps2, ws.root, {
|
|
4220
4853
|
schemaVersion: 1,
|
|
@@ -4410,7 +5043,7 @@ function registerDev(program, ctx) {
|
|
|
4410
5043
|
hint: "qore dev <project> | qore dev --project <project>"
|
|
4411
5044
|
});
|
|
4412
5045
|
}
|
|
4413
|
-
const c = await projectCtx(ctx, { ref: project ?? f.project, module: f.module, app: f.app, needModule: false });
|
|
5046
|
+
const c = await projectCtx(ctx, { ref: project ?? f.project, module: f.module, app: f.app, needModule: false, command: "dev" });
|
|
4414
5047
|
const ws = await workspaceRoot(c.deps);
|
|
4415
5048
|
if (ws && f.module === void 0) return workspaceDev(c, ws, f, waitTimeout);
|
|
4416
5049
|
return moduleDev(c, f, waitTimeout);
|
|
@@ -4425,7 +5058,7 @@ function parseWaitTimeout(raw) {
|
|
|
4425
5058
|
}
|
|
4426
5059
|
return n;
|
|
4427
5060
|
}
|
|
4428
|
-
var
|
|
5061
|
+
var str3 = (v) => typeof v === "string" && v.length > 0;
|
|
4429
5062
|
var SHELL_SAFE = /^[A-Za-z0-9_./,:@%+=-]*$/;
|
|
4430
5063
|
var shellQuote = (s) => SHELL_SAFE.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
|
|
4431
5064
|
function cdAndDev(home, dir2) {
|
|
@@ -4453,7 +5086,7 @@ async function providerFindings(ctx, root, mc, app, env) {
|
|
|
4453
5086
|
let fix = `qore clone ${app.slug}`;
|
|
4454
5087
|
if (ws && ws.appId === app.id) fix = cdAndDev(deps2.home, ws.root);
|
|
4455
5088
|
else if (providers.length === 1) {
|
|
4456
|
-
const sibling =
|
|
5089
|
+
const sibling = join23(dirname7(root), providers[0].slug);
|
|
4457
5090
|
if ((await deps2.git.inspect(sibling)).isRepo) fix = cdAndDev(deps2.home, sibling);
|
|
4458
5091
|
}
|
|
4459
5092
|
return providerDown({
|
|
@@ -4471,7 +5104,7 @@ async function providerFindings(ctx, root, mc, app, env) {
|
|
|
4471
5104
|
function usableLast2(last) {
|
|
4472
5105
|
if (!last || last.kind !== "module") return false;
|
|
4473
5106
|
const m = last.module;
|
|
4474
|
-
return
|
|
5107
|
+
return str3(last.savedAt) && !!m && str3(m.id) && str3(m.slug) && str3(m.type) && !!m.tenant && str3(m.tenant.name) && !!last.app && str3(last.app.id) && str3(last.app.slug) && str3(last.app.name);
|
|
4475
5108
|
}
|
|
4476
5109
|
async function devContext(ctx, f, root) {
|
|
4477
5110
|
let mc;
|
|
@@ -4515,9 +5148,9 @@ async function moduleDev(ctx, f, waitTimeout) {
|
|
|
4515
5148
|
childRunning.current = false;
|
|
4516
5149
|
await removeRunFile(deps2, root);
|
|
4517
5150
|
}
|
|
4518
|
-
const
|
|
5151
|
+
const ms2 = deps2.now().getTime() - startedAt.getTime();
|
|
4519
5152
|
if (exit.code !== 0 && exit.signal === null) {
|
|
4520
|
-
io2.err(
|
|
5153
|
+
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}.`);
|
|
4521
5154
|
const hint = exitHint(tee.lines(), hintContext(recipe, pkg, devEnv2));
|
|
4522
5155
|
if (hint) io2.err(` \u2192 ${hint.hint}`);
|
|
4523
5156
|
}
|
|
@@ -4647,8 +5280,8 @@ function registerDebugTelemetry(program, ctx) {
|
|
|
4647
5280
|
import pc3 from "picocolors";
|
|
4648
5281
|
|
|
4649
5282
|
// src/core/doctor/checks.ts
|
|
4650
|
-
import { realpath as realpath9, stat as
|
|
4651
|
-
import { basename as
|
|
5283
|
+
import { realpath as realpath9, stat as stat6 } from "fs/promises";
|
|
5284
|
+
import { basename as basename4, join as join24 } from "path";
|
|
4652
5285
|
|
|
4653
5286
|
// src/core/diagnostics/ssh.ts
|
|
4654
5287
|
var SSH_TEST_ARGS = ["-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "git@github.com"];
|
|
@@ -4698,19 +5331,19 @@ var withError = (status2, label3, e) => ({ status: status2, label: label3, error
|
|
|
4698
5331
|
var isCancel = (e, signal) => e instanceof CancelledError || signal.aborted;
|
|
4699
5332
|
var portalLabel = (p) => p.builtin ?? p.apiOrigin;
|
|
4700
5333
|
var ToolTimeout = class extends Error {
|
|
4701
|
-
constructor(tool,
|
|
4702
|
-
super(`${tool} did not answer within ${Math.round(
|
|
5334
|
+
constructor(tool, ms2) {
|
|
5335
|
+
super(`${tool} did not answer within ${Math.round(ms2 / 1e3)} s`);
|
|
4703
5336
|
this.tool = tool;
|
|
4704
|
-
this.ms =
|
|
5337
|
+
this.ms = ms2;
|
|
4705
5338
|
}
|
|
4706
5339
|
tool;
|
|
4707
5340
|
ms;
|
|
4708
5341
|
};
|
|
4709
|
-
async function timed(tool, signal,
|
|
5342
|
+
async function timed(tool, signal, ms2, fn) {
|
|
4710
5343
|
try {
|
|
4711
|
-
return await fn(AbortSignal.any([signal, AbortSignal.timeout(
|
|
5344
|
+
return await fn(AbortSignal.any([signal, AbortSignal.timeout(ms2)]));
|
|
4712
5345
|
} catch (e) {
|
|
4713
|
-
if (e instanceof CancelledError && !signal.aborted) throw new ToolTimeout(tool,
|
|
5346
|
+
if (e instanceof CancelledError && !signal.aborted) throw new ToolTimeout(tool, ms2);
|
|
4714
5347
|
throw e;
|
|
4715
5348
|
}
|
|
4716
5349
|
}
|
|
@@ -4741,8 +5374,8 @@ function checkOwnNode(deps2) {
|
|
|
4741
5374
|
}
|
|
4742
5375
|
return ok(`Node ${v} (\u226522 required)`);
|
|
4743
5376
|
}
|
|
4744
|
-
async function checkGit(deps2, signal,
|
|
4745
|
-
const res = await timed("git", signal,
|
|
5377
|
+
async function checkGit(deps2, signal, ms2) {
|
|
5378
|
+
const res = await timed("git", signal, ms2, (s) => runTool(deps2, "git", ["--version"], { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
|
|
4746
5379
|
if (res.spawnError === "ENOENT") {
|
|
4747
5380
|
const e = new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
|
|
4748
5381
|
hint: "Install git (https://git-scm.com/downloads) and open a new terminal."
|
|
@@ -4756,14 +5389,14 @@ async function checkGit(deps2, signal, ms) {
|
|
|
4756
5389
|
const m = /git version (\d+)\.(\d+)/.exec(res.tail.join("\n"));
|
|
4757
5390
|
return ok(m ? `git ${m[1]}.${m[2]}` : "git");
|
|
4758
5391
|
}
|
|
4759
|
-
async function checkGithubSsh(deps2, signal,
|
|
4760
|
-
const res = await timed("ssh", signal,
|
|
5392
|
+
async function checkGithubSsh(deps2, signal, ms2) {
|
|
5393
|
+
const res = await timed("ssh", signal, ms2, (s) => runTool(deps2, "ssh", SSH_TEST_ARGS, { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
|
|
4761
5394
|
const r = classifySshTest(res);
|
|
4762
5395
|
if (r.ok) return ok(`GitHub SSH ok (${r.user})`, { user: r.user });
|
|
4763
5396
|
return withError("warn", `GitHub SSH: ${r.error.message} (https still works)`, r.error);
|
|
4764
5397
|
}
|
|
4765
|
-
async function checkDocker(deps2, signal,
|
|
4766
|
-
return timed("docker", signal,
|
|
5398
|
+
async function checkDocker(deps2, signal, ms2) {
|
|
5399
|
+
return timed("docker", signal, ms2, async (s) => {
|
|
4767
5400
|
const o = { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s };
|
|
4768
5401
|
const info = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], o);
|
|
4769
5402
|
const infoErr = classifyDockerInfo(info, deps2.platform);
|
|
@@ -4791,9 +5424,9 @@ function jwtExp(token) {
|
|
|
4791
5424
|
}
|
|
4792
5425
|
function validDays(rt, now) {
|
|
4793
5426
|
const exp = jwtExp(rt.rt);
|
|
4794
|
-
const
|
|
4795
|
-
if (Number.isNaN(
|
|
4796
|
-
return Math.max(0, Math.floor((
|
|
5427
|
+
const end2 = exp !== null ? exp * 1e3 : Date.parse(rt.obtainedAt) + IDLE_DAYS * DAY_MS;
|
|
5428
|
+
if (Number.isNaN(end2)) return null;
|
|
5429
|
+
return Math.max(0, Math.floor((end2 - now.getTime()) / DAY_MS));
|
|
4797
5430
|
}
|
|
4798
5431
|
async function checkSession(deps2, portal, onRequest, sessions, selected, signal) {
|
|
4799
5432
|
try {
|
|
@@ -4849,7 +5482,7 @@ async function moduleBody(deps2, m, runs, signal) {
|
|
|
4849
5482
|
if (text2 === null) return { status: "warn", label: "port unknown (run qore env pull)" };
|
|
4850
5483
|
env = Object.fromEntries(parseEnv(text2));
|
|
4851
5484
|
const port = envPort(env, "PORT");
|
|
4852
|
-
if (port === null) return { status: "warn", label: `port unknown (no PORT in ${
|
|
5485
|
+
if (port === null) return { status: "warn", label: `port unknown (no PORT in ${basename4(target)})` };
|
|
4853
5486
|
if (!await deps2.ports.inUse(port)) return { status: "ok", label: `port ${port} free` };
|
|
4854
5487
|
const e = await portInUseError(deps2, { slug: m.slug, port, key: "PORT", runs: await runs() });
|
|
4855
5488
|
return { status: "fail", label: `port ${port} in use${ownerText(e)}`, error: e };
|
|
@@ -4868,13 +5501,13 @@ async function moduleBody(deps2, m, runs, signal) {
|
|
|
4868
5501
|
flags: { install: true, services: true }
|
|
4869
5502
|
});
|
|
4870
5503
|
if (recipe.services.length === 0) return { status: "ok", label: "db none" };
|
|
4871
|
-
const
|
|
4872
|
-
if (
|
|
4873
|
-
if (!await deps2.ports.inUse(
|
|
4874
|
-
const pub = await deps2.ports.dockerPublisher(
|
|
5504
|
+
const dbPort2 = env ? envPort(env, "DB_PORT") : null;
|
|
5505
|
+
if (dbPort2 === null) return { status: "warn", label: "db port unknown (run qore env pull)" };
|
|
5506
|
+
if (!await deps2.ports.inUse(dbPort2)) return { status: "ok", label: `db:up (DB_PORT ${dbPort2} free)` };
|
|
5507
|
+
const pub = await deps2.ports.dockerPublisher(dbPort2);
|
|
4875
5508
|
if (pub?.workingDir && await sameDir(pub.workingDir, m.root)) return { status: "ok", label: "db running" };
|
|
4876
|
-
const e = await portInUseError(deps2, { slug: m.slug, port:
|
|
4877
|
-
return { status: "fail", label: `DB_PORT ${
|
|
5509
|
+
const e = await portInUseError(deps2, { slug: m.slug, port: dbPort2, key: "DB_PORT" });
|
|
5510
|
+
return { status: "fail", label: `DB_PORT ${dbPort2} in use${ownerText(e)}`, error: e };
|
|
4878
5511
|
});
|
|
4879
5512
|
const status2 = parts.reduce((w, p) => RANK[p.status] > RANK[w] ? p.status : w, "ok");
|
|
4880
5513
|
const shown = parts.filter((p) => p.status !== "ok");
|
|
@@ -4895,16 +5528,16 @@ function runsAt(deps2, root) {
|
|
|
4895
5528
|
return () => p ??= leftoverRun(deps2, root, { prune: false }).then((r) => r ? [r] : []);
|
|
4896
5529
|
}
|
|
4897
5530
|
var UNTRUSTED = { status: "warn", label: "not checked (repository not trusted)" };
|
|
4898
|
-
async function checkRepoPm(deps2, m, recipe, signal,
|
|
5531
|
+
async function checkRepoPm(deps2, m, recipe, signal, ms2) {
|
|
4899
5532
|
if (!await isTrusted(deps2, m.root, m.id)) return UNTRUSTED;
|
|
4900
|
-
const found = await timed(recipe.pm.name, signal,
|
|
5533
|
+
const found = await timed(recipe.pm.name, signal, ms2, (s) => checkPackageManager(deps2, recipe, noop, s));
|
|
4901
5534
|
const { name, version } = recipe.pm;
|
|
4902
5535
|
if (found === null) return ok(`${name} (version unknown)`);
|
|
4903
5536
|
return ok(version ? `${name} ${found} (matches)` : `${name} ${found}`, { found, ...version ? { pinned: version } : {} });
|
|
4904
5537
|
}
|
|
4905
|
-
async function checkRepoNode(deps2, m, recipe, pkg, signal,
|
|
5538
|
+
async function checkRepoNode(deps2, m, recipe, pkg, signal, ms2) {
|
|
4906
5539
|
if (!await isTrusted(deps2, m.root, m.id)) return UNTRUSTED;
|
|
4907
|
-
const found = await timed("node", signal,
|
|
5540
|
+
const found = await timed("node", signal, ms2, (s) => checkNode(deps2, recipe, pkg, m.slug, s));
|
|
4908
5541
|
return ok(`Node ${found ?? "?"} (engines ${pkg.engines.node})`);
|
|
4909
5542
|
}
|
|
4910
5543
|
async function loggedInPortals(deps2, profiles) {
|
|
@@ -4931,7 +5564,7 @@ async function resolveRepoModule(deps2, session, portal, meta, root) {
|
|
|
4931
5564
|
async function checkProjectsRoot(cfg) {
|
|
4932
5565
|
const root = cfg?.projectsRoot;
|
|
4933
5566
|
if (!root) return ok("projects folder not set (optional \u2014 qore init)");
|
|
4934
|
-
const s = await
|
|
5567
|
+
const s = await stat6(root).catch(() => null);
|
|
4935
5568
|
if (!s?.isDirectory()) return { status: "warn", label: `projects folder ${root} does not exist \u2014 qore init` };
|
|
4936
5569
|
const e = await writableError(root);
|
|
4937
5570
|
if (e) return { status: "fail", label: `projects folder ${root} is not writable (${e.code ?? e.message})` };
|
|
@@ -5034,7 +5667,7 @@ async function runDoctor(deps2, o = {}) {
|
|
|
5034
5667
|
const repoRoot = wsRoot ? null : await findRepoRoot(deps2.cwd);
|
|
5035
5668
|
if (wsRoot) {
|
|
5036
5669
|
context.kind = "workspace";
|
|
5037
|
-
context.label =
|
|
5670
|
+
context.label = basename4(wsRoot.root);
|
|
5038
5671
|
if (!session) {
|
|
5039
5672
|
await notChecked();
|
|
5040
5673
|
return { results, context };
|
|
@@ -5054,7 +5687,7 @@ async function runDoctor(deps2, o = {}) {
|
|
|
5054
5687
|
for (const m of am.modules) await workspaceModule(deps2, wsRoot, am, m, runs, signal, push, perModule);
|
|
5055
5688
|
} else if (repoRoot) {
|
|
5056
5689
|
context.kind = "module";
|
|
5057
|
-
context.label =
|
|
5690
|
+
context.label = basename4(repoRoot);
|
|
5058
5691
|
if (!session) {
|
|
5059
5692
|
await notChecked();
|
|
5060
5693
|
return { results, context };
|
|
@@ -5085,7 +5718,7 @@ async function workspaceModule(deps2, ws, am, m, runs, signal, push, perModule)
|
|
|
5085
5718
|
await push(`module:${m.slug}`, "workspace", () => ({ status: "warn", label: "not a safe directory name \u2014 skipped" }), m.slug);
|
|
5086
5719
|
return;
|
|
5087
5720
|
}
|
|
5088
|
-
const mod = { root:
|
|
5721
|
+
const mod = { root: join24(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
|
|
5089
5722
|
let prepared = null;
|
|
5090
5723
|
await push(`module:${m.slug}`, "workspace", async () => {
|
|
5091
5724
|
const info = await deps2.git.inspect(mod.root);
|
|
@@ -5117,12 +5750,12 @@ function registerDoctor(program, ctx) {
|
|
|
5117
5750
|
// src/cli/commands/env.ts
|
|
5118
5751
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
5119
5752
|
import { chmod as chmod3, realpath as realpath10 } from "fs/promises";
|
|
5120
|
-
import { dirname as dirname8, join as
|
|
5753
|
+
import { dirname as dirname8, join as join25, relative as relative8 } from "path";
|
|
5121
5754
|
var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.length} chars)`;
|
|
5122
5755
|
function registerEnv(program, ctx) {
|
|
5123
5756
|
const env = program.command("env").description("Local environment");
|
|
5124
5757
|
env.command("pull").description("Write this module's local secrets into its env file").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "the project to pull for, from anywhere (see qore init)").option("--target <path>", "env file to write (inside the repository)").option("--use-portal", "take the portal's value for keys that differ locally").option("--yes", "trust this repository without asking").action(async (f) => {
|
|
5125
|
-
const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true });
|
|
5758
|
+
const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true, command: "env pull" });
|
|
5126
5759
|
const { deps: deps2, io: io2 } = c;
|
|
5127
5760
|
const g = c.opts();
|
|
5128
5761
|
const mc = await moduleContextFor(c, f, "env pull");
|
|
@@ -5136,7 +5769,7 @@ function registerEnv(program, ctx) {
|
|
|
5136
5769
|
const target = f.target ? await resolveTarget(mc.repoRoot, f.target, deps2.cwd) : await resolveTarget(mc.repoRoot, cfg.envFile ?? defaults.envFile);
|
|
5137
5770
|
await ensureTrusted(c, mc, app, { yes: f.yes });
|
|
5138
5771
|
const existing = await readOrNull(target);
|
|
5139
|
-
const example = existing === null ? await readOrNull(
|
|
5772
|
+
const example = existing === null ? await readOrNull(join25(dirname8(target), ".env.example")) : null;
|
|
5140
5773
|
const local = await fetchLocalEnv(deps2, mc, app);
|
|
5141
5774
|
const inputs = {
|
|
5142
5775
|
existing,
|
|
@@ -5152,13 +5785,13 @@ function registerEnv(program, ctx) {
|
|
|
5152
5785
|
const current = parseEnv(existing ?? example ?? "");
|
|
5153
5786
|
const choices = {};
|
|
5154
5787
|
let bulk;
|
|
5155
|
-
for (const
|
|
5788
|
+
for (const key2 of plan.conflicts) {
|
|
5156
5789
|
if (bulk) {
|
|
5157
|
-
choices[
|
|
5790
|
+
choices[key2] = bulk;
|
|
5158
5791
|
continue;
|
|
5159
5792
|
}
|
|
5160
|
-
const
|
|
5161
|
-
`${
|
|
5793
|
+
const pick2 = await io2.select(
|
|
5794
|
+
`${key2} differs (local ${maskLen(current.get(key2))}, portal ${maskLen(local.env[key2])})`,
|
|
5162
5795
|
[
|
|
5163
5796
|
{ name: "keep local", value: "keep" },
|
|
5164
5797
|
{ name: "use portal", value: "portal" },
|
|
@@ -5166,9 +5799,9 @@ function registerEnv(program, ctx) {
|
|
|
5166
5799
|
{ name: "keep local for all", value: "skip" }
|
|
5167
5800
|
]
|
|
5168
5801
|
);
|
|
5169
|
-
if (
|
|
5170
|
-
if (
|
|
5171
|
-
choices[
|
|
5802
|
+
if (pick2 === "all") bulk = "use-portal";
|
|
5803
|
+
if (pick2 === "skip") bulk = "keep";
|
|
5804
|
+
choices[key2] = pick2 === "portal" || pick2 === "all" ? "use-portal" : "keep";
|
|
5172
5805
|
}
|
|
5173
5806
|
plan = planPull({ ...inputs, choices });
|
|
5174
5807
|
}
|
|
@@ -5206,12 +5839,486 @@ function registerEnv(program, ctx) {
|
|
|
5206
5839
|
});
|
|
5207
5840
|
}
|
|
5208
5841
|
|
|
5842
|
+
// src/core/observability/compose.ts
|
|
5843
|
+
function pageWarnings(...pages) {
|
|
5844
|
+
return pages.some((p) => p.ingestPaused) ? ["ingest_paused"] : [];
|
|
5845
|
+
}
|
|
5846
|
+
function scope(q) {
|
|
5847
|
+
const s = { env: q.env };
|
|
5848
|
+
if (q.since !== void 0) s.since = q.since;
|
|
5849
|
+
if (q.start !== void 0) s.start = q.start;
|
|
5850
|
+
if (q.end !== void 0) s.end = q.end;
|
|
5851
|
+
return s;
|
|
5852
|
+
}
|
|
5853
|
+
async function errorDetail(c, id, q, withs) {
|
|
5854
|
+
const w = scope(q);
|
|
5855
|
+
const markdown = await c.issueMarkdown(id, w);
|
|
5856
|
+
const event = await c.issueEvent(id, w);
|
|
5857
|
+
const out = { issue: { markdown, event }, warnings: [] };
|
|
5858
|
+
if (withs.size === 0) return out;
|
|
5859
|
+
if (!event.traceId) {
|
|
5860
|
+
out.warnings.push("no_trace_id");
|
|
5861
|
+
return out;
|
|
5862
|
+
}
|
|
5863
|
+
if (withs.has("trace")) out.trace = await c.trace(event.traceId, w);
|
|
5864
|
+
if (withs.has("logs")) {
|
|
5865
|
+
const logs = await c.logs({ ...w, trace: event.traceId, limit: 100 });
|
|
5866
|
+
out.logs = logs.items;
|
|
5867
|
+
out.warnings.push(...pageWarnings(logs));
|
|
5868
|
+
}
|
|
5869
|
+
return out;
|
|
5870
|
+
}
|
|
5871
|
+
async function traceDetail(c, id, q, withs) {
|
|
5872
|
+
const w = scope(q);
|
|
5873
|
+
const out = { trace: await c.trace(id, w), warnings: [] };
|
|
5874
|
+
const pages = [];
|
|
5875
|
+
if (withs.has("logs")) {
|
|
5876
|
+
const p = await c.logs({ ...w, trace: id, limit: 100 });
|
|
5877
|
+
out.logs = p.items;
|
|
5878
|
+
pages.push(p);
|
|
5879
|
+
}
|
|
5880
|
+
if (withs.has("errors")) {
|
|
5881
|
+
const p = await c.issues({ ...w, trace: id, status: "all" });
|
|
5882
|
+
out.errors = p.items;
|
|
5883
|
+
pages.push(p);
|
|
5884
|
+
}
|
|
5885
|
+
out.warnings.push(...pageWarnings(...pages));
|
|
5886
|
+
return out;
|
|
5887
|
+
}
|
|
5888
|
+
|
|
5889
|
+
// src/core/observability/client.ts
|
|
5890
|
+
var MAX_RETRY_WAIT_S = 30;
|
|
5891
|
+
var NOT_PROVISIONED = "observability_not_provisioned";
|
|
5892
|
+
var wait = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
5893
|
+
var ObsClient = class {
|
|
5894
|
+
constructor(api, moduleId, sleep = wait, slug = moduleId) {
|
|
5895
|
+
this.api = api;
|
|
5896
|
+
this.moduleId = moduleId;
|
|
5897
|
+
this.sleep = sleep;
|
|
5898
|
+
this.slug = slug;
|
|
5899
|
+
}
|
|
5900
|
+
api;
|
|
5901
|
+
moduleId;
|
|
5902
|
+
sleep;
|
|
5903
|
+
slug;
|
|
5904
|
+
path(resource, o, extra = {}) {
|
|
5905
|
+
const p = new URLSearchParams();
|
|
5906
|
+
const all = { env: o.env, since: o.since, start: o.start, end: o.end, q: o.q, limit: o.limit, ...extra };
|
|
5907
|
+
for (const [k, v] of Object.entries(all)) if (v !== void 0 && v !== "") p.set(k, String(v));
|
|
5908
|
+
return `/api/cli/v1/modules/${encodeURIComponent(this.moduleId)}/observability/${resource}?${p}`;
|
|
5909
|
+
}
|
|
5910
|
+
async call(fn) {
|
|
5911
|
+
try {
|
|
5912
|
+
return await this.retryOnce(fn);
|
|
5913
|
+
} catch (e) {
|
|
5914
|
+
if (e instanceof QoreError && e.code === "QORE_ACCESS_DENIED" && e.serverCode === "auth.scope.insufficient") {
|
|
5915
|
+
throw new QoreError("QORE_ACCESS_DENIED", "This login predates `qore errors/traces/logs`", {
|
|
5916
|
+
hint: "Run `qore login` again.",
|
|
5917
|
+
status: e.status,
|
|
5918
|
+
requestId: e.requestId,
|
|
5919
|
+
serverCode: e.serverCode
|
|
5920
|
+
});
|
|
5921
|
+
}
|
|
5922
|
+
if (e instanceof QoreError && e.serverCode === NOT_PROVISIONED) {
|
|
5923
|
+
throw new QoreError("QORE_RESOURCE_NOT_FOUND", `Module "${this.slug}" does not report to Sentry yet`, {
|
|
5924
|
+
hint: "Its Sentry project is not provisioned; pick another module with --module.",
|
|
5925
|
+
status: e.status,
|
|
5926
|
+
requestId: e.requestId,
|
|
5927
|
+
serverCode: e.serverCode,
|
|
5928
|
+
details: e.details
|
|
5929
|
+
});
|
|
5930
|
+
}
|
|
5931
|
+
throw e;
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5934
|
+
async retryOnce(fn) {
|
|
5935
|
+
try {
|
|
5936
|
+
return await fn();
|
|
5937
|
+
} catch (e) {
|
|
5938
|
+
if (!(e instanceof QoreError) || e.code !== "QORE_RATE_LIMITED") throw e;
|
|
5939
|
+
const secs2 = typeof e.details?.retryAfter === "number" ? e.details.retryAfter : 1;
|
|
5940
|
+
await this.sleep(Math.min(secs2, MAX_RETRY_WAIT_S) * 1e3);
|
|
5941
|
+
return fn();
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
issues(o) {
|
|
5945
|
+
return this.call(() => this.api.get(this.path("issues", o, { status: o.status, sort: o.sort, trace: o.trace })));
|
|
5946
|
+
}
|
|
5947
|
+
issueMarkdown(id, o) {
|
|
5948
|
+
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" })));
|
|
5949
|
+
}
|
|
5950
|
+
issueEvent(id, o) {
|
|
5951
|
+
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 })));
|
|
5952
|
+
}
|
|
5953
|
+
logs(o) {
|
|
5954
|
+
return this.call(() => this.api.get(this.path("logs", o, { level: o.level, trace: o.trace })));
|
|
5955
|
+
}
|
|
5956
|
+
spans(o) {
|
|
5957
|
+
return this.call(() => this.api.get(this.path("spans", o, { sort: o.sort })));
|
|
5958
|
+
}
|
|
5959
|
+
trace(id, o) {
|
|
5960
|
+
return this.call(() => this.api.get(this.path(`traces/${encodeURIComponent(id)}`, { env: o.env, since: o.since, start: o.start, end: o.end })));
|
|
5961
|
+
}
|
|
5962
|
+
};
|
|
5963
|
+
|
|
5964
|
+
// src/core/observability/query.ts
|
|
5965
|
+
var ROOT_SPAN_FILTER = "is_transaction:true";
|
|
5966
|
+
var DURATION = /^\d{1,6}(ms|s|m)$/;
|
|
5967
|
+
var SINCE = /^(\d{1,4})([mhdw])$/;
|
|
5968
|
+
var SECONDS = { m: 60, h: 3600, d: 86400, w: 604800 };
|
|
5969
|
+
function parseDuration(s) {
|
|
5970
|
+
if (!DURATION.test(s)) throw new QoreError("QORE_CONTEXT_INVALID", `"${s}" is not a duration`, { hint: "Use e.g. 500ms, 1s or 2m." });
|
|
5971
|
+
return s;
|
|
5972
|
+
}
|
|
5973
|
+
function parseSince(s) {
|
|
5974
|
+
const m = SINCE.exec(s);
|
|
5975
|
+
if (!m || Number(m[1]) * SECONDS[m[2]] > 90 * 86400) {
|
|
5976
|
+
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)." });
|
|
5977
|
+
}
|
|
5978
|
+
return s;
|
|
5979
|
+
}
|
|
5980
|
+
function joinQuery(...parts) {
|
|
5981
|
+
const q = parts.filter((p) => typeof p === "string" && p.length > 0).join(" ");
|
|
5982
|
+
return q || void 0;
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
// src/cli/observability/context.ts
|
|
5986
|
+
var ALL = { kind: "module", slug: "all", name: "All modules" };
|
|
5987
|
+
var modCandidates = (mods, withAll) => [...mods.map((m) => ({ kind: "module", id: m.id, slug: m.slug, name: m.name })), ...withAll ? [ALL] : []];
|
|
5988
|
+
function withRepo(mods) {
|
|
5989
|
+
const r = mods.filter((m) => m.repoUrl !== null);
|
|
5990
|
+
if (r.length === 0) throw new QoreError("QORE_RESOURCE_NOT_FOUND", "No modules with a repository in this app");
|
|
5991
|
+
return r;
|
|
5992
|
+
}
|
|
5993
|
+
function windowOf(f) {
|
|
5994
|
+
if (Boolean(f.start) !== Boolean(f.end)) throw new QoreError("QORE_CONTEXT_INVALID", "--start and --end go together; pass both");
|
|
5995
|
+
if (f.start || f.end) return { start: f.start, end: f.end };
|
|
5996
|
+
return { since: parseSince(f.since ?? "24h") };
|
|
5997
|
+
}
|
|
5998
|
+
var windowText = (w) => w.since ? `last ${w.since}` : `${w.start ?? "\u2026"} \u2013 ${w.end ?? "now"}`;
|
|
5999
|
+
async function chooseModule(ctx, mods, flag, single) {
|
|
6000
|
+
if (flag === "all" && !single) return mods;
|
|
6001
|
+
if (flag && flag !== "all") {
|
|
6002
|
+
const m = mods.find((x) => x.slug === flag || x.id === flag);
|
|
6003
|
+
if (m) return [m];
|
|
6004
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", `No module "${flag}" in this app`, { candidates: modCandidates(mods, !single) });
|
|
6005
|
+
}
|
|
6006
|
+
const err = new QoreError(
|
|
6007
|
+
"QORE_CONTEXT_REQUIRED",
|
|
6008
|
+
single && flag === "all" ? "A detail view needs one module" : "Which module?",
|
|
6009
|
+
{ candidates: modCandidates(mods, !single), hint: "Pass --module <slug>" + (single ? "." : " or --module all.") }
|
|
6010
|
+
);
|
|
6011
|
+
if (flag === "all") throw err;
|
|
6012
|
+
const choice = await pick(
|
|
6013
|
+
ctx,
|
|
6014
|
+
err,
|
|
6015
|
+
"Select a module",
|
|
6016
|
+
[...mods.map((m) => ({ name: m.slug, value: m.id })), ...single ? [] : [{ name: "All modules", value: "all" }]]
|
|
6017
|
+
);
|
|
6018
|
+
return choice === "all" ? mods : mods.filter((m) => m.id === choice);
|
|
6019
|
+
}
|
|
6020
|
+
async function chooseEnv(ctx, api, moduleId, appId, flag) {
|
|
6021
|
+
const st = await api.get(`/api/cli/v1/modules/${encodeURIComponent(moduleId)}/status?app_id=${encodeURIComponent(appId)}`);
|
|
6022
|
+
const names = st.environments.map((e) => e.name);
|
|
6023
|
+
if (names.length === 0) throw new QoreError("QORE_RESOURCE_NOT_FOUND", "No environments yet");
|
|
6024
|
+
const candidates = names.map((n) => ({ kind: "env", slug: n }));
|
|
6025
|
+
if (flag) {
|
|
6026
|
+
if (names.includes(flag)) return flag;
|
|
6027
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", `Unknown environment "${flag}"`, { candidates, hint: `Pass --env ${names.join("|")}.` });
|
|
6028
|
+
}
|
|
6029
|
+
const err = new QoreError("QORE_CONTEXT_REQUIRED", "Which environment?", { candidates, hint: `Pass --env ${names.join("|")}.` });
|
|
6030
|
+
return pick(ctx, err, "Select an environment", names.map((n) => ({ name: n, value: n })));
|
|
6031
|
+
}
|
|
6032
|
+
async function obsContext(ctx, flags, command, o) {
|
|
6033
|
+
const { deps: deps2, io: io2 } = ctx;
|
|
6034
|
+
const g = ctx.opts();
|
|
6035
|
+
const window = windowOf(flags);
|
|
6036
|
+
const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
|
|
6037
|
+
const ws = flags.module === void 0 || flags.module === "all" ? await workspaceRoot(deps2) : null;
|
|
6038
|
+
let api, tenant, app, mods, base, apiOrigin;
|
|
6039
|
+
if (ws) {
|
|
6040
|
+
const portal = await flagPortal(deps2, g.portal, ws);
|
|
6041
|
+
apiOrigin = portal.apiOrigin;
|
|
6042
|
+
const session = await openSession(deps2, portal, command, onRequest);
|
|
6043
|
+
await ensureCapability(deps2, apiOrigin, "module.observability");
|
|
6044
|
+
await ensureCapability(deps2, apiOrigin, "app.modules");
|
|
6045
|
+
const am = await workspaceAppModules(session, portal, ws);
|
|
6046
|
+
api = session.api;
|
|
6047
|
+
({ tenant } = am.app);
|
|
6048
|
+
app = { id: am.app.id, slug: am.app.slug, name: am.app.name };
|
|
6049
|
+
mods = await chooseModule(ctx, withRepo(am.modules), flags.module, o.single);
|
|
6050
|
+
base = { portal: portal.builtin ?? portal.apiOrigin, tenant, app, source: WORKSPACE_FILE };
|
|
6051
|
+
} else {
|
|
6052
|
+
const mc = await moduleContextFor(ctx, { module: flags.module === "all" ? void 0 : flags.module }, command);
|
|
6053
|
+
apiOrigin = mc.portal.apiOrigin;
|
|
6054
|
+
await ensureCapability(deps2, apiOrigin, "module.observability");
|
|
6055
|
+
app = await appFor(ctx, mc, flags.app);
|
|
6056
|
+
api = mc.session.api;
|
|
6057
|
+
tenant = mc.module.tenant;
|
|
6058
|
+
if (flags.module === "all") {
|
|
6059
|
+
if (o.single) throw new QoreError(
|
|
6060
|
+
"QORE_CONTEXT_REQUIRED",
|
|
6061
|
+
"A detail view needs one module",
|
|
6062
|
+
{ candidates: [{ kind: "module", id: mc.module.id, slug: mc.module.slug, name: mc.module.name }] }
|
|
6063
|
+
);
|
|
6064
|
+
await ensureCapability(deps2, apiOrigin, "app.modules");
|
|
6065
|
+
const am = await api.get(`/api/cli/v1/apps/${encodeURIComponent(app.id)}/modules`);
|
|
6066
|
+
mods = withRepo(am.modules);
|
|
6067
|
+
} else {
|
|
6068
|
+
mods = [mc.module];
|
|
6069
|
+
}
|
|
6070
|
+
base = contextJson(mc, app);
|
|
6071
|
+
delete base.module;
|
|
6072
|
+
}
|
|
6073
|
+
await ensureCapability(deps2, apiOrigin, "module.status");
|
|
6074
|
+
const env = await chooseEnv(ctx, api, mods[0].id, app.id, flags.env);
|
|
6075
|
+
const fanOut2 = mods.length > 1 || flags.module === "all";
|
|
6076
|
+
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) }));
|
|
6077
|
+
const where = fanOut2 ? "all modules" : mods[0].slug;
|
|
6078
|
+
return {
|
|
6079
|
+
targets,
|
|
6080
|
+
fanOut: fanOut2,
|
|
6081
|
+
env,
|
|
6082
|
+
window,
|
|
6083
|
+
header: `\u2192 ${tenant.name} / ${app.name} / ${where} \xB7 ${env} \xB7 ${windowText(window)}`,
|
|
6084
|
+
json: { ...base, ...fanOut2 ? { modules: targets.map((t) => t.module) } : { module: targets[0].module }, env, window }
|
|
6085
|
+
};
|
|
6086
|
+
}
|
|
6087
|
+
|
|
6088
|
+
// src/cli/format.ts
|
|
6089
|
+
function ago(iso, now) {
|
|
6090
|
+
if (!iso) return "\u2013";
|
|
6091
|
+
const s = Math.max(0, Math.round((now.getTime() - new Date(iso).getTime()) / 1e3));
|
|
6092
|
+
if (s < 60) return "just now";
|
|
6093
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
6094
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
|
|
6095
|
+
return `${Math.floor(s / 86400)}d ago`;
|
|
6096
|
+
}
|
|
6097
|
+
var pct = (f) => String(Number((f * 100).toFixed(2)));
|
|
6098
|
+
function uptime(h) {
|
|
6099
|
+
if (!h) return "\u2013";
|
|
6100
|
+
const kind = h.cause && h.kind !== "up" ? `${h.kind}: ${h.cause}` : h.kind;
|
|
6101
|
+
return h.uptime24h === null ? kind : `${kind} (${pct(h.uptime24h)}% 24h)`;
|
|
6102
|
+
}
|
|
6103
|
+
function table(rows) {
|
|
6104
|
+
const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length)));
|
|
6105
|
+
return rows.map((r) => " " + r.map((c, i) => i === r.length - 1 ? c : c.padEnd(widths[i])).join(" "));
|
|
6106
|
+
}
|
|
6107
|
+
|
|
6108
|
+
// src/cli/observability/render.ts
|
|
6109
|
+
var CONTROL = /[\x00-\x08\x0b-\x1f\x7f\x80-\x9f]/g;
|
|
6110
|
+
var clean = (s) => s.replace(CONTROL, "");
|
|
6111
|
+
function fence(content) {
|
|
6112
|
+
const lines = content.map(clean);
|
|
6113
|
+
const longest = Math.max(0, ...lines.flatMap((l) => (l.match(/`+/g) ?? []).map((r) => r.length)));
|
|
6114
|
+
const ticks = "`".repeat(Math.max(3, longest + 1));
|
|
6115
|
+
return [`${ticks}untrusted-data`, ...lines, ticks];
|
|
6116
|
+
}
|
|
6117
|
+
function ms(n) {
|
|
6118
|
+
return n < 1e3 ? `${Math.round(n)}ms` : `${(n / 1e3).toFixed(1)}s`;
|
|
6119
|
+
}
|
|
6120
|
+
var oneLine = (s) => clean((s ?? "").replace(/\s+/g, " ")).trim();
|
|
6121
|
+
function issuesTable(items, now, withModule) {
|
|
6122
|
+
const head = ["ID", "LEVEL", "EVENTS", "USERS", "LAST SEEN", "TITLE"];
|
|
6123
|
+
const rows = items.map((i) => [oneLine(i.shortId), oneLine(i.level), String(i.count), String(i.userCount), ago(i.lastSeen, now), oneLine(i.title)]);
|
|
6124
|
+
return table(withModule ? [["MODULE", ...head], ...rows.map((r, n) => [oneLine(items[n].module.slug), ...r])] : [head, ...rows]);
|
|
6125
|
+
}
|
|
6126
|
+
function spansTable(items, now, withModule) {
|
|
6127
|
+
const head = ["TRACE", "DURATION", "ROOT", "WHEN"];
|
|
6128
|
+
const rows = items.map((s) => [oneLine(s.traceId), ms(s.durationMs), oneLine(s.transaction ?? s.description ?? s.name), ago(s.timestamp, now)]);
|
|
6129
|
+
return table(withModule ? [["MODULE", ...head], ...rows.map((r, n) => [oneLine(items[n].module.slug), ...r])] : [head, ...rows]);
|
|
6130
|
+
}
|
|
6131
|
+
function logLines(items, withModule) {
|
|
6132
|
+
return items.map((l) => [oneLine(l.timestamp), oneLine(l.severity).toUpperCase(), ...withModule ? [oneLine(l.module.slug)] : [], oneLine(l.message)].join(" "));
|
|
6133
|
+
}
|
|
6134
|
+
function spanTree(t) {
|
|
6135
|
+
const all = [];
|
|
6136
|
+
const walk = (n) => {
|
|
6137
|
+
all.push(n);
|
|
6138
|
+
n.children.forEach(walk);
|
|
6139
|
+
};
|
|
6140
|
+
t.roots.forEach(walk);
|
|
6141
|
+
const slowest = all.reduce((a, n) => !a || n.end - n.start > a.end - a.start ? n : a, null);
|
|
6142
|
+
const out = [];
|
|
6143
|
+
const line2 = (n, depth) => {
|
|
6144
|
+
const label3 = [oneLine(n.op), oneLine(n.description ?? n.transaction)].filter(Boolean).join(" ");
|
|
6145
|
+
const errs = n.errors.map((e) => `\u2716 ${oneLine(e.shortId ?? e.eventId)}`).join(" ");
|
|
6146
|
+
out.push([
|
|
6147
|
+
`${" ".repeat(depth)}${label3}`,
|
|
6148
|
+
ms((n.end - n.start) * 1e3),
|
|
6149
|
+
`[${oneLine(n.module.slug)}]`,
|
|
6150
|
+
...n === slowest ? ["\u25C0 slowest"] : [],
|
|
6151
|
+
...errs ? [errs] : []
|
|
6152
|
+
].join(" "));
|
|
6153
|
+
n.children.forEach((c) => line2(c, depth + 1));
|
|
6154
|
+
};
|
|
6155
|
+
t.roots.forEach((r) => line2(r, 0));
|
|
6156
|
+
return out;
|
|
6157
|
+
}
|
|
6158
|
+
var WARN = {
|
|
6159
|
+
ingest_paused: "! This tenant's Sentry quota is reached; new events are not being recorded.",
|
|
6160
|
+
no_trace_id: "! This error has no trace id, so there is no trace or trace logs to show.",
|
|
6161
|
+
sort_unavailable: "! Sorting by duration is not available here; showing the slowest of the most recent matches.",
|
|
6162
|
+
module_not_provisioned: "! Skipped, not reporting to Sentry yet:"
|
|
6163
|
+
};
|
|
6164
|
+
function warningLines(w, skipped = []) {
|
|
6165
|
+
return [...new Set(w)].map((k) => k === "module_not_provisioned" ? `${WARN[k]} ${skipped.join(", ")}` : WARN[k]);
|
|
6166
|
+
}
|
|
6167
|
+
|
|
6168
|
+
// src/cli/observability/flags.ts
|
|
6169
|
+
function addObsFlags(cmd) {
|
|
6170
|
+
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)");
|
|
6171
|
+
}
|
|
6172
|
+
function limitOf(s) {
|
|
6173
|
+
if (s === void 0) return 25;
|
|
6174
|
+
const n = Number(s);
|
|
6175
|
+
if (!Number.isInteger(n) || n < 1 || n > 100) throw new QoreError("QORE_CONTEXT_INVALID", `--limit must be 1-100, not "${s}"`);
|
|
6176
|
+
return n;
|
|
6177
|
+
}
|
|
6178
|
+
function withList(s, allowed) {
|
|
6179
|
+
if (!s) return /* @__PURE__ */ new Set();
|
|
6180
|
+
const parts = s.split(",").map((p) => p.trim()).filter(Boolean);
|
|
6181
|
+
const bad = parts.filter((p) => !allowed.includes(p));
|
|
6182
|
+
if (bad.length) throw new QoreError("QORE_CONTEXT_INVALID", `--with accepts ${allowed.join(", ")}; not ${bad.join(", ")}`);
|
|
6183
|
+
return new Set(parts);
|
|
6184
|
+
}
|
|
6185
|
+
function one(flag, value, allowed) {
|
|
6186
|
+
if (!allowed.includes(value)) {
|
|
6187
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `${flag} must be ${allowed.join("|")}, not "${value}"`);
|
|
6188
|
+
}
|
|
6189
|
+
return value;
|
|
6190
|
+
}
|
|
6191
|
+
async function fanOut(oc2, fn) {
|
|
6192
|
+
const out = { results: [], skipped: [] };
|
|
6193
|
+
let last;
|
|
6194
|
+
for (const target of oc2.targets) {
|
|
6195
|
+
try {
|
|
6196
|
+
out.results.push({ target, result: await fn(target) });
|
|
6197
|
+
} catch (e) {
|
|
6198
|
+
if (oc2.targets.length < 2 || !(e instanceof QoreError) || e.serverCode !== NOT_PROVISIONED) throw e;
|
|
6199
|
+
out.skipped.push({ id: target.module.id, slug: target.module.slug });
|
|
6200
|
+
last = e;
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
if (out.results.length === 0 && last) {
|
|
6204
|
+
throw new QoreError("QORE_RESOURCE_NOT_FOUND", "No module of this app reports to Sentry yet", {
|
|
6205
|
+
serverCode: NOT_PROVISIONED,
|
|
6206
|
+
details: { modules: out.skipped.map((m) => m.slug) }
|
|
6207
|
+
});
|
|
6208
|
+
}
|
|
6209
|
+
return out;
|
|
6210
|
+
}
|
|
6211
|
+
var skipWarnings = (f) => f.skipped.length ? ["module_not_provisioned"] : [];
|
|
6212
|
+
var skipJson = (f) => f.skipped.length ? { unprovisionedModules: f.skipped } : {};
|
|
6213
|
+
function printWarnings(io2, w, json2, skipped = []) {
|
|
6214
|
+
if (!json2) for (const l of warningLines(w, skipped.map((m) => m.slug))) io2.err(l);
|
|
6215
|
+
}
|
|
6216
|
+
|
|
6217
|
+
// src/cli/commands/errors.ts
|
|
6218
|
+
var WITHS = ["trace", "logs"];
|
|
6219
|
+
var STATUSES = ["unresolved", "resolved", "all"];
|
|
6220
|
+
var SORTS = ["date", "freq", "new"];
|
|
6221
|
+
var MERGE = {
|
|
6222
|
+
date: (a, b) => b.lastSeen.localeCompare(a.lastSeen),
|
|
6223
|
+
freq: (a, b) => b.count - a.count,
|
|
6224
|
+
new: (a, b) => b.firstSeen.localeCompare(a.firstSeen)
|
|
6225
|
+
};
|
|
6226
|
+
function printDetail(ctx, oc2, d, showIssue) {
|
|
6227
|
+
const { io: io2 } = ctx;
|
|
6228
|
+
printWarnings(io2, d.warnings, false);
|
|
6229
|
+
if (showIssue) for (const l of fence(d.issue.markdown.split("\n"))) io2.out(l);
|
|
6230
|
+
if (d.trace) {
|
|
6231
|
+
io2.out("");
|
|
6232
|
+
io2.out("Trace");
|
|
6233
|
+
for (const l of fence(spanTree(d.trace))) io2.out(l);
|
|
6234
|
+
}
|
|
6235
|
+
if (d.logs) {
|
|
6236
|
+
io2.out("");
|
|
6237
|
+
io2.out(`Logs (${d.logs.length})`);
|
|
6238
|
+
for (const l of fence(logLines(d.logs, oc2.fanOut))) io2.out(l);
|
|
6239
|
+
}
|
|
6240
|
+
}
|
|
6241
|
+
async function interactiveDetail(ctx, oc2, c, id) {
|
|
6242
|
+
const first = await errorDetail(c, id, { env: oc2.env, ...oc2.window }, /* @__PURE__ */ new Set());
|
|
6243
|
+
printDetail(ctx, oc2, first, true);
|
|
6244
|
+
const traceId = first.issue.event.traceId;
|
|
6245
|
+
if (!traceId) return;
|
|
6246
|
+
const scope2 = { env: oc2.env, ...oc2.window };
|
|
6247
|
+
for (; ; ) {
|
|
6248
|
+
const next = await ctx.io.select("Next?", [
|
|
6249
|
+
{ name: `Show trace ${oneLine(traceId).slice(0, 8)}`, value: "trace" },
|
|
6250
|
+
{ name: "Logs of this trace", value: "logs" },
|
|
6251
|
+
{ name: "Done", value: "done" }
|
|
6252
|
+
]);
|
|
6253
|
+
if (next === "done") return;
|
|
6254
|
+
const extra = { issue: first.issue, warnings: [] };
|
|
6255
|
+
if (next === "trace") extra.trace = await c.trace(traceId, scope2);
|
|
6256
|
+
else {
|
|
6257
|
+
const p = await c.logs({ ...scope2, trace: traceId, limit: 100 });
|
|
6258
|
+
extra.logs = p.items;
|
|
6259
|
+
extra.warnings.push(...pageWarnings(p));
|
|
6260
|
+
}
|
|
6261
|
+
printDetail(ctx, oc2, extra, false);
|
|
6262
|
+
}
|
|
6263
|
+
}
|
|
6264
|
+
function registerErrors(program, ctx) {
|
|
6265
|
+
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) => {
|
|
6266
|
+
const { io: io2, deps: deps2 } = ctx;
|
|
6267
|
+
const g = ctx.opts();
|
|
6268
|
+
const withs = withList(flags.with, WITHS);
|
|
6269
|
+
const status2 = one("--status", flags.status ?? "unresolved", STATUSES);
|
|
6270
|
+
const sort = one("--sort", flags.sort ?? "date", SORTS);
|
|
6271
|
+
const limit = limitOf(flags.limit);
|
|
6272
|
+
const oc2 = await obsContext(ctx, flags, "errors", { single: id !== void 0 });
|
|
6273
|
+
const q = { env: oc2.env, ...oc2.window, q: flags.query, limit };
|
|
6274
|
+
if (id) {
|
|
6275
|
+
const c2 = oc2.targets[0].client;
|
|
6276
|
+
if (g.json) {
|
|
6277
|
+
renderJson(io2, { context: oc2.json, ...await errorDetail(c2, id, q, withs) });
|
|
6278
|
+
return;
|
|
6279
|
+
}
|
|
6280
|
+
io2.err(oc2.header);
|
|
6281
|
+
if (io2.interactive && withs.size === 0) {
|
|
6282
|
+
await interactiveDetail(ctx, oc2, c2, id);
|
|
6283
|
+
return;
|
|
6284
|
+
}
|
|
6285
|
+
printDetail(ctx, oc2, await errorDetail(c2, id, q, withs), true);
|
|
6286
|
+
return;
|
|
6287
|
+
}
|
|
6288
|
+
const f = await fanOut(oc2, (t) => t.client.issues({ ...q, status: status2, sort }));
|
|
6289
|
+
const pages = f.results.map((r) => r.result);
|
|
6290
|
+
const items = pages.flatMap((p) => p.items);
|
|
6291
|
+
if (pages.length > 1) items.sort(MERGE[sort]);
|
|
6292
|
+
const warnings = [...pageWarnings(...pages), ...skipWarnings(f)];
|
|
6293
|
+
if (g.json) {
|
|
6294
|
+
renderJson(io2, { context: oc2.json, items, warnings, ...skipJson(f) });
|
|
6295
|
+
return;
|
|
6296
|
+
}
|
|
6297
|
+
io2.err(oc2.header);
|
|
6298
|
+
printWarnings(io2, warnings, false, f.skipped);
|
|
6299
|
+
if (items.length === 0) {
|
|
6300
|
+
io2.out(`No ${status2 === "all" ? "" : `${status2} `}errors in ${oc2.env} (${windowText(oc2.window)}).`);
|
|
6301
|
+
return;
|
|
6302
|
+
}
|
|
6303
|
+
for (const l of fence(issuesTable(items, deps2.now(), oc2.fanOut))) io2.out(l);
|
|
6304
|
+
if (!io2.interactive) return;
|
|
6305
|
+
const picked = await io2.select(
|
|
6306
|
+
"Which error to open?",
|
|
6307
|
+
[...items.map((i) => ({ name: `${oneLine(i.shortId)} ${oneLine(i.title)}`.slice(0, 100), value: i })), { name: "(none)", value: null }]
|
|
6308
|
+
);
|
|
6309
|
+
if (!picked) return;
|
|
6310
|
+
const c = oc2.targets.find((t) => t.module.id === picked.module.id)?.client;
|
|
6311
|
+
if (!c) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `module ${picked.module.slug} is not among the queried modules`);
|
|
6312
|
+
await interactiveDetail(ctx, oc2, c, picked.shortId);
|
|
6313
|
+
});
|
|
6314
|
+
}
|
|
6315
|
+
|
|
5209
6316
|
// src/cli/commands/init.ts
|
|
5210
6317
|
import { realpath as realpath11 } from "fs/promises";
|
|
5211
6318
|
|
|
5212
6319
|
// src/core/auth/oidc.ts
|
|
5213
6320
|
import * as oc from "openid-client";
|
|
5214
|
-
var LOGIN_SCOPE = "openid profile email offline_access qore:read qore:local-env";
|
|
6321
|
+
var LOGIN_SCOPE = "openid profile email offline_access qore:read qore:local-env qore:observability";
|
|
5215
6322
|
function decodeSub(accessToken) {
|
|
5216
6323
|
const part = accessToken.split(".")[1] ?? "";
|
|
5217
6324
|
const claims = JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
|
|
@@ -5262,9 +6369,9 @@ async function createOidc(opts) {
|
|
|
5262
6369
|
throw mapOidcError(e, host);
|
|
5263
6370
|
}
|
|
5264
6371
|
return {
|
|
5265
|
-
async startDevice(
|
|
6372
|
+
async startDevice(scope2) {
|
|
5266
6373
|
try {
|
|
5267
|
-
const r = await oc.initiateDeviceAuthorization(config, { scope });
|
|
6374
|
+
const r = await oc.initiateDeviceAuthorization(config, { scope: scope2 });
|
|
5268
6375
|
return {
|
|
5269
6376
|
userCode: r.user_code,
|
|
5270
6377
|
verificationUri: r.verification_uri,
|
|
@@ -5284,9 +6391,9 @@ async function createOidc(opts) {
|
|
|
5284
6391
|
throw mapOidcError(e, host);
|
|
5285
6392
|
}
|
|
5286
6393
|
},
|
|
5287
|
-
async refresh(rt,
|
|
6394
|
+
async refresh(rt, scope2) {
|
|
5288
6395
|
try {
|
|
5289
|
-
const t = await oc.refreshTokenGrant(config, rt,
|
|
6396
|
+
const t = await oc.refreshTokenGrant(config, rt, scope2 ? { scope: scope2 } : void 0);
|
|
5290
6397
|
return toTokenSet(t, rt);
|
|
5291
6398
|
} catch (e) {
|
|
5292
6399
|
throw mapOidcError(e, host);
|
|
@@ -5538,6 +6645,34 @@ function registerInit(program, ctx) {
|
|
|
5538
6645
|
});
|
|
5539
6646
|
}
|
|
5540
6647
|
|
|
6648
|
+
// src/cli/commands/logs.ts
|
|
6649
|
+
var LEVELS = ["error", "warn", "info", "debug"];
|
|
6650
|
+
function registerLogs(program, ctx) {
|
|
6651
|
+
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) => {
|
|
6652
|
+
const { io: io2 } = ctx;
|
|
6653
|
+
const g = ctx.opts();
|
|
6654
|
+
const limit = limitOf(flags.limit);
|
|
6655
|
+
const level = flags.level === void 0 ? void 0 : one("--level", flags.level, LEVELS);
|
|
6656
|
+
const oc2 = await obsContext(ctx, flags, "logs", { single: false });
|
|
6657
|
+
const q = { env: oc2.env, ...oc2.window, q: flags.query, limit, level, trace: flags.trace };
|
|
6658
|
+
const f = await fanOut(oc2, (t) => t.client.logs(q));
|
|
6659
|
+
const pages = f.results.map((r) => r.result);
|
|
6660
|
+
const items = pages.flatMap((p) => p.items).sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
6661
|
+
const warnings = [...pageWarnings(...pages), ...skipWarnings(f)];
|
|
6662
|
+
if (g.json) {
|
|
6663
|
+
renderJson(io2, { context: oc2.json, items, warnings, ...skipJson(f) });
|
|
6664
|
+
return;
|
|
6665
|
+
}
|
|
6666
|
+
io2.err(oc2.header);
|
|
6667
|
+
printWarnings(io2, warnings, false, f.skipped);
|
|
6668
|
+
if (items.length === 0) {
|
|
6669
|
+
io2.out(`No logs in ${oc2.env} (${windowText(oc2.window)}).`);
|
|
6670
|
+
return;
|
|
6671
|
+
}
|
|
6672
|
+
for (const l of fence(logLines(items, oc2.fanOut))) io2.out(l);
|
|
6673
|
+
});
|
|
6674
|
+
}
|
|
6675
|
+
|
|
5541
6676
|
// src/cli/commands/logout.ts
|
|
5542
6677
|
function registerLogout(program, ctx) {
|
|
5543
6678
|
program.command("logout").description("End the CLI session").option("--all", "log out of every portal").action(async (cmd) => {
|
|
@@ -5570,26 +6705,6 @@ function registerLogout(program, ctx) {
|
|
|
5570
6705
|
});
|
|
5571
6706
|
}
|
|
5572
6707
|
|
|
5573
|
-
// src/cli/format.ts
|
|
5574
|
-
function ago(iso, now) {
|
|
5575
|
-
if (!iso) return "\u2013";
|
|
5576
|
-
const s = Math.max(0, Math.round((now.getTime() - new Date(iso).getTime()) / 1e3));
|
|
5577
|
-
if (s < 60) return "just now";
|
|
5578
|
-
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
5579
|
-
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
|
|
5580
|
-
return `${Math.floor(s / 86400)}d ago`;
|
|
5581
|
-
}
|
|
5582
|
-
var pct = (f) => String(Number((f * 100).toFixed(2)));
|
|
5583
|
-
function uptime(h) {
|
|
5584
|
-
if (!h) return "\u2013";
|
|
5585
|
-
const kind = h.cause && h.kind !== "up" ? `${h.kind}: ${h.cause}` : h.kind;
|
|
5586
|
-
return h.uptime24h === null ? kind : `${kind} (${pct(h.uptime24h)}% 24h)`;
|
|
5587
|
-
}
|
|
5588
|
-
function table(rows) {
|
|
5589
|
-
const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length)));
|
|
5590
|
-
return rows.map((r) => " " + r.map((c, i) => i === r.length - 1 ? c : c.padEnd(widths[i])).join(" "));
|
|
5591
|
-
}
|
|
5592
|
-
|
|
5593
6708
|
// src/cli/commands/status.ts
|
|
5594
6709
|
var HEAD = ["ENV", "DEPLOY", "COMMIT", "DEPLOYED", "APP HEALTH"];
|
|
5595
6710
|
var envRow = (e, now) => [e.name, e.paused ? "paused" : e.status, e.commit?.sha ?? "\u2013", ago(e.commit?.deployedAt ?? null, now), uptime(e.health)];
|
|
@@ -5631,7 +6746,7 @@ async function appStatus(ctx, ws, appFlag) {
|
|
|
5631
6746
|
}
|
|
5632
6747
|
function registerStatus(program, ctx) {
|
|
5633
6748
|
program.command("status").description("Deployment and health per environment for this module (or, in a workspace root, the app)").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "the app or module, from anywhere (see qore init)").action(async (flags) => {
|
|
5634
|
-
const c = await projectCtx(ctx, { ref: flags.project, module: flags.module, app: flags.app, needModule: false });
|
|
6749
|
+
const c = await projectCtx(ctx, { ref: flags.project, module: flags.module, app: flags.app, needModule: false, command: "status" });
|
|
5635
6750
|
const { deps: deps2, io: io2 } = c;
|
|
5636
6751
|
const g = c.opts();
|
|
5637
6752
|
if (!flags.module) {
|
|
@@ -5664,7 +6779,7 @@ function registerStatus(program, ctx) {
|
|
|
5664
6779
|
}
|
|
5665
6780
|
|
|
5666
6781
|
// src/cli/commands/sync.ts
|
|
5667
|
-
import { join as
|
|
6782
|
+
import { join as join26, relative as relative9 } from "path";
|
|
5668
6783
|
import { Option as Option2 } from "commander";
|
|
5669
6784
|
function asFailure2(slug, e) {
|
|
5670
6785
|
if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
|
|
@@ -5696,7 +6811,7 @@ function registerSync(program, ctx) {
|
|
|
5696
6811
|
continue;
|
|
5697
6812
|
}
|
|
5698
6813
|
try {
|
|
5699
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
6814
|
+
dirs[m.slug] = await deps2.git.inspect(join26(ws.root, m.slug));
|
|
5700
6815
|
modulesForPlan.push(m);
|
|
5701
6816
|
} catch (e) {
|
|
5702
6817
|
earlyFailures.push(asFailure2(m.slug, e));
|
|
@@ -5717,6 +6832,109 @@ function registerSync(program, ctx) {
|
|
|
5717
6832
|
});
|
|
5718
6833
|
}
|
|
5719
6834
|
|
|
6835
|
+
// src/cli/commands/traces.ts
|
|
6836
|
+
var WITHS2 = ["logs", "errors"];
|
|
6837
|
+
async function spansOf(c, q, sort, warnings) {
|
|
6838
|
+
try {
|
|
6839
|
+
return await c.spans({ ...q, sort });
|
|
6840
|
+
} catch (e) {
|
|
6841
|
+
if (sort === "-duration" && e instanceof QoreError && e.serverCode === "observability.sort_unsupported") {
|
|
6842
|
+
warnings.push("sort_unavailable");
|
|
6843
|
+
return c.spans(q);
|
|
6844
|
+
}
|
|
6845
|
+
throw e;
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
function registerTraces(program, ctx) {
|
|
6849
|
+
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) => {
|
|
6850
|
+
const { io: io2, deps: deps2 } = ctx;
|
|
6851
|
+
const g = ctx.opts();
|
|
6852
|
+
const withs = withList(flags.with, WITHS2);
|
|
6853
|
+
const slower = flags.slowerThan === void 0 ? void 0 : parseDuration(flags.slowerThan);
|
|
6854
|
+
const limit = limitOf(flags.limit);
|
|
6855
|
+
const oc2 = await obsContext(ctx, flags, "traces", { single: id !== void 0 });
|
|
6856
|
+
const q = { env: oc2.env, ...oc2.window, limit };
|
|
6857
|
+
const scope2 = { env: oc2.env, ...oc2.window };
|
|
6858
|
+
const printLogs = (items2) => {
|
|
6859
|
+
io2.out("");
|
|
6860
|
+
io2.out(`Logs (${items2.length})`);
|
|
6861
|
+
for (const l of fence(logLines(items2, oc2.fanOut))) io2.out(l);
|
|
6862
|
+
};
|
|
6863
|
+
const printErrors = (items2) => {
|
|
6864
|
+
io2.out("");
|
|
6865
|
+
io2.out(`Errors (${items2.length})`);
|
|
6866
|
+
for (const l of fence(issuesTable(items2, deps2.now(), oc2.fanOut))) io2.out(l);
|
|
6867
|
+
};
|
|
6868
|
+
const printTree = (trace2) => {
|
|
6869
|
+
for (const l of fence(spanTree(trace2))) io2.out(l);
|
|
6870
|
+
};
|
|
6871
|
+
const more = async (c2, traceId, w) => {
|
|
6872
|
+
const f2 = { ...scope2, trace: traceId };
|
|
6873
|
+
if (w === "logs") {
|
|
6874
|
+
const p = await c2.logs({ ...f2, limit: 100 });
|
|
6875
|
+
printWarnings(io2, pageWarnings(p), false);
|
|
6876
|
+
printLogs(p.items);
|
|
6877
|
+
} else {
|
|
6878
|
+
const p = await c2.issues({ ...f2, status: "all" });
|
|
6879
|
+
printWarnings(io2, pageWarnings(p), false);
|
|
6880
|
+
printErrors(p.items);
|
|
6881
|
+
}
|
|
6882
|
+
};
|
|
6883
|
+
if (id) {
|
|
6884
|
+
const c2 = oc2.targets[0].client;
|
|
6885
|
+
const b = await traceDetail(c2, id, q, withs);
|
|
6886
|
+
if (g.json) {
|
|
6887
|
+
renderJson(io2, { context: oc2.json, ...b });
|
|
6888
|
+
return;
|
|
6889
|
+
}
|
|
6890
|
+
io2.err(oc2.header);
|
|
6891
|
+
printWarnings(io2, b.warnings, false);
|
|
6892
|
+
printTree(b.trace);
|
|
6893
|
+
if (b.logs) printLogs(b.logs);
|
|
6894
|
+
if (b.errors) printErrors(b.errors);
|
|
6895
|
+
return;
|
|
6896
|
+
}
|
|
6897
|
+
const warnings = [];
|
|
6898
|
+
const listQ = { ...q, q: joinQuery(ROOT_SPAN_FILTER, slower && `span.duration:>${slower}`, flags.query) };
|
|
6899
|
+
const sort = slower ? "-duration" : "-timestamp";
|
|
6900
|
+
const f = await fanOut(oc2, (t) => spansOf(t.client, listQ, sort, warnings));
|
|
6901
|
+
const pages = f.results.map((r) => r.result);
|
|
6902
|
+
warnings.push(...pageWarnings(...pages), ...skipWarnings(f));
|
|
6903
|
+
const items = pages.flatMap((p) => p.items).sort((a, b) => slower ? b.durationMs - a.durationMs : b.timestamp.localeCompare(a.timestamp));
|
|
6904
|
+
const uniq = [...new Set(warnings)];
|
|
6905
|
+
if (g.json) {
|
|
6906
|
+
renderJson(io2, { context: oc2.json, items, warnings: uniq, ...skipJson(f) });
|
|
6907
|
+
return;
|
|
6908
|
+
}
|
|
6909
|
+
io2.err(oc2.header);
|
|
6910
|
+
printWarnings(io2, uniq, false, f.skipped);
|
|
6911
|
+
if (items.length === 0) {
|
|
6912
|
+
io2.out(`No ${slower ? `requests slower than ${slower}` : "traces"} in ${oc2.env} (${windowText(oc2.window)}).`);
|
|
6913
|
+
return;
|
|
6914
|
+
}
|
|
6915
|
+
for (const l of fence(spansTable(items, deps2.now(), oc2.fanOut))) io2.out(l);
|
|
6916
|
+
if (!io2.interactive) return;
|
|
6917
|
+
const picked = await io2.select(
|
|
6918
|
+
"Which trace to open?",
|
|
6919
|
+
[...items.map((s) => ({ name: `${oneLine(s.traceId).slice(0, 8)} ${oneLine(s.transaction ?? s.op)}`, value: s })), { name: "(none)", value: null }]
|
|
6920
|
+
);
|
|
6921
|
+
if (!picked) return;
|
|
6922
|
+
const c = oc2.targets.find((t) => t.module.id === picked.module.id)?.client;
|
|
6923
|
+
if (!c) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `module ${picked.module.slug} is not among the queried modules`);
|
|
6924
|
+
const { trace } = await traceDetail(c, picked.traceId, q, /* @__PURE__ */ new Set());
|
|
6925
|
+
printTree(trace);
|
|
6926
|
+
for (; ; ) {
|
|
6927
|
+
const next = await io2.select("Next?", [
|
|
6928
|
+
{ name: "Logs of this trace", value: "logs" },
|
|
6929
|
+
{ name: "Errors in this trace", value: "errors" },
|
|
6930
|
+
{ name: "Done", value: "done" }
|
|
6931
|
+
]);
|
|
6932
|
+
if (next === "done") return;
|
|
6933
|
+
await more(c, picked.traceId, next);
|
|
6934
|
+
}
|
|
6935
|
+
});
|
|
6936
|
+
}
|
|
6937
|
+
|
|
5720
6938
|
// src/cli/commands/version.ts
|
|
5721
6939
|
function registerVersion(program, ctx) {
|
|
5722
6940
|
program.command("version").description("Show the CLI and portal version").action(async () => {
|
|
@@ -5785,9 +7003,13 @@ var COMMANDS = [
|
|
|
5785
7003
|
registerWhoami,
|
|
5786
7004
|
registerTenant,
|
|
5787
7005
|
registerStatus,
|
|
7006
|
+
registerLogs,
|
|
7007
|
+
registerTraces,
|
|
7008
|
+
registerErrors,
|
|
5788
7009
|
registerEnv,
|
|
5789
7010
|
registerRun,
|
|
5790
7011
|
registerDev,
|
|
7012
|
+
registerDb,
|
|
5791
7013
|
registerClone,
|
|
5792
7014
|
registerSync,
|
|
5793
7015
|
registerVersion,
|
|
@@ -5844,13 +7066,13 @@ function createTelemetry(deps2, o) {
|
|
|
5844
7066
|
},
|
|
5845
7067
|
async end(signal) {
|
|
5846
7068
|
if (!sdk) return { drained: true };
|
|
5847
|
-
const
|
|
7069
|
+
const ms2 = crashed ? flushMs.crash : flushMs.clean;
|
|
5848
7070
|
const work = sdk.then(async (s) => {
|
|
5849
7071
|
if (!s) return true;
|
|
5850
7072
|
if (crashed) s.crash(crashed.e);
|
|
5851
|
-
return s.end(
|
|
7073
|
+
return s.end(ms2);
|
|
5852
7074
|
}).catch(() => false);
|
|
5853
|
-
return { drained: await withDeadline(work,
|
|
7075
|
+
return { drained: await withDeadline(work, ms2, signal) };
|
|
5854
7076
|
}
|
|
5855
7077
|
};
|
|
5856
7078
|
}
|
|
@@ -5871,16 +7093,16 @@ async function start(deps2, argv2, command, load, onScrub) {
|
|
|
5871
7093
|
sdk.start();
|
|
5872
7094
|
return sdk;
|
|
5873
7095
|
}
|
|
5874
|
-
function withDeadline(work,
|
|
7096
|
+
function withDeadline(work, ms2, signal) {
|
|
5875
7097
|
if (signal?.aborted) return Promise.resolve(false);
|
|
5876
|
-
return new Promise((
|
|
7098
|
+
return new Promise((resolve7) => {
|
|
5877
7099
|
const done = (v) => {
|
|
5878
7100
|
clearTimeout(timer);
|
|
5879
7101
|
signal?.removeEventListener("abort", onAbort);
|
|
5880
|
-
|
|
7102
|
+
resolve7(v);
|
|
5881
7103
|
};
|
|
5882
7104
|
const onAbort = () => done(false);
|
|
5883
|
-
const timer = setTimeout(() => done(false),
|
|
7105
|
+
const timer = setTimeout(() => done(false), ms2);
|
|
5884
7106
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
5885
7107
|
work.then(done, () => done(false));
|
|
5886
7108
|
});
|
|
@@ -5928,8 +7150,8 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
|
|
|
5928
7150
|
}
|
|
5929
7151
|
|
|
5930
7152
|
// src/core/config.ts
|
|
5931
|
-
import { readFile as
|
|
5932
|
-
import { join as
|
|
7153
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
7154
|
+
import { join as join27 } from "path";
|
|
5933
7155
|
var empty = () => ({ schemaVersion: 1, profiles: {} });
|
|
5934
7156
|
var ConfigStore = class {
|
|
5935
7157
|
constructor(dir2) {
|
|
@@ -5937,12 +7159,12 @@ var ConfigStore = class {
|
|
|
5937
7159
|
}
|
|
5938
7160
|
dir;
|
|
5939
7161
|
get file() {
|
|
5940
|
-
return
|
|
7162
|
+
return join27(this.dir, "config.json");
|
|
5941
7163
|
}
|
|
5942
7164
|
async read() {
|
|
5943
7165
|
let raw;
|
|
5944
7166
|
try {
|
|
5945
|
-
raw = await
|
|
7167
|
+
raw = await readFile8(this.file, "utf8");
|
|
5946
7168
|
} catch (e) {
|
|
5947
7169
|
if (e.code === "ENOENT") return empty();
|
|
5948
7170
|
throw e;
|
|
@@ -5968,9 +7190,9 @@ var ConfigStore = class {
|
|
|
5968
7190
|
|
|
5969
7191
|
// src/platform/git.ts
|
|
5970
7192
|
import { execFile, execFileSync } from "child_process";
|
|
5971
|
-
import { mkdir as mkdir4, readdir as readdir4, stat as
|
|
7193
|
+
import { mkdir as mkdir4, readdir as readdir4, stat as stat7 } from "fs/promises";
|
|
5972
7194
|
import { homedir } from "os";
|
|
5973
|
-
import { dirname as dirname9, join as
|
|
7195
|
+
import { dirname as dirname9, join as join28 } from "path";
|
|
5974
7196
|
function parseRemotes(stdout) {
|
|
5975
7197
|
const seen = /* @__PURE__ */ new Set();
|
|
5976
7198
|
const out = [];
|
|
@@ -6008,8 +7230,8 @@ function parseStatus(stdout) {
|
|
|
6008
7230
|
}
|
|
6009
7231
|
var configuredSshCommand = /* @__PURE__ */ new Map();
|
|
6010
7232
|
function hasConfiguredSshCommand(repoDir) {
|
|
6011
|
-
const
|
|
6012
|
-
let v = configuredSshCommand.get(
|
|
7233
|
+
const key2 = repoDir ?? "";
|
|
7234
|
+
let v = configuredSshCommand.get(key2);
|
|
6013
7235
|
if (v === void 0) {
|
|
6014
7236
|
const args = repoDir ? ["-C", repoDir, "config", "--get", "core.sshCommand"] : ["config", "--get", "core.sshCommand"];
|
|
6015
7237
|
try {
|
|
@@ -6023,7 +7245,7 @@ function hasConfiguredSshCommand(repoDir) {
|
|
|
6023
7245
|
} catch {
|
|
6024
7246
|
v = false;
|
|
6025
7247
|
}
|
|
6026
|
-
configuredSshCommand.set(
|
|
7248
|
+
configuredSshCommand.set(key2, v);
|
|
6027
7249
|
}
|
|
6028
7250
|
return v;
|
|
6029
7251
|
}
|
|
@@ -6037,7 +7259,7 @@ function gitExecOptions(repoDir) {
|
|
|
6037
7259
|
return { cwd: homedir(), env, windowsHide: true, maxBuffer: 8 * 1024 * 1024 };
|
|
6038
7260
|
}
|
|
6039
7261
|
function runGit(args, repoDir) {
|
|
6040
|
-
return new Promise((
|
|
7262
|
+
return new Promise((resolve7, reject) => {
|
|
6041
7263
|
execFile("git", args, gitExecOptions(repoDir), (err, stdout, stderr) => {
|
|
6042
7264
|
if (err && err.code === "ENOENT") {
|
|
6043
7265
|
reject(new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
|
|
@@ -6046,7 +7268,7 @@ function runGit(args, repoDir) {
|
|
|
6046
7268
|
return;
|
|
6047
7269
|
}
|
|
6048
7270
|
const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
|
|
6049
|
-
|
|
7271
|
+
resolve7({ code, stdout: String(stdout), stderr: String(stderr), err: err ?? void 0 });
|
|
6050
7272
|
});
|
|
6051
7273
|
});
|
|
6052
7274
|
}
|
|
@@ -6084,7 +7306,7 @@ async function status(repoRoot) {
|
|
|
6084
7306
|
}
|
|
6085
7307
|
async function statOrThrow(path) {
|
|
6086
7308
|
try {
|
|
6087
|
-
return await
|
|
7309
|
+
return await stat7(path);
|
|
6088
7310
|
} catch (err) {
|
|
6089
7311
|
if (err.code === "ENOENT") return void 0;
|
|
6090
7312
|
throw new QoreError("QORE_INTERNAL", `could not stat ${path}`, { cause: err });
|
|
@@ -6101,7 +7323,7 @@ async function inspect(dir2) {
|
|
|
6101
7323
|
throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
|
|
6102
7324
|
}
|
|
6103
7325
|
const empty2 = entries.length === 0;
|
|
6104
|
-
const isRepo = await statOrThrow(
|
|
7326
|
+
const isRepo = await statOrThrow(join28(dir2, ".git")) !== void 0;
|
|
6105
7327
|
const dirRemotes = isRepo ? await remotes(dir2) : [];
|
|
6106
7328
|
return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
|
|
6107
7329
|
}
|
|
@@ -6163,13 +7385,13 @@ var OsKeychain = class {
|
|
|
6163
7385
|
|
|
6164
7386
|
// src/platform/lock.ts
|
|
6165
7387
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
6166
|
-
import { join as
|
|
7388
|
+
import { join as join29 } from "path";
|
|
6167
7389
|
import lockfile from "proper-lockfile";
|
|
6168
7390
|
function fileLock(dir2) {
|
|
6169
7391
|
return async (name, fn) => {
|
|
6170
|
-
const locks =
|
|
7392
|
+
const locks = join29(dir2, "locks");
|
|
6171
7393
|
await mkdir5(locks, { recursive: true, mode: 448 });
|
|
6172
|
-
const target =
|
|
7394
|
+
const target = join29(locks, `${name}.lock`);
|
|
6173
7395
|
await writeFile5(target, "", { flag: "a" });
|
|
6174
7396
|
let release;
|
|
6175
7397
|
let compromised;
|
|
@@ -6213,7 +7435,7 @@ var STDERR_DRAIN_MS = 500;
|
|
|
6213
7435
|
function makeSpawner(spawnFn, proc) {
|
|
6214
7436
|
return {
|
|
6215
7437
|
run(cmd, args, env, o) {
|
|
6216
|
-
return new Promise((
|
|
7438
|
+
return new Promise((resolve7, reject) => {
|
|
6217
7439
|
let child;
|
|
6218
7440
|
try {
|
|
6219
7441
|
child = spawnFn(cmd, args, {
|
|
@@ -6247,7 +7469,7 @@ function makeSpawner(spawnFn, proc) {
|
|
|
6247
7469
|
cleanup();
|
|
6248
7470
|
const stderr = child.stderr;
|
|
6249
7471
|
if (!o?.onStderr || !stderr || stderr.readableEnded) {
|
|
6250
|
-
|
|
7472
|
+
resolve7({ code, signal });
|
|
6251
7473
|
return;
|
|
6252
7474
|
}
|
|
6253
7475
|
let settled = false;
|
|
@@ -6255,7 +7477,7 @@ function makeSpawner(spawnFn, proc) {
|
|
|
6255
7477
|
if (settled) return;
|
|
6256
7478
|
settled = true;
|
|
6257
7479
|
clearTimeout(timer);
|
|
6258
|
-
|
|
7480
|
+
resolve7({ code, signal });
|
|
6259
7481
|
};
|
|
6260
7482
|
const timer = setTimeout(finish, STDERR_DRAIN_MS);
|
|
6261
7483
|
timer.unref?.();
|
|
@@ -6310,13 +7532,13 @@ var systemLauncher = {
|
|
|
6310
7532
|
child.stdout?.on("data", (c) => out.push(c));
|
|
6311
7533
|
child.stderr?.on("data", (c) => err.push(c));
|
|
6312
7534
|
let closed = false;
|
|
6313
|
-
const exited = new Promise((
|
|
6314
|
-
child.once("error", (e) =>
|
|
7535
|
+
const exited = new Promise((resolve7) => {
|
|
7536
|
+
child.once("error", (e) => resolve7({ code: null, signal: null, spawnError: e.code ?? "error" }));
|
|
6315
7537
|
child.once("close", (code, signal) => {
|
|
6316
7538
|
closed = true;
|
|
6317
7539
|
out.flush();
|
|
6318
7540
|
err.flush();
|
|
6319
|
-
|
|
7541
|
+
resolve7({ code, signal });
|
|
6320
7542
|
});
|
|
6321
7543
|
});
|
|
6322
7544
|
return {
|
|
@@ -6362,11 +7584,11 @@ var systemLauncher = {
|
|
|
6362
7584
|
import { execFile as execFile2 } from "child_process";
|
|
6363
7585
|
import { connect } from "net";
|
|
6364
7586
|
function accepts(host, port) {
|
|
6365
|
-
return new Promise((
|
|
7587
|
+
return new Promise((resolve7) => {
|
|
6366
7588
|
const s = connect({ host, port });
|
|
6367
7589
|
const done = (v) => {
|
|
6368
7590
|
s.destroy();
|
|
6369
|
-
|
|
7591
|
+
resolve7(v);
|
|
6370
7592
|
};
|
|
6371
7593
|
s.setTimeout(500, () => done(false));
|
|
6372
7594
|
s.once("connect", () => done(true));
|
|
@@ -6374,12 +7596,12 @@ function accepts(host, port) {
|
|
|
6374
7596
|
});
|
|
6375
7597
|
}
|
|
6376
7598
|
function run2(cmd, args) {
|
|
6377
|
-
return new Promise((
|
|
7599
|
+
return new Promise((resolve7) => {
|
|
6378
7600
|
execFile2(
|
|
6379
7601
|
cmd,
|
|
6380
7602
|
args,
|
|
6381
7603
|
{ timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
|
|
6382
|
-
(e, stdout) =>
|
|
7604
|
+
(e, stdout) => resolve7(e && !stdout ? null : String(stdout))
|
|
6383
7605
|
);
|
|
6384
7606
|
});
|
|
6385
7607
|
}
|
|
@@ -6442,6 +7664,26 @@ var systemPorts = {
|
|
|
6442
7664
|
}
|
|
6443
7665
|
};
|
|
6444
7666
|
|
|
7667
|
+
// src/platform/postgres.ts
|
|
7668
|
+
import pg from "pg";
|
|
7669
|
+
async function connectPostgres(url, o) {
|
|
7670
|
+
const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: o.timeoutMs });
|
|
7671
|
+
client.on("error", () => {
|
|
7672
|
+
});
|
|
7673
|
+
try {
|
|
7674
|
+
await client.connect();
|
|
7675
|
+
await client.query("set default_transaction_read_only = on");
|
|
7676
|
+
} catch (e) {
|
|
7677
|
+
await client.end().catch(() => {
|
|
7678
|
+
});
|
|
7679
|
+
throw e;
|
|
7680
|
+
}
|
|
7681
|
+
return {
|
|
7682
|
+
query: async (sql, params) => ({ rows: (await client.query(sql, params)).rows }),
|
|
7683
|
+
close: () => client.end()
|
|
7684
|
+
};
|
|
7685
|
+
}
|
|
7686
|
+
|
|
6445
7687
|
// src/buildInfo.ts
|
|
6446
7688
|
var SENTRY_DSN = true ? "https://50b592efb53dbc1a31cb10d3945c6d52@o4512129906049024.ingest.de.sentry.io/4512174341750864" : "";
|
|
6447
7689
|
var SENTRY_ENVIRONMENT = true ? "production" : "development";
|
|
@@ -6451,7 +7693,7 @@ import { execFile as execFile3 } from "child_process";
|
|
|
6451
7693
|
|
|
6452
7694
|
// src/core/diagnostics/proxy.ts
|
|
6453
7695
|
function parseScutilProxy(stdout) {
|
|
6454
|
-
const value = (
|
|
7696
|
+
const value = (key2) => stdout.match(new RegExp(`^\\s*${key2}\\s*:\\s*(.+)$`, "m"))?.[1]?.trim();
|
|
6455
7697
|
if (value("HTTPSEnable") === "1") {
|
|
6456
7698
|
const host = value("HTTPSProxy");
|
|
6457
7699
|
const port = value("HTTPSPort");
|
|
@@ -6472,12 +7714,12 @@ function parseRegProxy(stdout) {
|
|
|
6472
7714
|
|
|
6473
7715
|
// src/platform/proxy.ts
|
|
6474
7716
|
function run3(cmd, args) {
|
|
6475
|
-
return new Promise((
|
|
7717
|
+
return new Promise((resolve7) => {
|
|
6476
7718
|
execFile3(
|
|
6477
7719
|
cmd,
|
|
6478
7720
|
args,
|
|
6479
7721
|
{ timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
|
|
6480
|
-
(e, stdout) =>
|
|
7722
|
+
(e, stdout) => resolve7(e && !stdout ? null : String(stdout))
|
|
6481
7723
|
);
|
|
6482
7724
|
});
|
|
6483
7725
|
}
|
|
@@ -6488,10 +7730,10 @@ async function detect(platform) {
|
|
|
6488
7730
|
return out ? parseScutilProxy(out) : null;
|
|
6489
7731
|
}
|
|
6490
7732
|
if (platform === "win32") {
|
|
6491
|
-
const
|
|
7733
|
+
const key2 = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
|
|
6492
7734
|
const [enable, server] = await Promise.all([
|
|
6493
|
-
run3("reg", ["query",
|
|
6494
|
-
run3("reg", ["query",
|
|
7735
|
+
run3("reg", ["query", key2, "/v", "ProxyEnable"]),
|
|
7736
|
+
run3("reg", ["query", key2, "/v", "ProxyServer"])
|
|
6495
7737
|
]);
|
|
6496
7738
|
const combined = [enable, server].filter(Boolean).join("\n");
|
|
6497
7739
|
return combined ? parseRegProxy(combined) : null;
|
|
@@ -6508,7 +7750,7 @@ function systemProxy(platform = process.platform) {
|
|
|
6508
7750
|
}
|
|
6509
7751
|
|
|
6510
7752
|
// src/version.ts
|
|
6511
|
-
var VERSION = true ? "0.1.
|
|
7753
|
+
var VERSION = true ? "0.1.8" : "0.0.0-dev";
|
|
6512
7754
|
|
|
6513
7755
|
// src/cli.ts
|
|
6514
7756
|
if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
@@ -6556,7 +7798,9 @@ var deps = {
|
|
|
6556
7798
|
},
|
|
6557
7799
|
sentryDsn: SENTRY_DSN,
|
|
6558
7800
|
sentryEnvironment: SENTRY_ENVIRONMENT,
|
|
6559
|
-
systemProxy: () => systemProxy(process.platform)
|
|
7801
|
+
systemProxy: () => systemProxy(process.platform),
|
|
7802
|
+
viewerDir: fileURLToPath(new URL("../viewer/dist/", import.meta.url)),
|
|
7803
|
+
postgres: connectPostgres
|
|
6560
7804
|
};
|
|
6561
7805
|
var termination = preflightTermination(() => ac.abort(), deps.raise);
|
|
6562
7806
|
var io = processIo(isInteractive({
|
|
@@ -6586,4 +7830,4 @@ if (!drained) {
|
|
|
6586
7830
|
process.exit();
|
|
6587
7831
|
}
|
|
6588
7832
|
//# sourceMappingURL=cli.js.map
|
|
6589
|
-
//# debugId=
|
|
7833
|
+
//# debugId=c4bda056-b13b-52b5-831f-2afd3f6c6867
|