@quatronic/qore 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -2
- package/dist/cli.js +1720 -1089
- package/package.json +23 -4
- package/viewer/dist/assets/elk.bundled-C8gcDXx2.js +24 -0
- package/viewer/dist/assets/index-BRgYtLRN.css +1 -0
- package/viewer/dist/assets/index-VDGZO23h.js +15 -0
- package/viewer/dist/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
- package/viewer/dist/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
- package/viewer/dist/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
- package/viewer/dist/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
- package/viewer/dist/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
- package/viewer/dist/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
- package/viewer/dist/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
- package/viewer/dist/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
- package/viewer/dist/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
- package/viewer/dist/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
- package/viewer/dist/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
- package/viewer/dist/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
- package/viewer/dist/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
- package/viewer/dist/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
- package/viewer/dist/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
- package/viewer/dist/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
- package/viewer/dist/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
- package/viewer/dist/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
- package/viewer/dist/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
- package/viewer/dist/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
- package/viewer/dist/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
- package/viewer/dist/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
- package/viewer/dist/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
- package/viewer/dist/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
- package/viewer/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/viewer/dist/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/viewer/dist/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/viewer/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/viewer/dist/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/viewer/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/viewer/dist/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
- package/viewer/dist/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
- package/viewer/dist/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
- package/viewer/dist/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
- package/viewer/dist/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
- package/viewer/dist/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
- package/viewer/dist/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
- package/viewer/dist/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
- package/viewer/dist/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
- package/viewer/dist/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
- package/viewer/dist/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
- package/viewer/dist/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
- package/viewer/dist/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/viewer/dist/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
- package/viewer/dist/index.html +13 -0
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c4bda056-b13b-52b5-831f-2afd3f6c6867")}catch(e){}}();
|
|
4
4
|
import { createRequire as __qoreCreateRequire } from "node:module"; const require = __qoreCreateRequire(import.meta.url);
|
|
5
5
|
import {
|
|
6
6
|
addSecretValues,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
|
|
10
10
|
// src/cli.ts
|
|
11
11
|
import { homedir as homedir2 } from "os";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
12
13
|
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
|
|
13
14
|
|
|
14
15
|
// src/cli/io.ts
|
|
@@ -169,7 +170,7 @@ function processIo(interactive, stderrTTY = Boolean(process.stderr.isTTY)) {
|
|
|
169
170
|
};
|
|
170
171
|
}
|
|
171
172
|
function createWaitForEnter(input2, output) {
|
|
172
|
-
return (message, signal) => new Promise((
|
|
173
|
+
return (message, signal) => new Promise((resolve7, reject) => {
|
|
173
174
|
const rl = createInterface({ input: input2, output, terminal: true });
|
|
174
175
|
const onAbort = () => {
|
|
175
176
|
rl.close();
|
|
@@ -178,7 +179,7 @@ function createWaitForEnter(input2, output) {
|
|
|
178
179
|
const done = () => {
|
|
179
180
|
signal.removeEventListener("abort", onAbort);
|
|
180
181
|
rl.close();
|
|
181
|
-
|
|
182
|
+
resolve7();
|
|
182
183
|
};
|
|
183
184
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
184
185
|
rl.on("SIGINT", () => {
|
|
@@ -382,16 +383,16 @@ function groupByModule(results) {
|
|
|
382
383
|
const order = [];
|
|
383
384
|
const groups = /* @__PURE__ */ new Map();
|
|
384
385
|
for (const r of results) {
|
|
385
|
-
const
|
|
386
|
-
if (!groups.has(
|
|
387
|
-
groups.set(
|
|
388
|
-
order.push(
|
|
386
|
+
const key2 = r.module ?? r.id;
|
|
387
|
+
if (!groups.has(key2)) {
|
|
388
|
+
groups.set(key2, []);
|
|
389
|
+
order.push(key2);
|
|
389
390
|
}
|
|
390
|
-
groups.get(
|
|
391
|
+
groups.get(key2).push(r);
|
|
391
392
|
}
|
|
392
393
|
const isSub = (r) => r.id.startsWith("pm:") || r.id.startsWith("node:");
|
|
393
|
-
return order.flatMap((
|
|
394
|
-
const g = groups.get(
|
|
394
|
+
return order.flatMap((key2) => {
|
|
395
|
+
const g = groups.get(key2);
|
|
395
396
|
return [...g.filter((r) => !isSub(r)), ...g.filter(isSub)];
|
|
396
397
|
});
|
|
397
398
|
}
|
|
@@ -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))];
|
|
@@ -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);
|
|
@@ -1435,9 +1436,9 @@ 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
1443
|
async function underAFile(path) {
|
|
1443
1444
|
for (let p = dirname3(path); ; p = dirname3(p)) {
|
|
@@ -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 path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
2765
|
+
const produce = o.routes[path];
|
|
2766
|
+
if (produce) {
|
|
2767
|
+
try {
|
|
2768
|
+
const body = JSON.stringify(await produce());
|
|
2769
|
+
res.writeHead(200, { "content-type": TYPES[".json"], "cache-control": "no-store" });
|
|
2770
|
+
res.end(body);
|
|
2771
|
+
return;
|
|
2772
|
+
} catch (e) {
|
|
2773
|
+
try {
|
|
2774
|
+
o.onError(e);
|
|
2775
|
+
} catch {
|
|
2776
|
+
}
|
|
2777
|
+
res.writeHead(500, { "content-type": TYPES[".json"] });
|
|
2778
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
const file = resolve6(root, `.${path === "/" ? "/index.html" : path}`);
|
|
2783
|
+
if (file !== root && !file.startsWith(root + sep3)) return end(res, 404);
|
|
2784
|
+
try {
|
|
2785
|
+
const body = await readFile6(file);
|
|
2786
|
+
res.writeHead(200, { "content-type": TYPES[extname(file)] ?? "application/octet-stream" });
|
|
2787
|
+
res.end(body);
|
|
2788
|
+
} catch {
|
|
2789
|
+
end(res, 404);
|
|
2790
|
+
}
|
|
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, path) {
|
|
3015
|
+
if (path === ctx.deps.cwd) return ctx;
|
|
3016
|
+
if (!ctx.opts().json) ctx.io.err(`\u2192 in ${path}`);
|
|
3017
|
+
return withCwd(ctx, path);
|
|
2872
3018
|
}
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
if (tail.length > 50) tail.shift();
|
|
2884
|
-
o.onLine(l);
|
|
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 } });
|
|
2907
|
-
}
|
|
2908
|
-
if (res.code !== 0) {
|
|
2909
|
-
throw new QoreError("QORE_PACKAGE_MANAGER", `${r.pm.name} is not installed`, { hint, details: { packageManager: r.pm, stderr: res.tail } });
|
|
2910
|
-
}
|
|
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
|
-
}
|
|
2915
|
-
async function checkNode(deps2, r, pkg, slug, signal) {
|
|
2916
|
-
const range = pkg.engines?.node;
|
|
2917
|
-
if (!range) return null;
|
|
2918
|
-
const res = await runTool(deps2, "node", ["--version"], { cwd: r.root, env: deps2.env, onLine: () => {
|
|
2919
|
-
}, signal });
|
|
2920
|
-
if (res.spawnError === "ENOENT") {
|
|
2921
|
-
throw new QoreError("QORE_NODE_VERSION", "node is not on PATH", {
|
|
2922
|
-
hint: "Install Node (https://nodejs.org) or run nvm use, then reinstall qore.",
|
|
2923
|
-
details: { required: range }
|
|
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
|
|
2924
3029
|
});
|
|
3030
|
+
return;
|
|
2925
3031
|
}
|
|
2926
|
-
if (
|
|
2927
|
-
|
|
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
|
+
});
|
|
2928
3043
|
}
|
|
2929
|
-
const found = (res.tail.at(-1) ?? "").trim().replace(/^v/, "");
|
|
2930
|
-
if (nodeSatisfies(found, range) === false) throw nodeVersionError(found, range, slug);
|
|
2931
|
-
return found;
|
|
2932
3044
|
}
|
|
2933
|
-
async function
|
|
2934
|
-
if (
|
|
2935
|
-
|
|
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);
|
|
2936
3057
|
return;
|
|
2937
3058
|
}
|
|
2938
|
-
if (!
|
|
2939
|
-
|
|
2940
|
-
|
|
3059
|
+
if (!ctx.io.interactive) {
|
|
3060
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", `This repository is not trusted for ${mc.module.slug} yet`, {
|
|
3061
|
+
hint: "Run interactively once, or pass --yes.",
|
|
3062
|
+
details: { reason: "untrusted", repository: root }
|
|
3063
|
+
});
|
|
2941
3064
|
}
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
const res = await runTool(deps2, cmd, args, { cwd: r.root, env: deps2.env, onLine, signal });
|
|
2946
|
-
if (res.code !== 0) {
|
|
2947
|
-
const command = [cmd, ...args].join(" ");
|
|
2948
|
-
const err = classifyInstall(res.tail, command, r.root);
|
|
2949
|
-
if (err.details?.reason === "outdated-lockfile") throw err;
|
|
2950
|
-
throw new QoreError("QORE_INSTALL_FAILED", `${command} failed in ${slug}`, { hint: err.hint, details: err.details });
|
|
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)"}`);
|
|
2951
3068
|
}
|
|
2952
|
-
|
|
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);
|
|
2953
3071
|
}
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
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."
|
|
2961
3107
|
});
|
|
2962
3108
|
}
|
|
2963
|
-
if (
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
}
|
|
2973
|
-
const detail2 = await deps2.ports.ownerDetail(owner.pid);
|
|
2974
|
-
const extras = [];
|
|
2975
|
-
if (detail2.ageSeconds !== null) extras.push(`started ${humanAge(detail2.ageSeconds)} ago`);
|
|
2976
|
-
if (detail2.cwd !== null) {
|
|
2977
|
-
const cwd = deps2.home && detail2.cwd.startsWith(deps2.home) ? `~${detail2.cwd.slice(deps2.home.length)}` : detail2.cwd;
|
|
2978
|
-
extras.push(`in ${cwd}`);
|
|
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;
|
|
2979
3118
|
}
|
|
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 } : {}
|
|
2992
|
-
}
|
|
2993
|
-
});
|
|
2994
3119
|
}
|
|
2995
|
-
const
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
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);
|
|
2999
3137
|
});
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
if (!r.dockerServices) {
|
|
3003
|
-
return new QoreError("QORE_SERVICE_UNHEALTHY", message, { details: { command, stderr } });
|
|
3004
|
-
}
|
|
3005
|
-
const logs = await runTool(deps2, "docker", ["compose", "logs", "--no-color", "--tail", "50"], { cwd: r.root, env, onLine: () => {
|
|
3006
|
-
}, signal });
|
|
3007
|
-
const dataErr = classifyComposeLogs(logs.tail, { slug, script: "db:up" });
|
|
3008
|
-
if (dataErr) return dataErr;
|
|
3009
|
-
return new QoreError("QORE_SERVICE_UNHEALTHY", message, {
|
|
3010
|
-
hint: `The last lines of docker compose logs are in details.logs; run \`docker compose logs\` in ${r.root} for all of them.`,
|
|
3011
|
-
details: { command, stderr, logs: logs.tail.slice(-20) }
|
|
3138
|
+
const first = await model().catch((e) => {
|
|
3139
|
+
throw ctx.signal.aborted ? new CancelledError() : classifyDbError(e, src.url);
|
|
3012
3140
|
});
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
if (r.services.length === 0) {
|
|
3016
|
-
report({ module: slug, step: "services", status: "skip" });
|
|
3141
|
+
if (json2) {
|
|
3142
|
+
renderJson(io2, { context: contextJson(mc, app), source: src.source, model: first });
|
|
3017
3143
|
return;
|
|
3018
3144
|
}
|
|
3019
|
-
|
|
3020
|
-
const
|
|
3021
|
-
if (
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
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();
|
|
3029
3182
|
}
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3183
|
+
}
|
|
3184
|
+
async function erdApp(ctx, f, ws) {
|
|
3185
|
+
const { deps: deps2, io: io2 } = ctx;
|
|
3186
|
+
const g = ctx.opts();
|
|
3187
|
+
const json2 = Boolean(g.json);
|
|
3188
|
+
const portal = await flagPortal(deps2, g.portal, ws);
|
|
3189
|
+
const session = await openSession(deps2, portal, "db erd", g.verbose ? (l) => io2.err(l) : void 0);
|
|
3190
|
+
await ensureCapability(deps2, portal.apiOrigin, "app.modules");
|
|
3191
|
+
const am = await workspaceAppModules(session, portal, ws);
|
|
3192
|
+
const { tenant, ...app } = am.app;
|
|
3193
|
+
if (f.app && f.app !== app.id && f.app !== app.slug) {
|
|
3194
|
+
throw new QoreError("QORE_RESOURCE_NOT_FOUND", `This workspace is for ${app.slug}, not "${f.app}"`, { hint: "Pass --module <slug> --app <slug>." });
|
|
3195
|
+
}
|
|
3196
|
+
const modules = [];
|
|
3197
|
+
const sources = /* @__PURE__ */ new Map();
|
|
3198
|
+
const roots = /* @__PURE__ */ new Map();
|
|
3199
|
+
for (const m of orderModules(am.modules)) {
|
|
3200
|
+
const dir2 = join15(ws.root, m.slug);
|
|
3201
|
+
const repoRoot = isSafeSlug(m.slug) && await isDir2(dir2) ? dir2 : null;
|
|
3202
|
+
const mc = { portal, session, module: { ...m, tenant, apps: [app] }, source: { kind: "workspace" }, repoRoot, workspace: ws };
|
|
3203
|
+
roots.set(m.slug, repoRoot);
|
|
3204
|
+
if (repoRoot) await ensureTrusted(ctx, mc, app, { yes: f.yes });
|
|
3205
|
+
try {
|
|
3206
|
+
const src = await resolveDatabaseUrl(deps2, mc, app);
|
|
3207
|
+
modules.push({ slug: m.slug, dependsOn: m.dependsOn, url: src.url });
|
|
3208
|
+
sources.set(m.slug, src.source);
|
|
3209
|
+
} catch (e) {
|
|
3210
|
+
if (e instanceof QoreError && e.code === "QORE_CONTEXT_UNSUPPORTED") {
|
|
3211
|
+
modules.push({ slug: m.slug, dependsOn: m.dependsOn, url: null });
|
|
3212
|
+
continue;
|
|
3036
3213
|
}
|
|
3037
|
-
|
|
3214
|
+
if (!json2 && e instanceof QoreError && e.code === "QORE_ACCESS_DENIED") {
|
|
3215
|
+
io2.err(`${m.slug} is left out: ${e.message}${e.hint ? `
|
|
3216
|
+
${e.hint}` : ""}`);
|
|
3217
|
+
modules.push({ slug: m.slug, dependsOn: m.dependsOn, url: null });
|
|
3218
|
+
continue;
|
|
3219
|
+
}
|
|
3220
|
+
throw e;
|
|
3038
3221
|
}
|
|
3039
3222
|
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3223
|
+
const groups = groupDatabases(modules);
|
|
3224
|
+
if (groups.length === 0) {
|
|
3225
|
+
throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `${app.slug} has no module with a database`, {
|
|
3226
|
+
hint: "qore db erd shows the Postgres of an app's modules; none of them declares a DATABASE_URL."
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
for (const gdb of groups) deps2.registerSecrets([gdb.url]);
|
|
3230
|
+
if (!json2) for (const { modules: mods, connection: c } of groups) {
|
|
3231
|
+
if (mods.length > 1) io2.err(`${mods.join(" and ")} use the same database (${c.host}:${c.port}/${c.database}); each module normally has its own \u2014 check their DATABASE_URL.`);
|
|
3232
|
+
}
|
|
3233
|
+
const readAll = (strict) => Promise.all(groups.map(async (gdb) => {
|
|
3234
|
+
try {
|
|
3235
|
+
const conn = await deps2.postgres(gdb.url, { timeoutMs: 3e3 });
|
|
3236
|
+
try {
|
|
3237
|
+
const model = await introspect(conn.query);
|
|
3238
|
+
const migrations = gdb.modules.length === 1 ? await checkMigrations(roots.get(gdb.modules[0]) ?? null, conn.query) : void 0;
|
|
3239
|
+
return { ...gdb, model, ...migrations ? { migrations } : {} };
|
|
3240
|
+
} finally {
|
|
3241
|
+
await conn.close().catch(() => {
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
} catch (e) {
|
|
3245
|
+
const err = classifyDbError(e, gdb.url);
|
|
3246
|
+
if (strict) throw err;
|
|
3247
|
+
return { ...gdb, model: null, error: err.message };
|
|
3054
3248
|
}
|
|
3249
|
+
}));
|
|
3250
|
+
const first = await readAll(json2).catch((e) => {
|
|
3251
|
+
throw ctx.signal.aborted ? new CancelledError() : e;
|
|
3252
|
+
});
|
|
3253
|
+
if (first.every((d) => d.model === null)) throw new QoreError("QORE_SERVICE_UNHEALTHY", `No database of ${app.slug} is reachable`, { hint: first.map((d) => `${d.key}: ${d.error}`).join("\n") });
|
|
3254
|
+
const context = {
|
|
3255
|
+
portal: portal.builtin ?? portal.apiOrigin,
|
|
3256
|
+
tenant,
|
|
3257
|
+
app,
|
|
3258
|
+
module: null,
|
|
3259
|
+
source: WORKSPACE_FILE,
|
|
3260
|
+
modules: am.modules.map(({ id, slug, name, type }) => ({ id, slug, name, type, database: groups.find((gdb) => gdb.modules.includes(slug))?.key ?? null }))
|
|
3261
|
+
};
|
|
3262
|
+
const databases = groups.map(({ key: key2, modules: mods, connection }) => ({ key: key2, modules: mods, connection, source: sources.get(mods[0]) }));
|
|
3263
|
+
if (json2) {
|
|
3264
|
+
renderJson(io2, { context, databases, model: combineModels(first) });
|
|
3265
|
+
return;
|
|
3055
3266
|
}
|
|
3056
|
-
const
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3267
|
+
for (const d of first) if (d.error) io2.err(`${d.key} (${d.modules.join(", ")}): ${d.error}`);
|
|
3268
|
+
for (const d of first) for (const line2 of migrationLines(d.migrations, `The database of ${d.modules.join(", ")}`)) io2.err(line2);
|
|
3269
|
+
if (first.every((d) => !d.model || d.model.tables.length === 0)) io2.err("No tables yet \u2014 have the migrations run? Start the app once with `qore dev`.");
|
|
3270
|
+
if (ctx.signal.aborted) throw new CancelledError();
|
|
3271
|
+
const meta = { context, connection: null, databases: databases.map(({ key: key2, modules: mods, connection }) => ({ key: key2, modules: mods, connection })) };
|
|
3272
|
+
const server = await serveErd({
|
|
3273
|
+
dir: deps2.viewerDir,
|
|
3274
|
+
port: f.port ?? 0,
|
|
3275
|
+
routes: { "/model.json": () => readAll(false).then(combineModels), "/meta.json": async () => meta },
|
|
3276
|
+
onError: (e) => {
|
|
3277
|
+
if (!ctx.signal.aborted) io2.err(`Could not refresh the model: ${e instanceof Error ? e.message : String(e)}`);
|
|
3278
|
+
}
|
|
3279
|
+
}).catch((e) => {
|
|
3280
|
+
const code = e.code;
|
|
3281
|
+
throw f.port !== void 0 && (code === "EADDRINUSE" || code === "EACCES") ? portInUse(f.port) : e;
|
|
3282
|
+
});
|
|
3283
|
+
await serve(ctx, f, server);
|
|
3063
3284
|
}
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3285
|
+
|
|
3286
|
+
// src/cli/commands/dev.ts
|
|
3287
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
3288
|
+
import { realpath as realpath8 } from "fs/promises";
|
|
3289
|
+
import { constants as constants3 } from "os";
|
|
3290
|
+
import { dirname as dirname7, join as join23, relative as relative7 } from "path";
|
|
3291
|
+
import { StringDecoder } from "string_decoder";
|
|
3292
|
+
|
|
3293
|
+
// src/core/diagnostics/node.ts
|
|
3294
|
+
var KEEP_LINES = 50;
|
|
3295
|
+
function exitHint(tail, h) {
|
|
3296
|
+
const text2 = tail.join("\n");
|
|
3297
|
+
const missing = text2.match(/Cannot find (?:module|package) '([^']+)'/);
|
|
3298
|
+
if (missing) {
|
|
3299
|
+
return { reason: "missing-module", hint: `A dependency is missing (${missing[1]}). Run \`${h.installCommand}\`, then qore dev again.` };
|
|
3300
|
+
}
|
|
3301
|
+
if (/ZodError:/.test(text2)) {
|
|
3302
|
+
const keys = [...text2.matchAll(/"path":\s*\[\s*"([^"]+)"/g)].map((m) => m[1]);
|
|
3303
|
+
if (keys.length > 0) {
|
|
3304
|
+
return {
|
|
3305
|
+
reason: "invalid-env",
|
|
3306
|
+
keys,
|
|
3307
|
+
hint: `Missing or invalid environment variables: ${keys.join(", ")}. Set them in the portal's local secrets (then qore env pull), or in your env file.`
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
const addrInUse = text2.match(/listen EADDRINUSE: address already in use (?:\S*:)?(\d+)/);
|
|
3312
|
+
if (addrInUse) {
|
|
3313
|
+
const port = Number(addrInUse[1]);
|
|
3314
|
+
return {
|
|
3315
|
+
reason: "port-in-use",
|
|
3316
|
+
port,
|
|
3317
|
+
hint: `Port ${port} was taken while the server started. Run qore dev again; if it repeats, lsof -iTCP:${port} -sTCP:LISTEN names the holder.`
|
|
3070
3318
|
};
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3319
|
+
}
|
|
3320
|
+
if (/ECONNREFUSED/.test(text2)) {
|
|
3321
|
+
const m = text2.match(/ECONNREFUSED\s+\S*:(\d+)/) ?? text2.match(/code: 'ECONNREFUSED'[^}]*?port: (\d+)/);
|
|
3322
|
+
if (!m) return { reason: "conn-refused", hint: "A connection was refused (ECONNREFUSED) \u2014 a service this module calls is not running." };
|
|
3323
|
+
const port = Number(m[1]);
|
|
3324
|
+
const role = h.ports.get(port);
|
|
3325
|
+
if (role?.kind === "database") {
|
|
3326
|
+
const up = h.runCommand("db:up");
|
|
3327
|
+
return {
|
|
3328
|
+
reason: "conn-refused",
|
|
3329
|
+
port,
|
|
3330
|
+
hint: `The database on :${port} is not accepting connections (ECONNREFUSED). ` + (up ? `Fix: ${up} (docker compose ps shows its state)` : "docker compose ps shows its state.")
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
if (role?.kind === "provider") {
|
|
3334
|
+
return {
|
|
3335
|
+
reason: "conn-refused",
|
|
3336
|
+
port,
|
|
3337
|
+
hint: `${role.key} points at :${port}, where nothing listens (ECONNREFUSED). Start that module \u2014 qore dev in the workspace root starts providers first.`
|
|
3338
|
+
};
|
|
3339
|
+
}
|
|
3340
|
+
return { reason: "conn-refused", port, hint: `Nothing listens on :${port} (ECONNREFUSED) \u2014 a service this module calls is not running.` };
|
|
3341
|
+
}
|
|
3342
|
+
const auth = text2.match(/password authentication failed for user "([^"]+)"/);
|
|
3343
|
+
if (auth) {
|
|
3344
|
+
const reset = h.runCommand("db:reset");
|
|
3345
|
+
return {
|
|
3346
|
+
reason: "db-auth",
|
|
3347
|
+
hint: `The local database was created with other credentials than the portal now provides (user ${auth[1]}).` + (reset ? ` Fix: ${reset} \u2014 this wipes local data.` : "")
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
const relation = text2.match(/relation "([^"]+)" does not exist/);
|
|
3351
|
+
if (relation) {
|
|
3352
|
+
const reset = h.runCommand("db:reset");
|
|
3353
|
+
return {
|
|
3354
|
+
reason: "db-schema",
|
|
3355
|
+
hint: `The database schema is behind the code (relation ${relation[1]} does not exist). Migrations run when the app starts; if they failed above, that error is the cause.` + (reset ? ` To start clean: ${reset} (wipes local data).` : "")
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
if (/ENOSPC: System limit for number of file watchers reached/.test(text2)) {
|
|
3359
|
+
return {
|
|
3360
|
+
reason: "watcher-limit",
|
|
3361
|
+
hint: "Linux ran out of file watchers. Raise the limit: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
return null;
|
|
3074
3365
|
}
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
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) {
|
|
3086
3407
|
try {
|
|
3087
|
-
|
|
3088
|
-
|
|
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
|
+
};
|
|
3089
3426
|
} catch (e) {
|
|
3090
|
-
if (e
|
|
3091
|
-
|
|
3427
|
+
if (!isOffline(e)) throw e;
|
|
3428
|
+
cause = e;
|
|
3092
3429
|
}
|
|
3093
|
-
if (classifyDockerInfo(res, deps2.platform) === null) return true;
|
|
3094
3430
|
}
|
|
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: [] };
|
|
3095
3439
|
}
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
const
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
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;
|
|
3445
|
+
}
|
|
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)";
|
|
3108
3458
|
}
|
|
3109
|
-
return
|
|
3459
|
+
return "run qore sync";
|
|
3110
3460
|
}
|
|
3111
3461
|
|
|
3112
|
-
// src/core/dev/
|
|
3113
|
-
var
|
|
3114
|
-
function
|
|
3115
|
-
const
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
}
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
const
|
|
3126
|
-
if (
|
|
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);
|
|
3127
3489
|
}
|
|
3128
|
-
|
|
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 };
|
|
3129
3503
|
}
|
|
3130
|
-
function
|
|
3131
|
-
return
|
|
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.` };
|
|
3132
3506
|
}
|
|
3133
|
-
function
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
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) {
|
|
3137
3518
|
const db = envPort(env, "DB_PORT");
|
|
3138
|
-
if (db
|
|
3519
|
+
if (db === null || recipe.services.length > 0 || !o.servicesFlag || o.configured) return null;
|
|
3139
3520
|
return {
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
}
|
|
3146
|
-
|
|
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 }
|
|
3147
3539
|
};
|
|
3148
3540
|
}
|
|
3149
3541
|
|
|
3150
|
-
// src/core/dev/
|
|
3151
|
-
import {
|
|
3152
|
-
import { join as
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
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.");
|
|
3560
|
+
}
|
|
3157
3561
|
}
|
|
3158
|
-
async function
|
|
3159
|
-
|
|
3160
|
-
|
|
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] };
|
|
3567
|
+
}
|
|
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.');
|
|
3161
3570
|
}
|
|
3162
|
-
|
|
3163
|
-
const
|
|
3164
|
-
|
|
3165
|
-
|
|
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
|
+
});
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
const services = !o.flags.services ? [] : configured ?? (scripts["db:up"] !== void 0 ? ["db:up"] : []);
|
|
3594
|
+
return {
|
|
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] ?? ""))
|
|
3601
|
+
};
|
|
3166
3602
|
}
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
if (o.prune !== false) await unlink2(path).catch(() => {
|
|
3173
|
-
});
|
|
3174
|
-
return null;
|
|
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"] };
|
|
3175
3608
|
}
|
|
3176
3609
|
|
|
3177
|
-
// src/
|
|
3178
|
-
import {
|
|
3179
|
-
import { join as
|
|
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;
|
|
3627
|
+
}
|
|
3180
3628
|
|
|
3181
|
-
// src/core/
|
|
3182
|
-
function
|
|
3183
|
-
|
|
3184
|
-
if (
|
|
3185
|
-
|
|
3186
|
-
if (s.length === 2) return [{ app: s[0], module: s[1] }, { tenant: s[0], app: s[1] }];
|
|
3187
|
-
if (s.length === 3) return [{ tenant: s[0], app: s[1], module: s[2] }];
|
|
3188
|
-
return [];
|
|
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/";
|
|
3189
3634
|
}
|
|
3190
|
-
function
|
|
3191
|
-
if (
|
|
3192
|
-
|
|
3193
|
-
|
|
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 } });
|
|
3647
|
+
}
|
|
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 } });
|
|
3655
|
+
}
|
|
3656
|
+
|
|
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
|
+
}
|
|
3673
|
+
|
|
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 };
|
|
3681
|
+
}
|
|
3682
|
+
function parseLsofCwd(stdout) {
|
|
3683
|
+
const m = stdout.match(/^fcwd\nn(.+)$/m);
|
|
3684
|
+
return m ? m[1] : null;
|
|
3685
|
+
}
|
|
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`;
|
|
3194
3700
|
}
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
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 } = await runFilePath(deps2, root);
|
|
4040
|
+
await unlink2(path).catch(() => {
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
async function leftoverRun(deps2, root, o = {}) {
|
|
4044
|
+
const { path } = await runFilePath(deps2, root);
|
|
4045
|
+
const rec = await readJson(path).catch(() => null);
|
|
4046
|
+
if (!rec || rec.schemaVersion !== 1) return null;
|
|
4047
|
+
if (deps2.launch.isAlive(rec.qorePid)) return rec;
|
|
4048
|
+
if (o.prune !== false) await unlink2(path).catch(() => {
|
|
4049
|
+
});
|
|
4050
|
+
return null;
|
|
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
|
}
|
|
@@ -6171,7 +6779,7 @@ function registerStatus(program, ctx) {
|
|
|
6171
6779
|
}
|
|
6172
6780
|
|
|
6173
6781
|
// src/cli/commands/sync.ts
|
|
6174
|
-
import { join as
|
|
6782
|
+
import { join as join26, relative as relative9 } from "path";
|
|
6175
6783
|
import { Option as Option2 } from "commander";
|
|
6176
6784
|
function asFailure2(slug, e) {
|
|
6177
6785
|
if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
|
|
@@ -6203,7 +6811,7 @@ function registerSync(program, ctx) {
|
|
|
6203
6811
|
continue;
|
|
6204
6812
|
}
|
|
6205
6813
|
try {
|
|
6206
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
6814
|
+
dirs[m.slug] = await deps2.git.inspect(join26(ws.root, m.slug));
|
|
6207
6815
|
modulesForPlan.push(m);
|
|
6208
6816
|
} catch (e) {
|
|
6209
6817
|
earlyFailures.push(asFailure2(m.slug, e));
|
|
@@ -6401,6 +7009,7 @@ var COMMANDS = [
|
|
|
6401
7009
|
registerEnv,
|
|
6402
7010
|
registerRun,
|
|
6403
7011
|
registerDev,
|
|
7012
|
+
registerDb,
|
|
6404
7013
|
registerClone,
|
|
6405
7014
|
registerSync,
|
|
6406
7015
|
registerVersion,
|
|
@@ -6486,11 +7095,11 @@ async function start(deps2, argv2, command, load, onScrub) {
|
|
|
6486
7095
|
}
|
|
6487
7096
|
function withDeadline(work, ms2, signal) {
|
|
6488
7097
|
if (signal?.aborted) return Promise.resolve(false);
|
|
6489
|
-
return new Promise((
|
|
7098
|
+
return new Promise((resolve7) => {
|
|
6490
7099
|
const done = (v) => {
|
|
6491
7100
|
clearTimeout(timer);
|
|
6492
7101
|
signal?.removeEventListener("abort", onAbort);
|
|
6493
|
-
|
|
7102
|
+
resolve7(v);
|
|
6494
7103
|
};
|
|
6495
7104
|
const onAbort = () => done(false);
|
|
6496
7105
|
const timer = setTimeout(() => done(false), ms2);
|
|
@@ -6541,8 +7150,8 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
|
|
|
6541
7150
|
}
|
|
6542
7151
|
|
|
6543
7152
|
// src/core/config.ts
|
|
6544
|
-
import { readFile as
|
|
6545
|
-
import { join as
|
|
7153
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
7154
|
+
import { join as join27 } from "path";
|
|
6546
7155
|
var empty = () => ({ schemaVersion: 1, profiles: {} });
|
|
6547
7156
|
var ConfigStore = class {
|
|
6548
7157
|
constructor(dir2) {
|
|
@@ -6550,12 +7159,12 @@ var ConfigStore = class {
|
|
|
6550
7159
|
}
|
|
6551
7160
|
dir;
|
|
6552
7161
|
get file() {
|
|
6553
|
-
return
|
|
7162
|
+
return join27(this.dir, "config.json");
|
|
6554
7163
|
}
|
|
6555
7164
|
async read() {
|
|
6556
7165
|
let raw;
|
|
6557
7166
|
try {
|
|
6558
|
-
raw = await
|
|
7167
|
+
raw = await readFile8(this.file, "utf8");
|
|
6559
7168
|
} catch (e) {
|
|
6560
7169
|
if (e.code === "ENOENT") return empty();
|
|
6561
7170
|
throw e;
|
|
@@ -6581,9 +7190,9 @@ var ConfigStore = class {
|
|
|
6581
7190
|
|
|
6582
7191
|
// src/platform/git.ts
|
|
6583
7192
|
import { execFile, execFileSync } from "child_process";
|
|
6584
|
-
import { mkdir as mkdir4, readdir as readdir4, stat as
|
|
7193
|
+
import { mkdir as mkdir4, readdir as readdir4, stat as stat7 } from "fs/promises";
|
|
6585
7194
|
import { homedir } from "os";
|
|
6586
|
-
import { dirname as dirname9, join as
|
|
7195
|
+
import { dirname as dirname9, join as join28 } from "path";
|
|
6587
7196
|
function parseRemotes(stdout) {
|
|
6588
7197
|
const seen = /* @__PURE__ */ new Set();
|
|
6589
7198
|
const out = [];
|
|
@@ -6621,8 +7230,8 @@ function parseStatus(stdout) {
|
|
|
6621
7230
|
}
|
|
6622
7231
|
var configuredSshCommand = /* @__PURE__ */ new Map();
|
|
6623
7232
|
function hasConfiguredSshCommand(repoDir) {
|
|
6624
|
-
const
|
|
6625
|
-
let v = configuredSshCommand.get(
|
|
7233
|
+
const key2 = repoDir ?? "";
|
|
7234
|
+
let v = configuredSshCommand.get(key2);
|
|
6626
7235
|
if (v === void 0) {
|
|
6627
7236
|
const args = repoDir ? ["-C", repoDir, "config", "--get", "core.sshCommand"] : ["config", "--get", "core.sshCommand"];
|
|
6628
7237
|
try {
|
|
@@ -6636,7 +7245,7 @@ function hasConfiguredSshCommand(repoDir) {
|
|
|
6636
7245
|
} catch {
|
|
6637
7246
|
v = false;
|
|
6638
7247
|
}
|
|
6639
|
-
configuredSshCommand.set(
|
|
7248
|
+
configuredSshCommand.set(key2, v);
|
|
6640
7249
|
}
|
|
6641
7250
|
return v;
|
|
6642
7251
|
}
|
|
@@ -6650,7 +7259,7 @@ function gitExecOptions(repoDir) {
|
|
|
6650
7259
|
return { cwd: homedir(), env, windowsHide: true, maxBuffer: 8 * 1024 * 1024 };
|
|
6651
7260
|
}
|
|
6652
7261
|
function runGit(args, repoDir) {
|
|
6653
|
-
return new Promise((
|
|
7262
|
+
return new Promise((resolve7, reject) => {
|
|
6654
7263
|
execFile("git", args, gitExecOptions(repoDir), (err, stdout, stderr) => {
|
|
6655
7264
|
if (err && err.code === "ENOENT") {
|
|
6656
7265
|
reject(new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
|
|
@@ -6659,7 +7268,7 @@ function runGit(args, repoDir) {
|
|
|
6659
7268
|
return;
|
|
6660
7269
|
}
|
|
6661
7270
|
const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
|
|
6662
|
-
|
|
7271
|
+
resolve7({ code, stdout: String(stdout), stderr: String(stderr), err: err ?? void 0 });
|
|
6663
7272
|
});
|
|
6664
7273
|
});
|
|
6665
7274
|
}
|
|
@@ -6697,7 +7306,7 @@ async function status(repoRoot) {
|
|
|
6697
7306
|
}
|
|
6698
7307
|
async function statOrThrow(path) {
|
|
6699
7308
|
try {
|
|
6700
|
-
return await
|
|
7309
|
+
return await stat7(path);
|
|
6701
7310
|
} catch (err) {
|
|
6702
7311
|
if (err.code === "ENOENT") return void 0;
|
|
6703
7312
|
throw new QoreError("QORE_INTERNAL", `could not stat ${path}`, { cause: err });
|
|
@@ -6714,7 +7323,7 @@ async function inspect(dir2) {
|
|
|
6714
7323
|
throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
|
|
6715
7324
|
}
|
|
6716
7325
|
const empty2 = entries.length === 0;
|
|
6717
|
-
const isRepo = await statOrThrow(
|
|
7326
|
+
const isRepo = await statOrThrow(join28(dir2, ".git")) !== void 0;
|
|
6718
7327
|
const dirRemotes = isRepo ? await remotes(dir2) : [];
|
|
6719
7328
|
return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
|
|
6720
7329
|
}
|
|
@@ -6776,13 +7385,13 @@ var OsKeychain = class {
|
|
|
6776
7385
|
|
|
6777
7386
|
// src/platform/lock.ts
|
|
6778
7387
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
6779
|
-
import { join as
|
|
7388
|
+
import { join as join29 } from "path";
|
|
6780
7389
|
import lockfile from "proper-lockfile";
|
|
6781
7390
|
function fileLock(dir2) {
|
|
6782
7391
|
return async (name, fn) => {
|
|
6783
|
-
const locks =
|
|
7392
|
+
const locks = join29(dir2, "locks");
|
|
6784
7393
|
await mkdir5(locks, { recursive: true, mode: 448 });
|
|
6785
|
-
const target =
|
|
7394
|
+
const target = join29(locks, `${name}.lock`);
|
|
6786
7395
|
await writeFile5(target, "", { flag: "a" });
|
|
6787
7396
|
let release;
|
|
6788
7397
|
let compromised;
|
|
@@ -6826,7 +7435,7 @@ var STDERR_DRAIN_MS = 500;
|
|
|
6826
7435
|
function makeSpawner(spawnFn, proc) {
|
|
6827
7436
|
return {
|
|
6828
7437
|
run(cmd, args, env, o) {
|
|
6829
|
-
return new Promise((
|
|
7438
|
+
return new Promise((resolve7, reject) => {
|
|
6830
7439
|
let child;
|
|
6831
7440
|
try {
|
|
6832
7441
|
child = spawnFn(cmd, args, {
|
|
@@ -6860,7 +7469,7 @@ function makeSpawner(spawnFn, proc) {
|
|
|
6860
7469
|
cleanup();
|
|
6861
7470
|
const stderr = child.stderr;
|
|
6862
7471
|
if (!o?.onStderr || !stderr || stderr.readableEnded) {
|
|
6863
|
-
|
|
7472
|
+
resolve7({ code, signal });
|
|
6864
7473
|
return;
|
|
6865
7474
|
}
|
|
6866
7475
|
let settled = false;
|
|
@@ -6868,7 +7477,7 @@ function makeSpawner(spawnFn, proc) {
|
|
|
6868
7477
|
if (settled) return;
|
|
6869
7478
|
settled = true;
|
|
6870
7479
|
clearTimeout(timer);
|
|
6871
|
-
|
|
7480
|
+
resolve7({ code, signal });
|
|
6872
7481
|
};
|
|
6873
7482
|
const timer = setTimeout(finish, STDERR_DRAIN_MS);
|
|
6874
7483
|
timer.unref?.();
|
|
@@ -6923,13 +7532,13 @@ var systemLauncher = {
|
|
|
6923
7532
|
child.stdout?.on("data", (c) => out.push(c));
|
|
6924
7533
|
child.stderr?.on("data", (c) => err.push(c));
|
|
6925
7534
|
let closed = false;
|
|
6926
|
-
const exited = new Promise((
|
|
6927
|
-
child.once("error", (e) =>
|
|
7535
|
+
const exited = new Promise((resolve7) => {
|
|
7536
|
+
child.once("error", (e) => resolve7({ code: null, signal: null, spawnError: e.code ?? "error" }));
|
|
6928
7537
|
child.once("close", (code, signal) => {
|
|
6929
7538
|
closed = true;
|
|
6930
7539
|
out.flush();
|
|
6931
7540
|
err.flush();
|
|
6932
|
-
|
|
7541
|
+
resolve7({ code, signal });
|
|
6933
7542
|
});
|
|
6934
7543
|
});
|
|
6935
7544
|
return {
|
|
@@ -6975,11 +7584,11 @@ var systemLauncher = {
|
|
|
6975
7584
|
import { execFile as execFile2 } from "child_process";
|
|
6976
7585
|
import { connect } from "net";
|
|
6977
7586
|
function accepts(host, port) {
|
|
6978
|
-
return new Promise((
|
|
7587
|
+
return new Promise((resolve7) => {
|
|
6979
7588
|
const s = connect({ host, port });
|
|
6980
7589
|
const done = (v) => {
|
|
6981
7590
|
s.destroy();
|
|
6982
|
-
|
|
7591
|
+
resolve7(v);
|
|
6983
7592
|
};
|
|
6984
7593
|
s.setTimeout(500, () => done(false));
|
|
6985
7594
|
s.once("connect", () => done(true));
|
|
@@ -6987,12 +7596,12 @@ function accepts(host, port) {
|
|
|
6987
7596
|
});
|
|
6988
7597
|
}
|
|
6989
7598
|
function run2(cmd, args) {
|
|
6990
|
-
return new Promise((
|
|
7599
|
+
return new Promise((resolve7) => {
|
|
6991
7600
|
execFile2(
|
|
6992
7601
|
cmd,
|
|
6993
7602
|
args,
|
|
6994
7603
|
{ timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
|
|
6995
|
-
(e, stdout) =>
|
|
7604
|
+
(e, stdout) => resolve7(e && !stdout ? null : String(stdout))
|
|
6996
7605
|
);
|
|
6997
7606
|
});
|
|
6998
7607
|
}
|
|
@@ -7055,6 +7664,26 @@ var systemPorts = {
|
|
|
7055
7664
|
}
|
|
7056
7665
|
};
|
|
7057
7666
|
|
|
7667
|
+
// src/platform/postgres.ts
|
|
7668
|
+
import pg from "pg";
|
|
7669
|
+
async function connectPostgres(url, o) {
|
|
7670
|
+
const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: o.timeoutMs });
|
|
7671
|
+
client.on("error", () => {
|
|
7672
|
+
});
|
|
7673
|
+
try {
|
|
7674
|
+
await client.connect();
|
|
7675
|
+
await client.query("set default_transaction_read_only = on");
|
|
7676
|
+
} catch (e) {
|
|
7677
|
+
await client.end().catch(() => {
|
|
7678
|
+
});
|
|
7679
|
+
throw e;
|
|
7680
|
+
}
|
|
7681
|
+
return {
|
|
7682
|
+
query: async (sql, params) => ({ rows: (await client.query(sql, params)).rows }),
|
|
7683
|
+
close: () => client.end()
|
|
7684
|
+
};
|
|
7685
|
+
}
|
|
7686
|
+
|
|
7058
7687
|
// src/buildInfo.ts
|
|
7059
7688
|
var SENTRY_DSN = true ? "https://50b592efb53dbc1a31cb10d3945c6d52@o4512129906049024.ingest.de.sentry.io/4512174341750864" : "";
|
|
7060
7689
|
var SENTRY_ENVIRONMENT = true ? "production" : "development";
|
|
@@ -7064,7 +7693,7 @@ import { execFile as execFile3 } from "child_process";
|
|
|
7064
7693
|
|
|
7065
7694
|
// src/core/diagnostics/proxy.ts
|
|
7066
7695
|
function parseScutilProxy(stdout) {
|
|
7067
|
-
const value = (
|
|
7696
|
+
const value = (key2) => stdout.match(new RegExp(`^\\s*${key2}\\s*:\\s*(.+)$`, "m"))?.[1]?.trim();
|
|
7068
7697
|
if (value("HTTPSEnable") === "1") {
|
|
7069
7698
|
const host = value("HTTPSProxy");
|
|
7070
7699
|
const port = value("HTTPSPort");
|
|
@@ -7085,12 +7714,12 @@ function parseRegProxy(stdout) {
|
|
|
7085
7714
|
|
|
7086
7715
|
// src/platform/proxy.ts
|
|
7087
7716
|
function run3(cmd, args) {
|
|
7088
|
-
return new Promise((
|
|
7717
|
+
return new Promise((resolve7) => {
|
|
7089
7718
|
execFile3(
|
|
7090
7719
|
cmd,
|
|
7091
7720
|
args,
|
|
7092
7721
|
{ timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
|
|
7093
|
-
(e, stdout) =>
|
|
7722
|
+
(e, stdout) => resolve7(e && !stdout ? null : String(stdout))
|
|
7094
7723
|
);
|
|
7095
7724
|
});
|
|
7096
7725
|
}
|
|
@@ -7101,10 +7730,10 @@ async function detect(platform) {
|
|
|
7101
7730
|
return out ? parseScutilProxy(out) : null;
|
|
7102
7731
|
}
|
|
7103
7732
|
if (platform === "win32") {
|
|
7104
|
-
const
|
|
7733
|
+
const key2 = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
|
|
7105
7734
|
const [enable, server] = await Promise.all([
|
|
7106
|
-
run3("reg", ["query",
|
|
7107
|
-
run3("reg", ["query",
|
|
7735
|
+
run3("reg", ["query", key2, "/v", "ProxyEnable"]),
|
|
7736
|
+
run3("reg", ["query", key2, "/v", "ProxyServer"])
|
|
7108
7737
|
]);
|
|
7109
7738
|
const combined = [enable, server].filter(Boolean).join("\n");
|
|
7110
7739
|
return combined ? parseRegProxy(combined) : null;
|
|
@@ -7121,7 +7750,7 @@ function systemProxy(platform = process.platform) {
|
|
|
7121
7750
|
}
|
|
7122
7751
|
|
|
7123
7752
|
// src/version.ts
|
|
7124
|
-
var VERSION = true ? "0.1.
|
|
7753
|
+
var VERSION = true ? "0.1.8" : "0.0.0-dev";
|
|
7125
7754
|
|
|
7126
7755
|
// src/cli.ts
|
|
7127
7756
|
if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
@@ -7169,7 +7798,9 @@ var deps = {
|
|
|
7169
7798
|
},
|
|
7170
7799
|
sentryDsn: SENTRY_DSN,
|
|
7171
7800
|
sentryEnvironment: SENTRY_ENVIRONMENT,
|
|
7172
|
-
systemProxy: () => systemProxy(process.platform)
|
|
7801
|
+
systemProxy: () => systemProxy(process.platform),
|
|
7802
|
+
viewerDir: fileURLToPath(new URL("../viewer/dist/", import.meta.url)),
|
|
7803
|
+
postgres: connectPostgres
|
|
7173
7804
|
};
|
|
7174
7805
|
var termination = preflightTermination(() => ac.abort(), deps.raise);
|
|
7175
7806
|
var io = processIo(isInteractive({
|
|
@@ -7199,4 +7830,4 @@ if (!drained) {
|
|
|
7199
7830
|
process.exit();
|
|
7200
7831
|
}
|
|
7201
7832
|
//# sourceMappingURL=cli.js.map
|
|
7202
|
-
//# debugId=
|
|
7833
|
+
//# debugId=c4bda056-b13b-52b5-831f-2afd3f6c6867
|