@quatronic/qore 0.1.7 → 0.1.9
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 +83 -2
- package/dist/cli.js +1947 -1142
- package/package.json +24 -5
- 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]="f7b490f6-b0af-5874-9ffa-f8010e3ba137")}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
|
}
|
|
@@ -654,7 +655,7 @@ var QoreApiClient = class {
|
|
|
654
655
|
this.o = o;
|
|
655
656
|
}
|
|
656
657
|
o;
|
|
657
|
-
async once(method,
|
|
658
|
+
async once(method, path2, body, force) {
|
|
658
659
|
const token = await this.o.token(force);
|
|
659
660
|
const headers = {
|
|
660
661
|
authorization: `Bearer ${token}`,
|
|
@@ -666,7 +667,7 @@ var QoreApiClient = class {
|
|
|
666
667
|
if (body !== void 0) headers["content-type"] = "application/json";
|
|
667
668
|
let res;
|
|
668
669
|
try {
|
|
669
|
-
res = await this.o.fetch(`${this.o.apiOrigin}${
|
|
670
|
+
res = await this.o.fetch(`${this.o.apiOrigin}${path2}`, {
|
|
670
671
|
method,
|
|
671
672
|
headers,
|
|
672
673
|
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
@@ -675,13 +676,13 @@ var QoreApiClient = class {
|
|
|
675
676
|
throw fetchError(e, new URL(this.o.apiOrigin).host);
|
|
676
677
|
}
|
|
677
678
|
const rid = res.headers.get("x-request-id");
|
|
678
|
-
this.o.onRequest?.(`${method} ${
|
|
679
|
+
this.o.onRequest?.(`${method} ${path2} \u2192 ${res.status}${rid ? ` (${rid})` : ""}`);
|
|
679
680
|
return res;
|
|
680
681
|
}
|
|
681
|
-
async request(method,
|
|
682
|
-
let res = await this.once(method,
|
|
682
|
+
async request(method, path2, body, parse2 = (r) => r.json()) {
|
|
683
|
+
let res = await this.once(method, path2, body, false);
|
|
683
684
|
if (res.status === 401) {
|
|
684
|
-
res = await this.once(method,
|
|
685
|
+
res = await this.once(method, path2, body, true);
|
|
685
686
|
if (res.status === 401) {
|
|
686
687
|
const skew = clockSkewSeconds(res.headers.get("date"), (this.o.now ?? (() => /* @__PURE__ */ new Date()))());
|
|
687
688
|
if (skew !== null && Math.abs(skew) > 60) {
|
|
@@ -702,14 +703,14 @@ var QoreApiClient = class {
|
|
|
702
703
|
if (!res.ok) throw await problemToError(res);
|
|
703
704
|
return parse2(res);
|
|
704
705
|
}
|
|
705
|
-
get(
|
|
706
|
-
return this.request("GET",
|
|
706
|
+
get(path2) {
|
|
707
|
+
return this.request("GET", path2);
|
|
707
708
|
}
|
|
708
|
-
getText(
|
|
709
|
-
return this.request("GET",
|
|
709
|
+
getText(path2) {
|
|
710
|
+
return this.request("GET", path2, void 0, (r) => r.text());
|
|
710
711
|
}
|
|
711
|
-
post(
|
|
712
|
-
return this.request("POST",
|
|
712
|
+
post(path2, body) {
|
|
713
|
+
return this.request("POST", path2, body);
|
|
713
714
|
}
|
|
714
715
|
};
|
|
715
716
|
|
|
@@ -930,23 +931,23 @@ var ResolutionCache = class {
|
|
|
930
931
|
fresh(e) {
|
|
931
932
|
return !!e && this.now().getTime() - new Date(e.resolvedAt).getTime() < TTL_MS2;
|
|
932
933
|
}
|
|
933
|
-
async get(origin,
|
|
934
|
-
const e = (await this.load()).entries[`${origin}|${
|
|
934
|
+
async get(origin, key2) {
|
|
935
|
+
const e = (await this.load()).entries[`${origin}|${key2}`];
|
|
935
936
|
return this.fresh(e) ? e.moduleId : null;
|
|
936
937
|
}
|
|
937
938
|
/** Never throws: a read-only config dir, a full disk or a Windows EPERM on rename
|
|
938
939
|
* (another qore holding the file) must not fail a command that already resolved. */
|
|
939
|
-
async put(origin,
|
|
940
|
+
async put(origin, key2, moduleId) {
|
|
940
941
|
const f = await this.load();
|
|
941
|
-
f.entries[`${origin}|${
|
|
942
|
+
f.entries[`${origin}|${key2}`] = { moduleId, resolvedAt: this.now().toISOString() };
|
|
942
943
|
for (const [k, e] of Object.entries(f.entries)) if (!this.fresh(e)) delete f.entries[k];
|
|
943
944
|
await this.save(f);
|
|
944
945
|
}
|
|
945
946
|
/** Never throws, like `put`. */
|
|
946
|
-
async drop(origin,
|
|
947
|
+
async drop(origin, key2) {
|
|
947
948
|
const f = await this.load();
|
|
948
|
-
if (!(`${origin}|${
|
|
949
|
-
delete f.entries[`${origin}|${
|
|
949
|
+
if (!(`${origin}|${key2}` in f.entries)) return;
|
|
950
|
+
delete f.entries[`${origin}|${key2}`];
|
|
950
951
|
await this.save(f);
|
|
951
952
|
}
|
|
952
953
|
async save(f) {
|
|
@@ -959,8 +960,8 @@ var ResolutionCache = class {
|
|
|
959
960
|
const f = await this.load();
|
|
960
961
|
const hits = [];
|
|
961
962
|
for (const [k, e] of Object.entries(f.entries)) {
|
|
962
|
-
const
|
|
963
|
-
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() });
|
|
964
965
|
}
|
|
965
966
|
hits.sort((a, b) => b.at - a.at);
|
|
966
967
|
return [...new Set(hits.map((h) => h.origin))];
|
|
@@ -977,7 +978,7 @@ function repoKey(url) {
|
|
|
977
978
|
if (raw.startsWith("git+")) raw = raw.slice(4);
|
|
978
979
|
if (!raw || raw.includes("\\")) return null;
|
|
979
980
|
let host;
|
|
980
|
-
let
|
|
981
|
+
let path2;
|
|
981
982
|
if (raw.includes("://")) {
|
|
982
983
|
if (hasDotSegment(raw.slice(raw.indexOf("://") + 3).split("/").slice(1))) return null;
|
|
983
984
|
let u;
|
|
@@ -988,14 +989,14 @@ function repoKey(url) {
|
|
|
988
989
|
}
|
|
989
990
|
if (!SCHEMES.has(u.protocol.toLowerCase())) return null;
|
|
990
991
|
host = u.hostname;
|
|
991
|
-
|
|
992
|
+
path2 = u.pathname;
|
|
992
993
|
} else {
|
|
993
994
|
const m = SCP.exec(raw);
|
|
994
995
|
if (!m) return null;
|
|
995
996
|
host = m[1];
|
|
996
|
-
|
|
997
|
+
path2 = m[2];
|
|
997
998
|
}
|
|
998
|
-
const segments =
|
|
999
|
+
const segments = path2.split("/").filter(Boolean);
|
|
999
1000
|
if (!host || segments.length !== 2 || hasDotSegment(segments)) return null;
|
|
1000
1001
|
const owner = segments[0];
|
|
1001
1002
|
let repo = segments[1];
|
|
@@ -1023,12 +1024,12 @@ async function isTrusted(deps2, root, moduleId) {
|
|
|
1023
1024
|
return (cfg.trustedRepos?.[await keyOf(deps2, root)] ?? []).includes(moduleId);
|
|
1024
1025
|
}
|
|
1025
1026
|
async function trust(deps2, root, moduleId) {
|
|
1026
|
-
const
|
|
1027
|
+
const key2 = await keyOf(deps2, root);
|
|
1027
1028
|
await deps2.config.update((c) => {
|
|
1028
1029
|
const repos = c.trustedRepos ??= {};
|
|
1029
|
-
const list = new Set(repos[
|
|
1030
|
+
const list = new Set(repos[key2] ?? []);
|
|
1030
1031
|
list.add(moduleId);
|
|
1031
|
-
repos[
|
|
1032
|
+
repos[key2] = [...list];
|
|
1032
1033
|
});
|
|
1033
1034
|
}
|
|
1034
1035
|
|
|
@@ -1189,8 +1190,8 @@ async function applyActions(deps2, o) {
|
|
|
1189
1190
|
const r = await deps2.git.clone(url, action.dir);
|
|
1190
1191
|
if (r.ok) {
|
|
1191
1192
|
await trust(deps2, action.dir, action.module.id);
|
|
1192
|
-
const
|
|
1193
|
-
if (
|
|
1193
|
+
const key2 = repoKey(action.module.repoUrl);
|
|
1194
|
+
if (key2) await cache.put(o.portal.apiOrigin, key2, action.module.id);
|
|
1194
1195
|
result = { slug: action.module.slug, status: "cloned", dir: action.dir };
|
|
1195
1196
|
} else {
|
|
1196
1197
|
const err = classifyCloneFailure(r.stderr, action.module, o.app, o.protocol);
|
|
@@ -1358,10 +1359,10 @@ async function scanRoot(root) {
|
|
|
1358
1359
|
const out = [];
|
|
1359
1360
|
for (const tenant of (await subdirs(root)).sort()) {
|
|
1360
1361
|
for (const app of (await subdirs(join6(root, tenant))).sort()) {
|
|
1361
|
-
const
|
|
1362
|
+
const path2 = join6(root, tenant, app);
|
|
1362
1363
|
try {
|
|
1363
|
-
const ws = await readWorkspace(
|
|
1364
|
-
if (ws) out.push({ kind: "workspace", path, portal: ws.portal, tenantSlug: tenant, appId: ws.appId, appSlug: app });
|
|
1364
|
+
const ws = await readWorkspace(path2);
|
|
1365
|
+
if (ws) out.push({ kind: "workspace", path: path2, portal: ws.portal, tenantSlug: tenant, appId: ws.appId, appSlug: app });
|
|
1365
1366
|
} catch {
|
|
1366
1367
|
}
|
|
1367
1368
|
}
|
|
@@ -1435,12 +1436,12 @@ function expandPath(deps2, input2) {
|
|
|
1435
1436
|
}
|
|
1436
1437
|
function trimSeparator(p) {
|
|
1437
1438
|
const root = parse(p).root;
|
|
1438
|
-
let
|
|
1439
|
-
while (
|
|
1440
|
-
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);
|
|
1441
1442
|
}
|
|
1442
|
-
async function underAFile(
|
|
1443
|
-
for (let p = dirname3(
|
|
1443
|
+
async function underAFile(path2) {
|
|
1444
|
+
for (let p = dirname3(path2); ; p = dirname3(p)) {
|
|
1444
1445
|
const s = await stat2(p).catch((e) => e.code === "ENOENT" ? void 0 : null);
|
|
1445
1446
|
if (s === null) return false;
|
|
1446
1447
|
if (s) return !s.isDirectory();
|
|
@@ -1455,8 +1456,8 @@ function scannedBy(root, e) {
|
|
|
1455
1456
|
if (e.kind !== "workspace" || !isInside(root, e.path)) return false;
|
|
1456
1457
|
return relative2(root, e.path).split(sep).filter(Boolean).length === 2;
|
|
1457
1458
|
}
|
|
1458
|
-
async function writableError(
|
|
1459
|
-
const probe = join8(
|
|
1459
|
+
async function writableError(path2) {
|
|
1460
|
+
const probe = join8(path2, `.qore-write-test-${randomBytes3(6).toString("hex")}`);
|
|
1460
1461
|
try {
|
|
1461
1462
|
await writeFile3(probe, "");
|
|
1462
1463
|
await rm2(probe, { force: true });
|
|
@@ -1465,42 +1466,42 @@ async function writableError(path) {
|
|
|
1465
1466
|
return e;
|
|
1466
1467
|
}
|
|
1467
1468
|
}
|
|
1468
|
-
var invalid2 = (
|
|
1469
|
-
var notWritable = (
|
|
1469
|
+
var invalid2 = (path2, why) => new QoreError("QORE_ROOT_INVALID", `${path2} cannot be the projects root: ${why}`, { hint: "Pick another folder: qore init --root <path>." });
|
|
1470
|
+
var notWritable = (path2, e) => new QoreError("QORE_ROOT_NOT_WRITABLE", `${path2} is not writable (${e.code ?? e.message})`, {
|
|
1470
1471
|
hint: "Fix its permissions, or pick another folder: qore init --root <path>.",
|
|
1471
1472
|
cause: e
|
|
1472
1473
|
});
|
|
1473
1474
|
async function checkRoot(deps2, input2) {
|
|
1474
|
-
const
|
|
1475
|
-
if (dirname3(
|
|
1475
|
+
const path2 = expandPath(deps2, input2);
|
|
1476
|
+
if (dirname3(path2) === path2) throw invalid2(path2, "it is the filesystem root");
|
|
1476
1477
|
try {
|
|
1477
|
-
if (!(await stat2(
|
|
1478
|
+
if (!(await stat2(path2)).isDirectory()) throw invalid2(path2, "it is a file");
|
|
1478
1479
|
} catch (e) {
|
|
1479
1480
|
if (e instanceof QoreError) throw e;
|
|
1480
1481
|
const err = e;
|
|
1481
|
-
if (err.code === "ENOTDIR") throw invalid2(
|
|
1482
|
-
if (NOT_WRITABLE.has(err.code ?? "")) throw notWritable(
|
|
1482
|
+
if (err.code === "ENOTDIR") throw invalid2(path2, "a parent of it is a file");
|
|
1483
|
+
if (NOT_WRITABLE.has(err.code ?? "")) throw notWritable(path2, err);
|
|
1483
1484
|
if (err.code !== "ENOENT") throw e;
|
|
1484
|
-
if (await underAFile(
|
|
1485
|
+
if (await underAFile(path2)) throw invalid2(path2, "a parent of it is a file");
|
|
1485
1486
|
}
|
|
1486
|
-
const repo = await findRepoRoot(
|
|
1487
|
-
if (await findWorkspace(
|
|
1488
|
-
throw invalid2(
|
|
1487
|
+
const repo = await findRepoRoot(path2);
|
|
1488
|
+
if (await findWorkspace(path2, deps2.home) || repo && !isInside(repo, deps2.home)) {
|
|
1489
|
+
throw invalid2(path2, "it lies inside a project (an app workspace or a git repository)");
|
|
1489
1490
|
}
|
|
1490
|
-
return
|
|
1491
|
+
return path2;
|
|
1491
1492
|
}
|
|
1492
|
-
async function createRoot(
|
|
1493
|
+
async function createRoot(path2) {
|
|
1493
1494
|
try {
|
|
1494
|
-
await mkdir3(
|
|
1495
|
+
await mkdir3(path2, { recursive: true });
|
|
1495
1496
|
} catch (e) {
|
|
1496
1497
|
const err2 = e;
|
|
1497
|
-
if (NOT_WRITABLE.has(err2.code ?? "")) throw notWritable(
|
|
1498
|
-
if (err2.code === "ENOTDIR" || err2.code === "EEXIST") throw invalid2(
|
|
1498
|
+
if (NOT_WRITABLE.has(err2.code ?? "")) throw notWritable(path2, err2);
|
|
1499
|
+
if (err2.code === "ENOTDIR" || err2.code === "EEXIST") throw invalid2(path2, "a parent of it is a file");
|
|
1499
1500
|
throw e;
|
|
1500
1501
|
}
|
|
1501
|
-
const err = await writableError(
|
|
1502
|
-
if (err) throw notWritable(
|
|
1503
|
-
return realpath3(
|
|
1502
|
+
const err = await writableError(path2);
|
|
1503
|
+
if (err) throw notWritable(path2, err);
|
|
1504
|
+
return realpath3(path2);
|
|
1504
1505
|
}
|
|
1505
1506
|
async function validateRoot(deps2, input2) {
|
|
1506
1507
|
return createRoot(await checkRoot(deps2, input2));
|
|
@@ -1518,15 +1519,15 @@ async function quietly(fn) {
|
|
|
1518
1519
|
}
|
|
1519
1520
|
async function remember(deps2, e) {
|
|
1520
1521
|
await quietly(async () => {
|
|
1521
|
-
const
|
|
1522
|
-
const entry = { ...e, path };
|
|
1522
|
+
const path2 = await realpath4(e.path);
|
|
1523
|
+
const entry = { ...e, path: path2 };
|
|
1523
1524
|
const cfg = await deps2.config.read();
|
|
1524
1525
|
const root = cfg.projectsRoot ? await realpath4(cfg.projectsRoot).catch(() => cfg.projectsRoot) : void 0;
|
|
1525
1526
|
if (root && scannedBy(root, entry)) return;
|
|
1526
|
-
const current = cfg.projects?.find((p) => p.path ===
|
|
1527
|
+
const current = cfg.projects?.find((p) => p.path === path2);
|
|
1527
1528
|
if (current && same(current, entry)) return;
|
|
1528
1529
|
await deps2.withLock(LOCK, () => deps2.config.update((c) => {
|
|
1529
|
-
c.projects = [...(c.projects ?? []).filter((p) => p.path !==
|
|
1530
|
+
c.projects = [...(c.projects ?? []).filter((p) => p.path !== path2), entry];
|
|
1530
1531
|
}).then(() => void 0));
|
|
1531
1532
|
});
|
|
1532
1533
|
}
|
|
@@ -1824,12 +1825,12 @@ function registerConfig(program, ctx) {
|
|
|
1824
1825
|
io2.out(describeConsent(consent, built));
|
|
1825
1826
|
if (state && consent.source !== "config") io2.err(`${consent.source} in your environment overrides this setting.`);
|
|
1826
1827
|
});
|
|
1827
|
-
config.command("root").description("Show or set the projects folder qore clones into (see qore init)").argument("[path]", "the new projects folder").option("--unset", "clear it; projects already there stay findable").action(async (
|
|
1828
|
+
config.command("root").description("Show or set the projects folder qore clones into (see qore init)").argument("[path]", "the new projects folder").option("--unset", "clear it; projects already there stay findable").action(async (path2, f) => {
|
|
1828
1829
|
const { deps: deps2, io: io2 } = ctx;
|
|
1829
|
-
if (
|
|
1830
|
+
if (path2 !== void 0 && f.unset) {
|
|
1830
1831
|
throw new QoreError("QORE_CONTEXT_INVALID", "Pass a path or --unset, not both", { hint: "qore config root <path> | qore config root --unset" });
|
|
1831
1832
|
}
|
|
1832
|
-
if (
|
|
1833
|
+
if (path2 !== void 0) await setRoot(deps2, await validateRoot(deps2, path2));
|
|
1833
1834
|
if (f.unset) await setRoot(deps2, void 0);
|
|
1834
1835
|
const d = await describeRoot(deps2);
|
|
1835
1836
|
if (ctx.opts().json) {
|
|
@@ -1841,82 +1842,13 @@ function registerConfig(program, ctx) {
|
|
|
1841
1842
|
});
|
|
1842
1843
|
}
|
|
1843
1844
|
|
|
1844
|
-
// src/cli/commands/
|
|
1845
|
-
import {
|
|
1846
|
-
import {
|
|
1847
|
-
import {
|
|
1848
|
-
import { dirname as dirname7, join as join21, relative as relative7 } from "path";
|
|
1849
|
-
import { StringDecoder } from "string_decoder";
|
|
1850
|
-
|
|
1851
|
-
// src/core/context/repoConfig.ts
|
|
1852
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
1853
|
-
import { join as join10 } from "path";
|
|
1854
|
-
var FORBIDDEN = ["portal", "api", "apiOrigin", "issuer", "tenant", "tenantId", "app", "appId", "module", "moduleId"];
|
|
1855
|
-
var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1856
|
-
var SCRIPT = /^[A-Za-z0-9:_.-]+$/;
|
|
1857
|
-
var invalid3 = (msg, details) => new QoreError("QORE_CONTEXT_INVALID", `.qore.json: ${msg}`, { hint: "Fix .qore.json in the repository root; see the $schema it names.", details });
|
|
1858
|
-
function moduleDefaults(type) {
|
|
1859
|
-
return type === "frontend" ? { envFile: ".env.local", localSecrets: ["SESSION_COOKIE_SECRET"] } : { envFile: ".env", localSecrets: [] };
|
|
1860
|
-
}
|
|
1861
|
-
async function readRepoConfig(root) {
|
|
1862
|
-
let raw;
|
|
1863
|
-
try {
|
|
1864
|
-
raw = await readFile3(join10(root, ".qore.json"), "utf8");
|
|
1865
|
-
} catch (e) {
|
|
1866
|
-
if (e.code === "ENOENT") return {};
|
|
1867
|
-
throw e;
|
|
1868
|
-
}
|
|
1869
|
-
let doc;
|
|
1870
|
-
try {
|
|
1871
|
-
doc = JSON.parse(raw);
|
|
1872
|
-
} catch {
|
|
1873
|
-
throw invalid3("not valid JSON");
|
|
1874
|
-
}
|
|
1875
|
-
if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw invalid3("must be an object");
|
|
1876
|
-
const v = doc.schemaVersion === void 0 ? 1 : doc.schemaVersion;
|
|
1877
|
-
if (!Number.isInteger(v) || v < 1) throw invalid3("schemaVersion must be a positive integer");
|
|
1878
|
-
if (v > 1) {
|
|
1879
|
-
throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `.qore.json schemaVersion ${v} is newer than this qore`, { hint: "Run: npm i -g @quatronic/qore" });
|
|
1880
|
-
}
|
|
1881
|
-
for (const f of FORBIDDEN) if (f in doc) throw invalid3(`"${f}" is not allowed \u2014 .qore.json never selects a portal or a resource`, { field: f });
|
|
1882
|
-
const out = {};
|
|
1883
|
-
if (doc.envFile !== void 0) {
|
|
1884
|
-
if (typeof doc.envFile !== "string" || !doc.envFile) throw invalid3("envFile must be a non-empty string");
|
|
1885
|
-
out.envFile = doc.envFile;
|
|
1886
|
-
}
|
|
1887
|
-
if (doc.localSecrets !== void 0) {
|
|
1888
|
-
if (!Array.isArray(doc.localSecrets) || !doc.localSecrets.every((k) => typeof k === "string" && KEY.test(k))) {
|
|
1889
|
-
throw invalid3("localSecrets must be a list of env key names");
|
|
1890
|
-
}
|
|
1891
|
-
if (new Set(doc.localSecrets).size !== doc.localSecrets.length) throw invalid3("localSecrets must not contain duplicates");
|
|
1892
|
-
out.localSecrets = doc.localSecrets;
|
|
1893
|
-
}
|
|
1894
|
-
if (doc.dev !== void 0) {
|
|
1895
|
-
const d = doc.dev;
|
|
1896
|
-
if (!d || typeof d !== "object" || Array.isArray(d)) throw invalid3("dev must be an object");
|
|
1897
|
-
const dev = {};
|
|
1898
|
-
if (d.install !== void 0) {
|
|
1899
|
-
if (typeof d.install !== "boolean") throw invalid3("dev.install must be true or false");
|
|
1900
|
-
dev.install = d.install;
|
|
1901
|
-
}
|
|
1902
|
-
if (d.services !== void 0) {
|
|
1903
|
-
if (!Array.isArray(d.services) || !d.services.every((s) => typeof s === "string" && SCRIPT.test(s))) {
|
|
1904
|
-
throw invalid3("dev.services must be a list of script names");
|
|
1905
|
-
}
|
|
1906
|
-
if (new Set(d.services).size !== d.services.length) throw invalid3("dev.services must not contain duplicates");
|
|
1907
|
-
dev.services = d.services;
|
|
1908
|
-
}
|
|
1909
|
-
if (d.script !== void 0) {
|
|
1910
|
-
if (typeof d.script !== "string" || !SCRIPT.test(d.script)) throw invalid3("dev.script must be a script name");
|
|
1911
|
-
dev.script = d.script;
|
|
1912
|
-
}
|
|
1913
|
-
out.dev = dev;
|
|
1914
|
-
}
|
|
1915
|
-
return out;
|
|
1916
|
-
}
|
|
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";
|
|
1917
1849
|
|
|
1918
1850
|
// src/core/context/resolver.ts
|
|
1919
|
-
import { isAbsolute as isAbsolute2, join as
|
|
1851
|
+
import { isAbsolute as isAbsolute2, join as join10, relative as relative4 } from "path";
|
|
1920
1852
|
var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1921
1853
|
var HINT_FLAG = "cd into a module repository, or pass --module <slug|id>.";
|
|
1922
1854
|
var RESOLVE = "/api/cli/v1/repos/resolve";
|
|
@@ -1954,10 +1886,10 @@ async function flagOverrides(deps2, session, portal, cache, moduleId) {
|
|
|
1954
1886
|
const keyed = (await repoRemotes(deps2))?.keyed ?? [];
|
|
1955
1887
|
const derived = /* @__PURE__ */ new Map();
|
|
1956
1888
|
const uncached = [];
|
|
1957
|
-
for (const
|
|
1958
|
-
const id = await cache.get(portal.apiOrigin,
|
|
1959
|
-
if (id) derived.set(
|
|
1960
|
-
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);
|
|
1961
1893
|
}
|
|
1962
1894
|
if (uncached.length > 0) {
|
|
1963
1895
|
try {
|
|
@@ -2130,7 +2062,7 @@ async function anyLoggedIn(deps2, cfg) {
|
|
|
2130
2062
|
}
|
|
2131
2063
|
var isForeignRepo = (e) => e instanceof QoreError && e.code === "QORE_CONTEXT_REQUIRED" && e.details?.foreignRepo === true;
|
|
2132
2064
|
async function resolveModuleContext(deps2, o) {
|
|
2133
|
-
const cache = new ResolutionCache(
|
|
2065
|
+
const cache = new ResolutionCache(join10(deps2.config.dir, "cache"), deps2.now);
|
|
2134
2066
|
const ws = await findWorkspace(deps2.cwd, deps2.home);
|
|
2135
2067
|
if (o.module) return byFlag(deps2, o, cache, ws);
|
|
2136
2068
|
const cfg = await deps2.config.read();
|
|
@@ -2207,82 +2139,356 @@ function pickApp2(mc, app) {
|
|
|
2207
2139
|
throw new QoreError("QORE_APP_AMBIGUOUS", `${mc.module.slug} is part of several apps`, { hint: "Pass --app <slug>.", candidates: appCandidates(apps) });
|
|
2208
2140
|
}
|
|
2209
2141
|
|
|
2210
|
-
// src/core/
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
return
|
|
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;
|
|
2217
2149
|
}
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
};
|
|
2226
|
-
}
|
|
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: "" };
|
|
2227
2157
|
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
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
|
+
});
|
|
2236
2168
|
}
|
|
2237
|
-
if (
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
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
|
+
});
|
|
2174
|
+
}
|
|
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
|
+
});
|
|
2180
|
+
}
|
|
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
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
|
|
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);
|
|
2249
2198
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
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;
|
|
2256
2214
|
}
|
|
2257
|
-
|
|
2215
|
+
groups.push({ key: m.slug, modules: [m.slug], url: m.url, connection: c });
|
|
2258
2216
|
}
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2217
|
+
return groups;
|
|
2218
|
+
}
|
|
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 });
|
|
2266
2236
|
}
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2237
|
+
return refs;
|
|
2238
|
+
}
|
|
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, ""));
|
|
2274
2317
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2318
|
+
return [];
|
|
2319
|
+
}
|
|
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: []
|
|
2279
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
|
+
});
|
|
2280
2349
|
}
|
|
2281
|
-
|
|
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;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
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
|
+
};
|
|
2282
2385
|
}
|
|
2283
2386
|
|
|
2284
|
-
// src/core/
|
|
2285
|
-
import {
|
|
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;
|
|
2397
|
+
}
|
|
2398
|
+
try {
|
|
2399
|
+
return JSON.parse(text2).entries ?? [];
|
|
2400
|
+
} catch {
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
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;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
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
|
+
}
|
|
2286
2492
|
|
|
2287
2493
|
// src/core/env/fetch.ts
|
|
2288
2494
|
async function fetchLocalEnv(deps2, mc, app) {
|
|
@@ -2316,11 +2522,11 @@ function parseEnv(contents) {
|
|
|
2316
2522
|
}
|
|
2317
2523
|
return map;
|
|
2318
2524
|
}
|
|
2319
|
-
function isManagedKey(
|
|
2525
|
+
function isManagedKey(key2, managed) {
|
|
2320
2526
|
for (const pattern of managed) {
|
|
2321
2527
|
if (pattern.endsWith("*")) {
|
|
2322
|
-
if (
|
|
2323
|
-
} else if (
|
|
2528
|
+
if (key2.startsWith(pattern.slice(0, -1))) return true;
|
|
2529
|
+
} else if (key2 === pattern) return true;
|
|
2324
2530
|
}
|
|
2325
2531
|
return false;
|
|
2326
2532
|
}
|
|
@@ -2336,27 +2542,27 @@ function planEnvUpdate({
|
|
|
2336
2542
|
const added = [];
|
|
2337
2543
|
const unchanged = [];
|
|
2338
2544
|
const overwritten = [];
|
|
2339
|
-
for (const [
|
|
2340
|
-
const hasLocal = current.has(
|
|
2341
|
-
const localValue = current.get(
|
|
2545
|
+
for (const [key2, portalValue] of Object.entries(fetched)) {
|
|
2546
|
+
const hasLocal = current.has(key2);
|
|
2547
|
+
const localValue = current.get(key2);
|
|
2342
2548
|
const localEmpty = !hasLocal || localValue === "" || localValue === '""' || localValue === "''";
|
|
2343
2549
|
if (localEmpty) {
|
|
2344
|
-
merged.set(
|
|
2345
|
-
added.push(
|
|
2550
|
+
merged.set(key2, portalValue);
|
|
2551
|
+
added.push(key2);
|
|
2346
2552
|
continue;
|
|
2347
2553
|
}
|
|
2348
2554
|
if (localValue === portalValue) {
|
|
2349
|
-
unchanged.push(
|
|
2555
|
+
unchanged.push(key2);
|
|
2350
2556
|
continue;
|
|
2351
2557
|
}
|
|
2352
|
-
if (isManagedKey(
|
|
2353
|
-
merged.set(
|
|
2354
|
-
overwritten.push(
|
|
2558
|
+
if (isManagedKey(key2, managed)) {
|
|
2559
|
+
merged.set(key2, portalValue);
|
|
2560
|
+
overwritten.push(key2);
|
|
2355
2561
|
continue;
|
|
2356
2562
|
}
|
|
2357
|
-
conflicts.push(
|
|
2358
|
-
const choice = choices[
|
|
2359
|
-
merged.set(
|
|
2563
|
+
conflicts.push(key2);
|
|
2564
|
+
const choice = choices[key2] ?? defaultChoice;
|
|
2565
|
+
merged.set(key2, choice === "use-portal" ? portalValue : localValue);
|
|
2360
2566
|
}
|
|
2361
2567
|
return { merged, conflicts, added, unchanged, overwritten };
|
|
2362
2568
|
}
|
|
@@ -2366,14 +2572,14 @@ function renderEnv(original, merged) {
|
|
|
2366
2572
|
const out = lines.map((line2) => {
|
|
2367
2573
|
const m = line2.replace(/\r$/, "").match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
2368
2574
|
if (!m?.[1]) return line2;
|
|
2369
|
-
const
|
|
2370
|
-
if (!merged.has(
|
|
2371
|
-
seen.add(
|
|
2372
|
-
return `${
|
|
2575
|
+
const key2 = m[1];
|
|
2576
|
+
if (!merged.has(key2)) return line2;
|
|
2577
|
+
seen.add(key2);
|
|
2578
|
+
return `${key2}=${merged.get(key2) ?? ""}`;
|
|
2373
2579
|
});
|
|
2374
2580
|
const appended = [];
|
|
2375
|
-
for (const [
|
|
2376
|
-
if (!seen.has(
|
|
2581
|
+
for (const [key2, value] of merged) {
|
|
2582
|
+
if (!seen.has(key2)) appended.push(`${key2}=${value}`);
|
|
2377
2583
|
}
|
|
2378
2584
|
let text2 = out.join("\n");
|
|
2379
2585
|
if (appended.length) {
|
|
@@ -2429,11 +2635,11 @@ function planPull(o) {
|
|
|
2429
2635
|
const r = planEnvUpdate({ current, fetched: o.fetched, choices: o.choices ?? {}, defaultChoice: o.defaultChoice, managed: o.managed });
|
|
2430
2636
|
const choiceOf = (k) => o.choices?.[k] ?? o.defaultChoice;
|
|
2431
2637
|
const generated = [];
|
|
2432
|
-
for (const
|
|
2433
|
-
const v = r.merged.get(
|
|
2638
|
+
for (const key2 of o.localSecrets) {
|
|
2639
|
+
const v = r.merged.get(key2);
|
|
2434
2640
|
if (v === void 0 || v === "" || v === '""' || v === "''") {
|
|
2435
|
-
r.merged.set(
|
|
2436
|
-
generated.push(
|
|
2641
|
+
r.merged.set(key2, o.random());
|
|
2642
|
+
generated.push(key2);
|
|
2437
2643
|
}
|
|
2438
2644
|
}
|
|
2439
2645
|
return {
|
|
@@ -2451,12 +2657,12 @@ function planPull(o) {
|
|
|
2451
2657
|
}
|
|
2452
2658
|
|
|
2453
2659
|
// src/core/env/write.ts
|
|
2454
|
-
import { chmod as chmod2, readFile as
|
|
2660
|
+
import { chmod as chmod2, readFile as readFile5, rename as rename3, unlink, writeFile as writeFile4 } from "fs/promises";
|
|
2455
2661
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
2456
|
-
import { basename as basename2, dirname as dirname5, join as
|
|
2662
|
+
import { basename as basename2, dirname as dirname5, join as join13 } from "path";
|
|
2457
2663
|
async function readOrNull(p) {
|
|
2458
2664
|
try {
|
|
2459
|
-
return await
|
|
2665
|
+
return await readFile5(p, "utf8");
|
|
2460
2666
|
} catch (e) {
|
|
2461
2667
|
if (e.code === "ENOENT") return null;
|
|
2462
2668
|
throw e;
|
|
@@ -2465,7 +2671,7 @@ async function readOrNull(p) {
|
|
|
2465
2671
|
async function writeSecurely(target, text2) {
|
|
2466
2672
|
const base = basename2(target);
|
|
2467
2673
|
const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes4(6).toString("hex")}.local` : `${base}.qore-${randomBytes4(6).toString("hex")}.tmp`;
|
|
2468
|
-
const tmp =
|
|
2674
|
+
const tmp = join13(dirname5(target), tmpName);
|
|
2469
2675
|
try {
|
|
2470
2676
|
await writeFile4(tmp, text2, { mode: 384, flag: "wx" });
|
|
2471
2677
|
await chmod2(tmp, 384);
|
|
@@ -2491,934 +2697,1359 @@ async function syncEnvFile(o) {
|
|
|
2491
2697
|
return { created: plan.created, kept: plan.kept };
|
|
2492
2698
|
}
|
|
2493
2699
|
|
|
2494
|
-
// src/core/
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
function isOffline(e) {
|
|
2500
|
-
if (!(e instanceof QoreError)) return false;
|
|
2501
|
-
if (e.code === "QORE_API_UNREACHABLE" || e.code === "QORE_API_UNAVAILABLE") return true;
|
|
2502
|
-
if (e.code !== "QORE_AUTH_UNAVAILABLE") return false;
|
|
2503
|
-
return (e.status ?? 0) >= 500 || NETWORK_REASONS.has(String(e.details?.reason));
|
|
2504
|
-
}
|
|
2505
|
-
async function stateKey(deps2, root) {
|
|
2506
|
-
const real = await realpath6(root).catch(() => root);
|
|
2507
|
-
return createHash2("sha256").update(trustKey(real, deps2.platform)).digest("hex").slice(0, 16);
|
|
2508
|
-
}
|
|
2509
|
-
async function saveLastStart(deps2, root, rec) {
|
|
2510
|
-
await writeJsonAtomic(join13(deps2.config.dir, "dev"), `${await stateKey(deps2, root)}.json`, rec);
|
|
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
|
+
});
|
|
2511
2705
|
}
|
|
2512
|
-
async function
|
|
2513
|
-
|
|
2514
|
-
|
|
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 };
|
|
2515
2721
|
}
|
|
2516
2722
|
|
|
2517
|
-
// src/
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
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) {
|
|
2531
2753
|
try {
|
|
2532
|
-
|
|
2533
|
-
const example = await readOrNull(join14(dirname6(target), ".env.example"));
|
|
2534
|
-
const w = await syncEnvFile({
|
|
2535
|
-
target,
|
|
2536
|
-
example,
|
|
2537
|
-
fetched: local.env,
|
|
2538
|
-
managed: local.managedKeys,
|
|
2539
|
-
localSecrets: o.cfg.localSecrets ?? defaults.localSecrets,
|
|
2540
|
-
random: o.random
|
|
2541
|
-
});
|
|
2542
|
-
return {
|
|
2543
|
-
env: local.env,
|
|
2544
|
-
offline: false,
|
|
2545
|
-
envFile: target,
|
|
2546
|
-
port: envPort(local.env, "PORT"),
|
|
2547
|
-
dbPort: envPort(local.env, "DB_PORT"),
|
|
2548
|
-
...w
|
|
2549
|
-
};
|
|
2754
|
+
await route(req, res);
|
|
2550
2755
|
} catch (e) {
|
|
2551
|
-
if (
|
|
2552
|
-
|
|
2756
|
+
if (res.headersSent) res.destroy();
|
|
2757
|
+
else end(res, e instanceof TypeError ? 400 : 500);
|
|
2553
2758
|
}
|
|
2554
2759
|
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
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 path2 = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
2765
|
+
const produce = o.routes[path2];
|
|
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, `.${path2 === "/" ? "/index.html" : path2}`);
|
|
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
|
+
}
|
|
2560
2791
|
}
|
|
2561
|
-
|
|
2562
|
-
|
|
2792
|
+
return {
|
|
2793
|
+
port,
|
|
2794
|
+
close: () => new Promise((ok2) => {
|
|
2795
|
+
server.closeAllConnections();
|
|
2796
|
+
server.close(() => ok2());
|
|
2797
|
+
})
|
|
2798
|
+
};
|
|
2563
2799
|
}
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
const own = m.devTeamIds?.length ? am.devTeams.filter((t) => m.devTeamIds.includes(t.id)) : [];
|
|
2568
|
-
return own.length ? own : am.devTeams;
|
|
2800
|
+
function end(res, status2) {
|
|
2801
|
+
res.writeHead(status2);
|
|
2802
|
+
res.end();
|
|
2569
2803
|
}
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
return
|
|
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);
|
|
2576
2810
|
}
|
|
2577
|
-
function
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
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;
|
|
2582
2833
|
}
|
|
2583
|
-
return "run qore sync";
|
|
2584
2834
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
const blocked = [];
|
|
2593
|
-
const reasons = {};
|
|
2594
|
-
const own = (m, reason, rest) => {
|
|
2595
|
-
reasons[m.slug] = reason;
|
|
2596
|
-
return { code: "dev.modules-missing", severity: "warn", module: m.slug, modules: [m.slug], message: `${m.slug} is not cloned \u2014 ${reason}`, ...rest };
|
|
2597
|
-
};
|
|
2598
|
-
for (const m of o.missing) {
|
|
2599
|
-
const a = actionFor.get(m.slug);
|
|
2600
|
-
if (a && a.kind !== "clone") {
|
|
2601
|
-
if (a.kind === "conflict") {
|
|
2602
|
-
const c = conflictText(o.cwd, a);
|
|
2603
|
-
blocked.push(own(m, c.message, { action: { command: c.hint } }));
|
|
2604
|
-
} else if (a.kind === "unsupported") {
|
|
2605
|
-
blocked.push(own(m, "its repository URL is not supported", { action: { command: unsupportedRepoUrlError(m.slug, "").hint } }));
|
|
2606
|
-
} else {
|
|
2607
|
-
blocked.push(own(m, a.reason ?? "no repository yet", { why: "It will not run in this session." }));
|
|
2608
|
-
}
|
|
2609
|
-
} else if (o.am !== null && (!m.githubLinked || m.repoAccess === "none")) {
|
|
2610
|
-
const am = o.am;
|
|
2611
|
-
blocked.push(own(m, notClonedReason(m, am), { action: { command: !m.githubLinked ? `Link your GitHub account: ${am.accountUrl}` : askMaintainer(am, m).text } }));
|
|
2612
|
-
} else fixable.push(m);
|
|
2613
|
-
}
|
|
2614
|
-
if (fixable.length > 0) {
|
|
2615
|
-
const slugs = fixable.map((m) => m.slug);
|
|
2616
|
-
const deps2 = slugs.flatMap((s) => (o.consumers[s] ?? []).map((c) => `${c} depends on ${s}`));
|
|
2617
|
-
findings.push({
|
|
2618
|
-
code: "dev.modules-missing",
|
|
2619
|
-
severity: "warn",
|
|
2620
|
-
message: `${fixable.length} ${plural(fixable.length, "module of this app is", "modules of this app are")} not cloned: ${slugs.join(", ")}`,
|
|
2621
|
-
why: deps2.length > 0 ? `${deps2.join("; ")}.` : `${plural(fixable.length, "It", "They")} will not run in this session.`,
|
|
2622
|
-
modules: slugs,
|
|
2623
|
-
action: o.am ? { command: "qore sync", fix: "sync" } : { command: "qore sync" }
|
|
2624
|
-
});
|
|
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);
|
|
2625
2842
|
}
|
|
2626
|
-
return { findings: [...findings, ...blocked], fixable, reasons };
|
|
2627
|
-
}
|
|
2628
|
-
function strayClone(name, appName) {
|
|
2629
|
-
return { code: "dev.stray-clone", severity: "info", module: name, message: `${name} is no longer part of ${appName} \u2014 you can remove the directory.` };
|
|
2630
2843
|
}
|
|
2631
|
-
function
|
|
2632
|
-
if (
|
|
2633
|
-
return
|
|
2634
|
-
|
|
2635
|
-
severity: "warn",
|
|
2636
|
-
module: slug,
|
|
2637
|
-
message: `${slug} is ${s.behind} ${plural(s.behind, "commit", "commits")} behind ${s.upstreamRef ?? "its upstream"} (as of your last fetch).`,
|
|
2638
|
-
action: { command: o.workspace ? "qore sync --pull" : "git pull" }
|
|
2639
|
-
};
|
|
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";
|
|
2640
2848
|
}
|
|
2641
|
-
function
|
|
2642
|
-
const
|
|
2643
|
-
|
|
2644
|
-
return {
|
|
2645
|
-
code: "dev.services-missing",
|
|
2646
|
-
severity: "warn",
|
|
2647
|
-
module: slug,
|
|
2648
|
-
message: `The portal gives ${slug} a database (DB_PORT=${db}), but ${slug} has no db:up script \u2014 no database will be started.`,
|
|
2649
|
-
why: "This repository is probably older than its template.",
|
|
2650
|
-
action: { command: 'add a "db:up" script (see the current app template), or set dev.services in .qore.json' }
|
|
2651
|
-
};
|
|
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)})`;
|
|
2652
2852
|
}
|
|
2653
|
-
function
|
|
2654
|
-
const
|
|
2853
|
+
function contextJson(mc, app) {
|
|
2854
|
+
const { id, slug, name, type, access: access2 } = mc.module;
|
|
2655
2855
|
return {
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
action: { command: o.fix }
|
|
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 } : {}
|
|
2663
2862
|
};
|
|
2664
2863
|
}
|
|
2665
2864
|
|
|
2666
|
-
// src/
|
|
2667
|
-
import {
|
|
2668
|
-
import { join as
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
throw unsupported("This repository has no package.json", "qore dev starts Node repositories through their package.json scripts.");
|
|
2679
|
-
}
|
|
2680
|
-
try {
|
|
2681
|
-
return JSON.parse(raw);
|
|
2682
|
-
} catch {
|
|
2683
|
-
throw unsupported("package.json is not valid JSON", "Fix package.json, then run qore dev again.");
|
|
2684
|
-
}
|
|
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 [];
|
|
2685
2877
|
}
|
|
2686
|
-
|
|
2687
|
-
if (
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
return { name: m[1], version: m[2].split("+")[0] };
|
|
2691
|
-
}
|
|
2692
|
-
for (const [file, name] of LOCKFILES) if (await exists(join15(root, file))) return { name, version: null };
|
|
2693
|
-
throw unsupported("No package manager declared and no lockfile found", 'Add "packageManager": "pnpm@<version>" to package.json.');
|
|
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 };
|
|
2694
2882
|
}
|
|
2695
|
-
function
|
|
2696
|
-
const
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
}
|
|
2702
|
-
if (LEGACY_DEV_SCRIPTS.includes(found.trim())) {
|
|
2703
|
-
throw unsupported(
|
|
2704
|
-
`${o.slug}'s dev script is the old backend one, which needs a build and a .env first`,
|
|
2705
|
-
'Update it to the current template: "dev": "node scripts/dev.mjs" plus scripts/dev.mjs.',
|
|
2706
|
-
{ script, found }
|
|
2707
|
-
);
|
|
2708
|
-
}
|
|
2709
|
-
const configured = o.cfg.dev?.services;
|
|
2710
|
-
for (const s of configured ?? []) {
|
|
2711
|
-
if (scripts[s] === void 0) {
|
|
2712
|
-
throw new QoreError("QORE_CONTEXT_INVALID", `.qore.json: dev.services names "${s}", which is not a script in package.json`, {
|
|
2713
|
-
hint: "Fix dev.services in .qore.json."
|
|
2714
|
-
});
|
|
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);
|
|
2715
2889
|
}
|
|
2716
2890
|
}
|
|
2717
|
-
|
|
2718
|
-
return {
|
|
2719
|
-
root: o.root,
|
|
2720
|
-
pm: o.pm,
|
|
2721
|
-
script,
|
|
2722
|
-
services,
|
|
2723
|
-
install: (o.cfg.dev?.install ?? true) && o.flags.install,
|
|
2724
|
-
dockerServices: services.some((s) => /\bdocker\b/.test(scripts[s] ?? ""))
|
|
2725
|
-
};
|
|
2891
|
+
return out;
|
|
2726
2892
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
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;
|
|
2729
2901
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
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;
|
|
2732
2910
|
}
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
}
|
|
2742
|
-
var LOCK2 = { pnpm: "pnpm-lock.yaml", npm: "package-lock.json", yarn: "yarn.lock" };
|
|
2743
|
-
var mtime = (p) => stat3(p).then((s) => s.mtimeMs, () => null);
|
|
2744
|
-
async function needsInstall(root, pm) {
|
|
2745
|
-
if (await mtime(join16(root, "node_modules")) === null) return true;
|
|
2746
|
-
let marker = null;
|
|
2747
|
-
for (const m of MARKERS[pm.name]) marker ??= await mtime(join16(root, "node_modules", m));
|
|
2748
|
-
if (marker === null) return true;
|
|
2749
|
-
const lock = await mtime(join16(root, LOCK2[pm.name]));
|
|
2750
|
-
return lock !== null && lock > marker;
|
|
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
|
+
}
|
|
2751
2920
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
return
|
|
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();
|
|
2758
2927
|
}
|
|
2759
|
-
function
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
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}.`
|
|
2763
2946
|
});
|
|
2764
2947
|
}
|
|
2765
|
-
if (
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
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
|
+
});
|
|
2769
2953
|
}
|
|
2770
|
-
return
|
|
2954
|
+
return join14(p.path, await ctx.io.select("Select a module", dirs.map((d) => ({ name: d, value: d }))));
|
|
2771
2955
|
}
|
|
2772
|
-
function
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
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);
|
|
2777
2961
|
}
|
|
2778
|
-
return
|
|
2962
|
+
return live.length > 0 ? live : matches;
|
|
2779
2963
|
}
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
const m = text2.match(/initialized by PostgreSQL version (\d+).*this version (\d+)/);
|
|
2788
|
-
if (m) {
|
|
2789
|
-
details.found = m[1];
|
|
2790
|
-
details.expected = m[2];
|
|
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
|
+
});
|
|
2791
2971
|
}
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
});
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
// src/core/diagnostics/ports.ts
|
|
2799
|
-
function parseDockerPs(stdout) {
|
|
2800
|
-
const line2 = stdout.split(/\r?\n/).find((l) => l.trim());
|
|
2801
|
-
if (!line2) return null;
|
|
2802
|
-
const [id, name, project, workingDir] = line2.split(" ");
|
|
2803
|
-
if (!id || !name) return null;
|
|
2804
|
-
return { id, name, project: project || null, workingDir: workingDir || null };
|
|
2805
|
-
}
|
|
2806
|
-
function parseLsofCwd(stdout) {
|
|
2807
|
-
const m = stdout.match(/^fcwd\nn(.+)$/m);
|
|
2808
|
-
return m ? m[1] : null;
|
|
2809
|
-
}
|
|
2810
|
-
function parseEtime(s) {
|
|
2811
|
-
const m = s.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/);
|
|
2812
|
-
if (!m) return null;
|
|
2813
|
-
const days = m[1] ? Number(m[1]) : 0;
|
|
2814
|
-
const hours = m[2] ? Number(m[2]) : 0;
|
|
2815
|
-
const minutes = Number(m[3]);
|
|
2816
|
-
const seconds = Number(m[4]);
|
|
2817
|
-
return days * 86400 + hours * 3600 + minutes * 60 + seconds;
|
|
2818
|
-
}
|
|
2819
|
-
function humanAge(seconds) {
|
|
2820
|
-
if (seconds < 60) return `${seconds} s`;
|
|
2821
|
-
if (seconds < 3600) return `${Math.round(seconds / 60)} min`;
|
|
2822
|
-
if (seconds < 86400) return `${Math.round(seconds / 3600)} h`;
|
|
2823
|
-
return `${Math.round(seconds / 86400)} d`;
|
|
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];
|
|
2824
2977
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
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
|
+
});
|
|
2836
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));
|
|
2837
2993
|
}
|
|
2838
|
-
if (
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
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>`."
|
|
2851
3008
|
});
|
|
2852
3009
|
}
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
});
|
|
2857
|
-
}
|
|
2858
|
-
|
|
2859
|
-
// src/core/diagnostics/versions.ts
|
|
2860
|
-
import semver from "semver";
|
|
2861
|
-
function nodeSatisfies(found, range) {
|
|
2862
|
-
if (semver.validRange(range) === null) return null;
|
|
2863
|
-
return semver.satisfies(found, range);
|
|
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));
|
|
2864
3013
|
}
|
|
2865
|
-
function
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
return
|
|
2869
|
-
hint: `nvm install ${major} / fnm use ${major}, then run qore dev again.`,
|
|
2870
|
-
details: { found, required: range }
|
|
2871
|
-
});
|
|
3014
|
+
function relocate(ctx, path2) {
|
|
3015
|
+
if (path2 === ctx.deps.cwd) return ctx;
|
|
3016
|
+
if (!ctx.opts().json) ctx.io.err(`\u2192 in ${path2}`);
|
|
3017
|
+
return withCwd(ctx, path2);
|
|
2872
3018
|
}
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
} });
|
|
2886
|
-
let kill;
|
|
2887
|
-
const onAbort = () => {
|
|
2888
|
-
c.stop("SIGTERM");
|
|
2889
|
-
kill = setTimeout(() => c.stop("SIGKILL"), GRACE_MS);
|
|
2890
|
-
};
|
|
2891
|
-
o.signal.addEventListener("abort", onAbort, { once: true });
|
|
2892
|
-
let exit;
|
|
2893
|
-
try {
|
|
2894
|
-
exit = await c.exited;
|
|
2895
|
-
} finally {
|
|
2896
|
-
o.signal.removeEventListener("abort", onAbort);
|
|
2897
|
-
clearTimeout(kill);
|
|
2898
|
-
}
|
|
2899
|
-
if (o.signal.aborted) throw new CancelledError();
|
|
2900
|
-
return { code: exit.code, spawnError: exit.spawnError, tail };
|
|
2901
|
-
}
|
|
2902
|
-
async function checkPackageManager(deps2, r, onLine, signal) {
|
|
2903
|
-
const res = await runTool(deps2, r.pm.name, ["--version"], { cwd: r.root, env: deps2.env, onLine, signal });
|
|
2904
|
-
const hint = `Install it: npm i -g ${r.pm.name}@${pmMajor(r.pm.version)}`;
|
|
2905
|
-
if (res.spawnError === "ENOENT") {
|
|
2906
|
-
throw new QoreError("QORE_PACKAGE_MANAGER", `${r.pm.name} is not installed`, { hint, details: { packageManager: r.pm } });
|
|
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;
|
|
2907
3031
|
}
|
|
2908
|
-
if (
|
|
2909
|
-
|
|
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
|
+
});
|
|
2910
3043
|
}
|
|
2911
|
-
const mismatch = pmVersionMismatch(r.pm, res.tail.join("\n"));
|
|
2912
|
-
if (mismatch) throw mismatch;
|
|
2913
|
-
return res.tail.map((l) => l.trim()).findLast((l) => /^\d+\.\d+\.\d+/.test(l)) ?? null;
|
|
2914
3044
|
}
|
|
2915
|
-
async function
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
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 }
|
|
2924
3063
|
});
|
|
2925
3064
|
}
|
|
2926
|
-
if (
|
|
2927
|
-
|
|
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)"}`);
|
|
2928
3068
|
}
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
return found;
|
|
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);
|
|
2932
3071
|
}
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
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
|
+
});
|
|
2937
3108
|
}
|
|
2938
|
-
if (
|
|
2939
|
-
|
|
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 });
|
|
2940
3143
|
return;
|
|
2941
3144
|
}
|
|
2942
|
-
|
|
2943
|
-
const
|
|
2944
|
-
|
|
2945
|
-
const
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
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();
|
|
2951
3182
|
}
|
|
2952
|
-
report({ module: slug, step: "install", status: "done", detail: secs(deps2.now().getTime() - began) });
|
|
2953
3183
|
}
|
|
2954
|
-
async function
|
|
2955
|
-
const
|
|
2956
|
-
const
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
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>." });
|
|
2962
3195
|
}
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
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;
|
|
2971
3213
|
}
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
const cwd = deps2.home && detail2.cwd.startsWith(deps2.home) ? `~${detail2.cwd.slice(deps2.home.length)}` : detail2.cwd;
|
|
2978
|
-
extras.push(`in ${cwd}`);
|
|
2979
|
-
}
|
|
2980
|
-
const suffix = extras.length ? `, ${extras.join(" ")}` : "";
|
|
2981
|
-
return new QoreError("QORE_PORT_IN_USE", `Port ${o.port} is already in use (needed by ${o.slug})`, {
|
|
2982
|
-
hint: `Held by: ${owner.command} (pid ${owner.pid})${suffix}. Stop that process. Qore never moves a port: other modules and the local login rely on ${o.port}.`,
|
|
2983
|
-
details: {
|
|
2984
|
-
port: o.port,
|
|
2985
|
-
module: o.slug,
|
|
2986
|
-
key: o.key,
|
|
2987
|
-
owner: "process",
|
|
2988
|
-
pid: owner.pid,
|
|
2989
|
-
command: owner.command,
|
|
2990
|
-
...detail2.cwd !== null ? { cwd: detail2.cwd } : {},
|
|
2991
|
-
...detail2.ageSeconds !== null ? { ageSeconds: detail2.ageSeconds } : {}
|
|
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;
|
|
2992
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."
|
|
2993
3227
|
});
|
|
2994
3228
|
}
|
|
2995
|
-
const
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
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;
|
|
2999
3252
|
});
|
|
3000
|
-
}
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
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;
|
|
3004
3266
|
}
|
|
3005
|
-
const
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
if (
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
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;
|
|
3012
3282
|
});
|
|
3283
|
+
await serve(ctx, f, server);
|
|
3013
3284
|
}
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
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.` };
|
|
3018
3300
|
}
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
if (composeErr) throw composeErr;
|
|
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
|
+
}
|
|
3029
3310
|
}
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
const
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
}
|
|
3037
|
-
|
|
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
|
+
};
|
|
3038
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.` };
|
|
3039
3341
|
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
);
|
|
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
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
return null;
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
// src/core/dev/env.ts
|
|
3368
|
+
import { dirname as dirname6, join as join17 } from "path";
|
|
3369
|
+
|
|
3370
|
+
// src/core/dev/lastStart.ts
|
|
3371
|
+
import { createHash as createHash2 } from "crypto";
|
|
3372
|
+
import { realpath as realpath6 } from "fs/promises";
|
|
3373
|
+
import { join as join16 } from "path";
|
|
3374
|
+
var NETWORK_REASONS = /* @__PURE__ */ new Set(["connect", "dns", "tls", "timeout"]);
|
|
3375
|
+
function isOffline(e) {
|
|
3376
|
+
if (!(e instanceof QoreError)) return false;
|
|
3377
|
+
if (e.code === "QORE_API_UNREACHABLE" || e.code === "QORE_API_UNAVAILABLE") return true;
|
|
3378
|
+
if (e.code !== "QORE_AUTH_UNAVAILABLE") return false;
|
|
3379
|
+
return (e.status ?? 0) >= 500 || NETWORK_REASONS.has(String(e.details?.reason));
|
|
3380
|
+
}
|
|
3381
|
+
async function stateKey(deps2, root) {
|
|
3382
|
+
const real = await realpath6(root).catch(() => root);
|
|
3383
|
+
return createHash2("sha256").update(trustKey(real, deps2.platform)).digest("hex").slice(0, 16);
|
|
3384
|
+
}
|
|
3385
|
+
async function saveLastStart(deps2, root, rec) {
|
|
3386
|
+
await writeJsonAtomic(join16(deps2.config.dir, "dev"), `${await stateKey(deps2, root)}.json`, rec);
|
|
3387
|
+
}
|
|
3388
|
+
async function loadLastStart(deps2, root) {
|
|
3389
|
+
const rec = await readJson(join16(deps2.config.dir, "dev", `${await stateKey(deps2, root)}.json`)).catch(() => null);
|
|
3390
|
+
return rec && rec.schemaVersion === 1 ? rec : null;
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
// src/core/dev/env.ts
|
|
3394
|
+
function envPort(env, key2) {
|
|
3395
|
+
const raw = env[key2];
|
|
3396
|
+
if (raw === void 0) return null;
|
|
3397
|
+
const v = raw.trim().replace(/^["']|["']$/g, "");
|
|
3398
|
+
if (!/^\d+$/.test(v)) return null;
|
|
3399
|
+
const n = Number(v);
|
|
3400
|
+
return n >= 1024 && n <= 65535 ? n : null;
|
|
3401
|
+
}
|
|
3402
|
+
async function devEnv(deps2, o) {
|
|
3403
|
+
const defaults = moduleDefaults(o.type);
|
|
3404
|
+
const target = await resolveTarget(o.root, o.cfg.envFile ?? defaults.envFile);
|
|
3405
|
+
let cause = null;
|
|
3406
|
+
if (o.mc && o.app) {
|
|
3407
|
+
try {
|
|
3408
|
+
const local = await fetchLocalEnv(deps2, o.mc, o.app);
|
|
3409
|
+
const example = await readOrNull(join17(dirname6(target), ".env.example"));
|
|
3410
|
+
const w = await syncEnvFile({
|
|
3411
|
+
target,
|
|
3412
|
+
example,
|
|
3413
|
+
fetched: local.env,
|
|
3414
|
+
managed: local.managedKeys,
|
|
3415
|
+
localSecrets: o.cfg.localSecrets ?? defaults.localSecrets,
|
|
3416
|
+
random: o.random
|
|
3417
|
+
});
|
|
3418
|
+
return {
|
|
3419
|
+
env: local.env,
|
|
3420
|
+
offline: false,
|
|
3421
|
+
envFile: target,
|
|
3422
|
+
port: envPort(local.env, "PORT"),
|
|
3423
|
+
dbPort: envPort(local.env, "DB_PORT"),
|
|
3424
|
+
...w
|
|
3425
|
+
};
|
|
3426
|
+
} catch (e) {
|
|
3427
|
+
if (!isOffline(e)) throw e;
|
|
3428
|
+
cause = e;
|
|
3054
3429
|
}
|
|
3055
3430
|
}
|
|
3056
|
-
const
|
|
3057
|
-
|
|
3431
|
+
const text2 = await readOrNull(target);
|
|
3432
|
+
if (text2 === null || !await isTrusted(deps2, o.root, o.moduleId)) {
|
|
3433
|
+
throw cause ?? new QoreError("QORE_API_UNREACHABLE", "The portal is unreachable", {
|
|
3434
|
+
hint: "The portal is unreachable and there is no env file to start from yet."
|
|
3435
|
+
});
|
|
3436
|
+
}
|
|
3437
|
+
const env = Object.fromEntries(parseEnv(text2));
|
|
3438
|
+
return { env, offline: true, envFile: target, port: envPort(env, "PORT"), dbPort: envPort(env, "DB_PORT"), created: false, kept: [] };
|
|
3058
3439
|
}
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3440
|
+
|
|
3441
|
+
// src/core/workspace/access.ts
|
|
3442
|
+
function devTeamsOf(am, m) {
|
|
3443
|
+
const own = m.devTeamIds?.length ? am.devTeams.filter((t) => m.devTeamIds.includes(t.id)) : [];
|
|
3444
|
+
return own.length ? own : am.devTeams;
|
|
3063
3445
|
}
|
|
3064
|
-
function
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3446
|
+
function askMaintainer(am, m) {
|
|
3447
|
+
const teams = devTeamsOf(am, m);
|
|
3448
|
+
const names = teams.map((t) => t.name);
|
|
3449
|
+
const maintainers = [...new Set(teams.flatMap((t) => t.maintainers))];
|
|
3450
|
+
const who = names.length ? names.join(", ") : "the app's dev team";
|
|
3451
|
+
return { teams: names, maintainers, text: `ask a maintainer of ${who}${maintainers.length ? ` (${maintainers.join(", ")})` : ""}` };
|
|
3452
|
+
}
|
|
3453
|
+
function notClonedReason(m, am) {
|
|
3454
|
+
if (am && !m.githubLinked) return `GitHub account not linked (${am.accountUrl})`;
|
|
3455
|
+
if (am && m.repoAccess === "none") {
|
|
3456
|
+
const names = devTeamsOf(am, m).map((t) => t.name);
|
|
3457
|
+
return names.length ? `no repository access (dev team ${names.join(", ")})` : "no repository access (ask your tenant owner)";
|
|
3458
|
+
}
|
|
3459
|
+
return "run qore sync";
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
// src/core/dev/findings.ts
|
|
3463
|
+
var plural2 = (n, one2, many) => n === 1 ? one2 : many;
|
|
3464
|
+
function modulesMissing(o) {
|
|
3465
|
+
const actionFor = new Map(o.actions.map((a) => [a.module.slug, a]));
|
|
3466
|
+
const findings = [];
|
|
3467
|
+
const fixable = [];
|
|
3468
|
+
const blocked = [];
|
|
3469
|
+
const reasons = {};
|
|
3470
|
+
const own = (m, reason, rest) => {
|
|
3471
|
+
reasons[m.slug] = reason;
|
|
3472
|
+
return { code: "dev.modules-missing", severity: "warn", module: m.slug, modules: [m.slug], message: `${m.slug} is not cloned \u2014 ${reason}`, ...rest };
|
|
3473
|
+
};
|
|
3474
|
+
for (const m of o.missing) {
|
|
3475
|
+
const a = actionFor.get(m.slug);
|
|
3476
|
+
if (a && a.kind !== "clone") {
|
|
3477
|
+
if (a.kind === "conflict") {
|
|
3478
|
+
const c = conflictText(o.cwd, a);
|
|
3479
|
+
blocked.push(own(m, c.message, { action: { command: c.hint } }));
|
|
3480
|
+
} else if (a.kind === "unsupported") {
|
|
3481
|
+
blocked.push(own(m, "its repository URL is not supported", { action: { command: unsupportedRepoUrlError(m.slug, "").hint } }));
|
|
3482
|
+
} else {
|
|
3483
|
+
blocked.push(own(m, a.reason ?? "no repository yet", { why: "It will not run in this session." }));
|
|
3484
|
+
}
|
|
3485
|
+
} else if (o.am !== null && (!m.githubLinked || m.repoAccess === "none")) {
|
|
3486
|
+
const am = o.am;
|
|
3487
|
+
blocked.push(own(m, notClonedReason(m, am), { action: { command: !m.githubLinked ? `Link your GitHub account: ${am.accountUrl}` : askMaintainer(am, m).text } }));
|
|
3488
|
+
} else fixable.push(m);
|
|
3489
|
+
}
|
|
3490
|
+
if (fixable.length > 0) {
|
|
3491
|
+
const slugs = fixable.map((m) => m.slug);
|
|
3492
|
+
const deps2 = slugs.flatMap((s) => (o.consumers[s] ?? []).map((c) => `${c} depends on ${s}`));
|
|
3493
|
+
findings.push({
|
|
3494
|
+
code: "dev.modules-missing",
|
|
3495
|
+
severity: "warn",
|
|
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.`,
|
|
3498
|
+
modules: slugs,
|
|
3499
|
+
action: o.am ? { command: "qore sync", fix: "sync" } : { command: "qore sync" }
|
|
3500
|
+
});
|
|
3501
|
+
}
|
|
3502
|
+
return { findings: [...findings, ...blocked], fixable, reasons };
|
|
3503
|
+
}
|
|
3504
|
+
function strayClone(name, appName) {
|
|
3505
|
+
return { code: "dev.stray-clone", severity: "info", module: name, message: `${name} is no longer part of ${appName} \u2014 you can remove the directory.` };
|
|
3506
|
+
}
|
|
3507
|
+
function repoBehind(slug, s, o) {
|
|
3508
|
+
if (!s.ok || !s.upstream || !s.behind) return null;
|
|
3509
|
+
return {
|
|
3510
|
+
code: "dev.repo-behind",
|
|
3511
|
+
severity: "warn",
|
|
3512
|
+
module: slug,
|
|
3513
|
+
message: `${slug} is ${s.behind} ${plural2(s.behind, "commit", "commits")} behind ${s.upstreamRef ?? "its upstream"} (as of your last fetch).`,
|
|
3514
|
+
action: { command: o.workspace ? "qore sync --pull" : "git pull" }
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
function servicesMissing(slug, env, recipe, o) {
|
|
3518
|
+
const db = envPort(env, "DB_PORT");
|
|
3519
|
+
if (db === null || recipe.services.length > 0 || !o.servicesFlag || o.configured) return null;
|
|
3520
|
+
return {
|
|
3521
|
+
code: "dev.services-missing",
|
|
3522
|
+
severity: "warn",
|
|
3523
|
+
module: slug,
|
|
3524
|
+
message: `The portal gives ${slug} a database (DB_PORT=${db}), but ${slug} has no db:up script \u2014 no database will be started.`,
|
|
3525
|
+
why: "This repository is probably older than its template.",
|
|
3526
|
+
action: { command: 'add a "db:up" script (see the current app template), or set dev.services in .qore.json' }
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3529
|
+
function providerDown(o) {
|
|
3530
|
+
const single = o.providers.length === 1 && o.keys.length === 1;
|
|
3531
|
+
return {
|
|
3532
|
+
code: "dev.provider-down",
|
|
3533
|
+
severity: "warn",
|
|
3534
|
+
module: o.slug,
|
|
3535
|
+
modules: o.providers,
|
|
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(", ")}).`,
|
|
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.`,
|
|
3538
|
+
action: { command: o.fix }
|
|
3539
|
+
};
|
|
3074
3540
|
}
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3541
|
+
|
|
3542
|
+
// src/core/dev/project.ts
|
|
3543
|
+
import { access, readFile as readFile7 } from "fs/promises";
|
|
3544
|
+
import { join as join18 } from "path";
|
|
3545
|
+
var LEGACY_DEV_SCRIPTS = ["node --env-file=.env --import ./dist/instrumentation.js dist/index.js"];
|
|
3546
|
+
var LOCKFILES = [["pnpm-lock.yaml", "pnpm"], ["package-lock.json", "npm"], ["yarn.lock", "yarn"]];
|
|
3547
|
+
var exists = (p) => access(p).then(() => true, () => false);
|
|
3548
|
+
var unsupported = (message, hint, details) => new QoreError("QORE_DEV_UNSUPPORTED", message, { hint, details });
|
|
3549
|
+
async function readPackageJson(root) {
|
|
3550
|
+
let raw;
|
|
3551
|
+
try {
|
|
3552
|
+
raw = await readFile7(join18(root, "package.json"), "utf8");
|
|
3553
|
+
} catch {
|
|
3554
|
+
throw unsupported("This repository has no package.json", "qore dev starts Node repositories through their package.json scripts.");
|
|
3555
|
+
}
|
|
3556
|
+
try {
|
|
3557
|
+
return JSON.parse(raw);
|
|
3558
|
+
} catch {
|
|
3559
|
+
throw unsupported("package.json is not valid JSON", "Fix package.json, then run qore dev again.");
|
|
3094
3560
|
}
|
|
3095
3561
|
}
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
const recordAge = (deps2.now().getTime() - Date.parse(run4.startedAt)) / 1e3;
|
|
3102
|
-
if (ageSeconds === null || !Number.isFinite(recordAge) || ageSeconds < recordAge - AGE_SLACK_S) return false;
|
|
3103
|
-
const ours = /* @__PURE__ */ new Set([run4.qorePid, ...run4.children.map((c) => c.pid).filter((p) => p !== null)]);
|
|
3104
|
-
let pid = (await deps2.ports.owner(port))?.pid ?? null;
|
|
3105
|
-
for (let hop = 0; pid !== null && pid > 1 && hop <= MAX_PARENT_HOPS; hop++) {
|
|
3106
|
-
if (ours.has(pid)) return true;
|
|
3107
|
-
pid = await deps2.ports.parentPid(pid);
|
|
3562
|
+
async function detectPackageManager(root, pkg) {
|
|
3563
|
+
if (typeof pkg.packageManager === "string") {
|
|
3564
|
+
const m = /^(pnpm|npm|yarn)@(.+)$/.exec(pkg.packageManager.trim());
|
|
3565
|
+
if (!m) throw unsupported(`packageManager "${pkg.packageManager}" is not pnpm, npm or yarn`, "qore dev supports pnpm, npm and yarn.");
|
|
3566
|
+
return { name: m[1], version: m[2].split("+")[0] };
|
|
3108
3567
|
}
|
|
3109
|
-
return
|
|
3568
|
+
for (const [file, name] of LOCKFILES) if (await exists(join18(root, file))) return { name, version: null };
|
|
3569
|
+
throw unsupported("No package manager declared and no lockfile found", 'Add "packageManager": "pnpm@<version>" to package.json.');
|
|
3110
3570
|
}
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3571
|
+
function planRecipe(o) {
|
|
3572
|
+
const scripts = o.pkg.scripts ?? {};
|
|
3573
|
+
const script = o.cfg.dev?.script ?? "dev";
|
|
3574
|
+
const found = scripts[script];
|
|
3575
|
+
if (found === void 0) {
|
|
3576
|
+
throw unsupported(`${o.slug} has no "${script}" script in package.json`, 'Add a "dev" script, or set dev.script in .qore.json.', { script });
|
|
3577
|
+
}
|
|
3578
|
+
if (LEGACY_DEV_SCRIPTS.includes(found.trim())) {
|
|
3579
|
+
throw unsupported(
|
|
3580
|
+
`${o.slug}'s dev script is the old backend one, which needs a build and a .env first`,
|
|
3581
|
+
'Update it to the current template: "dev": "node scripts/dev.mjs" plus scripts/dev.mjs.',
|
|
3582
|
+
{ script, found }
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
const configured = o.cfg.dev?.services;
|
|
3586
|
+
for (const s of configured ?? []) {
|
|
3587
|
+
if (scripts[s] === void 0) {
|
|
3588
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `.qore.json: dev.services names "${s}", which is not a script in package.json`, {
|
|
3589
|
+
hint: "Fix dev.services in .qore.json."
|
|
3590
|
+
});
|
|
3124
3591
|
}
|
|
3125
|
-
const port = Number(url.port);
|
|
3126
|
-
if (port > 0 && LOCAL_HOSTS.has(url.hostname)) out.push({ key, alias: m[1], port });
|
|
3127
3592
|
}
|
|
3128
|
-
|
|
3129
|
-
}
|
|
3130
|
-
function deadAliases(env, listening) {
|
|
3131
|
-
return connPorts(env).filter((c) => !listening.has(c.port)).map((c) => c.key);
|
|
3132
|
-
}
|
|
3133
|
-
function hintContext(recipe, pkg, env) {
|
|
3134
|
-
const { cmd, args } = installArgs(recipe.pm);
|
|
3135
|
-
const ports = /* @__PURE__ */ new Map();
|
|
3136
|
-
for (const c of connPorts(env)) ports.set(c.port, { kind: "provider", key: c.key });
|
|
3137
|
-
const db = envPort(env, "DB_PORT");
|
|
3138
|
-
if (db !== null) ports.set(db, { kind: "database" });
|
|
3593
|
+
const services = !o.flags.services ? [] : configured ?? (scripts["db:up"] !== void 0 ? ["db:up"] : []);
|
|
3139
3594
|
return {
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
ports
|
|
3595
|
+
root: o.root,
|
|
3596
|
+
pm: o.pm,
|
|
3597
|
+
script,
|
|
3598
|
+
services,
|
|
3599
|
+
install: (o.cfg.dev?.install ?? true) && o.flags.install,
|
|
3600
|
+
dockerServices: services.some((s) => /\bdocker\b/.test(scripts[s] ?? ""))
|
|
3147
3601
|
};
|
|
3148
3602
|
}
|
|
3603
|
+
function runArgs(pm, script) {
|
|
3604
|
+
return { cmd: pm.name, args: ["run", script] };
|
|
3605
|
+
}
|
|
3606
|
+
function installArgs(pm) {
|
|
3607
|
+
return { cmd: pm.name, args: ["install"] };
|
|
3608
|
+
}
|
|
3149
3609
|
|
|
3150
|
-
// src/core/dev/
|
|
3151
|
-
import {
|
|
3152
|
-
import { join as
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3610
|
+
// src/core/dev/install.ts
|
|
3611
|
+
import { stat as stat5 } from "fs/promises";
|
|
3612
|
+
import { join as join19 } from "path";
|
|
3613
|
+
var MARKERS = {
|
|
3614
|
+
pnpm: [".modules.yaml"],
|
|
3615
|
+
npm: [".package-lock.json"],
|
|
3616
|
+
yarn: [".yarn-state.yml", ".yarn-integrity"]
|
|
3617
|
+
};
|
|
3618
|
+
var LOCK2 = { pnpm: "pnpm-lock.yaml", npm: "package-lock.json", yarn: "yarn.lock" };
|
|
3619
|
+
var mtime = (p) => stat5(p).then((s) => s.mtimeMs, () => null);
|
|
3620
|
+
async function needsInstall(root, pm) {
|
|
3621
|
+
if (await mtime(join19(root, "node_modules")) === null) return true;
|
|
3622
|
+
let marker = null;
|
|
3623
|
+
for (const m of MARKERS[pm.name]) marker ??= await mtime(join19(root, "node_modules", m));
|
|
3624
|
+
if (marker === null) return true;
|
|
3625
|
+
const lock = await mtime(join19(root, LOCK2[pm.name]));
|
|
3626
|
+
return lock !== null && lock > marker;
|
|
3157
3627
|
}
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3628
|
+
|
|
3629
|
+
// src/core/diagnostics/docker.ts
|
|
3630
|
+
function dockerInstallUrl(platform) {
|
|
3631
|
+
if (platform === "darwin") return "https://docs.docker.com/desktop/setup/install/mac-install/";
|
|
3632
|
+
if (platform === "win32") return "https://docs.docker.com/desktop/setup/install/windows-install/";
|
|
3633
|
+
return "https://docs.docker.com/engine/install/";
|
|
3161
3634
|
}
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3635
|
+
function classifyDockerInfo(r, platform) {
|
|
3636
|
+
if (r.spawnError === "ENOENT") {
|
|
3637
|
+
return new QoreError("QORE_DOCKER_MISSING", "Docker is not installed", {
|
|
3638
|
+
hint: `Install Docker: ${dockerInstallUrl(platform)} \u2014 or run qore dev --no-services.`
|
|
3639
|
+
});
|
|
3640
|
+
}
|
|
3641
|
+
if (r.code === 0) return null;
|
|
3642
|
+
const hint = "Start Docker Desktop, then run qore dev again \u2014 or qore dev --no-services.";
|
|
3643
|
+
if (/Cannot connect to the Docker daemon|Is the docker daemon running|error during connect/i.test(r.tail.join("\n"))) {
|
|
3644
|
+
return new QoreError("QORE_DOCKER_NOT_RUNNING", "Docker is not running", { hint, details: { reason: "daemon" } });
|
|
3645
|
+
}
|
|
3646
|
+
return new QoreError("QORE_DOCKER_NOT_RUNNING", "Docker is not running", { hint, details: { stderr: r.tail } });
|
|
3166
3647
|
}
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
const
|
|
3170
|
-
if (
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
});
|
|
3174
|
-
return null;
|
|
3648
|
+
function classifyComposeVersion(r) {
|
|
3649
|
+
if (r.code === 0) return null;
|
|
3650
|
+
const hint = "Update Docker Desktop (or install the compose plugin). The legacy docker-compose v1 is not supported.";
|
|
3651
|
+
if (/unknown command: docker compose|'compose' is not a docker command/.test(r.tail.join("\n"))) {
|
|
3652
|
+
return new QoreError("QORE_DOCKER_COMPOSE_MISSING", "Docker Compose v2 is missing", { hint });
|
|
3653
|
+
}
|
|
3654
|
+
return new QoreError("QORE_DOCKER_COMPOSE_MISSING", "Docker Compose v2 is missing", { hint, details: { stderr: r.tail } });
|
|
3175
3655
|
}
|
|
3176
3656
|
|
|
3177
|
-
// src/
|
|
3178
|
-
|
|
3179
|
-
|
|
3657
|
+
// src/core/diagnostics/postgres.ts
|
|
3658
|
+
var INCOMPATIBLE = /database files are incompatible with server|Counter to that, there appears to be PostgreSQL data in|in 18\+, these Docker images are configured to store database data/;
|
|
3659
|
+
function classifyComposeLogs(logs, o) {
|
|
3660
|
+
const text2 = logs.join("\n");
|
|
3661
|
+
if (!INCOMPATIBLE.test(text2)) return null;
|
|
3662
|
+
const details = { reason: "pg-data-version" };
|
|
3663
|
+
const m = text2.match(/initialized by PostgreSQL version (\d+).*this version (\d+)/);
|
|
3664
|
+
if (m) {
|
|
3665
|
+
details.found = m[1];
|
|
3666
|
+
details.expected = m[2];
|
|
3667
|
+
}
|
|
3668
|
+
return new QoreError("QORE_SERVICE_DATA_INCOMPATIBLE", `The local database volume of ${o.slug} was created by another Postgres version`, {
|
|
3669
|
+
hint: "pnpm db:reset recreates it \u2014 this deletes all local data in it. Qore never runs it for you.",
|
|
3670
|
+
details
|
|
3671
|
+
});
|
|
3672
|
+
}
|
|
3180
3673
|
|
|
3181
|
-
// src/core/
|
|
3182
|
-
function
|
|
3183
|
-
const
|
|
3184
|
-
if (
|
|
3185
|
-
|
|
3186
|
-
if (
|
|
3187
|
-
|
|
3188
|
-
return [];
|
|
3674
|
+
// src/core/diagnostics/ports.ts
|
|
3675
|
+
function parseDockerPs(stdout) {
|
|
3676
|
+
const line2 = stdout.split(/\r?\n/).find((l) => l.trim());
|
|
3677
|
+
if (!line2) return null;
|
|
3678
|
+
const [id, name, project, workingDir] = line2.split(" ");
|
|
3679
|
+
if (!id || !name) return null;
|
|
3680
|
+
return { id, name, project: project || null, workingDir: workingDir || null };
|
|
3189
3681
|
}
|
|
3190
|
-
function
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
return w.module ? { project: p, module: w.module } : { project: p };
|
|
3682
|
+
function parseLsofCwd(stdout) {
|
|
3683
|
+
const m = stdout.match(/^fcwd\nn(.+)$/m);
|
|
3684
|
+
return m ? m[1] : null;
|
|
3194
3685
|
}
|
|
3195
|
-
function
|
|
3196
|
-
const
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3686
|
+
function parseEtime(s) {
|
|
3687
|
+
const m = s.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/);
|
|
3688
|
+
if (!m) return null;
|
|
3689
|
+
const days = m[1] ? Number(m[1]) : 0;
|
|
3690
|
+
const hours = m[2] ? Number(m[2]) : 0;
|
|
3691
|
+
const minutes = Number(m[3]);
|
|
3692
|
+
const seconds = Number(m[4]);
|
|
3693
|
+
return days * 86400 + hours * 3600 + minutes * 60 + seconds;
|
|
3694
|
+
}
|
|
3695
|
+
function humanAge(seconds) {
|
|
3696
|
+
if (seconds < 60) return `${seconds} s`;
|
|
3697
|
+
if (seconds < 3600) return `${Math.round(seconds / 60)} min`;
|
|
3698
|
+
if (seconds < 86400) return `${Math.round(seconds / 3600)} h`;
|
|
3699
|
+
return `${Math.round(seconds / 86400)} d`;
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
// src/core/diagnostics/pm.ts
|
|
3703
|
+
function pmVersionMismatch(pm, printed) {
|
|
3704
|
+
if (pm.version === null) return null;
|
|
3705
|
+
const lines = printed.split("\n");
|
|
3706
|
+
let found = null;
|
|
3707
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3708
|
+
const line2 = lines[i].trim();
|
|
3709
|
+
if (/^\d+\.\d+\.\d+/.test(line2)) {
|
|
3710
|
+
found = line2;
|
|
3711
|
+
break;
|
|
3201
3712
|
}
|
|
3202
3713
|
}
|
|
3203
|
-
return
|
|
3714
|
+
if (found === null) return null;
|
|
3715
|
+
if (found.split(".")[0] === pm.version.split(".")[0]) return null;
|
|
3716
|
+
return new QoreError("QORE_PACKAGE_MANAGER", `${pm.name} ${found} does not match the pinned ${pm.name}@${pm.version}`, {
|
|
3717
|
+
hint: `Install the pinned major: npm i -g ${pm.name}@${pm.version.split(".")[0]}. (pnpm \u2265 9.7 switches to the pinned version by itself.)`,
|
|
3718
|
+
details: { found, pinned: pm.version }
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
3721
|
+
function classifyInstall(tail, command, root) {
|
|
3722
|
+
const joined = tail.join("\n");
|
|
3723
|
+
if (/ERR_PNPM_OUTDATED_LOCKFILE/.test(joined)) {
|
|
3724
|
+
return new QoreError("QORE_INSTALL_FAILED", "The lockfile does not match package.json", {
|
|
3725
|
+
hint: `Someone committed one without the other. Run \`${command} --no-frozen-lockfile\` in ${root} and commit the lockfile.`,
|
|
3726
|
+
details: { reason: "outdated-lockfile", command, stderr: tail }
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
return new QoreError("QORE_INSTALL_FAILED", `${command} failed`, {
|
|
3730
|
+
hint: `Run \`${command}\` in ${root} to see the full output.`,
|
|
3731
|
+
details: { command, stderr: tail }
|
|
3732
|
+
});
|
|
3204
3733
|
}
|
|
3205
3734
|
|
|
3206
|
-
// src/
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
if (!p || !await new TokenStore(ctx.deps.keychain, profileHash(p.apiOrigin, p.issuer)).getRefresh()) return null;
|
|
3212
|
-
return p.email ?? p.sub ?? portal.apiOrigin;
|
|
3735
|
+
// src/core/diagnostics/versions.ts
|
|
3736
|
+
import semver from "semver";
|
|
3737
|
+
function nodeSatisfies(found, range) {
|
|
3738
|
+
if (semver.validRange(range) === null) return null;
|
|
3739
|
+
return semver.satisfies(found, range);
|
|
3213
3740
|
}
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
return repo !== null && !isInside(repo, ctx.deps.home) ? { repo } : null;
|
|
3741
|
+
function nodeVersionError(found, range, slug) {
|
|
3742
|
+
const min = semver.minVersion(range);
|
|
3743
|
+
const major = min ? min.major : range;
|
|
3744
|
+
return new QoreError("QORE_NODE_VERSION", `${slug} needs Node ${range}, found ${found}`, {
|
|
3745
|
+
hint: `nvm install ${major} / fnm use ${major}, then run qore dev again.`,
|
|
3746
|
+
details: { found, required: range }
|
|
3747
|
+
});
|
|
3222
3748
|
}
|
|
3223
|
-
|
|
3224
|
-
|
|
3749
|
+
|
|
3750
|
+
// src/core/dev/preflight.ts
|
|
3751
|
+
var pmMajor = (v) => v ? v.split(".")[0] : "latest";
|
|
3752
|
+
var GRACE_MS = 1e4;
|
|
3753
|
+
var secs = (ms2) => `${Math.round(ms2 / 1e3)} s`;
|
|
3754
|
+
async function runTool(deps2, cmd, args, o) {
|
|
3755
|
+
if (o.signal.aborted) throw new CancelledError();
|
|
3756
|
+
const tail = [];
|
|
3757
|
+
const c = deps2.launch.launch(cmd, args, { cwd: o.cwd, env: { ...o.env, LC_ALL: "C" }, onLine: (l) => {
|
|
3758
|
+
tail.push(l.line);
|
|
3759
|
+
if (tail.length > 50) tail.shift();
|
|
3760
|
+
o.onLine(l);
|
|
3761
|
+
} });
|
|
3762
|
+
let kill;
|
|
3763
|
+
const onAbort = () => {
|
|
3764
|
+
c.stop("SIGTERM");
|
|
3765
|
+
kill = setTimeout(() => c.stop("SIGKILL"), GRACE_MS);
|
|
3766
|
+
};
|
|
3767
|
+
o.signal.addEventListener("abort", onAbort, { once: true });
|
|
3768
|
+
let exit;
|
|
3225
3769
|
try {
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
return isForeignRepo(e);
|
|
3770
|
+
exit = await c.exited;
|
|
3771
|
+
} finally {
|
|
3772
|
+
o.signal.removeEventListener("abort", onAbort);
|
|
3773
|
+
clearTimeout(kill);
|
|
3231
3774
|
}
|
|
3775
|
+
if (o.signal.aborted) throw new CancelledError();
|
|
3776
|
+
return { code: exit.code, spawnError: exit.spawnError, tail };
|
|
3232
3777
|
}
|
|
3233
|
-
async function
|
|
3234
|
-
const
|
|
3235
|
-
|
|
3236
|
-
|
|
3778
|
+
async function checkPackageManager(deps2, r, onLine, signal) {
|
|
3779
|
+
const res = await runTool(deps2, r.pm.name, ["--version"], { cwd: r.root, env: deps2.env, onLine, signal });
|
|
3780
|
+
const hint = `Install it: npm i -g ${r.pm.name}@${pmMajor(r.pm.version)}`;
|
|
3781
|
+
if (res.spawnError === "ENOENT") {
|
|
3782
|
+
throw new QoreError("QORE_PACKAGE_MANAGER", `${r.pm.name} is not installed`, { hint, details: { packageManager: r.pm } });
|
|
3237
3783
|
}
|
|
3238
|
-
|
|
3239
|
-
}
|
|
3240
|
-
async function pathFor(ctx, m, needModule) {
|
|
3241
|
-
const p = m.project;
|
|
3242
|
-
if (p.kind === "module") return p.path;
|
|
3243
|
-
if (m.module) {
|
|
3244
|
-
const dir2 = join18(p.path, m.module);
|
|
3245
|
-
if (!await isDir(dir2)) {
|
|
3246
|
-
throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no checkout of module ${m.module} in ${p.path}`, {
|
|
3247
|
-
hint: `Run \`qore sync\` in ${p.path} to clone the app's missing modules.`
|
|
3248
|
-
});
|
|
3249
|
-
}
|
|
3250
|
-
return dir2;
|
|
3784
|
+
if (res.code !== 0) {
|
|
3785
|
+
throw new QoreError("QORE_PACKAGE_MANAGER", `${r.pm.name} is not installed`, { hint, details: { packageManager: r.pm, stderr: res.tail } });
|
|
3251
3786
|
}
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3787
|
+
const mismatch = pmVersionMismatch(r.pm, res.tail.join("\n"));
|
|
3788
|
+
if (mismatch) throw mismatch;
|
|
3789
|
+
return res.tail.map((l) => l.trim()).findLast((l) => /^\d+\.\d+\.\d+/.test(l)) ?? null;
|
|
3790
|
+
}
|
|
3791
|
+
async function checkNode(deps2, r, pkg, slug, signal) {
|
|
3792
|
+
const range = pkg.engines?.node;
|
|
3793
|
+
if (!range) return null;
|
|
3794
|
+
const res = await runTool(deps2, "node", ["--version"], { cwd: r.root, env: deps2.env, onLine: () => {
|
|
3795
|
+
}, signal });
|
|
3796
|
+
if (res.spawnError === "ENOENT") {
|
|
3797
|
+
throw new QoreError("QORE_NODE_VERSION", "node is not on PATH", {
|
|
3798
|
+
hint: "Install Node (https://nodejs.org) or run nvm use, then reinstall qore.",
|
|
3799
|
+
details: { required: range }
|
|
3258
3800
|
});
|
|
3259
3801
|
}
|
|
3260
|
-
if (
|
|
3261
|
-
throw new QoreError("
|
|
3262
|
-
hint: `Pass ${p.appSlug}/<module>.`,
|
|
3263
|
-
candidates: dirs.map((slug) => ({ kind: "module", slug, tenant: p.tenantSlug, name: join18(p.path, slug) }))
|
|
3264
|
-
});
|
|
3802
|
+
if (res.spawnError || res.code !== 0) {
|
|
3803
|
+
throw new QoreError("QORE_NODE_VERSION", "node --version failed", { details: { required: range, stderr: res.tail } });
|
|
3265
3804
|
}
|
|
3266
|
-
|
|
3805
|
+
const found = (res.tail.at(-1) ?? "").trim().replace(/^v/, "");
|
|
3806
|
+
if (nodeSatisfies(found, range) === false) throw nodeVersionError(found, range, slug);
|
|
3807
|
+
return found;
|
|
3267
3808
|
}
|
|
3268
|
-
async function
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
live.push(m);
|
|
3809
|
+
async function installIfNeeded(deps2, r, slug, onLine, report, signal) {
|
|
3810
|
+
if (!r.install) {
|
|
3811
|
+
report({ module: slug, step: "install", status: "skip", detail: "disabled" });
|
|
3812
|
+
return;
|
|
3273
3813
|
}
|
|
3274
|
-
|
|
3814
|
+
if (!await needsInstall(r.root, r.pm)) {
|
|
3815
|
+
report({ module: slug, step: "install", status: "skip", detail: "up to date" });
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3818
|
+
report({ module: slug, step: "install", status: "start" });
|
|
3819
|
+
const began = deps2.now().getTime();
|
|
3820
|
+
const { cmd, args } = installArgs(r.pm);
|
|
3821
|
+
const res = await runTool(deps2, cmd, args, { cwd: r.root, env: deps2.env, onLine, signal });
|
|
3822
|
+
if (res.code !== 0) {
|
|
3823
|
+
const command = [cmd, ...args].join(" ");
|
|
3824
|
+
const err = classifyInstall(res.tail, command, r.root);
|
|
3825
|
+
if (err.details?.reason === "outdated-lockfile") throw err;
|
|
3826
|
+
throw new QoreError("QORE_INSTALL_FAILED", `${command} failed in ${slug}`, { hint: err.hint, details: err.details });
|
|
3827
|
+
}
|
|
3828
|
+
report({ module: slug, step: "install", status: "done", detail: secs(deps2.now().getTime() - began) });
|
|
3275
3829
|
}
|
|
3276
|
-
async function
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3830
|
+
async function portInUseError(deps2, o) {
|
|
3831
|
+
const owner = await deps2.ports.owner(o.port);
|
|
3832
|
+
const qoreRun = (o.runs ?? []).find((r) => r.children.some((c) => c.port === o.port) || owner !== null && (r.qorePid === owner.pid || r.children.some((c) => c.pid === owner.pid)));
|
|
3833
|
+
if (qoreRun) {
|
|
3834
|
+
return new QoreError("QORE_PORT_IN_USE", `A previous qore dev is still running (pid ${qoreRun.qorePid})`, {
|
|
3835
|
+
hint: `Stop it (Ctrl+C in its terminal, or kill ${qoreRun.qorePid}), then run qore dev again.`,
|
|
3836
|
+
details: { port: o.port, module: o.slug, key: o.key, owner: "qore", pid: qoreRun.qorePid }
|
|
3282
3837
|
});
|
|
3283
3838
|
}
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
}
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
const portal = g.portal ? resolvePortal(g.portal, cfg.defaultProfile).apiOrigin : void 0;
|
|
3294
|
-
if (o.ref) {
|
|
3295
|
-
const matches = matchProjects(await knownProjects(ctx.deps, { portal }), o.ref);
|
|
3296
|
-
if (matches.length === 0) {
|
|
3297
|
-
const parts = o.ref.split("/");
|
|
3298
|
-
const app = parts.length === 1 ? o.ref : parts.length === 3 ? parts[1] : "<app>";
|
|
3299
|
-
throw new QoreError("QORE_PROJECT_NOT_FOUND", `No local project "${o.ref}"`, {
|
|
3300
|
-
hint: cfg.projectsRoot ? `Run \`qore clone ${app}\`.` : `Run \`qore init\`, or \`qore clone ${app}\`.`
|
|
3301
|
-
});
|
|
3839
|
+
if (owner) {
|
|
3840
|
+
if (/^(com\.docker\.backend|com\.docker\.vpnkit|docker-proxy|vpnkit|dockerd)$/.test(owner.command)) {
|
|
3841
|
+
const pub = await deps2.ports.dockerPublisher(o.port);
|
|
3842
|
+
if (pub) {
|
|
3843
|
+
return new QoreError("QORE_PORT_IN_USE", `Port ${o.port} is held by the Docker container ${pub.name} (needed by ${o.slug})`, {
|
|
3844
|
+
hint: `It belongs to compose project ${pub.project} in ${pub.workingDir} \u2014 typically another module's or tenant's database. Stop it there with docker compose down, or docker stop ${pub.name}.`,
|
|
3845
|
+
details: { port: o.port, module: o.slug, key: o.key, owner: "docker", ...pub }
|
|
3846
|
+
});
|
|
3847
|
+
}
|
|
3302
3848
|
}
|
|
3303
|
-
const
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
const known = await knownProjects(ctx.deps, { portal });
|
|
3310
|
-
if (here && (known.length === 0 || known.some((p) => isInside(p.path, here.repo)) || !await isForeign(ctx, o.command ?? "project"))) return ctx;
|
|
3311
|
-
if (known.length === 0) {
|
|
3312
|
-
if (!cfg.projectsRoot) {
|
|
3313
|
-
if (!await loggedInAs(ctx)) return ctx;
|
|
3314
|
-
throw new QoreError("QORE_ROOT_NOT_SET", "Not inside a project, and qore knows no projects yet", {
|
|
3315
|
-
hint: "Run `qore init` to choose a projects folder, or cd into a project."
|
|
3316
|
-
});
|
|
3849
|
+
const detail2 = await deps2.ports.ownerDetail(owner.pid);
|
|
3850
|
+
const extras = [];
|
|
3851
|
+
if (detail2.ageSeconds !== null) extras.push(`started ${humanAge(detail2.ageSeconds)} ago`);
|
|
3852
|
+
if (detail2.cwd !== null) {
|
|
3853
|
+
const cwd = deps2.home && detail2.cwd.startsWith(deps2.home) ? `~${detail2.cwd.slice(deps2.home.length)}` : detail2.cwd;
|
|
3854
|
+
extras.push(`in ${cwd}`);
|
|
3317
3855
|
}
|
|
3318
|
-
|
|
3319
|
-
|
|
3856
|
+
const suffix = extras.length ? `, ${extras.join(" ")}` : "";
|
|
3857
|
+
return new QoreError("QORE_PORT_IN_USE", `Port ${o.port} is already in use (needed by ${o.slug})`, {
|
|
3858
|
+
hint: `Held by: ${owner.command} (pid ${owner.pid})${suffix}. Stop that process. Qore never moves a port: other modules and the local login rely on ${o.port}.`,
|
|
3859
|
+
details: {
|
|
3860
|
+
port: o.port,
|
|
3861
|
+
module: o.slug,
|
|
3862
|
+
key: o.key,
|
|
3863
|
+
owner: "process",
|
|
3864
|
+
pid: owner.pid,
|
|
3865
|
+
command: owner.command,
|
|
3866
|
+
...detail2.cwd !== null ? { cwd: detail2.cwd } : {},
|
|
3867
|
+
...detail2.ageSeconds !== null ? { ageSeconds: detail2.ageSeconds } : {}
|
|
3868
|
+
}
|
|
3320
3869
|
});
|
|
3321
3870
|
}
|
|
3322
|
-
const
|
|
3323
|
-
|
|
3324
|
-
|
|
3871
|
+
const findHint = deps2.platform === "win32" ? `netstat -ano | findstr :${o.port}` : `lsof -iTCP:${o.port} -sTCP:LISTEN`;
|
|
3872
|
+
return new QoreError("QORE_PORT_IN_USE", `Port ${o.port} is already in use (needed by ${o.slug})`, {
|
|
3873
|
+
hint: `Find it with: ${findHint}`,
|
|
3874
|
+
details: { port: o.port, module: o.slug, key: o.key }
|
|
3875
|
+
});
|
|
3325
3876
|
}
|
|
3326
|
-
function
|
|
3327
|
-
if (
|
|
3328
|
-
|
|
3329
|
-
|
|
3877
|
+
async function composeFailure(deps2, r, slug, env, signal, message, command, stderr) {
|
|
3878
|
+
if (!r.dockerServices) {
|
|
3879
|
+
return new QoreError("QORE_SERVICE_UNHEALTHY", message, { details: { command, stderr } });
|
|
3880
|
+
}
|
|
3881
|
+
const logs = await runTool(deps2, "docker", ["compose", "logs", "--no-color", "--tail", "50"], { cwd: r.root, env, onLine: () => {
|
|
3882
|
+
}, signal });
|
|
3883
|
+
const dataErr = classifyComposeLogs(logs.tail, { slug, script: "db:up" });
|
|
3884
|
+
if (dataErr) return dataErr;
|
|
3885
|
+
return new QoreError("QORE_SERVICE_UNHEALTHY", message, {
|
|
3886
|
+
hint: `The last lines of docker compose logs are in details.logs; run \`docker compose logs\` in ${r.root} for all of them.`,
|
|
3887
|
+
details: { command, stderr, logs: logs.tail.slice(-20) }
|
|
3888
|
+
});
|
|
3330
3889
|
}
|
|
3331
|
-
async function
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
await remember(ctx.deps, {
|
|
3335
|
-
kind: "workspace",
|
|
3336
|
-
path: mc.workspace.root,
|
|
3337
|
-
portal: mc.workspace.portal,
|
|
3338
|
-
tenantSlug,
|
|
3339
|
-
appId: app.id,
|
|
3340
|
-
appSlug: app.slug
|
|
3341
|
-
});
|
|
3890
|
+
async function startServices(deps2, r, slug, env, dbPort2, waitMs, signal, onLine, report) {
|
|
3891
|
+
if (r.services.length === 0) {
|
|
3892
|
+
report({ module: slug, step: "services", status: "skip" });
|
|
3342
3893
|
return;
|
|
3343
3894
|
}
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3895
|
+
report({ module: slug, step: "services", status: "start", ...dbPort2 !== null ? { detail: `:${dbPort2}` } : {} });
|
|
3896
|
+
const began = deps2.now().getTime();
|
|
3897
|
+
if (r.dockerServices) {
|
|
3898
|
+
const res = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], { cwd: r.root, env, onLine, signal });
|
|
3899
|
+
const dockerErr = classifyDockerInfo(res, deps2.platform);
|
|
3900
|
+
if (dockerErr) throw dockerErr;
|
|
3901
|
+
const compose = await runTool(deps2, "docker", ["compose", "version"], { cwd: r.root, env, onLine: () => {
|
|
3902
|
+
}, signal });
|
|
3903
|
+
const composeErr = classifyComposeVersion(compose);
|
|
3904
|
+
if (composeErr) throw composeErr;
|
|
3905
|
+
}
|
|
3906
|
+
for (const script of r.services) {
|
|
3907
|
+
const { cmd, args } = runArgs(r.pm, script);
|
|
3908
|
+
const res = await runTool(deps2, cmd, args, { cwd: r.root, env, onLine, signal });
|
|
3909
|
+
if (res.code !== 0) {
|
|
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" });
|
|
3912
|
+
}
|
|
3913
|
+
throw await composeFailure(deps2, r, slug, env, signal, `${script} failed in ${slug}`, `${cmd} ${args.join(" ")}`, res.tail);
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
if (dbPort2 !== null) {
|
|
3917
|
+
const ok2 = await deps2.ports.waitFor(dbPort2, waitMs, signal);
|
|
3918
|
+
if (signal.aborted) throw new CancelledError();
|
|
3919
|
+
if (!ok2) {
|
|
3920
|
+
throw await composeFailure(
|
|
3921
|
+
deps2,
|
|
3922
|
+
r,
|
|
3923
|
+
slug,
|
|
3924
|
+
env,
|
|
3925
|
+
signal,
|
|
3926
|
+
`The database for ${slug} did not accept connections on port ${dbPort2} within ${waitMs / 1e3} s`,
|
|
3927
|
+
void 0,
|
|
3928
|
+
[]
|
|
3929
|
+
);
|
|
3930
|
+
}
|
|
3355
3931
|
}
|
|
3932
|
+
const took = secs(deps2.now().getTime() - began);
|
|
3933
|
+
report({ module: slug, step: "services", status: "done", detail: dbPort2 !== null ? `:${dbPort2} (${took})` : `(${took})` });
|
|
3356
3934
|
}
|
|
3357
|
-
async function
|
|
3358
|
-
if (
|
|
3359
|
-
|
|
3935
|
+
async function checkPort(deps2, o) {
|
|
3936
|
+
if (o.port === null) return;
|
|
3937
|
+
if (!await deps2.ports.inUse(o.port)) return;
|
|
3938
|
+
throw await portInUseError(deps2, { slug: o.slug, port: o.port, key: o.key, runs: o.runs });
|
|
3360
3939
|
}
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3940
|
+
function abortableDelay(signal) {
|
|
3941
|
+
return (ms2) => new Promise((resolve7) => {
|
|
3942
|
+
const done = () => {
|
|
3943
|
+
clearTimeout(t);
|
|
3944
|
+
signal.removeEventListener("abort", done);
|
|
3945
|
+
resolve7();
|
|
3946
|
+
};
|
|
3947
|
+
const t = setTimeout(done, ms2);
|
|
3948
|
+
signal.addEventListener("abort", done, { once: true });
|
|
3949
|
+
});
|
|
3367
3950
|
}
|
|
3368
|
-
async function
|
|
3369
|
-
const
|
|
3370
|
-
const
|
|
3371
|
-
const
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
if (
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
if (allOfKind(e.candidates, "tenant")) {
|
|
3386
|
-
const tenant = await pick(ctx, e, "Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.id })));
|
|
3387
|
-
return resolveModuleContext(ctx.deps, { ...base, tenant });
|
|
3951
|
+
async function waitForDocker(deps2, o) {
|
|
3952
|
+
const delay = o.delay ?? abortableDelay(o.signal);
|
|
3953
|
+
const now = o.now ?? Date.now;
|
|
3954
|
+
const deadline = now() + o.timeoutMs;
|
|
3955
|
+
for (; ; ) {
|
|
3956
|
+
await delay(Math.min(o.pollMs, Math.max(0, deadline - now())));
|
|
3957
|
+
if (o.signal.aborted) throw new CancelledError();
|
|
3958
|
+
const remaining = deadline - now();
|
|
3959
|
+
if (remaining <= 0) return false;
|
|
3960
|
+
const poll = AbortSignal.any([o.signal, AbortSignal.timeout(remaining)]);
|
|
3961
|
+
let res;
|
|
3962
|
+
try {
|
|
3963
|
+
res = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], { cwd: o.cwd, env: o.env ?? deps2.env, onLine: () => {
|
|
3964
|
+
}, signal: poll });
|
|
3965
|
+
} catch (e) {
|
|
3966
|
+
if (e instanceof CancelledError && !o.signal.aborted) return false;
|
|
3967
|
+
throw e;
|
|
3388
3968
|
}
|
|
3389
|
-
|
|
3969
|
+
if (classifyDockerInfo(res, deps2.platform) === null) return true;
|
|
3390
3970
|
}
|
|
3391
3971
|
}
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3972
|
+
var MAX_PARENT_HOPS = 10;
|
|
3973
|
+
var AGE_SLACK_S = 5;
|
|
3974
|
+
async function leftoverOwnsPort(deps2, run4, port) {
|
|
3975
|
+
if (deps2.platform === "win32" || !deps2.launch.signal(run4.qorePid, 0)) return false;
|
|
3976
|
+
const { ageSeconds } = await deps2.ports.ownerDetail(run4.qorePid);
|
|
3977
|
+
const recordAge = (deps2.now().getTime() - Date.parse(run4.startedAt)) / 1e3;
|
|
3978
|
+
if (ageSeconds === null || !Number.isFinite(recordAge) || ageSeconds < recordAge - AGE_SLACK_S) return false;
|
|
3979
|
+
const ours = /* @__PURE__ */ new Set([run4.qorePid, ...run4.children.map((c) => c.pid).filter((p) => p !== null)]);
|
|
3980
|
+
let pid = (await deps2.ports.owner(port))?.pid ?? null;
|
|
3981
|
+
for (let hop = 0; pid !== null && pid > 1 && hop <= MAX_PARENT_HOPS; hop++) {
|
|
3982
|
+
if (ours.has(pid)) return true;
|
|
3983
|
+
pid = await deps2.ports.parentPid(pid);
|
|
3399
3984
|
}
|
|
3985
|
+
return false;
|
|
3400
3986
|
}
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3987
|
+
|
|
3988
|
+
// src/core/dev/providers.ts
|
|
3989
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
3990
|
+
function connPorts(env) {
|
|
3991
|
+
const out = [];
|
|
3992
|
+
for (const key2 of Object.keys(env).sort()) {
|
|
3993
|
+
const m = /^CONN_(.+)_URL$/.exec(key2);
|
|
3994
|
+
if (!m) continue;
|
|
3995
|
+
let url;
|
|
3996
|
+
try {
|
|
3997
|
+
url = new URL(env[key2].trim().replace(/^["']|["']$/g, ""));
|
|
3998
|
+
} catch {
|
|
3999
|
+
continue;
|
|
4000
|
+
}
|
|
4001
|
+
const port = Number(url.port);
|
|
4002
|
+
if (port > 0 && LOCAL_HOSTS.has(url.hostname)) out.push({ key: key2, alias: m[1], port });
|
|
4003
|
+
}
|
|
4004
|
+
return out;
|
|
3405
4005
|
}
|
|
3406
|
-
function
|
|
3407
|
-
|
|
3408
|
-
return `\u2192 ${parts.join(" / ")} (${sourceText(mc)})`;
|
|
4006
|
+
function deadAliases(env, listening) {
|
|
4007
|
+
return connPorts(env).filter((c) => !listening.has(c.port)).map((c) => c.key);
|
|
3409
4008
|
}
|
|
3410
|
-
function
|
|
3411
|
-
const {
|
|
4009
|
+
function hintContext(recipe, pkg, env) {
|
|
4010
|
+
const { cmd, args } = installArgs(recipe.pm);
|
|
4011
|
+
const ports = /* @__PURE__ */ new Map();
|
|
4012
|
+
for (const c of connPorts(env)) ports.set(c.port, { kind: "provider", key: c.key });
|
|
4013
|
+
const db = envPort(env, "DB_PORT");
|
|
4014
|
+
if (db !== null) ports.set(db, { kind: "database" });
|
|
3412
4015
|
return {
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
4016
|
+
installCommand: [cmd, ...args].join(" "),
|
|
4017
|
+
runCommand: (script) => {
|
|
4018
|
+
if (pkg.scripts?.[script] === void 0) return null;
|
|
4019
|
+
const r = runArgs(recipe.pm, script);
|
|
4020
|
+
return [r.cmd, ...r.args].join(" ");
|
|
4021
|
+
},
|
|
4022
|
+
ports
|
|
3419
4023
|
};
|
|
3420
4024
|
}
|
|
3421
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) };
|
|
4033
|
+
}
|
|
4034
|
+
async function writeRunFile(deps2, root, rec) {
|
|
4035
|
+
const { dir: dir2, name } = await runFilePath(deps2, root);
|
|
4036
|
+
await writeJsonAtomic(dir2, name, rec);
|
|
4037
|
+
}
|
|
4038
|
+
async function removeRunFile(deps2, root) {
|
|
4039
|
+
const { path: path2 } = await runFilePath(deps2, root);
|
|
4040
|
+
await unlink2(path2).catch(() => {
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
async function leftoverRun(deps2, root, o = {}) {
|
|
4044
|
+
const { path: path2 } = await runFilePath(deps2, root);
|
|
4045
|
+
const rec = await readJson(path2).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(path2).catch(() => {
|
|
4049
|
+
});
|
|
4050
|
+
return null;
|
|
4051
|
+
}
|
|
4052
|
+
|
|
3422
4053
|
// src/cli/findings.ts
|
|
3423
4054
|
function canOffer(ctx) {
|
|
3424
4055
|
const g = ctx.opts();
|
|
@@ -3564,33 +4195,10 @@ async function inPreflight(signals, outer, fn) {
|
|
|
3564
4195
|
}
|
|
3565
4196
|
}
|
|
3566
4197
|
|
|
3567
|
-
// src/cli/trustPrompt.ts
|
|
3568
|
-
async function ensureTrusted(ctx, mc, app, o) {
|
|
3569
|
-
const root = mc.repoRoot ?? ctx.deps.cwd;
|
|
3570
|
-
if (!ctx.opts().json) ctx.io.err(contextLine(mc, app));
|
|
3571
|
-
if (await isTrusted(ctx.deps, root, mc.module.id)) return;
|
|
3572
|
-
if (o.yes) {
|
|
3573
|
-
await trust(ctx.deps, root, mc.module.id);
|
|
3574
|
-
return;
|
|
3575
|
-
}
|
|
3576
|
-
if (!ctx.io.interactive) {
|
|
3577
|
-
throw new QoreError("QORE_CONTEXT_REQUIRED", `This repository is not trusted for ${mc.module.slug} yet`, {
|
|
3578
|
-
hint: "Run interactively once, or pass --yes.",
|
|
3579
|
-
details: { reason: "untrusted", repository: root }
|
|
3580
|
-
});
|
|
3581
|
-
}
|
|
3582
|
-
if (mc.source.kind === "remote") {
|
|
3583
|
-
const key = repoKey(mc.source.url);
|
|
3584
|
-
ctx.io.err(` from remote ${mc.source.remote}: ${key ?? "(unparsable remote URL)"}`);
|
|
3585
|
-
}
|
|
3586
|
-
if (!await ctx.io.confirm("Pull local secrets for this module into this repository?")) throw new CancelledError();
|
|
3587
|
-
await trust(ctx.deps, root, mc.module.id);
|
|
3588
|
-
}
|
|
3589
|
-
|
|
3590
4198
|
// src/cli/commands/devWorkspace.ts
|
|
3591
4199
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
3592
4200
|
import { realpath as realpath7 } from "fs/promises";
|
|
3593
|
-
import { join as
|
|
4201
|
+
import { join as join22, relative as relative6 } from "path";
|
|
3594
4202
|
|
|
3595
4203
|
// src/core/dev/order.ts
|
|
3596
4204
|
function selectSkips(modules, sel) {
|
|
@@ -3747,15 +4355,15 @@ function supervise(o) {
|
|
|
3747
4355
|
});
|
|
3748
4356
|
const readiness = /* @__PURE__ */ new Map();
|
|
3749
4357
|
for (const m of o.modules) {
|
|
3750
|
-
let
|
|
4358
|
+
let resolve7;
|
|
3751
4359
|
let settled = false;
|
|
3752
4360
|
const promise = new Promise((r) => {
|
|
3753
|
-
|
|
4361
|
+
resolve7 = r;
|
|
3754
4362
|
});
|
|
3755
4363
|
readiness.set(m.slug, { promise, settle: (r) => {
|
|
3756
4364
|
if (settled) return false;
|
|
3757
4365
|
settled = true;
|
|
3758
|
-
|
|
4366
|
+
resolve7(r);
|
|
3759
4367
|
return true;
|
|
3760
4368
|
} });
|
|
3761
4369
|
}
|
|
@@ -3847,7 +4455,7 @@ function supervise(o) {
|
|
|
3847
4455
|
|
|
3848
4456
|
// src/core/workspace/stray.ts
|
|
3849
4457
|
import { lstat as lstat2, readdir as readdir3 } from "fs/promises";
|
|
3850
|
-
import { join as
|
|
4458
|
+
import { join as join21 } from "path";
|
|
3851
4459
|
async function strayRepos(root, slugs) {
|
|
3852
4460
|
let entries;
|
|
3853
4461
|
try {
|
|
@@ -3859,7 +4467,7 @@ async function strayRepos(root, slugs) {
|
|
|
3859
4467
|
for (const name of entries) {
|
|
3860
4468
|
if (slugs.has(name)) continue;
|
|
3861
4469
|
try {
|
|
3862
|
-
await lstat2(
|
|
4470
|
+
await lstat2(join21(root, name, ".git"));
|
|
3863
4471
|
names.push(name);
|
|
3864
4472
|
} catch {
|
|
3865
4473
|
}
|
|
@@ -3930,12 +4538,12 @@ function registerRun(program, ctx) {
|
|
|
3930
4538
|
|
|
3931
4539
|
// src/cli/commands/devWorkspace.ts
|
|
3932
4540
|
var INSTALL_PARALLELISM = 4;
|
|
3933
|
-
var
|
|
4541
|
+
var str2 = (v) => typeof v === "string" && v.length > 0;
|
|
3934
4542
|
var slugList = (raw) => raw?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3935
4543
|
function usableLast(last, appId) {
|
|
3936
4544
|
if (!last || last.kind !== "workspace") return false;
|
|
3937
4545
|
const a = last.app;
|
|
3938
|
-
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));
|
|
3939
4547
|
}
|
|
3940
4548
|
async function workspaceContext(ctx, ws) {
|
|
3941
4549
|
const { deps: deps2 } = ctx;
|
|
@@ -4021,7 +4629,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
4021
4629
|
continue;
|
|
4022
4630
|
}
|
|
4023
4631
|
try {
|
|
4024
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
4632
|
+
dirs[m.slug] = await deps2.git.inspect(join22(ws.root, m.slug));
|
|
4025
4633
|
safe.push(m);
|
|
4026
4634
|
} catch (e) {
|
|
4027
4635
|
if (!(e instanceof QoreError) || e.code === "QORE_GIT_UNAVAILABLE") throw e;
|
|
@@ -4058,7 +4666,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
4058
4666
|
const results = await applyActions(deps2, { portal: online.portal, app: am, actions, protocol: cloneProtocol(dirs), onResult: () => {
|
|
4059
4667
|
} });
|
|
4060
4668
|
renderResults(io2, results, false);
|
|
4061
|
-
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));
|
|
4062
4670
|
({ present, plan } = computePlan());
|
|
4063
4671
|
}
|
|
4064
4672
|
for (const name of await strayRepos(ws.root, new Set(am.modules.map((m) => m.slug)))) renderFinding(ctx, strayClone(name, am.app.name));
|
|
@@ -4239,7 +4847,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
4239
4847
|
if (online && units.every((u) => !u.dev.offline)) {
|
|
4240
4848
|
const envFile = (m) => {
|
|
4241
4849
|
const u = units.find((x) => x.m.id === m.id);
|
|
4242
|
-
return u ? relative6(rootReal, u.dev.envFile) :
|
|
4850
|
+
return u ? relative6(rootReal, u.dev.envFile) : join22(m.slug, moduleDefaults(m.type).envFile);
|
|
4243
4851
|
};
|
|
4244
4852
|
await saveLastStart(deps2, ws.root, {
|
|
4245
4853
|
schemaVersion: 1,
|
|
@@ -4450,7 +5058,7 @@ function parseWaitTimeout(raw) {
|
|
|
4450
5058
|
}
|
|
4451
5059
|
return n;
|
|
4452
5060
|
}
|
|
4453
|
-
var
|
|
5061
|
+
var str3 = (v) => typeof v === "string" && v.length > 0;
|
|
4454
5062
|
var SHELL_SAFE = /^[A-Za-z0-9_./,:@%+=-]*$/;
|
|
4455
5063
|
var shellQuote = (s) => SHELL_SAFE.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
|
|
4456
5064
|
function cdAndDev(home, dir2) {
|
|
@@ -4478,7 +5086,7 @@ async function providerFindings(ctx, root, mc, app, env) {
|
|
|
4478
5086
|
let fix = `qore clone ${app.slug}`;
|
|
4479
5087
|
if (ws && ws.appId === app.id) fix = cdAndDev(deps2.home, ws.root);
|
|
4480
5088
|
else if (providers.length === 1) {
|
|
4481
|
-
const sibling =
|
|
5089
|
+
const sibling = join23(dirname7(root), providers[0].slug);
|
|
4482
5090
|
if ((await deps2.git.inspect(sibling)).isRepo) fix = cdAndDev(deps2.home, sibling);
|
|
4483
5091
|
}
|
|
4484
5092
|
return providerDown({
|
|
@@ -4496,7 +5104,7 @@ async function providerFindings(ctx, root, mc, app, env) {
|
|
|
4496
5104
|
function usableLast2(last) {
|
|
4497
5105
|
if (!last || last.kind !== "module") return false;
|
|
4498
5106
|
const m = last.module;
|
|
4499
|
-
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);
|
|
4500
5108
|
}
|
|
4501
5109
|
async function devContext(ctx, f, root) {
|
|
4502
5110
|
let mc;
|
|
@@ -4672,8 +5280,8 @@ function registerDebugTelemetry(program, ctx) {
|
|
|
4672
5280
|
import pc3 from "picocolors";
|
|
4673
5281
|
|
|
4674
5282
|
// src/core/doctor/checks.ts
|
|
4675
|
-
import { realpath as realpath9, stat as
|
|
4676
|
-
import { basename as
|
|
5283
|
+
import { realpath as realpath9, stat as stat6 } from "fs/promises";
|
|
5284
|
+
import { basename as basename4, join as join24 } from "path";
|
|
4677
5285
|
|
|
4678
5286
|
// src/core/diagnostics/ssh.ts
|
|
4679
5287
|
var SSH_TEST_ARGS = ["-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "git@github.com"];
|
|
@@ -4816,9 +5424,9 @@ function jwtExp(token) {
|
|
|
4816
5424
|
}
|
|
4817
5425
|
function validDays(rt, now) {
|
|
4818
5426
|
const exp = jwtExp(rt.rt);
|
|
4819
|
-
const
|
|
4820
|
-
if (Number.isNaN(
|
|
4821
|
-
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));
|
|
4822
5430
|
}
|
|
4823
5431
|
async function checkSession(deps2, portal, onRequest, sessions, selected, signal) {
|
|
4824
5432
|
try {
|
|
@@ -4874,7 +5482,7 @@ async function moduleBody(deps2, m, runs, signal) {
|
|
|
4874
5482
|
if (text2 === null) return { status: "warn", label: "port unknown (run qore env pull)" };
|
|
4875
5483
|
env = Object.fromEntries(parseEnv(text2));
|
|
4876
5484
|
const port = envPort(env, "PORT");
|
|
4877
|
-
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)})` };
|
|
4878
5486
|
if (!await deps2.ports.inUse(port)) return { status: "ok", label: `port ${port} free` };
|
|
4879
5487
|
const e = await portInUseError(deps2, { slug: m.slug, port, key: "PORT", runs: await runs() });
|
|
4880
5488
|
return { status: "fail", label: `port ${port} in use${ownerText(e)}`, error: e };
|
|
@@ -4893,13 +5501,13 @@ async function moduleBody(deps2, m, runs, signal) {
|
|
|
4893
5501
|
flags: { install: true, services: true }
|
|
4894
5502
|
});
|
|
4895
5503
|
if (recipe.services.length === 0) return { status: "ok", label: "db none" };
|
|
4896
|
-
const
|
|
4897
|
-
if (
|
|
4898
|
-
if (!await deps2.ports.inUse(
|
|
4899
|
-
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);
|
|
4900
5508
|
if (pub?.workingDir && await sameDir(pub.workingDir, m.root)) return { status: "ok", label: "db running" };
|
|
4901
|
-
const e = await portInUseError(deps2, { slug: m.slug, port:
|
|
4902
|
-
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 };
|
|
4903
5511
|
});
|
|
4904
5512
|
const status2 = parts.reduce((w, p) => RANK[p.status] > RANK[w] ? p.status : w, "ok");
|
|
4905
5513
|
const shown = parts.filter((p) => p.status !== "ok");
|
|
@@ -4956,7 +5564,7 @@ async function resolveRepoModule(deps2, session, portal, meta, root) {
|
|
|
4956
5564
|
async function checkProjectsRoot(cfg) {
|
|
4957
5565
|
const root = cfg?.projectsRoot;
|
|
4958
5566
|
if (!root) return ok("projects folder not set (optional \u2014 qore init)");
|
|
4959
|
-
const s = await
|
|
5567
|
+
const s = await stat6(root).catch(() => null);
|
|
4960
5568
|
if (!s?.isDirectory()) return { status: "warn", label: `projects folder ${root} does not exist \u2014 qore init` };
|
|
4961
5569
|
const e = await writableError(root);
|
|
4962
5570
|
if (e) return { status: "fail", label: `projects folder ${root} is not writable (${e.code ?? e.message})` };
|
|
@@ -5059,7 +5667,7 @@ async function runDoctor(deps2, o = {}) {
|
|
|
5059
5667
|
const repoRoot = wsRoot ? null : await findRepoRoot(deps2.cwd);
|
|
5060
5668
|
if (wsRoot) {
|
|
5061
5669
|
context.kind = "workspace";
|
|
5062
|
-
context.label =
|
|
5670
|
+
context.label = basename4(wsRoot.root);
|
|
5063
5671
|
if (!session) {
|
|
5064
5672
|
await notChecked();
|
|
5065
5673
|
return { results, context };
|
|
@@ -5079,7 +5687,7 @@ async function runDoctor(deps2, o = {}) {
|
|
|
5079
5687
|
for (const m of am.modules) await workspaceModule(deps2, wsRoot, am, m, runs, signal, push, perModule);
|
|
5080
5688
|
} else if (repoRoot) {
|
|
5081
5689
|
context.kind = "module";
|
|
5082
|
-
context.label =
|
|
5690
|
+
context.label = basename4(repoRoot);
|
|
5083
5691
|
if (!session) {
|
|
5084
5692
|
await notChecked();
|
|
5085
5693
|
return { results, context };
|
|
@@ -5110,7 +5718,7 @@ async function workspaceModule(deps2, ws, am, m, runs, signal, push, perModule)
|
|
|
5110
5718
|
await push(`module:${m.slug}`, "workspace", () => ({ status: "warn", label: "not a safe directory name \u2014 skipped" }), m.slug);
|
|
5111
5719
|
return;
|
|
5112
5720
|
}
|
|
5113
|
-
const mod = { root:
|
|
5721
|
+
const mod = { root: join24(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
|
|
5114
5722
|
let prepared = null;
|
|
5115
5723
|
await push(`module:${m.slug}`, "workspace", async () => {
|
|
5116
5724
|
const info = await deps2.git.inspect(mod.root);
|
|
@@ -5142,7 +5750,7 @@ function registerDoctor(program, ctx) {
|
|
|
5142
5750
|
// src/cli/commands/env.ts
|
|
5143
5751
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
5144
5752
|
import { chmod as chmod3, realpath as realpath10 } from "fs/promises";
|
|
5145
|
-
import { dirname as dirname8, join as
|
|
5753
|
+
import { dirname as dirname8, join as join25, relative as relative8 } from "path";
|
|
5146
5754
|
var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.length} chars)`;
|
|
5147
5755
|
function registerEnv(program, ctx) {
|
|
5148
5756
|
const env = program.command("env").description("Local environment");
|
|
@@ -5161,7 +5769,7 @@ function registerEnv(program, ctx) {
|
|
|
5161
5769
|
const target = f.target ? await resolveTarget(mc.repoRoot, f.target, deps2.cwd) : await resolveTarget(mc.repoRoot, cfg.envFile ?? defaults.envFile);
|
|
5162
5770
|
await ensureTrusted(c, mc, app, { yes: f.yes });
|
|
5163
5771
|
const existing = await readOrNull(target);
|
|
5164
|
-
const example = existing === null ? await readOrNull(
|
|
5772
|
+
const example = existing === null ? await readOrNull(join25(dirname8(target), ".env.example")) : null;
|
|
5165
5773
|
const local = await fetchLocalEnv(deps2, mc, app);
|
|
5166
5774
|
const inputs = {
|
|
5167
5775
|
existing,
|
|
@@ -5177,13 +5785,13 @@ function registerEnv(program, ctx) {
|
|
|
5177
5785
|
const current = parseEnv(existing ?? example ?? "");
|
|
5178
5786
|
const choices = {};
|
|
5179
5787
|
let bulk;
|
|
5180
|
-
for (const
|
|
5788
|
+
for (const key2 of plan.conflicts) {
|
|
5181
5789
|
if (bulk) {
|
|
5182
|
-
choices[
|
|
5790
|
+
choices[key2] = bulk;
|
|
5183
5791
|
continue;
|
|
5184
5792
|
}
|
|
5185
5793
|
const pick2 = await io2.select(
|
|
5186
|
-
`${
|
|
5794
|
+
`${key2} differs (local ${maskLen(current.get(key2))}, portal ${maskLen(local.env[key2])})`,
|
|
5187
5795
|
[
|
|
5188
5796
|
{ name: "keep local", value: "keep" },
|
|
5189
5797
|
{ name: "use portal", value: "portal" },
|
|
@@ -5193,7 +5801,7 @@ function registerEnv(program, ctx) {
|
|
|
5193
5801
|
);
|
|
5194
5802
|
if (pick2 === "all") bulk = "use-portal";
|
|
5195
5803
|
if (pick2 === "skip") bulk = "keep";
|
|
5196
|
-
choices[
|
|
5804
|
+
choices[key2] = pick2 === "portal" || pick2 === "all" ? "use-portal" : "keep";
|
|
5197
5805
|
}
|
|
5198
5806
|
plan = planPull({ ...inputs, choices });
|
|
5199
5807
|
}
|
|
@@ -6097,6 +6705,179 @@ function registerLogout(program, ctx) {
|
|
|
6097
6705
|
});
|
|
6098
6706
|
}
|
|
6099
6707
|
|
|
6708
|
+
// src/core/roles/api.ts
|
|
6709
|
+
var CAPABILITY = "module.local-roles";
|
|
6710
|
+
var path = (mc, app) => `/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/local-roles?app_id=${encodeURIComponent(app.id)}`;
|
|
6711
|
+
async function fetchLocalRoles(deps2, mc, app) {
|
|
6712
|
+
await ensureCapability(deps2, mc.portal.apiOrigin, CAPABILITY);
|
|
6713
|
+
return explained(mc, () => mc.session.api.get(path(mc, app)));
|
|
6714
|
+
}
|
|
6715
|
+
async function useLocalRoles(deps2, mc, app, roles, use) {
|
|
6716
|
+
await ensureCapability(deps2, mc.portal.apiOrigin, CAPABILITY);
|
|
6717
|
+
return explained(mc, () => mc.session.api.post(path(mc, app), { roles, use }));
|
|
6718
|
+
}
|
|
6719
|
+
async function explained(mc, call) {
|
|
6720
|
+
try {
|
|
6721
|
+
return await call();
|
|
6722
|
+
} catch (e) {
|
|
6723
|
+
if (!(e instanceof QoreError)) throw e;
|
|
6724
|
+
if (e.serverCode === "app.access.denied") {
|
|
6725
|
+
throw withDetails(
|
|
6726
|
+
e,
|
|
6727
|
+
{ required: "write", have: mc.module.access },
|
|
6728
|
+
`You have ${mc.module.access} on ${mc.module.slug}; local roles need write. Ask a maintainer of the app's dev team for write access.`
|
|
6729
|
+
);
|
|
6730
|
+
}
|
|
6731
|
+
if (e.serverCode === "membership.permission.denied") {
|
|
6732
|
+
throw withDetails(e, {}, "Taking roles on this app needs write on one of its dev teams.");
|
|
6733
|
+
}
|
|
6734
|
+
if (e.serverCode === "auth.scope.insufficient") throw withDetails(e, {}, "Run `qore login` again.");
|
|
6735
|
+
if (e.serverCode === "app.no_platform_identity") {
|
|
6736
|
+
throw new QoreError("QORE_CONTEXT_UNSUPPORTED", "This app manages its own users; the portal hands out no roles for it", {
|
|
6737
|
+
hint: "Give yourself the role in the app's own user admin.",
|
|
6738
|
+
serverCode: e.serverCode,
|
|
6739
|
+
requestId: e.requestId
|
|
6740
|
+
});
|
|
6741
|
+
}
|
|
6742
|
+
throw e;
|
|
6743
|
+
}
|
|
6744
|
+
}
|
|
6745
|
+
|
|
6746
|
+
// src/core/roles/read.ts
|
|
6747
|
+
import { join as join26 } from "path";
|
|
6748
|
+
|
|
6749
|
+
// src/core/roles/extract.ts
|
|
6750
|
+
var KNOWN = { user: "User", admin: "App admin" };
|
|
6751
|
+
function defaultLabel(slug) {
|
|
6752
|
+
return KNOWN[slug] ?? slug.charAt(0).toUpperCase() + slug.slice(1);
|
|
6753
|
+
}
|
|
6754
|
+
async function extractRoles(source) {
|
|
6755
|
+
const { default: ts } = await import("typescript");
|
|
6756
|
+
const sf = ts.createSourceFile("permissions.ts", source, ts.ScriptTarget.Latest, true);
|
|
6757
|
+
let obj;
|
|
6758
|
+
const visit = (node) => {
|
|
6759
|
+
const first = ts.isCallExpression(node) ? node.arguments[0] : void 0;
|
|
6760
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "definePermissions" && first !== void 0 && ts.isObjectLiteralExpression(first)) {
|
|
6761
|
+
obj = first;
|
|
6762
|
+
}
|
|
6763
|
+
if (!obj) ts.forEachChild(node, visit);
|
|
6764
|
+
};
|
|
6765
|
+
visit(sf);
|
|
6766
|
+
if (!obj) return null;
|
|
6767
|
+
const slugs = [];
|
|
6768
|
+
for (const prop of obj.properties) {
|
|
6769
|
+
if (ts.isPropertyAssignment(prop) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))) {
|
|
6770
|
+
slugs.push(prop.name.text);
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
return slugs.map((slug) => ({ slug, label: defaultLabel(slug) }));
|
|
6774
|
+
}
|
|
6775
|
+
|
|
6776
|
+
// src/core/roles/read.ts
|
|
6777
|
+
var PERMISSIONS_FILE = "src/lib/permissions.ts";
|
|
6778
|
+
var SLUG = /^[a-z0-9][a-z0-9_-]*$/;
|
|
6779
|
+
var MAX_SLUG = 64;
|
|
6780
|
+
var MAX_ROLES = 100;
|
|
6781
|
+
async function readCodeRoles(repoRoot) {
|
|
6782
|
+
const source = await readOrNull(join26(repoRoot, PERMISSIONS_FILE));
|
|
6783
|
+
if (source === null) {
|
|
6784
|
+
throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `No ${PERMISSIONS_FILE} in this module`, {
|
|
6785
|
+
hint: "App roles live in the app's frontend module; run this there."
|
|
6786
|
+
});
|
|
6787
|
+
}
|
|
6788
|
+
const roles = await extractRoles(source);
|
|
6789
|
+
if (!roles?.length) {
|
|
6790
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `${PERMISSIONS_FILE} declares no roles in a definePermissions({ ... }) object`, {
|
|
6791
|
+
hint: "The portal reads only an object literal passed straight to definePermissions \u2014 no variable, no spread, no shorthand ({ admin, user })."
|
|
6792
|
+
});
|
|
6793
|
+
}
|
|
6794
|
+
const invalid4 = roles.filter((r) => !SLUG.test(r.slug) || r.slug.length > MAX_SLUG).map((r) => r.slug);
|
|
6795
|
+
if (invalid4.length) {
|
|
6796
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `The portal refuses these role names: ${invalid4.join(", ")}`, {
|
|
6797
|
+
hint: `Use lowercase letters, digits, - and _, starting with a letter or digit, at most ${MAX_SLUG} characters.`,
|
|
6798
|
+
details: { invalid: invalid4 }
|
|
6799
|
+
});
|
|
6800
|
+
}
|
|
6801
|
+
const unique = [...new Map(roles.map((r) => [r.slug, r])).values()];
|
|
6802
|
+
if (unique.length > MAX_ROLES) {
|
|
6803
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `The portal refuses more than ${MAX_ROLES} roles; ${PERMISSIONS_FILE} declares ${unique.length}`, {
|
|
6804
|
+
hint: "Merge roles that grant the same permissions.",
|
|
6805
|
+
details: { count: unique.length, max: MAX_ROLES }
|
|
6806
|
+
});
|
|
6807
|
+
}
|
|
6808
|
+
return unique;
|
|
6809
|
+
}
|
|
6810
|
+
|
|
6811
|
+
// src/cli/commands/roles.ts
|
|
6812
|
+
async function rolesContext(ctx, f, command) {
|
|
6813
|
+
const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true, command });
|
|
6814
|
+
const mc = await moduleContextFor(c, f, command);
|
|
6815
|
+
if (!mc.repoRoot) {
|
|
6816
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", `${command} reads the roles in a module repository`, {
|
|
6817
|
+
hint: "cd into the app's frontend module."
|
|
6818
|
+
});
|
|
6819
|
+
}
|
|
6820
|
+
const app = await appFor(c, mc, f.app);
|
|
6821
|
+
await rememberContext(c, mc, app);
|
|
6822
|
+
return { c, mc, app, code: await readCodeRoles(mc.repoRoot) };
|
|
6823
|
+
}
|
|
6824
|
+
function withFlags(cmd) {
|
|
6825
|
+
return cmd.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, from anywhere (see qore init)");
|
|
6826
|
+
}
|
|
6827
|
+
function registerRoles(program, ctx) {
|
|
6828
|
+
const roles = program.command("roles").description("App roles on your own machine (the local environment)");
|
|
6829
|
+
withFlags(roles.command("list").description("Roles in your code, roles local knows, and yours there")).action(async (f) => {
|
|
6830
|
+
const { c, mc, app, code } = await rolesContext(ctx, f, "roles list");
|
|
6831
|
+
const local = await fetchLocalRoles(c.deps, mc, app);
|
|
6832
|
+
if (c.opts().json) {
|
|
6833
|
+
renderJson(c.io, {
|
|
6834
|
+
context: contextJson(mc, app),
|
|
6835
|
+
code: code.map((r) => r.slug),
|
|
6836
|
+
local: local.roles,
|
|
6837
|
+
direct: local.direct,
|
|
6838
|
+
effective: local.effective
|
|
6839
|
+
});
|
|
6840
|
+
return;
|
|
6841
|
+
}
|
|
6842
|
+
const known = new Set(local.roles.map((r) => r.slug));
|
|
6843
|
+
const fresh = code.filter((r) => !known.has(r.slug)).map((r) => r.slug);
|
|
6844
|
+
c.io.err(contextLine(mc, app));
|
|
6845
|
+
c.io.out(`In your code ${code.map((r) => r.slug).join(", ")}`);
|
|
6846
|
+
c.io.out(`Known on local ${[...known].join(", ") || "\u2014"}`);
|
|
6847
|
+
c.io.out(`Yours on local ${local.effective.join(", ") || "\u2014"}`);
|
|
6848
|
+
if (fresh.length) {
|
|
6849
|
+
c.io.out(`New in your code: ${fresh.join(", ")} \u2014 \`qore roles use <role...>\` announces them and makes them yours on local.`);
|
|
6850
|
+
}
|
|
6851
|
+
});
|
|
6852
|
+
withFlags(roles.command("use").description("Announce your code's roles for local and make exactly these yours there").argument("<role...>", "the roles you want on local")).action(async (wanted, f) => {
|
|
6853
|
+
const { c, mc, app, code } = await rolesContext(ctx, f, "roles use");
|
|
6854
|
+
const inCode = new Set(code.map((r) => r.slug));
|
|
6855
|
+
const unknown = wanted.filter((r) => !inCode.has(r));
|
|
6856
|
+
if (unknown.length) {
|
|
6857
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `Not a role in ${PERMISSIONS_FILE}: ${unknown.join(", ")}`, {
|
|
6858
|
+
details: { unknown, known: [...inCode] }
|
|
6859
|
+
});
|
|
6860
|
+
}
|
|
6861
|
+
const before = await fetchLocalRoles(c.deps, mc, app);
|
|
6862
|
+
const res = await useLocalRoles(c.deps, mc, app, code, [...new Set(wanted)]);
|
|
6863
|
+
const changed = !sameSet(before.direct, res.direct);
|
|
6864
|
+
const viaGroups = res.effective.filter((r) => !res.direct.includes(r));
|
|
6865
|
+
if (c.opts().json) {
|
|
6866
|
+
renderJson(c.io, { context: contextJson(mc, app), added: res.added, direct: res.direct, effective: res.effective });
|
|
6867
|
+
return;
|
|
6868
|
+
}
|
|
6869
|
+
c.io.err(contextLine(mc, app));
|
|
6870
|
+
c.io.out(`\u2713 Your roles on local: ${res.direct.join(", ")}`);
|
|
6871
|
+
if (res.added.length) c.io.out(` New on local: ${res.added.join(", ")} \u2014 teammates can pick it in the Access tab too.`);
|
|
6872
|
+
if (viaGroups.length) c.io.err(`! You also hold ${viaGroups.join(", ")} on local through a group.`);
|
|
6873
|
+
if (changed) c.io.err("! Sign out and back in to your local app: your current session still has your old roles.");
|
|
6874
|
+
});
|
|
6875
|
+
}
|
|
6876
|
+
function sameSet(a, b) {
|
|
6877
|
+
const s = new Set(a);
|
|
6878
|
+
return s.size === new Set(b).size && b.every((x) => s.has(x));
|
|
6879
|
+
}
|
|
6880
|
+
|
|
6100
6881
|
// src/cli/commands/status.ts
|
|
6101
6882
|
var HEAD = ["ENV", "DEPLOY", "COMMIT", "DEPLOYED", "APP HEALTH"];
|
|
6102
6883
|
var envRow = (e, now) => [e.name, e.paused ? "paused" : e.status, e.commit?.sha ?? "\u2013", ago(e.commit?.deployedAt ?? null, now), uptime(e.health)];
|
|
@@ -6171,7 +6952,7 @@ function registerStatus(program, ctx) {
|
|
|
6171
6952
|
}
|
|
6172
6953
|
|
|
6173
6954
|
// src/cli/commands/sync.ts
|
|
6174
|
-
import { join as
|
|
6955
|
+
import { join as join27, relative as relative9 } from "path";
|
|
6175
6956
|
import { Option as Option2 } from "commander";
|
|
6176
6957
|
function asFailure2(slug, e) {
|
|
6177
6958
|
if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
|
|
@@ -6203,7 +6984,7 @@ function registerSync(program, ctx) {
|
|
|
6203
6984
|
continue;
|
|
6204
6985
|
}
|
|
6205
6986
|
try {
|
|
6206
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
6987
|
+
dirs[m.slug] = await deps2.git.inspect(join27(ws.root, m.slug));
|
|
6207
6988
|
modulesForPlan.push(m);
|
|
6208
6989
|
} catch (e) {
|
|
6209
6990
|
earlyFailures.push(asFailure2(m.slug, e));
|
|
@@ -6399,8 +7180,10 @@ var COMMANDS = [
|
|
|
6399
7180
|
registerTraces,
|
|
6400
7181
|
registerErrors,
|
|
6401
7182
|
registerEnv,
|
|
7183
|
+
registerRoles,
|
|
6402
7184
|
registerRun,
|
|
6403
7185
|
registerDev,
|
|
7186
|
+
registerDb,
|
|
6404
7187
|
registerClone,
|
|
6405
7188
|
registerSync,
|
|
6406
7189
|
registerVersion,
|
|
@@ -6486,11 +7269,11 @@ async function start(deps2, argv2, command, load, onScrub) {
|
|
|
6486
7269
|
}
|
|
6487
7270
|
function withDeadline(work, ms2, signal) {
|
|
6488
7271
|
if (signal?.aborted) return Promise.resolve(false);
|
|
6489
|
-
return new Promise((
|
|
7272
|
+
return new Promise((resolve7) => {
|
|
6490
7273
|
const done = (v) => {
|
|
6491
7274
|
clearTimeout(timer);
|
|
6492
7275
|
signal?.removeEventListener("abort", onAbort);
|
|
6493
|
-
|
|
7276
|
+
resolve7(v);
|
|
6494
7277
|
};
|
|
6495
7278
|
const onAbort = () => done(false);
|
|
6496
7279
|
const timer = setTimeout(() => done(false), ms2);
|
|
@@ -6541,8 +7324,8 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
|
|
|
6541
7324
|
}
|
|
6542
7325
|
|
|
6543
7326
|
// src/core/config.ts
|
|
6544
|
-
import { readFile as
|
|
6545
|
-
import { join as
|
|
7327
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
7328
|
+
import { join as join28 } from "path";
|
|
6546
7329
|
var empty = () => ({ schemaVersion: 1, profiles: {} });
|
|
6547
7330
|
var ConfigStore = class {
|
|
6548
7331
|
constructor(dir2) {
|
|
@@ -6550,12 +7333,12 @@ var ConfigStore = class {
|
|
|
6550
7333
|
}
|
|
6551
7334
|
dir;
|
|
6552
7335
|
get file() {
|
|
6553
|
-
return
|
|
7336
|
+
return join28(this.dir, "config.json");
|
|
6554
7337
|
}
|
|
6555
7338
|
async read() {
|
|
6556
7339
|
let raw;
|
|
6557
7340
|
try {
|
|
6558
|
-
raw = await
|
|
7341
|
+
raw = await readFile8(this.file, "utf8");
|
|
6559
7342
|
} catch (e) {
|
|
6560
7343
|
if (e.code === "ENOENT") return empty();
|
|
6561
7344
|
throw e;
|
|
@@ -6581,9 +7364,9 @@ var ConfigStore = class {
|
|
|
6581
7364
|
|
|
6582
7365
|
// src/platform/git.ts
|
|
6583
7366
|
import { execFile, execFileSync } from "child_process";
|
|
6584
|
-
import { mkdir as mkdir4, readdir as readdir4, stat as
|
|
7367
|
+
import { mkdir as mkdir4, readdir as readdir4, stat as stat7 } from "fs/promises";
|
|
6585
7368
|
import { homedir } from "os";
|
|
6586
|
-
import { dirname as dirname9, join as
|
|
7369
|
+
import { dirname as dirname9, join as join29 } from "path";
|
|
6587
7370
|
function parseRemotes(stdout) {
|
|
6588
7371
|
const seen = /* @__PURE__ */ new Set();
|
|
6589
7372
|
const out = [];
|
|
@@ -6621,8 +7404,8 @@ function parseStatus(stdout) {
|
|
|
6621
7404
|
}
|
|
6622
7405
|
var configuredSshCommand = /* @__PURE__ */ new Map();
|
|
6623
7406
|
function hasConfiguredSshCommand(repoDir) {
|
|
6624
|
-
const
|
|
6625
|
-
let v = configuredSshCommand.get(
|
|
7407
|
+
const key2 = repoDir ?? "";
|
|
7408
|
+
let v = configuredSshCommand.get(key2);
|
|
6626
7409
|
if (v === void 0) {
|
|
6627
7410
|
const args = repoDir ? ["-C", repoDir, "config", "--get", "core.sshCommand"] : ["config", "--get", "core.sshCommand"];
|
|
6628
7411
|
try {
|
|
@@ -6636,7 +7419,7 @@ function hasConfiguredSshCommand(repoDir) {
|
|
|
6636
7419
|
} catch {
|
|
6637
7420
|
v = false;
|
|
6638
7421
|
}
|
|
6639
|
-
configuredSshCommand.set(
|
|
7422
|
+
configuredSshCommand.set(key2, v);
|
|
6640
7423
|
}
|
|
6641
7424
|
return v;
|
|
6642
7425
|
}
|
|
@@ -6650,7 +7433,7 @@ function gitExecOptions(repoDir) {
|
|
|
6650
7433
|
return { cwd: homedir(), env, windowsHide: true, maxBuffer: 8 * 1024 * 1024 };
|
|
6651
7434
|
}
|
|
6652
7435
|
function runGit(args, repoDir) {
|
|
6653
|
-
return new Promise((
|
|
7436
|
+
return new Promise((resolve7, reject) => {
|
|
6654
7437
|
execFile("git", args, gitExecOptions(repoDir), (err, stdout, stderr) => {
|
|
6655
7438
|
if (err && err.code === "ENOENT") {
|
|
6656
7439
|
reject(new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
|
|
@@ -6659,7 +7442,7 @@ function runGit(args, repoDir) {
|
|
|
6659
7442
|
return;
|
|
6660
7443
|
}
|
|
6661
7444
|
const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
|
|
6662
|
-
|
|
7445
|
+
resolve7({ code, stdout: String(stdout), stderr: String(stderr), err: err ?? void 0 });
|
|
6663
7446
|
});
|
|
6664
7447
|
});
|
|
6665
7448
|
}
|
|
@@ -6695,12 +7478,12 @@ async function status(repoRoot) {
|
|
|
6695
7478
|
if (r.code !== 0) return { ok: false, stderr: r.stderr, command };
|
|
6696
7479
|
return parseStatus(r.stdout);
|
|
6697
7480
|
}
|
|
6698
|
-
async function statOrThrow(
|
|
7481
|
+
async function statOrThrow(path2) {
|
|
6699
7482
|
try {
|
|
6700
|
-
return await
|
|
7483
|
+
return await stat7(path2);
|
|
6701
7484
|
} catch (err) {
|
|
6702
7485
|
if (err.code === "ENOENT") return void 0;
|
|
6703
|
-
throw new QoreError("QORE_INTERNAL", `could not stat ${
|
|
7486
|
+
throw new QoreError("QORE_INTERNAL", `could not stat ${path2}`, { cause: err });
|
|
6704
7487
|
}
|
|
6705
7488
|
}
|
|
6706
7489
|
async function inspect(dir2) {
|
|
@@ -6714,7 +7497,7 @@ async function inspect(dir2) {
|
|
|
6714
7497
|
throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
|
|
6715
7498
|
}
|
|
6716
7499
|
const empty2 = entries.length === 0;
|
|
6717
|
-
const isRepo = await statOrThrow(
|
|
7500
|
+
const isRepo = await statOrThrow(join29(dir2, ".git")) !== void 0;
|
|
6718
7501
|
const dirRemotes = isRepo ? await remotes(dir2) : [];
|
|
6719
7502
|
return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
|
|
6720
7503
|
}
|
|
@@ -6776,13 +7559,13 @@ var OsKeychain = class {
|
|
|
6776
7559
|
|
|
6777
7560
|
// src/platform/lock.ts
|
|
6778
7561
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
6779
|
-
import { join as
|
|
7562
|
+
import { join as join30 } from "path";
|
|
6780
7563
|
import lockfile from "proper-lockfile";
|
|
6781
7564
|
function fileLock(dir2) {
|
|
6782
7565
|
return async (name, fn) => {
|
|
6783
|
-
const locks =
|
|
7566
|
+
const locks = join30(dir2, "locks");
|
|
6784
7567
|
await mkdir5(locks, { recursive: true, mode: 448 });
|
|
6785
|
-
const target =
|
|
7568
|
+
const target = join30(locks, `${name}.lock`);
|
|
6786
7569
|
await writeFile5(target, "", { flag: "a" });
|
|
6787
7570
|
let release;
|
|
6788
7571
|
let compromised;
|
|
@@ -6826,7 +7609,7 @@ var STDERR_DRAIN_MS = 500;
|
|
|
6826
7609
|
function makeSpawner(spawnFn, proc) {
|
|
6827
7610
|
return {
|
|
6828
7611
|
run(cmd, args, env, o) {
|
|
6829
|
-
return new Promise((
|
|
7612
|
+
return new Promise((resolve7, reject) => {
|
|
6830
7613
|
let child;
|
|
6831
7614
|
try {
|
|
6832
7615
|
child = spawnFn(cmd, args, {
|
|
@@ -6860,7 +7643,7 @@ function makeSpawner(spawnFn, proc) {
|
|
|
6860
7643
|
cleanup();
|
|
6861
7644
|
const stderr = child.stderr;
|
|
6862
7645
|
if (!o?.onStderr || !stderr || stderr.readableEnded) {
|
|
6863
|
-
|
|
7646
|
+
resolve7({ code, signal });
|
|
6864
7647
|
return;
|
|
6865
7648
|
}
|
|
6866
7649
|
let settled = false;
|
|
@@ -6868,7 +7651,7 @@ function makeSpawner(spawnFn, proc) {
|
|
|
6868
7651
|
if (settled) return;
|
|
6869
7652
|
settled = true;
|
|
6870
7653
|
clearTimeout(timer);
|
|
6871
|
-
|
|
7654
|
+
resolve7({ code, signal });
|
|
6872
7655
|
};
|
|
6873
7656
|
const timer = setTimeout(finish, STDERR_DRAIN_MS);
|
|
6874
7657
|
timer.unref?.();
|
|
@@ -6923,13 +7706,13 @@ var systemLauncher = {
|
|
|
6923
7706
|
child.stdout?.on("data", (c) => out.push(c));
|
|
6924
7707
|
child.stderr?.on("data", (c) => err.push(c));
|
|
6925
7708
|
let closed = false;
|
|
6926
|
-
const exited = new Promise((
|
|
6927
|
-
child.once("error", (e) =>
|
|
7709
|
+
const exited = new Promise((resolve7) => {
|
|
7710
|
+
child.once("error", (e) => resolve7({ code: null, signal: null, spawnError: e.code ?? "error" }));
|
|
6928
7711
|
child.once("close", (code, signal) => {
|
|
6929
7712
|
closed = true;
|
|
6930
7713
|
out.flush();
|
|
6931
7714
|
err.flush();
|
|
6932
|
-
|
|
7715
|
+
resolve7({ code, signal });
|
|
6933
7716
|
});
|
|
6934
7717
|
});
|
|
6935
7718
|
return {
|
|
@@ -6975,11 +7758,11 @@ var systemLauncher = {
|
|
|
6975
7758
|
import { execFile as execFile2 } from "child_process";
|
|
6976
7759
|
import { connect } from "net";
|
|
6977
7760
|
function accepts(host, port) {
|
|
6978
|
-
return new Promise((
|
|
7761
|
+
return new Promise((resolve7) => {
|
|
6979
7762
|
const s = connect({ host, port });
|
|
6980
7763
|
const done = (v) => {
|
|
6981
7764
|
s.destroy();
|
|
6982
|
-
|
|
7765
|
+
resolve7(v);
|
|
6983
7766
|
};
|
|
6984
7767
|
s.setTimeout(500, () => done(false));
|
|
6985
7768
|
s.once("connect", () => done(true));
|
|
@@ -6987,12 +7770,12 @@ function accepts(host, port) {
|
|
|
6987
7770
|
});
|
|
6988
7771
|
}
|
|
6989
7772
|
function run2(cmd, args) {
|
|
6990
|
-
return new Promise((
|
|
7773
|
+
return new Promise((resolve7) => {
|
|
6991
7774
|
execFile2(
|
|
6992
7775
|
cmd,
|
|
6993
7776
|
args,
|
|
6994
7777
|
{ timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
|
|
6995
|
-
(e, stdout) =>
|
|
7778
|
+
(e, stdout) => resolve7(e && !stdout ? null : String(stdout))
|
|
6996
7779
|
);
|
|
6997
7780
|
});
|
|
6998
7781
|
}
|
|
@@ -7055,6 +7838,26 @@ var systemPorts = {
|
|
|
7055
7838
|
}
|
|
7056
7839
|
};
|
|
7057
7840
|
|
|
7841
|
+
// src/platform/postgres.ts
|
|
7842
|
+
import pg from "pg";
|
|
7843
|
+
async function connectPostgres(url, o) {
|
|
7844
|
+
const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: o.timeoutMs });
|
|
7845
|
+
client.on("error", () => {
|
|
7846
|
+
});
|
|
7847
|
+
try {
|
|
7848
|
+
await client.connect();
|
|
7849
|
+
await client.query("set default_transaction_read_only = on");
|
|
7850
|
+
} catch (e) {
|
|
7851
|
+
await client.end().catch(() => {
|
|
7852
|
+
});
|
|
7853
|
+
throw e;
|
|
7854
|
+
}
|
|
7855
|
+
return {
|
|
7856
|
+
query: async (sql, params) => ({ rows: (await client.query(sql, params)).rows }),
|
|
7857
|
+
close: () => client.end()
|
|
7858
|
+
};
|
|
7859
|
+
}
|
|
7860
|
+
|
|
7058
7861
|
// src/buildInfo.ts
|
|
7059
7862
|
var SENTRY_DSN = true ? "https://50b592efb53dbc1a31cb10d3945c6d52@o4512129906049024.ingest.de.sentry.io/4512174341750864" : "";
|
|
7060
7863
|
var SENTRY_ENVIRONMENT = true ? "production" : "development";
|
|
@@ -7064,7 +7867,7 @@ import { execFile as execFile3 } from "child_process";
|
|
|
7064
7867
|
|
|
7065
7868
|
// src/core/diagnostics/proxy.ts
|
|
7066
7869
|
function parseScutilProxy(stdout) {
|
|
7067
|
-
const value = (
|
|
7870
|
+
const value = (key2) => stdout.match(new RegExp(`^\\s*${key2}\\s*:\\s*(.+)$`, "m"))?.[1]?.trim();
|
|
7068
7871
|
if (value("HTTPSEnable") === "1") {
|
|
7069
7872
|
const host = value("HTTPSProxy");
|
|
7070
7873
|
const port = value("HTTPSPort");
|
|
@@ -7085,12 +7888,12 @@ function parseRegProxy(stdout) {
|
|
|
7085
7888
|
|
|
7086
7889
|
// src/platform/proxy.ts
|
|
7087
7890
|
function run3(cmd, args) {
|
|
7088
|
-
return new Promise((
|
|
7891
|
+
return new Promise((resolve7) => {
|
|
7089
7892
|
execFile3(
|
|
7090
7893
|
cmd,
|
|
7091
7894
|
args,
|
|
7092
7895
|
{ timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
|
|
7093
|
-
(e, stdout) =>
|
|
7896
|
+
(e, stdout) => resolve7(e && !stdout ? null : String(stdout))
|
|
7094
7897
|
);
|
|
7095
7898
|
});
|
|
7096
7899
|
}
|
|
@@ -7101,10 +7904,10 @@ async function detect(platform) {
|
|
|
7101
7904
|
return out ? parseScutilProxy(out) : null;
|
|
7102
7905
|
}
|
|
7103
7906
|
if (platform === "win32") {
|
|
7104
|
-
const
|
|
7907
|
+
const key2 = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
|
|
7105
7908
|
const [enable, server] = await Promise.all([
|
|
7106
|
-
run3("reg", ["query",
|
|
7107
|
-
run3("reg", ["query",
|
|
7909
|
+
run3("reg", ["query", key2, "/v", "ProxyEnable"]),
|
|
7910
|
+
run3("reg", ["query", key2, "/v", "ProxyServer"])
|
|
7108
7911
|
]);
|
|
7109
7912
|
const combined = [enable, server].filter(Boolean).join("\n");
|
|
7110
7913
|
return combined ? parseRegProxy(combined) : null;
|
|
@@ -7121,7 +7924,7 @@ function systemProxy(platform = process.platform) {
|
|
|
7121
7924
|
}
|
|
7122
7925
|
|
|
7123
7926
|
// src/version.ts
|
|
7124
|
-
var VERSION = true ? "0.1.
|
|
7927
|
+
var VERSION = true ? "0.1.9" : "0.0.0-dev";
|
|
7125
7928
|
|
|
7126
7929
|
// src/cli.ts
|
|
7127
7930
|
if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
@@ -7169,7 +7972,9 @@ var deps = {
|
|
|
7169
7972
|
},
|
|
7170
7973
|
sentryDsn: SENTRY_DSN,
|
|
7171
7974
|
sentryEnvironment: SENTRY_ENVIRONMENT,
|
|
7172
|
-
systemProxy: () => systemProxy(process.platform)
|
|
7975
|
+
systemProxy: () => systemProxy(process.platform),
|
|
7976
|
+
viewerDir: fileURLToPath(new URL("../viewer/dist/", import.meta.url)),
|
|
7977
|
+
postgres: connectPostgres
|
|
7173
7978
|
};
|
|
7174
7979
|
var termination = preflightTermination(() => ac.abort(), deps.raise);
|
|
7175
7980
|
var io = processIo(isInteractive({
|
|
@@ -7199,4 +8004,4 @@ if (!drained) {
|
|
|
7199
8004
|
process.exit();
|
|
7200
8005
|
}
|
|
7201
8006
|
//# sourceMappingURL=cli.js.map
|
|
7202
|
-
//# debugId=
|
|
8007
|
+
//# debugId=f7b490f6-b0af-5874-9ffa-f8010e3ba137
|