@quatronic/qore 0.1.3 → 0.1.4
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 +20 -0
- package/dist/cli.js +623 -184
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir as homedir2 } from "os";
|
|
|
5
5
|
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
|
|
6
6
|
|
|
7
7
|
// src/cli/io.ts
|
|
8
|
-
import { confirm, select } from "@inquirer/prompts";
|
|
8
|
+
import { confirm, search, select } from "@inquirer/prompts";
|
|
9
9
|
import { createInterface } from "readline";
|
|
10
10
|
|
|
11
11
|
// src/platform/browser.ts
|
|
@@ -129,9 +129,17 @@ function processIo(interactive, stderrTTY = Boolean(process.stderr.isTTY)) {
|
|
|
129
129
|
throw e;
|
|
130
130
|
}
|
|
131
131
|
},
|
|
132
|
-
async
|
|
132
|
+
async search(message, source) {
|
|
133
133
|
try {
|
|
134
|
-
return await
|
|
134
|
+
return await search({ message, source: (term) => source(term ?? "") });
|
|
135
|
+
} catch (e) {
|
|
136
|
+
if (e.name === "ExitPromptError") throw new CancelledError();
|
|
137
|
+
throw e;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
async confirm(message, signal, o) {
|
|
141
|
+
try {
|
|
142
|
+
return await confirm({ message, default: o?.default ?? false }, { signal });
|
|
135
143
|
} catch (e) {
|
|
136
144
|
if (e.name === "ExitPromptError" || e.name === "AbortPromptError") throw new CancelledError();
|
|
137
145
|
throw e;
|
|
@@ -395,6 +403,23 @@ function argvBeforeSeparator(argv2) {
|
|
|
395
403
|
function jsonFromArgv(argv2) {
|
|
396
404
|
return argvBeforeSeparator(argv2).some((a) => a === "--json" || a.startsWith("--json="));
|
|
397
405
|
}
|
|
406
|
+
var DOCTOR_CODES = /* @__PURE__ */ new Set([
|
|
407
|
+
"QORE_DOCKER_MISSING",
|
|
408
|
+
"QORE_DOCKER_NOT_RUNNING",
|
|
409
|
+
"QORE_DOCKER_COMPOSE_MISSING",
|
|
410
|
+
"QORE_NODE_VERSION",
|
|
411
|
+
"QORE_PACKAGE_MANAGER",
|
|
412
|
+
"QORE_INSTALL_FAILED",
|
|
413
|
+
"QORE_PORT_IN_USE",
|
|
414
|
+
"QORE_SERVICE_UNHEALTHY"
|
|
415
|
+
]);
|
|
416
|
+
var machineFailure = (e) => DOCTOR_CODES.has(e.code) && !(e.code === "QORE_PORT_IN_USE" && e.details?.owner === "qore");
|
|
417
|
+
function needsDoctor(e) {
|
|
418
|
+
if (!(e instanceof QoreError)) return false;
|
|
419
|
+
const problems = e.details?.problems;
|
|
420
|
+
if (Array.isArray(problems)) return problems.some((p) => machineFailure(p));
|
|
421
|
+
return machineFailure(e);
|
|
422
|
+
}
|
|
398
423
|
function requestLog(ctx) {
|
|
399
424
|
return ctx.opts().verbose ? (line2) => ctx.io.err(line2) : void 0;
|
|
400
425
|
}
|
|
@@ -418,6 +443,9 @@ async function run(argv2, deps2, io2, signal, register) {
|
|
|
418
443
|
const program = buildProgram(deps2, parseIo, signal, register, (code) => {
|
|
419
444
|
state.exitCode = code;
|
|
420
445
|
});
|
|
446
|
+
program.hook("preAction", (_program, action) => {
|
|
447
|
+
state.command = action.name();
|
|
448
|
+
});
|
|
421
449
|
try {
|
|
422
450
|
await program.parseAsync(argv2, { from: "user" });
|
|
423
451
|
return state.exitCode ?? 0;
|
|
@@ -430,6 +458,7 @@ async function run(argv2, deps2, io2, signal, register) {
|
|
|
430
458
|
}
|
|
431
459
|
const opts = program.opts();
|
|
432
460
|
renderError(io2, e, Boolean(opts.json), pc2.isColorSupported && opts.color !== false);
|
|
461
|
+
if (!opts.json && needsDoctor(e) && state.command !== "doctor") io2.err("Run qore doctor for a full check of your machine.");
|
|
433
462
|
return exitCodeFor(e);
|
|
434
463
|
}
|
|
435
464
|
}
|
|
@@ -513,6 +542,9 @@ async function loadMeta(deps2, apiOrigin, refresh) {
|
|
|
513
542
|
});
|
|
514
543
|
return { meta, cached: false };
|
|
515
544
|
}
|
|
545
|
+
async function metaFor(deps2, apiOrigin) {
|
|
546
|
+
return (await loadMeta(deps2, apiOrigin, false)).meta;
|
|
547
|
+
}
|
|
516
548
|
async function ensureCapability(deps2, apiOrigin, capability) {
|
|
517
549
|
const first = await loadMeta(deps2, apiOrigin, false);
|
|
518
550
|
let meta = first.meta;
|
|
@@ -754,8 +786,7 @@ async function openSession(deps2, portal, command, onRequest) {
|
|
|
754
786
|
// src/core/workspace/apps.ts
|
|
755
787
|
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
756
788
|
var tenantCandidates = (tenants) => tenants.map((t) => ({ kind: "tenant", id: t.id, slug: t.slug, name: t.name }));
|
|
757
|
-
async function
|
|
758
|
-
if (UUID.test(arg)) return { appId: arg, tenant: null };
|
|
789
|
+
async function resolveTenant(deps2, session, tenantFlag, whatFor) {
|
|
759
790
|
const me = await session.api.get("/api/cli/v1/me");
|
|
760
791
|
let tenant;
|
|
761
792
|
if (tenantFlag) {
|
|
@@ -779,11 +810,16 @@ async function resolveAppRef(deps2, session, arg, tenantFlag) {
|
|
|
779
810
|
} else if (me.tenants.length === 1) {
|
|
780
811
|
tenant = { id: me.tenants[0].id, slug: me.tenants[0].slug };
|
|
781
812
|
} else {
|
|
782
|
-
throw new QoreError("QORE_CONTEXT_REQUIRED", `Which tenant is "${
|
|
813
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", `Which tenant is "${whatFor}" in?`, {
|
|
783
814
|
hint: "Run `qore tenant use <slug>`, or pass --tenant <slug>.",
|
|
784
815
|
candidates: tenantCandidates(me.tenants)
|
|
785
816
|
});
|
|
786
817
|
}
|
|
818
|
+
return { tenant, me };
|
|
819
|
+
}
|
|
820
|
+
async function resolveAppRef(deps2, session, arg, tenantFlag) {
|
|
821
|
+
if (UUID.test(arg)) return { appId: arg, tenant: null };
|
|
822
|
+
const { tenant } = await resolveTenant(deps2, session, tenantFlag, arg);
|
|
787
823
|
try {
|
|
788
824
|
const { id } = await session.api.get(
|
|
789
825
|
`/api/cli/v1/tenants/${encodeURIComponent(tenant.id)}/apps/resolve?slug=${encodeURIComponent(arg)}`
|
|
@@ -806,7 +842,7 @@ async function resolveAppRef(deps2, session, arg, tenantFlag) {
|
|
|
806
842
|
}
|
|
807
843
|
|
|
808
844
|
// src/core/workspace/apply.ts
|
|
809
|
-
import { join as join4
|
|
845
|
+
import { join as join4 } from "path";
|
|
810
846
|
|
|
811
847
|
// src/core/context/cache.ts
|
|
812
848
|
import { join as join2 } from "path";
|
|
@@ -956,7 +992,7 @@ async function trust(deps2, root, moduleId) {
|
|
|
956
992
|
}
|
|
957
993
|
|
|
958
994
|
// src/core/workspace/plan.ts
|
|
959
|
-
import { join as join3 } from "path";
|
|
995
|
+
import { join as join3, relative } from "path";
|
|
960
996
|
var SEGMENT = /^[A-Za-z0-9_.-]+$/;
|
|
961
997
|
function isSupportedRepoUrl(repoUrl) {
|
|
962
998
|
let u;
|
|
@@ -981,6 +1017,13 @@ function cloneUrl(repoUrl, protocol) {
|
|
|
981
1017
|
const u = new URL(repoUrl);
|
|
982
1018
|
return `git@${u.hostname}:${u.pathname.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/, "")}.git`;
|
|
983
1019
|
}
|
|
1020
|
+
function conflictText(cwd, action) {
|
|
1021
|
+
const dir2 = relative(cwd, action.dir) || ".";
|
|
1022
|
+
const notRepo = action.reason === "not a git repository";
|
|
1023
|
+
const what = notRepo ? "it is not a git repository" : action.origin ? `its origin is ${action.origin}, not ${action.module.repoUrl}` : "it has no origin remote";
|
|
1024
|
+
const message = notRepo ? `${dir2} is not a git repository` : action.origin ? `${dir2} holds another repository (its origin is ${action.origin}, not ${action.module.repoUrl})` : `${dir2} has no origin remote`;
|
|
1025
|
+
return { message, hint: `Move ${dir2} aside (${what}), then run \`qore sync\`.` };
|
|
1026
|
+
}
|
|
984
1027
|
function planWorkspace(root, modules, dirs, strayRepos2) {
|
|
985
1028
|
const actions = modules.map((module) => {
|
|
986
1029
|
const dir2 = join3(root, module.slug);
|
|
@@ -997,6 +1040,10 @@ function planWorkspace(root, modules, dirs, strayRepos2) {
|
|
|
997
1040
|
const slugs = new Set(modules.map((m) => m.slug));
|
|
998
1041
|
return { actions, removed: strayRepos2.filter((s) => !slugs.has(s)) };
|
|
999
1042
|
}
|
|
1043
|
+
function cloneProtocol(dirs) {
|
|
1044
|
+
const origins = Object.values(dirs).flatMap((d) => d.remotes.filter((r) => r.name === "origin").map((r) => r.url));
|
|
1045
|
+
return origins.some((u) => /^(git@|ssh:\/\/)/.test(u)) ? "ssh" : "https";
|
|
1046
|
+
}
|
|
1000
1047
|
|
|
1001
1048
|
// src/core/workspace/diagnose.ts
|
|
1002
1049
|
var ACCESS_CODES = /* @__PURE__ */ new Set(["QORE_GITHUB_NOT_LINKED", "QORE_REPO_NO_ACCESS", "QORE_REPO_ACCESS_PENDING"]);
|
|
@@ -1072,15 +1119,8 @@ function toFailure(slug, e) {
|
|
|
1072
1119
|
throw e;
|
|
1073
1120
|
}
|
|
1074
1121
|
function conflictResult(deps2, action) {
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
const message = action.reason === "not a git repository" ? `${dir2} is not a git repository` : action.origin ? `${dir2} holds another repository (its origin is ${action.origin}, not ${action.module.repoUrl})` : `${dir2} has no origin remote`;
|
|
1078
|
-
return {
|
|
1079
|
-
slug: action.module.slug,
|
|
1080
|
-
status: "conflict",
|
|
1081
|
-
reason: action.reason,
|
|
1082
|
-
error: { code: "QORE_WORKSPACE_CONFLICT", message, hint: `Move ${dir2} aside (${what}), then run \`qore sync\`.` }
|
|
1083
|
-
};
|
|
1122
|
+
const { message, hint } = conflictText(deps2.cwd, action);
|
|
1123
|
+
return { slug: action.module.slug, status: "conflict", reason: action.reason, error: { code: "QORE_WORKSPACE_CONFLICT", message, hint } };
|
|
1084
1124
|
}
|
|
1085
1125
|
async function applyActions(deps2, o) {
|
|
1086
1126
|
const cache = new ResolutionCache(join4(deps2.config.dir, "cache"), deps2.now);
|
|
@@ -1382,8 +1422,34 @@ async function resolveApp(deps2, io2, session, appArg, tenantFlag) {
|
|
|
1382
1422
|
return resolveAppRef(deps2, session, appArg, slug);
|
|
1383
1423
|
}
|
|
1384
1424
|
}
|
|
1425
|
+
async function pickApp(deps2, io2, session, tenantFlag) {
|
|
1426
|
+
if (!io2.interactive) {
|
|
1427
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", "An app is required", {
|
|
1428
|
+
hint: "Pass an app slug: `qore clone <app>`. Interactively, omitting it opens a picker."
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
let tenant, me;
|
|
1432
|
+
try {
|
|
1433
|
+
({ tenant, me } = await resolveTenant(deps2, session, tenantFlag, "the app you want to clone"));
|
|
1434
|
+
} catch (e) {
|
|
1435
|
+
if (!(e instanceof QoreError) || e.code !== "QORE_CONTEXT_REQUIRED" && e.code !== "QORE_TENANT_ACCESS_LOST" || !e.candidates?.length) throw e;
|
|
1436
|
+
const slug = await io2.select("Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.slug })));
|
|
1437
|
+
({ tenant, me } = await resolveTenant(deps2, session, slug, "the app you want to clone"));
|
|
1438
|
+
}
|
|
1439
|
+
const apps = me.tenants.find((t) => t.id === tenant.id)?.apps ?? [];
|
|
1440
|
+
if (apps.length === 0) {
|
|
1441
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", `No apps in ${tenant.slug} to clone`, {
|
|
1442
|
+
hint: "Run `qore whoami` to see what you have access to."
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
const app = await io2.search("Select an app", (term) => {
|
|
1446
|
+
const q = term.toLowerCase();
|
|
1447
|
+
return apps.filter((a) => !q || a.slug.toLowerCase().includes(q) || a.name.toLowerCase().includes(q)).map((a) => ({ name: `${a.name} (${a.slug})`, value: a.slug }));
|
|
1448
|
+
});
|
|
1449
|
+
return { tenant: tenant.slug, app };
|
|
1450
|
+
}
|
|
1385
1451
|
function registerClone(program, ctx) {
|
|
1386
|
-
program.command("clone").description("Clone an app's modules into a new workspace directory").argument("
|
|
1452
|
+
program.command("clone").description("Clone an app's modules into a new workspace directory").argument("[app]", "app slug or id (omit to pick one interactively)").argument("[dir]", "target directory (default: the app's slug)").option("--tenant <slug>", "tenant the app slug belongs to, when it is not your active one").addOption(new Option("--protocol <https|ssh>", "clone URL protocol").choices(["https", "ssh"]).default("https")).action(async (appArg, dirArg, flags) => {
|
|
1387
1453
|
const { deps: deps2, io: io2 } = ctx;
|
|
1388
1454
|
const g = ctx.opts();
|
|
1389
1455
|
const cfg = await deps2.config.read();
|
|
@@ -1391,7 +1457,9 @@ function registerClone(program, ctx) {
|
|
|
1391
1457
|
const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
|
|
1392
1458
|
const session = await openSession(deps2, portal, "clone", onRequest);
|
|
1393
1459
|
await ensureCapability(deps2, portal.apiOrigin, "app.modules");
|
|
1394
|
-
const
|
|
1460
|
+
const picked = appArg === void 0 ? await pickApp(deps2, io2, session, flags.tenant) : void 0;
|
|
1461
|
+
const appSlug = picked?.app ?? appArg;
|
|
1462
|
+
const ref = await resolveApp(deps2, io2, session, appSlug, picked?.tenant ?? flags.tenant);
|
|
1395
1463
|
const app = await session.api.get(`/api/cli/v1/apps/${encodeURIComponent(ref.appId)}/modules`);
|
|
1396
1464
|
if (dirArg === void 0 && !isSafeSlug(app.app.slug)) throw unsafeSlugError(app.app.slug);
|
|
1397
1465
|
const dirLabel = dirArg ?? app.app.slug;
|
|
@@ -1401,7 +1469,7 @@ function registerClone(program, ctx) {
|
|
|
1401
1469
|
const ws = await readWorkspace(root);
|
|
1402
1470
|
if (!ws || ws.appId !== app.app.id || ws.portal !== portal.apiOrigin) {
|
|
1403
1471
|
throw new QoreError("QORE_WORKSPACE_CONFLICT", `${root} exists and is not the workspace of ${app.app.slug}`, {
|
|
1404
|
-
hint: `Pick another directory: qore clone ${
|
|
1472
|
+
hint: `Pick another directory: qore clone ${appSlug} <dir>.`
|
|
1405
1473
|
});
|
|
1406
1474
|
}
|
|
1407
1475
|
}
|
|
@@ -1442,7 +1510,7 @@ function registerClone(program, ctx) {
|
|
|
1442
1510
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
1443
1511
|
import { realpath as realpath5 } from "fs/promises";
|
|
1444
1512
|
import { constants as constants3 } from "os";
|
|
1445
|
-
import { relative as relative6 } from "path";
|
|
1513
|
+
import { dirname as dirname6, join as join18, relative as relative6 } from "path";
|
|
1446
1514
|
import { StringDecoder } from "string_decoder";
|
|
1447
1515
|
|
|
1448
1516
|
// src/core/context/repoConfig.ts
|
|
@@ -1801,7 +1869,7 @@ async function resolveModuleContext(deps2, o) {
|
|
|
1801
1869
|
});
|
|
1802
1870
|
}
|
|
1803
1871
|
var appCandidates = (apps) => apps.map((a) => ({ kind: "app", id: a.id, slug: a.slug, name: a.name }));
|
|
1804
|
-
function
|
|
1872
|
+
function pickApp2(mc, app) {
|
|
1805
1873
|
const apps = mc.module.apps;
|
|
1806
1874
|
if (app) {
|
|
1807
1875
|
const found = apps.find((a) => a.id === app || a.slug === app);
|
|
@@ -1821,14 +1889,14 @@ function pickApp(mc, app) {
|
|
|
1821
1889
|
|
|
1822
1890
|
// src/core/diagnostics/node.ts
|
|
1823
1891
|
var KEEP_LINES = 50;
|
|
1824
|
-
function
|
|
1825
|
-
const
|
|
1826
|
-
const missing =
|
|
1892
|
+
function exitHint(tail, h) {
|
|
1893
|
+
const text2 = tail.join("\n");
|
|
1894
|
+
const missing = text2.match(/Cannot find (?:module|package) '([^']+)'/);
|
|
1827
1895
|
if (missing) {
|
|
1828
|
-
return { reason: "missing-module", hint: `A dependency is missing (${missing[1]}). Run \`${
|
|
1896
|
+
return { reason: "missing-module", hint: `A dependency is missing (${missing[1]}). Run \`${h.installCommand}\`, then qore dev again.` };
|
|
1829
1897
|
}
|
|
1830
|
-
if (/ZodError:/.test(
|
|
1831
|
-
const keys = [...
|
|
1898
|
+
if (/ZodError:/.test(text2)) {
|
|
1899
|
+
const keys = [...text2.matchAll(/"path":\s*\[\s*"([^"]+)"/g)].map((m) => m[1]);
|
|
1832
1900
|
if (keys.length > 0) {
|
|
1833
1901
|
return {
|
|
1834
1902
|
reason: "invalid-env",
|
|
@@ -1837,7 +1905,7 @@ function earlyExitHint(tail, o) {
|
|
|
1837
1905
|
};
|
|
1838
1906
|
}
|
|
1839
1907
|
}
|
|
1840
|
-
const addrInUse =
|
|
1908
|
+
const addrInUse = text2.match(/listen EADDRINUSE: address already in use (?:\S*:)?(\d+)/);
|
|
1841
1909
|
if (addrInUse) {
|
|
1842
1910
|
const port = Number(addrInUse[1]);
|
|
1843
1911
|
return {
|
|
@@ -1846,7 +1914,45 @@ function earlyExitHint(tail, o) {
|
|
|
1846
1914
|
hint: `Port ${port} was taken while the server started. Run qore dev again; if it repeats, lsof -iTCP:${port} -sTCP:LISTEN names the holder.`
|
|
1847
1915
|
};
|
|
1848
1916
|
}
|
|
1849
|
-
if (/
|
|
1917
|
+
if (/ECONNREFUSED/.test(text2)) {
|
|
1918
|
+
const m = text2.match(/ECONNREFUSED\s+\S*:(\d+)/) ?? text2.match(/code: 'ECONNREFUSED'[^}]*?port: (\d+)/);
|
|
1919
|
+
if (!m) return { reason: "conn-refused", hint: "A connection was refused (ECONNREFUSED) \u2014 a service this module calls is not running." };
|
|
1920
|
+
const port = Number(m[1]);
|
|
1921
|
+
const role = h.ports.get(port);
|
|
1922
|
+
if (role?.kind === "database") {
|
|
1923
|
+
const up = h.runCommand("db:up");
|
|
1924
|
+
return {
|
|
1925
|
+
reason: "conn-refused",
|
|
1926
|
+
port,
|
|
1927
|
+
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.")
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
if (role?.kind === "provider") {
|
|
1931
|
+
return {
|
|
1932
|
+
reason: "conn-refused",
|
|
1933
|
+
port,
|
|
1934
|
+
hint: `${role.key} points at :${port}, where nothing listens (ECONNREFUSED). Start that module \u2014 qore dev in the workspace root starts providers first.`
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
return { reason: "conn-refused", port, hint: `Nothing listens on :${port} (ECONNREFUSED) \u2014 a service this module calls is not running.` };
|
|
1938
|
+
}
|
|
1939
|
+
const auth = text2.match(/password authentication failed for user "([^"]+)"/);
|
|
1940
|
+
if (auth) {
|
|
1941
|
+
const reset = h.runCommand("db:reset");
|
|
1942
|
+
return {
|
|
1943
|
+
reason: "db-auth",
|
|
1944
|
+
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.` : "")
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
const relation = text2.match(/relation "([^"]+)" does not exist/);
|
|
1948
|
+
if (relation) {
|
|
1949
|
+
const reset = h.runCommand("db:reset");
|
|
1950
|
+
return {
|
|
1951
|
+
reason: "db-schema",
|
|
1952
|
+
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).` : "")
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
if (/ENOSPC: System limit for number of file watchers reached/.test(text2)) {
|
|
1850
1956
|
return {
|
|
1851
1957
|
reason: "watcher-limit",
|
|
1852
1958
|
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"
|
|
@@ -1947,12 +2053,12 @@ function renderEnv(original, merged) {
|
|
|
1947
2053
|
for (const [key, value] of merged) {
|
|
1948
2054
|
if (!seen.has(key)) appended.push(`${key}=${value}`);
|
|
1949
2055
|
}
|
|
1950
|
-
let
|
|
2056
|
+
let text2 = out.join("\n");
|
|
1951
2057
|
if (appended.length) {
|
|
1952
|
-
if (
|
|
1953
|
-
|
|
2058
|
+
if (text2.length && !text2.endsWith("\n")) text2 += "\n";
|
|
2059
|
+
text2 += appended.join("\n") + "\n";
|
|
1954
2060
|
}
|
|
1955
|
-
return
|
|
2061
|
+
return text2;
|
|
1956
2062
|
}
|
|
1957
2063
|
|
|
1958
2064
|
// src/core/env/pull.ts
|
|
@@ -2034,12 +2140,12 @@ async function readOrNull(p) {
|
|
|
2034
2140
|
throw e;
|
|
2035
2141
|
}
|
|
2036
2142
|
}
|
|
2037
|
-
async function writeSecurely(target,
|
|
2143
|
+
async function writeSecurely(target, text2) {
|
|
2038
2144
|
const base = basename2(target);
|
|
2039
2145
|
const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes3(6).toString("hex")}.local` : `${base}.qore-${randomBytes3(6).toString("hex")}.tmp`;
|
|
2040
2146
|
const tmp = join10(dirname4(target), tmpName);
|
|
2041
2147
|
try {
|
|
2042
|
-
await writeFile3(tmp,
|
|
2148
|
+
await writeFile3(tmp, text2, { mode: 384, flag: "wx" });
|
|
2043
2149
|
await chmod2(tmp, 384);
|
|
2044
2150
|
await rename3(tmp, target);
|
|
2045
2151
|
} finally {
|
|
@@ -2124,16 +2230,117 @@ async function devEnv(deps2, o) {
|
|
|
2124
2230
|
cause = e;
|
|
2125
2231
|
}
|
|
2126
2232
|
}
|
|
2127
|
-
const
|
|
2128
|
-
if (
|
|
2233
|
+
const text2 = await readOrNull(target);
|
|
2234
|
+
if (text2 === null || !await isTrusted(deps2, o.root, o.moduleId)) {
|
|
2129
2235
|
throw cause ?? new QoreError("QORE_API_UNREACHABLE", "The portal is unreachable", {
|
|
2130
2236
|
hint: "The portal is unreachable and there is no env file to start from yet."
|
|
2131
2237
|
});
|
|
2132
2238
|
}
|
|
2133
|
-
const env = Object.fromEntries(parseEnv(
|
|
2239
|
+
const env = Object.fromEntries(parseEnv(text2));
|
|
2134
2240
|
return { env, offline: true, envFile: target, port: envPort(env, "PORT"), dbPort: envPort(env, "DB_PORT"), created: false, kept: [] };
|
|
2135
2241
|
}
|
|
2136
2242
|
|
|
2243
|
+
// src/core/workspace/access.ts
|
|
2244
|
+
function devTeamsOf(am, m) {
|
|
2245
|
+
const own = m.devTeamIds?.length ? am.devTeams.filter((t) => m.devTeamIds.includes(t.id)) : [];
|
|
2246
|
+
return own.length ? own : am.devTeams;
|
|
2247
|
+
}
|
|
2248
|
+
function askMaintainer(am, m) {
|
|
2249
|
+
const teams = devTeamsOf(am, m);
|
|
2250
|
+
const names = teams.map((t) => t.name);
|
|
2251
|
+
const maintainers = [...new Set(teams.flatMap((t) => t.maintainers))];
|
|
2252
|
+
const who = names.length ? names.join(", ") : "the app's dev team";
|
|
2253
|
+
return { teams: names, maintainers, text: `ask a maintainer of ${who}${maintainers.length ? ` (${maintainers.join(", ")})` : ""}` };
|
|
2254
|
+
}
|
|
2255
|
+
function notClonedReason(m, am) {
|
|
2256
|
+
if (am && !m.githubLinked) return `GitHub account not linked (${am.accountUrl})`;
|
|
2257
|
+
if (am && m.repoAccess === "none") {
|
|
2258
|
+
const names = devTeamsOf(am, m).map((t) => t.name);
|
|
2259
|
+
return names.length ? `no repository access (dev team ${names.join(", ")})` : "no repository access (ask your tenant owner)";
|
|
2260
|
+
}
|
|
2261
|
+
return "run qore sync";
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// src/core/dev/findings.ts
|
|
2265
|
+
var plural = (n, one, many) => n === 1 ? one : many;
|
|
2266
|
+
function modulesMissing(o) {
|
|
2267
|
+
const actionFor = new Map(o.actions.map((a) => [a.module.slug, a]));
|
|
2268
|
+
const findings = [];
|
|
2269
|
+
const fixable = [];
|
|
2270
|
+
const blocked = [];
|
|
2271
|
+
const reasons = {};
|
|
2272
|
+
const own = (m, reason, rest) => {
|
|
2273
|
+
reasons[m.slug] = reason;
|
|
2274
|
+
return { code: "dev.modules-missing", severity: "warn", module: m.slug, modules: [m.slug], message: `${m.slug} is not cloned \u2014 ${reason}`, ...rest };
|
|
2275
|
+
};
|
|
2276
|
+
for (const m of o.missing) {
|
|
2277
|
+
const a = actionFor.get(m.slug);
|
|
2278
|
+
if (a && a.kind !== "clone") {
|
|
2279
|
+
if (a.kind === "conflict") {
|
|
2280
|
+
const c = conflictText(o.cwd, a);
|
|
2281
|
+
blocked.push(own(m, c.message, { action: { command: c.hint } }));
|
|
2282
|
+
} else if (a.kind === "unsupported") {
|
|
2283
|
+
blocked.push(own(m, "its repository URL is not supported", { action: { command: unsupportedRepoUrlError(m.slug, "").hint } }));
|
|
2284
|
+
} else {
|
|
2285
|
+
blocked.push(own(m, a.reason ?? "no repository yet", { why: "It will not run in this session." }));
|
|
2286
|
+
}
|
|
2287
|
+
} else if (o.am !== null && (!m.githubLinked || m.repoAccess === "none")) {
|
|
2288
|
+
const am = o.am;
|
|
2289
|
+
blocked.push(own(m, notClonedReason(m, am), { action: { command: !m.githubLinked ? `Link your GitHub account: ${am.accountUrl}` : askMaintainer(am, m).text } }));
|
|
2290
|
+
} else fixable.push(m);
|
|
2291
|
+
}
|
|
2292
|
+
if (fixable.length > 0) {
|
|
2293
|
+
const slugs = fixable.map((m) => m.slug);
|
|
2294
|
+
const deps2 = slugs.flatMap((s) => (o.consumers[s] ?? []).map((c) => `${c} depends on ${s}`));
|
|
2295
|
+
findings.push({
|
|
2296
|
+
code: "dev.modules-missing",
|
|
2297
|
+
severity: "warn",
|
|
2298
|
+
message: `${fixable.length} ${plural(fixable.length, "module of this app is", "modules of this app are")} not cloned: ${slugs.join(", ")}`,
|
|
2299
|
+
why: deps2.length > 0 ? `${deps2.join("; ")}.` : `${plural(fixable.length, "It", "They")} will not run in this session.`,
|
|
2300
|
+
modules: slugs,
|
|
2301
|
+
action: o.am ? { command: "qore sync", fix: "sync" } : { command: "qore sync" }
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
return { findings: [...findings, ...blocked], fixable, reasons };
|
|
2305
|
+
}
|
|
2306
|
+
function strayClone(name, appName) {
|
|
2307
|
+
return { code: "dev.stray-clone", severity: "info", module: name, message: `${name} is no longer part of ${appName} \u2014 you can remove the directory.` };
|
|
2308
|
+
}
|
|
2309
|
+
function repoBehind(slug, s, o) {
|
|
2310
|
+
if (!s.ok || !s.upstream || !s.behind) return null;
|
|
2311
|
+
return {
|
|
2312
|
+
code: "dev.repo-behind",
|
|
2313
|
+
severity: "warn",
|
|
2314
|
+
module: slug,
|
|
2315
|
+
message: `${slug} is ${s.behind} ${plural(s.behind, "commit", "commits")} behind ${s.upstreamRef ?? "its upstream"} (as of your last fetch).`,
|
|
2316
|
+
action: { command: o.workspace ? "qore sync --pull" : "git pull" }
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
function servicesMissing(slug, env, recipe, o) {
|
|
2320
|
+
const db = envPort(env, "DB_PORT");
|
|
2321
|
+
if (db === null || recipe.services.length > 0 || !o.servicesFlag || o.configured) return null;
|
|
2322
|
+
return {
|
|
2323
|
+
code: "dev.services-missing",
|
|
2324
|
+
severity: "warn",
|
|
2325
|
+
module: slug,
|
|
2326
|
+
message: `The portal gives ${slug} a database (DB_PORT=${db}), but ${slug} has no db:up script \u2014 no database will be started.`,
|
|
2327
|
+
why: "This repository is probably older than its template.",
|
|
2328
|
+
action: { command: 'add a "db:up" script (see the current app template), or set dev.services in .qore.json' }
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
function providerDown(o) {
|
|
2332
|
+
const single = o.providers.length === 1 && o.keys.length === 1;
|
|
2333
|
+
return {
|
|
2334
|
+
code: "dev.provider-down",
|
|
2335
|
+
severity: "warn",
|
|
2336
|
+
module: o.slug,
|
|
2337
|
+
modules: o.providers,
|
|
2338
|
+
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(", ")} ${plural(o.keys.length, "points", "point")} at ports nothing listens on (depends on: ${o.providers.join(", ")}).`,
|
|
2339
|
+
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.`,
|
|
2340
|
+
action: { command: o.fix }
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2137
2344
|
// src/core/dev/project.ts
|
|
2138
2345
|
import { access, readFile as readFile5 } from "fs/promises";
|
|
2139
2346
|
import { join as join13 } from "path";
|
|
@@ -2252,10 +2459,10 @@ function classifyComposeVersion(r) {
|
|
|
2252
2459
|
// src/core/diagnostics/postgres.ts
|
|
2253
2460
|
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/;
|
|
2254
2461
|
function classifyComposeLogs(logs, o) {
|
|
2255
|
-
const
|
|
2256
|
-
if (!INCOMPATIBLE.test(
|
|
2462
|
+
const text2 = logs.join("\n");
|
|
2463
|
+
if (!INCOMPATIBLE.test(text2)) return null;
|
|
2257
2464
|
const details = { reason: "pg-data-version" };
|
|
2258
|
-
const m =
|
|
2465
|
+
const m = text2.match(/initialized by PostgreSQL version (\d+).*this version (\d+)/);
|
|
2259
2466
|
if (m) {
|
|
2260
2467
|
details.found = m[1];
|
|
2261
2468
|
details.expected = m[2];
|
|
@@ -2345,6 +2552,7 @@ function nodeVersionError(found, range, slug) {
|
|
|
2345
2552
|
// src/core/dev/preflight.ts
|
|
2346
2553
|
var pmMajor = (v) => v ? v.split(".")[0] : "latest";
|
|
2347
2554
|
var GRACE_MS = 1e4;
|
|
2555
|
+
var secs = (ms) => `${Math.round(ms / 1e3)} s`;
|
|
2348
2556
|
async function runTool(deps2, cmd, args, o) {
|
|
2349
2557
|
if (o.signal.aborted) throw new CancelledError();
|
|
2350
2558
|
const tail = [];
|
|
@@ -2401,11 +2609,16 @@ async function checkNode(deps2, r, pkg, slug, signal) {
|
|
|
2401
2609
|
return found;
|
|
2402
2610
|
}
|
|
2403
2611
|
async function installIfNeeded(deps2, r, slug, onLine, report, signal) {
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2612
|
+
if (!r.install) {
|
|
2613
|
+
report({ module: slug, step: "install", status: "skip", detail: "disabled" });
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
if (!await needsInstall(r.root, r.pm)) {
|
|
2617
|
+
report({ module: slug, step: "install", status: "skip", detail: "up to date" });
|
|
2407
2618
|
return;
|
|
2408
2619
|
}
|
|
2620
|
+
report({ module: slug, step: "install", status: "start" });
|
|
2621
|
+
const began = deps2.now().getTime();
|
|
2409
2622
|
const { cmd, args } = installArgs(r.pm);
|
|
2410
2623
|
const res = await runTool(deps2, cmd, args, { cwd: r.root, env: deps2.env, onLine, signal });
|
|
2411
2624
|
if (res.code !== 0) {
|
|
@@ -2414,7 +2627,7 @@ async function installIfNeeded(deps2, r, slug, onLine, report, signal) {
|
|
|
2414
2627
|
if (err.details?.reason === "outdated-lockfile") throw err;
|
|
2415
2628
|
throw new QoreError("QORE_INSTALL_FAILED", `${command} failed in ${slug}`, { hint: err.hint, details: err.details });
|
|
2416
2629
|
}
|
|
2417
|
-
report({ module: slug, step: "install", status: "done" });
|
|
2630
|
+
report({ module: slug, step: "install", status: "done", detail: secs(deps2.now().getTime() - began) });
|
|
2418
2631
|
}
|
|
2419
2632
|
async function portInUseError(deps2, o) {
|
|
2420
2633
|
const owner = await deps2.ports.owner(o.port);
|
|
@@ -2477,7 +2690,12 @@ async function composeFailure(deps2, r, slug, env, signal, message, command, std
|
|
|
2477
2690
|
});
|
|
2478
2691
|
}
|
|
2479
2692
|
async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine, report) {
|
|
2480
|
-
|
|
2693
|
+
if (r.services.length === 0) {
|
|
2694
|
+
report({ module: slug, step: "services", status: "skip" });
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
report({ module: slug, step: "services", status: "start", ...dbPort !== null ? { detail: `:${dbPort}` } : {} });
|
|
2698
|
+
const began = deps2.now().getTime();
|
|
2481
2699
|
if (r.dockerServices) {
|
|
2482
2700
|
const res = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], { cwd: r.root, env, onLine, signal });
|
|
2483
2701
|
const dockerErr = classifyDockerInfo(res, deps2.platform);
|
|
@@ -2497,7 +2715,7 @@ async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine
|
|
|
2497
2715
|
throw await composeFailure(deps2, r, slug, env, signal, `${script} failed in ${slug}`, `${cmd} ${args.join(" ")}`, res.tail);
|
|
2498
2716
|
}
|
|
2499
2717
|
}
|
|
2500
|
-
if (dbPort !== null
|
|
2718
|
+
if (dbPort !== null) {
|
|
2501
2719
|
const ok2 = await deps2.ports.waitFor(dbPort, waitMs, signal);
|
|
2502
2720
|
if (signal.aborted) throw new CancelledError();
|
|
2503
2721
|
if (!ok2) {
|
|
@@ -2513,7 +2731,8 @@ async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine
|
|
|
2513
2731
|
);
|
|
2514
2732
|
}
|
|
2515
2733
|
}
|
|
2516
|
-
|
|
2734
|
+
const took = secs(deps2.now().getTime() - began);
|
|
2735
|
+
report({ module: slug, step: "services", status: "done", detail: dbPort !== null ? `:${dbPort} (${took})` : `(${took})` });
|
|
2517
2736
|
}
|
|
2518
2737
|
async function checkPort(deps2, o) {
|
|
2519
2738
|
if (o.port === null) return;
|
|
@@ -2568,6 +2787,44 @@ async function leftoverOwnsPort(deps2, run4, port) {
|
|
|
2568
2787
|
return false;
|
|
2569
2788
|
}
|
|
2570
2789
|
|
|
2790
|
+
// src/core/dev/providers.ts
|
|
2791
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
2792
|
+
function connPorts(env) {
|
|
2793
|
+
const out = [];
|
|
2794
|
+
for (const key of Object.keys(env).sort()) {
|
|
2795
|
+
const m = /^CONN_(.+)_URL$/.exec(key);
|
|
2796
|
+
if (!m) continue;
|
|
2797
|
+
let url;
|
|
2798
|
+
try {
|
|
2799
|
+
url = new URL(env[key].trim().replace(/^["']|["']$/g, ""));
|
|
2800
|
+
} catch {
|
|
2801
|
+
continue;
|
|
2802
|
+
}
|
|
2803
|
+
const port = Number(url.port);
|
|
2804
|
+
if (port > 0 && LOCAL_HOSTS.has(url.hostname)) out.push({ key, alias: m[1], port });
|
|
2805
|
+
}
|
|
2806
|
+
return out;
|
|
2807
|
+
}
|
|
2808
|
+
function deadAliases(env, listening) {
|
|
2809
|
+
return connPorts(env).filter((c) => !listening.has(c.port)).map((c) => c.key);
|
|
2810
|
+
}
|
|
2811
|
+
function hintContext(recipe, pkg, env) {
|
|
2812
|
+
const { cmd, args } = installArgs(recipe.pm);
|
|
2813
|
+
const ports = /* @__PURE__ */ new Map();
|
|
2814
|
+
for (const c of connPorts(env)) ports.set(c.port, { kind: "provider", key: c.key });
|
|
2815
|
+
const db = envPort(env, "DB_PORT");
|
|
2816
|
+
if (db !== null) ports.set(db, { kind: "database" });
|
|
2817
|
+
return {
|
|
2818
|
+
installCommand: [cmd, ...args].join(" "),
|
|
2819
|
+
runCommand: (script) => {
|
|
2820
|
+
if (pkg.scripts?.[script] === void 0) return null;
|
|
2821
|
+
const r = runArgs(recipe.pm, script);
|
|
2822
|
+
return [r.cmd, ...r.args].join(" ");
|
|
2823
|
+
},
|
|
2824
|
+
ports
|
|
2825
|
+
};
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2571
2828
|
// src/core/dev/runFile.ts
|
|
2572
2829
|
import { unlink as unlink2 } from "fs/promises";
|
|
2573
2830
|
import { join as join15 } from "path";
|
|
@@ -2621,11 +2878,11 @@ async function moduleContextFor(ctx, flags, command) {
|
|
|
2621
2878
|
}
|
|
2622
2879
|
async function appFor(ctx, mc, app) {
|
|
2623
2880
|
try {
|
|
2624
|
-
return
|
|
2881
|
+
return pickApp2(mc, app);
|
|
2625
2882
|
} catch (e) {
|
|
2626
2883
|
if (!(e instanceof QoreError) || e.code !== "QORE_APP_AMBIGUOUS" || !ctx.io.interactive) throw e;
|
|
2627
2884
|
const id = await ctx.io.select("Select an app", mc.module.apps.map((a) => ({ name: a.name, value: a.id })));
|
|
2628
|
-
return
|
|
2885
|
+
return pickApp2(mc, id);
|
|
2629
2886
|
}
|
|
2630
2887
|
}
|
|
2631
2888
|
function sourceText(mc) {
|
|
@@ -2649,13 +2906,32 @@ function contextJson(mc, app) {
|
|
|
2649
2906
|
};
|
|
2650
2907
|
}
|
|
2651
2908
|
|
|
2652
|
-
// src/cli/
|
|
2653
|
-
|
|
2654
|
-
var STOP_WAIT_MS = 1e4;
|
|
2655
|
-
function canRecover(ctx) {
|
|
2909
|
+
// src/cli/findings.ts
|
|
2910
|
+
function canOffer(ctx) {
|
|
2656
2911
|
const g = ctx.opts();
|
|
2657
2912
|
return ctx.io.interactive && !g.json && g.input !== false;
|
|
2658
2913
|
}
|
|
2914
|
+
function findingEvent(f) {
|
|
2915
|
+
const { severity, ...rest } = f;
|
|
2916
|
+
return { type: severity === "info" ? "info" : "warning", ...rest };
|
|
2917
|
+
}
|
|
2918
|
+
function renderFinding(ctx, f) {
|
|
2919
|
+
if (ctx.opts().json) {
|
|
2920
|
+
ctx.io.out(JSON.stringify(findingEvent(f)));
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
ctx.io.err(`${f.severity === "info" ? "\u2192" : "!"} ${f.message}`);
|
|
2924
|
+
if (f.why) ctx.io.err(` ${f.why}`);
|
|
2925
|
+
if (f.action) ctx.io.err(` Fix: ${f.action.command}`);
|
|
2926
|
+
}
|
|
2927
|
+
async function offerFix(ctx, question, o) {
|
|
2928
|
+
if (!canOffer(ctx)) return false;
|
|
2929
|
+
return ctx.io.confirm(question, o.signal, { default: o.default });
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
// src/cli/devRecover.ts
|
|
2933
|
+
var DOCKER_WAIT = { timeoutMs: 12e4, pollMs: 2e3 };
|
|
2934
|
+
var STOP_WAIT_MS = 1e4;
|
|
2659
2935
|
var daemonDown = (e) => e instanceof QoreError && e.code === "QORE_DOCKER_NOT_RUNNING" && e.details?.reason === "daemon";
|
|
2660
2936
|
function dockerRecovery(ctx, signal) {
|
|
2661
2937
|
let answer;
|
|
@@ -2663,8 +2939,9 @@ function dockerRecovery(ctx, signal) {
|
|
|
2663
2939
|
try {
|
|
2664
2940
|
await fn();
|
|
2665
2941
|
} catch (e) {
|
|
2666
|
-
if (!daemonDown(e) || !
|
|
2942
|
+
if (!daemonDown(e) || !canOffer(ctx)) throw e;
|
|
2667
2943
|
answer ??= (async () => {
|
|
2944
|
+
if (!await offerFix(ctx, "Docker is not running. Wait for it to start?", { default: true, signal })) return false;
|
|
2668
2945
|
ctx.io.err("Waiting for Docker\u2026 (Ctrl+C to stop)");
|
|
2669
2946
|
const ok2 = await waitForDocker(ctx.deps, { ...at, signal, ...DOCKER_WAIT });
|
|
2670
2947
|
if (ok2) ctx.io.err("\u2713 Docker is running");
|
|
@@ -2680,7 +2957,7 @@ async function stopLeftover(ctx, e, runs, signal) {
|
|
|
2680
2957
|
const port = e.details?.port;
|
|
2681
2958
|
const run4 = runs.find((r) => r.qorePid === e.details?.pid);
|
|
2682
2959
|
if (!run4 || typeof port !== "number" || !await leftoverOwnsPort(deps2, run4, port)) return false;
|
|
2683
|
-
if (!await
|
|
2960
|
+
if (!await offerFix(ctx, `Stop the previous qore dev (pid ${run4.qorePid})?`, { default: false, signal })) return false;
|
|
2684
2961
|
if (!deps2.launch.signal(run4.qorePid, "SIGTERM")) {
|
|
2685
2962
|
io2.err(`! pid ${run4.qorePid} could not be signalled`);
|
|
2686
2963
|
return false;
|
|
@@ -2693,7 +2970,7 @@ async function checkPortOrStopLeftover(ctx, o, signal) {
|
|
|
2693
2970
|
try {
|
|
2694
2971
|
await checkPort(ctx.deps, o);
|
|
2695
2972
|
} catch (e) {
|
|
2696
|
-
if (!(e instanceof QoreError) || e.code !== "QORE_PORT_IN_USE" || e.details?.owner !== "qore" || !
|
|
2973
|
+
if (!(e instanceof QoreError) || e.code !== "QORE_PORT_IN_USE" || e.details?.owner !== "qore" || !canOffer(ctx)) throw e;
|
|
2697
2974
|
const freed = await stopLeftover(ctx, e, o.runs, signal);
|
|
2698
2975
|
if (signal.aborted) throw new CancelledError();
|
|
2699
2976
|
if (!freed) throw e;
|
|
@@ -2701,6 +2978,42 @@ async function checkPortOrStopLeftover(ctx, o, signal) {
|
|
|
2701
2978
|
}
|
|
2702
2979
|
}
|
|
2703
2980
|
|
|
2981
|
+
// src/cli/progress.ts
|
|
2982
|
+
function text(e, o) {
|
|
2983
|
+
switch (`${e.step}:${e.status}`) {
|
|
2984
|
+
case "pm:done":
|
|
2985
|
+
return `\u2713 ${e.detail}`;
|
|
2986
|
+
case "install:skip":
|
|
2987
|
+
return e.detail === "up to date" ? "\u2713 dependencies up to date" : `- install skipped (${o.installFlag ? "dev.install is false in .qore.json" : "--no-install"})`;
|
|
2988
|
+
case "install:start":
|
|
2989
|
+
return "\u22EF installing dependencies\u2026";
|
|
2990
|
+
case "install:done":
|
|
2991
|
+
return `\u2713 dependencies installed (${e.detail})`;
|
|
2992
|
+
case "env:done":
|
|
2993
|
+
return `\u2713 env ${e.detail}`;
|
|
2994
|
+
case "services:skip":
|
|
2995
|
+
return o.servicesFlag ? null : "- services skipped (--no-services)";
|
|
2996
|
+
case "services:start":
|
|
2997
|
+
return e.detail ? `\u22EF database waiting for ${e.detail}\u2026` : "\u22EF starting services\u2026";
|
|
2998
|
+
case "services:done":
|
|
2999
|
+
return e.detail?.startsWith(":") ? `\u2713 database ready on ${e.detail}` : `\u2713 services up ${e.detail}`;
|
|
3000
|
+
case "ports:done":
|
|
3001
|
+
return `\u2713 port ${e.detail} free`;
|
|
3002
|
+
default:
|
|
3003
|
+
return null;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
function progressReporter(ctx, o) {
|
|
3007
|
+
return (e) => {
|
|
3008
|
+
if (ctx.opts().json) {
|
|
3009
|
+
ctx.io.out(JSON.stringify({ type: "step", ...e }));
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
const line2 = text(e, o);
|
|
3013
|
+
if (line2 !== null) ctx.io.err(o.prefix ? o.prefix(e.module, line2) : line2);
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
|
|
2704
3017
|
// src/cli/termination.ts
|
|
2705
3018
|
var preflightHook = {};
|
|
2706
3019
|
function preflightTermination(abort, raise) {
|
|
@@ -2764,7 +3077,7 @@ async function ensureTrusted(ctx, mc, app, o) {
|
|
|
2764
3077
|
// src/cli/commands/devWorkspace.ts
|
|
2765
3078
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
2766
3079
|
import { realpath as realpath4 } from "fs/promises";
|
|
2767
|
-
import { join as
|
|
3080
|
+
import { join as join17, relative as relative5 } from "path";
|
|
2768
3081
|
|
|
2769
3082
|
// src/core/dev/order.ts
|
|
2770
3083
|
function selectSkips(modules, sel) {
|
|
@@ -2986,7 +3299,7 @@ function supervise(o) {
|
|
|
2986
3299
|
const early = !!e.spawnError || now() - startedAt < EARLY_MS;
|
|
2987
3300
|
const rawCode = e.spawnError ? null : e.code;
|
|
2988
3301
|
const rawSignal = e.spawnError ? null : e.signal;
|
|
2989
|
-
const hint =
|
|
3302
|
+
const hint = stopCode === null && rawSignal === null && rawCode !== null && rawCode !== 0 ? exitHint(tails.get(m.slug) ?? [], m.hint) ?? void 0 : void 0;
|
|
2990
3303
|
emit({ type: "exit", module: m.slug, code: rawCode, signal: rawSignal, early, hint });
|
|
2991
3304
|
if (code !== 0 && firstFailure === null) firstFailure = code;
|
|
2992
3305
|
if (o.killOthers) beginStop("kill-others", code);
|
|
@@ -3019,25 +3332,26 @@ function supervise(o) {
|
|
|
3019
3332
|
};
|
|
3020
3333
|
}
|
|
3021
3334
|
|
|
3022
|
-
// src/core/workspace/
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
const who = names.length ? names.join(", ") : "the app's dev team";
|
|
3032
|
-
return { teams: names, maintainers, text: `ask a maintainer of ${who}${maintainers.length ? ` (${maintainers.join(", ")})` : ""}` };
|
|
3033
|
-
}
|
|
3034
|
-
function notClonedReason(m, am) {
|
|
3035
|
-
if (am && !m.githubLinked) return `GitHub account not linked (${am.accountUrl})`;
|
|
3036
|
-
if (am && m.repoAccess === "none") {
|
|
3037
|
-
const names = devTeamsOf(am, m).map((t) => t.name);
|
|
3038
|
-
return names.length ? `no repository access (dev team ${names.join(", ")})` : "no repository access (ask your tenant owner)";
|
|
3335
|
+
// src/core/workspace/stray.ts
|
|
3336
|
+
import { lstat as lstat2, readdir } from "fs/promises";
|
|
3337
|
+
import { join as join16 } from "path";
|
|
3338
|
+
async function strayRepos(root, slugs) {
|
|
3339
|
+
let entries;
|
|
3340
|
+
try {
|
|
3341
|
+
entries = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3342
|
+
} catch {
|
|
3343
|
+
return [];
|
|
3039
3344
|
}
|
|
3040
|
-
|
|
3345
|
+
const names = [];
|
|
3346
|
+
for (const name of entries) {
|
|
3347
|
+
if (slugs.has(name)) continue;
|
|
3348
|
+
try {
|
|
3349
|
+
await lstat2(join16(root, name, ".git"));
|
|
3350
|
+
names.push(name);
|
|
3351
|
+
} catch {
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
return names.sort();
|
|
3041
3355
|
}
|
|
3042
3356
|
|
|
3043
3357
|
// src/cli/devOutput.ts
|
|
@@ -3135,18 +3449,6 @@ async function workspaceContext(ctx, ws) {
|
|
|
3135
3449
|
return { am: { app: last.app, accountUrl: "", devTeams: [], modules }, online: null, offlineCause: e };
|
|
3136
3450
|
}
|
|
3137
3451
|
}
|
|
3138
|
-
function deadAliases(env, ports) {
|
|
3139
|
-
return Object.keys(env).filter((k) => {
|
|
3140
|
-
if (!/^CONN_(.+)_URL$/.test(k)) return false;
|
|
3141
|
-
let port;
|
|
3142
|
-
try {
|
|
3143
|
-
port = Number(new URL(env[k].trim().replace(/^["']|["']$/g, "")).port);
|
|
3144
|
-
} catch {
|
|
3145
|
-
return false;
|
|
3146
|
-
}
|
|
3147
|
-
return port > 0 && !ports.has(port);
|
|
3148
|
-
}).sort();
|
|
3149
|
-
}
|
|
3150
3452
|
async function pool(items, n, fn) {
|
|
3151
3453
|
let next = 0;
|
|
3152
3454
|
const results = await Promise.allSettled(Array.from({ length: Math.min(n, items.length) }, async () => {
|
|
@@ -3157,7 +3459,7 @@ async function pool(items, n, fn) {
|
|
|
3157
3459
|
}
|
|
3158
3460
|
async function workspaceDev(ctx, ws, f, waitTimeout) {
|
|
3159
3461
|
const ready = await inPreflight(ctx.deps.signals, ctx.signal, (p) => prepareWorkspace(ctx, ws, f, waitTimeout, p));
|
|
3160
|
-
await superviseAll(ctx, ws, ready.units, ready.waitFor, ready.o);
|
|
3462
|
+
await superviseAll(ctx, ws, ready.units, ready.waitFor, ready.o, ready.notRunning);
|
|
3161
3463
|
}
|
|
3162
3464
|
async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
3163
3465
|
const { deps: deps2, io: io2 } = ctx;
|
|
@@ -3202,22 +3504,48 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3202
3504
|
continue;
|
|
3203
3505
|
}
|
|
3204
3506
|
try {
|
|
3205
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
3507
|
+
dirs[m.slug] = await deps2.git.inspect(join17(ws.root, m.slug));
|
|
3206
3508
|
safe.push(m);
|
|
3207
3509
|
} catch (e) {
|
|
3208
3510
|
if (!(e instanceof QoreError) || e.code === "QORE_GIT_UNAVAILABLE") throw e;
|
|
3209
3511
|
problems.push(withDetails(e, { module: m.slug }));
|
|
3210
3512
|
}
|
|
3211
3513
|
}
|
|
3212
|
-
const present = new Map(planWorkspace(ws.root, safe, dirs, []).actions.filter((a) => a.kind === "present").map((a) => [a.module.slug, a.dir]));
|
|
3213
3514
|
const bySlug = new Map(safe.map((m) => [m.slug, m]));
|
|
3214
|
-
const
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3515
|
+
const computePlan = () => {
|
|
3516
|
+
const present2 = new Map(planWorkspace(ws.root, safe, dirs, []).actions.filter((a) => a.kind === "present").map((a) => [a.module.slug, a.dir]));
|
|
3517
|
+
const plan2 = planDev(safe.map((m) => ({
|
|
3518
|
+
id: m.id,
|
|
3519
|
+
slug: m.slug,
|
|
3520
|
+
present: present2.has(m.slug),
|
|
3521
|
+
canWrite: m.access === "write" || m.access === "maintain",
|
|
3522
|
+
dependsOn: m.dependsOn
|
|
3523
|
+
})), { only: slugList(f.only), skip: slugList(f.skip) });
|
|
3524
|
+
return { present: present2, plan: plan2 };
|
|
3525
|
+
};
|
|
3526
|
+
let { present, plan } = computePlan();
|
|
3527
|
+
const missingFindings = () => {
|
|
3528
|
+
const missing2 = plan.skipped.filter((s) => s.reason === "not-cloned").map((s) => bySlug.get(s.slug));
|
|
3529
|
+
const consumers = {};
|
|
3530
|
+
for (const [consumer, providers] of Object.entries(plan.missingProviders)) {
|
|
3531
|
+
for (const p2 of providers) (consumers[p2] ??= []).push(consumer);
|
|
3532
|
+
}
|
|
3533
|
+
const actions = planWorkspace(ws.root, missing2, dirs, []).actions;
|
|
3534
|
+
return modulesMissing({ missing: missing2, actions, cwd: deps2.cwd, consumers, am: online ? am : null });
|
|
3535
|
+
};
|
|
3536
|
+
const missing = missingFindings();
|
|
3537
|
+
for (const finding of missing.findings) renderFinding(ctx, finding);
|
|
3538
|
+
const canSync = online !== null && missing.findings.some((x) => x.action?.fix === "sync");
|
|
3539
|
+
if (canSync && await offerFix(ctx, `Clone ${missing.fixable.map((m) => m.slug).join(", ")} now with qore sync?`, { default: true, signal: ctx.signal })) {
|
|
3540
|
+
const actions = planWorkspace(ws.root, missing.fixable, dirs, []).actions;
|
|
3541
|
+
const results = await applyActions(deps2, { portal: online.portal, app: am, actions, protocol: cloneProtocol(dirs), onResult: () => {
|
|
3542
|
+
} });
|
|
3543
|
+
renderResults(io2, results, false);
|
|
3544
|
+
for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(join17(ws.root, r.slug));
|
|
3545
|
+
({ present, plan } = computePlan());
|
|
3546
|
+
}
|
|
3547
|
+
for (const name of await strayRepos(ws.root, new Set(am.modules.map((m) => m.slug)))) renderFinding(ctx, strayClone(name, am.app.name));
|
|
3548
|
+
if (ctx.signal.aborted) throw new CancelledError();
|
|
3221
3549
|
for (const s of plan.skipped) {
|
|
3222
3550
|
const m = bySlug.get(s.slug);
|
|
3223
3551
|
if (s.reason === "no-write") {
|
|
@@ -3226,8 +3554,10 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3226
3554
|
`- ${s.slug} no write access \u2014 local secrets need write; ${ask.text}`,
|
|
3227
3555
|
{ type: "skip", module: s.slug, reason: s.reason, devTeams: ask.teams, maintainers: ask.maintainers }
|
|
3228
3556
|
);
|
|
3557
|
+
} else if (s.reason === "not-cloned") {
|
|
3558
|
+
if (json2) io2.out(JSON.stringify({ type: "skip", module: s.slug, reason: s.reason }));
|
|
3229
3559
|
} else {
|
|
3230
|
-
note(`- ${s.slug}
|
|
3560
|
+
note(`- ${s.slug} excluded`, { type: "skip", module: s.slug, reason: s.reason });
|
|
3231
3561
|
}
|
|
3232
3562
|
}
|
|
3233
3563
|
for (const c of plan.cycles) warn(`${c.join(", ")} depend on each other \u2014 they start together, without waiting`, { modules: c });
|
|
@@ -3262,8 +3592,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3262
3592
|
else if (l.stream === "stdout") io2.out(prefix(slug, l.line));
|
|
3263
3593
|
else io2.err(prefix(slug, l.line));
|
|
3264
3594
|
};
|
|
3265
|
-
const report = (
|
|
3266
|
-
};
|
|
3595
|
+
const report = progressReporter(ctx, { prefix: json2 ? void 0 : prefix, installFlag: f.install, servicesFlag: f.services });
|
|
3267
3596
|
for (const u of units) {
|
|
3268
3597
|
await attempt(u.m.slug, async () => {
|
|
3269
3598
|
u.cfg = await readRepoConfig(u.dir);
|
|
@@ -3285,13 +3614,27 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3285
3614
|
const withRecipe = units.filter((u) => u.recipe && u.trusted);
|
|
3286
3615
|
for (const name of [...new Set(withRecipe.map((u) => u.recipe.pm.name))]) {
|
|
3287
3616
|
const users = withRecipe.filter((u) => u.recipe.pm.name === name);
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3617
|
+
let version = null;
|
|
3618
|
+
const pmOk = await attempt(users[0].m.slug, async () => {
|
|
3619
|
+
version = await checkPackageManager(deps2, users[0].recipe, () => {
|
|
3620
|
+
}, p.signal);
|
|
3621
|
+
});
|
|
3622
|
+
for (const u of users) {
|
|
3623
|
+
u.pmOk = pmOk;
|
|
3624
|
+
u.pmVersion = version;
|
|
3625
|
+
}
|
|
3291
3626
|
}
|
|
3292
3627
|
for (const u of withRecipe) {
|
|
3293
|
-
const nodeOk = await attempt(u.m.slug, () =>
|
|
3628
|
+
const nodeOk = await attempt(u.m.slug, async () => {
|
|
3629
|
+
u.node = await checkNode(deps2, u.recipe, u.pkg, u.m.slug, p.signal);
|
|
3630
|
+
});
|
|
3294
3631
|
u.pmOk = u.pmOk && nodeOk;
|
|
3632
|
+
if (u.pmOk) report({
|
|
3633
|
+
module: u.m.slug,
|
|
3634
|
+
step: "pm",
|
|
3635
|
+
status: "done",
|
|
3636
|
+
detail: `${u.recipe.pm.name}${u.pmVersion ? ` ${u.pmVersion}` : ""}${u.node ? ` \xB7 node ${u.node}` : ""}`
|
|
3637
|
+
});
|
|
3295
3638
|
}
|
|
3296
3639
|
await pool(units.filter((u) => u.pmOk && u.trusted), INSTALL_PARALLELISM, async (u) => {
|
|
3297
3640
|
u.installed = await attempt(u.m.slug, () => installIfNeeded(deps2, u.recipe, u.m.slug, onLine(u.m.slug), report, p.signal));
|
|
@@ -3337,11 +3680,21 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3337
3680
|
{ module: slug, keys: dev.kept }
|
|
3338
3681
|
);
|
|
3339
3682
|
}
|
|
3683
|
+
report({ module: slug, step: "env", status: "done", detail: relative5(rootReal, dev.envFile) });
|
|
3684
|
+
const services = servicesMissing(slug, dev.env, u.recipe, { servicesFlag: f.services, configured: u.cfg.dev?.services !== void 0 });
|
|
3685
|
+
if (services) renderFinding(ctx, services);
|
|
3686
|
+
try {
|
|
3687
|
+
const behind = repoBehind(slug, await deps2.git.status(u.dir), { workspace: true });
|
|
3688
|
+
if (behind) renderFinding(ctx, behind);
|
|
3689
|
+
} catch {
|
|
3690
|
+
}
|
|
3340
3691
|
if (u.installed) {
|
|
3341
3692
|
const env = { ...deps2.env, ...dev.env };
|
|
3342
3693
|
await attempt(slug, () => withDocker({ cwd: u.dir, env }, () => startServices(deps2, u.recipe, slug, env, dev.dbPort, waitTimeout * 1e3, p.signal, onLine(slug), report)));
|
|
3343
3694
|
}
|
|
3344
|
-
await attempt(slug, () => checkPortOrStopLeftover(ctx, { slug, port: dev.port, key: "PORT", runs }, p.signal))
|
|
3695
|
+
if (await attempt(slug, () => checkPortOrStopLeftover(ctx, { slug, port: dev.port, key: "PORT", runs }, p.signal)) && dev.port !== null) {
|
|
3696
|
+
report({ module: slug, step: "ports", status: "done", detail: String(dev.port) });
|
|
3697
|
+
}
|
|
3345
3698
|
}
|
|
3346
3699
|
if (p.signal.aborted) throw new CancelledError();
|
|
3347
3700
|
const byPort = /* @__PURE__ */ new Map();
|
|
@@ -3369,7 +3722,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3369
3722
|
if (online && units.every((u) => !u.dev.offline)) {
|
|
3370
3723
|
const envFile = (m) => {
|
|
3371
3724
|
const u = units.find((x) => x.m.id === m.id);
|
|
3372
|
-
return u ? relative5(rootReal, u.dev.envFile) :
|
|
3725
|
+
return u ? relative5(rootReal, u.dev.envFile) : join17(m.slug, moduleDefaults(m.type).envFile);
|
|
3373
3726
|
};
|
|
3374
3727
|
await saveLastStart(deps2, ws.root, {
|
|
3375
3728
|
schemaVersion: 1,
|
|
@@ -3390,9 +3743,14 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3390
3743
|
});
|
|
3391
3744
|
}
|
|
3392
3745
|
if (p.signal.aborted) throw new CancelledError();
|
|
3393
|
-
|
|
3746
|
+
const { reasons } = missingFindings();
|
|
3747
|
+
const notRunning = plan.skipped.filter((s) => s.reason !== "excluded").map((s) => {
|
|
3748
|
+
const m = bySlug.get(s.slug);
|
|
3749
|
+
return { slug: s.slug, reason: s.reason === "no-write" ? `no write access \u2014 ${askMaintainer(am, m).text}` : `not cloned \u2014 ${reasons[s.slug] ?? "Fix: qore sync"}` };
|
|
3750
|
+
});
|
|
3751
|
+
return { units, waitFor: plan.waitFor, notRunning, o: { waitTimeout, killOthers: Boolean(f.killOthers), json: json2, color, prefix } };
|
|
3394
3752
|
}
|
|
3395
|
-
async function superviseAll(ctx, ws, units, waitFor, o) {
|
|
3753
|
+
async function superviseAll(ctx, ws, units, waitFor, o, notRunning) {
|
|
3396
3754
|
const { deps: deps2, io: io2 } = ctx;
|
|
3397
3755
|
const startedAt = deps2.now().toISOString();
|
|
3398
3756
|
const began = /* @__PURE__ */ new Map();
|
|
@@ -3414,9 +3772,9 @@ async function superviseAll(ctx, ws, units, waitFor, o) {
|
|
|
3414
3772
|
case "line":
|
|
3415
3773
|
return e.stream === "stdout" ? io2.out(o.prefix(e.module, e.line)) : io2.err(o.prefix(e.module, e.line));
|
|
3416
3774
|
case "start":
|
|
3417
|
-
return io2.err(`\u25B6 ${e.module}${e.pid === null ? "" : ` (pid ${e.pid})`}`);
|
|
3775
|
+
return io2.err(`\u25B6 starting ${e.module}${e.pid === null ? "" : ` (pid ${e.pid})`}`);
|
|
3418
3776
|
case "ready":
|
|
3419
|
-
return io2.err(`\u2713 ${e.module}
|
|
3777
|
+
return io2.err(`\u2713 ${e.module} ready`);
|
|
3420
3778
|
case "provider-missing":
|
|
3421
3779
|
return io2.err(`! ${e.provider} did not come up within ${o.waitTimeout} s (or exited); starting ${e.module} anyway \u2014 see its output above`);
|
|
3422
3780
|
case "stopping":
|
|
@@ -3428,23 +3786,45 @@ async function superviseAll(ctx, ws, units, waitFor, o) {
|
|
|
3428
3786
|
const how = e.signal ? `was ended by ${e.signal}` : `exited with code ${e.code}`;
|
|
3429
3787
|
io2.err(`${e.code === 0 ? "\u25A0" : "\u2717"} ${e.module} ${how}${after}`);
|
|
3430
3788
|
if (e.hint) io2.err(` \u2192 ${e.hint.hint}`);
|
|
3789
|
+
if (e.code !== 0 && !o.killOthers && (ref.sup?.children().length ?? 0) > 0) {
|
|
3790
|
+
io2.err(` Restart it on its own: qore dev --only ${e.module} (in another terminal; the others keep running)`);
|
|
3791
|
+
}
|
|
3431
3792
|
return;
|
|
3432
3793
|
}
|
|
3433
3794
|
}
|
|
3434
3795
|
};
|
|
3796
|
+
const withPort = new Set(units.filter((u) => u.dev.port !== null).map((u) => u.m.slug));
|
|
3797
|
+
const readyPorts = /* @__PURE__ */ new Map();
|
|
3798
|
+
let summarized = false;
|
|
3799
|
+
const summarize = () => {
|
|
3800
|
+
if (summarized || o.json || readyPorts.size < withPort.size) return;
|
|
3801
|
+
summarized = true;
|
|
3802
|
+
const width = Math.max(0, ...[...readyPorts.keys(), ...notRunning.map((n) => n.slug)].map((s2) => s2.length));
|
|
3803
|
+
io2.err(notRunning.length === 0 ? "\u2714 All modules running:" : "\u2714 Running:");
|
|
3804
|
+
for (const slug of [...readyPorts.keys()].sort()) io2.err(` ${slug.padEnd(width)} http://localhost:${readyPorts.get(slug)}`);
|
|
3805
|
+
if (notRunning.length > 0) {
|
|
3806
|
+
io2.err("! Not running:");
|
|
3807
|
+
for (const n of notRunning) io2.err(` ${n.slug.padEnd(width)} ${n.reason}`);
|
|
3808
|
+
}
|
|
3809
|
+
};
|
|
3810
|
+
const crashed = /* @__PURE__ */ new Map();
|
|
3435
3811
|
const onEvent = (e) => {
|
|
3436
3812
|
if (e.type === "start") {
|
|
3437
3813
|
began.set(e.module, deps2.now().getTime());
|
|
3438
3814
|
writeRun();
|
|
3439
3815
|
}
|
|
3440
3816
|
if (e.type === "stopping") stopping = true;
|
|
3817
|
+
if (e.type === "ready") readyPorts.set(e.module, e.port);
|
|
3818
|
+
if (e.type === "exit" && !stopping && (e.code !== 0 || e.signal !== null)) {
|
|
3819
|
+
crashed.set(e.module, { code: e.code, signal: e.signal, ms: deps2.now().getTime() - (began.get(e.module) ?? deps2.now().getTime()) });
|
|
3820
|
+
}
|
|
3441
3821
|
if (o.json) io2.out(JSON.stringify(e));
|
|
3442
3822
|
else render(e);
|
|
3823
|
+
if (e.type === "ready") summarize();
|
|
3443
3824
|
};
|
|
3444
3825
|
const s = ref.sup = supervise({
|
|
3445
3826
|
modules: units.map((u) => {
|
|
3446
3827
|
const { cmd, args } = runArgs(u.recipe.pm, u.recipe.script);
|
|
3447
|
-
const { cmd: installCmd, args: installArgv } = installArgs(u.recipe.pm);
|
|
3448
3828
|
return {
|
|
3449
3829
|
slug: u.m.slug,
|
|
3450
3830
|
cmd,
|
|
@@ -3452,7 +3832,7 @@ async function superviseAll(ctx, ws, units, waitFor, o) {
|
|
|
3452
3832
|
cwd: u.dir,
|
|
3453
3833
|
port: u.dev.port,
|
|
3454
3834
|
waitFor: waitFor[u.m.slug] ?? [],
|
|
3455
|
-
|
|
3835
|
+
hint: hintContext(u.recipe, u.pkg, u.dev.env),
|
|
3456
3836
|
env: { ...deps2.env, ...u.dev.env, ...o.color ? { FORCE_COLOR: "1" } : {} }
|
|
3457
3837
|
};
|
|
3458
3838
|
}),
|
|
@@ -3474,6 +3854,11 @@ async function superviseAll(ctx, ws, units, waitFor, o) {
|
|
|
3474
3854
|
if (ctx.signal.aborted) onAbort();
|
|
3475
3855
|
writeRun();
|
|
3476
3856
|
ctx.setExitCode(await s.done);
|
|
3857
|
+
if (!o.json && crashed.size > 0) {
|
|
3858
|
+
const stopped = units.map((u) => u.m.slug).filter((s2) => !crashed.has(s2)).sort();
|
|
3859
|
+
const what = [...crashed].map(([s2, c]) => `${s2} (${c.code === null && c.signal === null ? "could not be started" : `${c.signal ?? `code ${c.code}`}, after ${humanAge(Math.round(c.ms / 1e3))}`})`).join(", ");
|
|
3860
|
+
io2.err(`${stopped.length ? `\u25A0 stopped: ${stopped.join(", ")} ` : ""}\u2717 crashed: ${what}`);
|
|
3861
|
+
}
|
|
3477
3862
|
} finally {
|
|
3478
3863
|
deps2.signals.off("SIGTERM", onTerm);
|
|
3479
3864
|
deps2.signals.off("SIGHUP", onHup);
|
|
@@ -3543,6 +3928,48 @@ function parseWaitTimeout(raw) {
|
|
|
3543
3928
|
return n;
|
|
3544
3929
|
}
|
|
3545
3930
|
var str2 = (v) => typeof v === "string" && v.length > 0;
|
|
3931
|
+
var SHELL_SAFE = /^[A-Za-z0-9_./,:@%+=-]*$/;
|
|
3932
|
+
var shellQuote = (s) => SHELL_SAFE.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
|
|
3933
|
+
function cdAndDev(home, dir2) {
|
|
3934
|
+
if (/^[A-Za-z]:[\\/]|^\\\\/.test(dir2)) return `cd ${/\s/.test(dir2) ? `"${dir2}"` : dir2} && qore dev`;
|
|
3935
|
+
const h = home.length > 1 ? home.replace(/\/+$/, "") : home;
|
|
3936
|
+
let target = shellQuote(dir2);
|
|
3937
|
+
if (h && dir2 === h) target = "~";
|
|
3938
|
+
else if (h && dir2.startsWith(h + "/")) target = `~/${shellQuote(dir2.slice(h.length + 1))}`;
|
|
3939
|
+
return `cd ${target} && qore dev`;
|
|
3940
|
+
}
|
|
3941
|
+
async function providerFindings(ctx, root, mc, app, env) {
|
|
3942
|
+
const { deps: deps2 } = ctx;
|
|
3943
|
+
try {
|
|
3944
|
+
if (!(await metaFor(deps2, mc.portal.apiOrigin)).capabilities.includes("app.modules")) return null;
|
|
3945
|
+
const am = await mc.session.api.get(`/api/cli/v1/apps/${encodeURIComponent(app.id)}/modules`);
|
|
3946
|
+
const self = am.modules.find((m) => m.id === mc.module.id);
|
|
3947
|
+
const providers = am.modules.filter((m) => self?.dependsOn.includes(m.id));
|
|
3948
|
+
if (providers.length === 0) return null;
|
|
3949
|
+
const conns = connPorts(env);
|
|
3950
|
+
const listening = /* @__PURE__ */ new Set();
|
|
3951
|
+
for (const c of conns) if (await deps2.ports.inUse(c.port)) listening.add(c.port);
|
|
3952
|
+
const keys = deadAliases(env, listening);
|
|
3953
|
+
if (keys.length === 0) return null;
|
|
3954
|
+
const ws = await findWorkspace(root, deps2.home);
|
|
3955
|
+
let fix = `qore clone ${app.slug}`;
|
|
3956
|
+
if (ws && ws.appId === app.id) fix = cdAndDev(deps2.home, ws.root);
|
|
3957
|
+
else if (providers.length === 1) {
|
|
3958
|
+
const sibling = join18(dirname6(root), providers[0].slug);
|
|
3959
|
+
if ((await deps2.git.inspect(sibling)).isRepo) fix = cdAndDev(deps2.home, sibling);
|
|
3960
|
+
}
|
|
3961
|
+
return providerDown({
|
|
3962
|
+
slug: mc.module.slug,
|
|
3963
|
+
providers: providers.map((p) => p.slug),
|
|
3964
|
+
keys,
|
|
3965
|
+
ports: new Map(conns.map((c) => [c.key, c.port])),
|
|
3966
|
+
fix
|
|
3967
|
+
});
|
|
3968
|
+
} catch (e) {
|
|
3969
|
+
if (e instanceof QoreError) return null;
|
|
3970
|
+
throw e;
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3546
3973
|
function usableLast2(last) {
|
|
3547
3974
|
if (!last || last.kind !== "module") return false;
|
|
3548
3975
|
const m = last.module;
|
|
@@ -3567,8 +3994,9 @@ async function devContext(ctx, f, root) {
|
|
|
3567
3994
|
}
|
|
3568
3995
|
async function moduleDev(ctx, f, waitTimeout) {
|
|
3569
3996
|
const { deps: deps2, io: io2 } = ctx;
|
|
3570
|
-
const { root, slug, recipe, env, port } = await inPreflight(deps2.signals, ctx.signal, (p) => prepareModule(ctx, f, waitTimeout, p));
|
|
3997
|
+
const { root, slug, recipe, env, port, pkg, devEnv: devEnv2 } = await inPreflight(deps2.signals, ctx.signal, (p) => prepareModule(ctx, f, waitTimeout, p));
|
|
3571
3998
|
const { cmd, args } = runArgs(recipe.pm, recipe.script);
|
|
3999
|
+
io2.err(`\u25B6 starting ${slug}${port === null ? "" : ` \u2014 http://localhost:${port}`}`);
|
|
3572
4000
|
const startedAt = deps2.now();
|
|
3573
4001
|
await writeRunFile(deps2, root, {
|
|
3574
4002
|
schemaVersion: 1,
|
|
@@ -3589,10 +4017,9 @@ async function moduleDev(ctx, f, waitTimeout) {
|
|
|
3589
4017
|
await removeRunFile(deps2, root);
|
|
3590
4018
|
}
|
|
3591
4019
|
const ms = deps2.now().getTime() - startedAt.getTime();
|
|
3592
|
-
if (exit.code !== 0 && exit.signal === null
|
|
3593
|
-
io2.err(`\u2717 ${slug} exited after ${Math.round(ms / 1e3)} s with code ${exit.code ?? 1} \u2014 its output above says why.`);
|
|
3594
|
-
const
|
|
3595
|
-
const hint = earlyExitHint(tee.lines(), { installCommand: [installCmd, ...installArgv].join(" ") });
|
|
4020
|
+
if (exit.code !== 0 && exit.signal === null) {
|
|
4021
|
+
io2.err(ms < EARLY_EXIT_MS ? `\u2717 ${slug} exited after ${Math.round(ms / 1e3)} s with code ${exit.code ?? 1} \u2014 its output above says why.` : `\u2717 ${slug} exited with code ${exit.code ?? 1}.`);
|
|
4022
|
+
const hint = exitHint(tee.lines(), hintContext(recipe, pkg, devEnv2));
|
|
3596
4023
|
if (hint) io2.err(` \u2192 ${hint.hint}`);
|
|
3597
4024
|
}
|
|
3598
4025
|
if (exit.signal) {
|
|
@@ -3637,11 +4064,16 @@ async function prepareModule(ctx, f, waitTimeout, p) {
|
|
|
3637
4064
|
});
|
|
3638
4065
|
const runs = [await leftoverRun(deps2, root)].filter((r) => r !== null);
|
|
3639
4066
|
const onLine = (l) => l.stream === "stdout" ? io2.out(l.line) : io2.err(l.line);
|
|
3640
|
-
const report = (
|
|
3641
|
-
|
|
3642
|
-
await checkPackageManager(deps2, recipe, () => {
|
|
4067
|
+
const report = progressReporter(ctx, { installFlag: f.install, servicesFlag: f.services });
|
|
4068
|
+
const pmVersion = await checkPackageManager(deps2, recipe, () => {
|
|
3643
4069
|
}, p.signal);
|
|
3644
|
-
await checkNode(deps2, recipe, pkg, slug, p.signal);
|
|
4070
|
+
const nodeVersion = await checkNode(deps2, recipe, pkg, slug, p.signal);
|
|
4071
|
+
report({
|
|
4072
|
+
module: slug,
|
|
4073
|
+
step: "pm",
|
|
4074
|
+
status: "done",
|
|
4075
|
+
detail: `${recipe.pm.name}${pmVersion ? ` ${pmVersion}` : ""}${nodeVersion ? ` \xB7 node ${nodeVersion}` : ""}`
|
|
4076
|
+
});
|
|
3645
4077
|
await installIfNeeded(deps2, recipe, slug, onLine, report, p.signal);
|
|
3646
4078
|
let dev;
|
|
3647
4079
|
try {
|
|
@@ -3663,9 +4095,22 @@ async function prepareModule(ctx, f, waitTimeout, p) {
|
|
|
3663
4095
|
if (dev.kept.length) {
|
|
3664
4096
|
io2.err(`! ${dev.kept.length} key(s) differ locally and were kept: ${dev.kept.join(", ")} \u2014 qore env pull --use-portal takes the portal's values`);
|
|
3665
4097
|
}
|
|
4098
|
+
report({ module: slug, step: "env", status: "done", detail: dev.offline ? `${envFile} (as last written \u2014 offline)` : envFile });
|
|
3666
4099
|
const env = { ...deps2.env, ...dev.env };
|
|
3667
4100
|
await dockerRecovery(ctx, p.signal)({ cwd: root, env }, () => startServices(deps2, recipe, slug, env, dev.dbPort, waitTimeout * 1e3, p.signal, onLine, report));
|
|
3668
4101
|
await checkPortOrStopLeftover(ctx, { slug, port: dev.port, key: "PORT", runs }, p.signal);
|
|
4102
|
+
if (dev.port !== null) report({ module: slug, step: "ports", status: "done", detail: String(dev.port) });
|
|
4103
|
+
if (c.online && !dev.offline) {
|
|
4104
|
+
const provider = await providerFindings(ctx, root, c.online.mc, c.online.app, dev.env);
|
|
4105
|
+
if (provider) renderFinding(ctx, provider);
|
|
4106
|
+
}
|
|
4107
|
+
const services = servicesMissing(slug, dev.env, recipe, { servicesFlag: f.services, configured: cfg.dev?.services !== void 0 });
|
|
4108
|
+
if (services) renderFinding(ctx, services);
|
|
4109
|
+
try {
|
|
4110
|
+
const behind = repoBehind(slug, await deps2.git.status(root), { workspace: false });
|
|
4111
|
+
if (behind) renderFinding(ctx, behind);
|
|
4112
|
+
} catch {
|
|
4113
|
+
}
|
|
3669
4114
|
if (c.online && !dev.offline) {
|
|
3670
4115
|
const { mc, app } = c.online;
|
|
3671
4116
|
const { id, name, type, access: access2, tenant, apps } = mc.module;
|
|
@@ -3688,7 +4133,7 @@ async function prepareModule(ctx, f, waitTimeout, p) {
|
|
|
3688
4133
|
});
|
|
3689
4134
|
}
|
|
3690
4135
|
if (p.signal.aborted) throw new CancelledError();
|
|
3691
|
-
return { root, slug, recipe, env, port: dev.port };
|
|
4136
|
+
return { root, slug, recipe, env, port: dev.port, pkg, devEnv: dev.env };
|
|
3692
4137
|
}
|
|
3693
4138
|
|
|
3694
4139
|
// src/cli/commands/doctor.ts
|
|
@@ -3696,14 +4141,14 @@ import pc3 from "picocolors";
|
|
|
3696
4141
|
|
|
3697
4142
|
// src/core/doctor/checks.ts
|
|
3698
4143
|
import { realpath as realpath6 } from "fs/promises";
|
|
3699
|
-
import { basename as basename3, join as
|
|
4144
|
+
import { basename as basename3, join as join19 } from "path";
|
|
3700
4145
|
|
|
3701
4146
|
// src/core/diagnostics/ssh.ts
|
|
3702
4147
|
var SSH_TEST_ARGS = ["-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "git@github.com"];
|
|
3703
4148
|
var COMMAND = "ssh -T git@github.com";
|
|
3704
4149
|
function classifySshTest(r) {
|
|
3705
|
-
const
|
|
3706
|
-
const hi = /Hi ([^!\s]+)! You've successfully authenticated/.exec(
|
|
4150
|
+
const text2 = r.tail.join("\n");
|
|
4151
|
+
const hi = /Hi ([^!\s]+)! You've successfully authenticated/.exec(text2);
|
|
3707
4152
|
if (hi) return { ok: true, user: hi[1] };
|
|
3708
4153
|
const details = { protocol: "ssh" };
|
|
3709
4154
|
if (r.spawnError) {
|
|
@@ -3712,13 +4157,13 @@ function classifySshTest(r) {
|
|
|
3712
4157
|
details: { ...details, reason: "ssh-missing" }
|
|
3713
4158
|
}) };
|
|
3714
4159
|
}
|
|
3715
|
-
if (/Permission denied \(publickey\)/.test(
|
|
4160
|
+
if (/Permission denied \(publickey\)/.test(text2)) {
|
|
3716
4161
|
return { ok: false, error: new QoreError("QORE_GIT_AUTH", "GitHub refused your SSH key", {
|
|
3717
4162
|
hint: "Add your key to GitHub (https://github.com/settings/keys) and your ssh agent, or keep cloning over https.",
|
|
3718
4163
|
details
|
|
3719
4164
|
}) };
|
|
3720
4165
|
}
|
|
3721
|
-
if (/Host key verification failed/.test(
|
|
4166
|
+
if (/Host key verification failed/.test(text2)) {
|
|
3722
4167
|
return { ok: false, error: new QoreError("QORE_GIT_AUTH", "ssh could not verify github.com's host key", {
|
|
3723
4168
|
hint: `Run \`${COMMAND}\` once to accept its host key (or to see why it changed).`,
|
|
3724
4169
|
details: { ...details, reason: "host-key" }
|
|
@@ -3893,9 +4338,9 @@ async function moduleBody(deps2, m, runs, signal) {
|
|
|
3893
4338
|
let env = null;
|
|
3894
4339
|
await part(async () => {
|
|
3895
4340
|
const target = await resolveTarget(m.root, cfg.envFile ?? moduleDefaults(m.type).envFile);
|
|
3896
|
-
const
|
|
3897
|
-
if (
|
|
3898
|
-
env = Object.fromEntries(parseEnv(
|
|
4341
|
+
const text2 = await readOrNull(target);
|
|
4342
|
+
if (text2 === null) return { status: "warn", label: "port unknown (run qore env pull)" };
|
|
4343
|
+
env = Object.fromEntries(parseEnv(text2));
|
|
3899
4344
|
const port = envPort(env, "PORT");
|
|
3900
4345
|
if (port === null) return { status: "warn", label: `port unknown (no PORT in ${basename3(target)})` };
|
|
3901
4346
|
if (!await deps2.ports.inUse(port)) return { status: "ok", label: `port ${port} free` };
|
|
@@ -4114,7 +4559,7 @@ async function workspaceModule(deps2, ws, am, m, runs, signal, push, perModule)
|
|
|
4114
4559
|
await push(`module:${m.slug}`, "workspace", () => ({ status: "warn", label: "not a safe directory name \u2014 skipped" }), m.slug);
|
|
4115
4560
|
return;
|
|
4116
4561
|
}
|
|
4117
|
-
const mod = { root:
|
|
4562
|
+
const mod = { root: join19(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
|
|
4118
4563
|
let prepared = null;
|
|
4119
4564
|
await push(`module:${m.slug}`, "workspace", async () => {
|
|
4120
4565
|
const info = await deps2.git.inspect(mod.root);
|
|
@@ -4146,7 +4591,7 @@ function registerDoctor(program, ctx) {
|
|
|
4146
4591
|
// src/cli/commands/env.ts
|
|
4147
4592
|
import { randomBytes as randomBytes6 } from "crypto";
|
|
4148
4593
|
import { chmod as chmod3, realpath as realpath7 } from "fs/promises";
|
|
4149
|
-
import { dirname as
|
|
4594
|
+
import { dirname as dirname7, join as join20, relative as relative7 } from "path";
|
|
4150
4595
|
var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.length} chars)`;
|
|
4151
4596
|
function registerEnv(program, ctx) {
|
|
4152
4597
|
const env = program.command("env").description("Local environment");
|
|
@@ -4163,7 +4608,7 @@ function registerEnv(program, ctx) {
|
|
|
4163
4608
|
const target = f.target ? await resolveTarget(mc.repoRoot, f.target, deps2.cwd) : await resolveTarget(mc.repoRoot, cfg.envFile ?? defaults.envFile);
|
|
4164
4609
|
await ensureTrusted(ctx, mc, app, { yes: f.yes });
|
|
4165
4610
|
const existing = await readOrNull(target);
|
|
4166
|
-
const example = existing === null ? await readOrNull(
|
|
4611
|
+
const example = existing === null ? await readOrNull(join20(dirname7(target), ".env.example")) : null;
|
|
4167
4612
|
const local = await fetchLocalEnv(deps2, mc, app);
|
|
4168
4613
|
const inputs = {
|
|
4169
4614
|
existing,
|
|
@@ -4562,8 +5007,7 @@ function registerStatus(program, ctx) {
|
|
|
4562
5007
|
}
|
|
4563
5008
|
|
|
4564
5009
|
// src/cli/commands/sync.ts
|
|
4565
|
-
import {
|
|
4566
|
-
import { join as join19, relative as relative8 } from "path";
|
|
5010
|
+
import { join as join21, relative as relative8 } from "path";
|
|
4567
5011
|
import { Option as Option2 } from "commander";
|
|
4568
5012
|
function asFailure2(slug, e) {
|
|
4569
5013
|
if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
|
|
@@ -4571,24 +5015,6 @@ function asFailure2(slug, e) {
|
|
|
4571
5015
|
}
|
|
4572
5016
|
throw e;
|
|
4573
5017
|
}
|
|
4574
|
-
async function strayRepos(root, slugs) {
|
|
4575
|
-
let entries;
|
|
4576
|
-
try {
|
|
4577
|
-
entries = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4578
|
-
} catch {
|
|
4579
|
-
return [];
|
|
4580
|
-
}
|
|
4581
|
-
const names = [];
|
|
4582
|
-
for (const name of entries) {
|
|
4583
|
-
if (slugs.has(name)) continue;
|
|
4584
|
-
try {
|
|
4585
|
-
await lstat2(join19(root, name, ".git"));
|
|
4586
|
-
names.push(name);
|
|
4587
|
-
} catch {
|
|
4588
|
-
}
|
|
4589
|
-
}
|
|
4590
|
-
return names.sort();
|
|
4591
|
-
}
|
|
4592
5018
|
function registerSync(program, ctx) {
|
|
4593
5019
|
program.command("sync").description("Clone new modules, fetch present ones, report removed and re-pointed ones").option("--pull", "fast-forward every clean present repository with an upstream").addOption(new Option2("--protocol <https|ssh>", "clone URL protocol").choices(["https", "ssh"]).default("https")).action(async (flags) => {
|
|
4594
5020
|
const { deps: deps2, io: io2 } = ctx;
|
|
@@ -4613,7 +5039,7 @@ function registerSync(program, ctx) {
|
|
|
4613
5039
|
continue;
|
|
4614
5040
|
}
|
|
4615
5041
|
try {
|
|
4616
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
5042
|
+
dirs[m.slug] = await deps2.git.inspect(join21(ws.root, m.slug));
|
|
4617
5043
|
modulesForPlan.push(m);
|
|
4618
5044
|
} catch (e) {
|
|
4619
5045
|
earlyFailures.push(asFailure2(m.slug, e));
|
|
@@ -4789,7 +5215,7 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
|
|
|
4789
5215
|
|
|
4790
5216
|
// src/core/config.ts
|
|
4791
5217
|
import { readFile as readFile6 } from "fs/promises";
|
|
4792
|
-
import { join as
|
|
5218
|
+
import { join as join22 } from "path";
|
|
4793
5219
|
var empty = () => ({ schemaVersion: 1, profiles: {} });
|
|
4794
5220
|
var ConfigStore = class {
|
|
4795
5221
|
constructor(dir2) {
|
|
@@ -4797,7 +5223,7 @@ var ConfigStore = class {
|
|
|
4797
5223
|
}
|
|
4798
5224
|
dir;
|
|
4799
5225
|
get file() {
|
|
4800
|
-
return
|
|
5226
|
+
return join22(this.dir, "config.json");
|
|
4801
5227
|
}
|
|
4802
5228
|
async read() {
|
|
4803
5229
|
let raw;
|
|
@@ -4830,7 +5256,7 @@ var ConfigStore = class {
|
|
|
4830
5256
|
import { execFile, execFileSync } from "child_process";
|
|
4831
5257
|
import { mkdir as mkdir3, readdir as readdir2, stat as stat3 } from "fs/promises";
|
|
4832
5258
|
import { homedir } from "os";
|
|
4833
|
-
import { dirname as
|
|
5259
|
+
import { dirname as dirname8, join as join23 } from "path";
|
|
4834
5260
|
function parseRemotes(stdout) {
|
|
4835
5261
|
const seen = /* @__PURE__ */ new Set();
|
|
4836
5262
|
const out = [];
|
|
@@ -4842,6 +5268,30 @@ function parseRemotes(stdout) {
|
|
|
4842
5268
|
}
|
|
4843
5269
|
return out;
|
|
4844
5270
|
}
|
|
5271
|
+
function parseStatus(stdout) {
|
|
5272
|
+
const s = { ok: true, dirty: false, branch: null, upstream: false };
|
|
5273
|
+
for (const raw of stdout.split("\n")) {
|
|
5274
|
+
const line2 = raw.replace(/\r$/, "");
|
|
5275
|
+
if (line2.startsWith("# branch.head ")) {
|
|
5276
|
+
const name = line2.slice("# branch.head ".length).trim();
|
|
5277
|
+
s.branch = name === "(detached)" ? null : name;
|
|
5278
|
+
} else if (line2.startsWith("# branch.upstream ")) {
|
|
5279
|
+
s.upstream = true;
|
|
5280
|
+
s.upstreamRef = line2.slice("# branch.upstream ".length).trim();
|
|
5281
|
+
} else if (line2.startsWith("# branch.ab ")) {
|
|
5282
|
+
const m = /^# branch\.ab \+(\d+) -(\d+)$/.exec(line2.trim());
|
|
5283
|
+
if (m) {
|
|
5284
|
+
s.ahead = Number(m[1]);
|
|
5285
|
+
s.behind = Number(m[2]);
|
|
5286
|
+
}
|
|
5287
|
+
} else if (line2.startsWith("#")) {
|
|
5288
|
+
continue;
|
|
5289
|
+
} else if (line2.trim().length > 0) {
|
|
5290
|
+
s.dirty = true;
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5293
|
+
return s;
|
|
5294
|
+
}
|
|
4845
5295
|
var configuredSshCommand = /* @__PURE__ */ new Map();
|
|
4846
5296
|
function hasConfiguredSshCommand(repoDir) {
|
|
4847
5297
|
const key = repoDir ?? "";
|
|
@@ -4900,7 +5350,7 @@ async function remotes(repoRoot) {
|
|
|
4900
5350
|
return parseRemotes(r.stdout);
|
|
4901
5351
|
}
|
|
4902
5352
|
async function clone(url, dir2) {
|
|
4903
|
-
await mkdir3(
|
|
5353
|
+
await mkdir3(dirname8(dir2), { recursive: true });
|
|
4904
5354
|
const r = await runGit(["clone", "--", url, dir2]);
|
|
4905
5355
|
return toResult(r, "git clone");
|
|
4906
5356
|
}
|
|
@@ -4916,22 +5366,7 @@ async function status(repoRoot) {
|
|
|
4916
5366
|
const command = "git status --porcelain=v2 --branch";
|
|
4917
5367
|
const r = await runGit(["--no-optional-locks", "-C", repoRoot, "status", "--porcelain=v2", "--branch"], repoRoot);
|
|
4918
5368
|
if (r.code !== 0) return { ok: false, stderr: r.stderr, command };
|
|
4919
|
-
|
|
4920
|
-
let upstream = false;
|
|
4921
|
-
let dirty = false;
|
|
4922
|
-
for (const line2 of r.stdout.split(/\r?\n/)) {
|
|
4923
|
-
if (line2.startsWith("# branch.head ")) {
|
|
4924
|
-
const name = line2.slice("# branch.head ".length).trim();
|
|
4925
|
-
branch = name === "(detached)" ? null : name;
|
|
4926
|
-
} else if (line2.startsWith("# branch.upstream ")) {
|
|
4927
|
-
upstream = true;
|
|
4928
|
-
} else if (line2.startsWith("#")) {
|
|
4929
|
-
continue;
|
|
4930
|
-
} else if (line2.trim().length > 0) {
|
|
4931
|
-
dirty = true;
|
|
4932
|
-
}
|
|
4933
|
-
}
|
|
4934
|
-
return { ok: true, dirty, branch, upstream };
|
|
5369
|
+
return parseStatus(r.stdout);
|
|
4935
5370
|
}
|
|
4936
5371
|
async function statOrThrow(path) {
|
|
4937
5372
|
try {
|
|
@@ -4952,7 +5387,7 @@ async function inspect(dir2) {
|
|
|
4952
5387
|
throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
|
|
4953
5388
|
}
|
|
4954
5389
|
const empty2 = entries.length === 0;
|
|
4955
|
-
const isRepo = await statOrThrow(
|
|
5390
|
+
const isRepo = await statOrThrow(join23(dir2, ".git")) !== void 0;
|
|
4956
5391
|
const dirRemotes = isRepo ? await remotes(dir2) : [];
|
|
4957
5392
|
return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
|
|
4958
5393
|
}
|
|
@@ -5014,13 +5449,13 @@ var OsKeychain = class {
|
|
|
5014
5449
|
|
|
5015
5450
|
// src/platform/lock.ts
|
|
5016
5451
|
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
5017
|
-
import { join as
|
|
5452
|
+
import { join as join24 } from "path";
|
|
5018
5453
|
import lockfile from "proper-lockfile";
|
|
5019
5454
|
function fileLock(dir2) {
|
|
5020
5455
|
return async (name, fn) => {
|
|
5021
|
-
const locks =
|
|
5456
|
+
const locks = join24(dir2, "locks");
|
|
5022
5457
|
await mkdir4(locks, { recursive: true, mode: 448 });
|
|
5023
|
-
const target =
|
|
5458
|
+
const target = join24(locks, `${name}.lock`);
|
|
5024
5459
|
await writeFile4(target, "", { flag: "a" });
|
|
5025
5460
|
let release;
|
|
5026
5461
|
let compromised;
|
|
@@ -5121,6 +5556,10 @@ var crossSpawner = makeSpawner(spawn, process);
|
|
|
5121
5556
|
|
|
5122
5557
|
// src/platform/launch.ts
|
|
5123
5558
|
import spawn2 from "cross-spawn";
|
|
5559
|
+
var RESET_OR_CLEAR = /\x1Bc|\x1B\[[23]J/g;
|
|
5560
|
+
function stripScreenClears(line2) {
|
|
5561
|
+
return line2.includes("\x1B") ? line2.replace(RESET_OR_CLEAR, "") : line2;
|
|
5562
|
+
}
|
|
5124
5563
|
function lineSplitter(stream, onLine) {
|
|
5125
5564
|
let buf = "";
|
|
5126
5565
|
return {
|
|
@@ -5128,13 +5567,13 @@ function lineSplitter(stream, onLine) {
|
|
|
5128
5567
|
buf += chunk;
|
|
5129
5568
|
let i;
|
|
5130
5569
|
while ((i = buf.indexOf("\n")) >= 0) {
|
|
5131
|
-
onLine({ stream, line: buf.slice(0, i).replace(/\r$/, "") });
|
|
5570
|
+
onLine({ stream, line: stripScreenClears(buf.slice(0, i).replace(/\r$/, "")) });
|
|
5132
5571
|
buf = buf.slice(i + 1);
|
|
5133
5572
|
}
|
|
5134
5573
|
},
|
|
5135
5574
|
flush() {
|
|
5136
5575
|
if (buf) {
|
|
5137
|
-
onLine({ stream, line: buf.replace(/\r$/, "") });
|
|
5576
|
+
onLine({ stream, line: stripScreenClears(buf.replace(/\r$/, "")) });
|
|
5138
5577
|
buf = "";
|
|
5139
5578
|
}
|
|
5140
5579
|
}
|
|
@@ -5351,7 +5790,7 @@ function systemProxy(platform = process.platform) {
|
|
|
5351
5790
|
}
|
|
5352
5791
|
|
|
5353
5792
|
// src/version.ts
|
|
5354
|
-
var VERSION = true ? "0.1.
|
|
5793
|
+
var VERSION = true ? "0.1.4" : "0.0.0-dev";
|
|
5355
5794
|
|
|
5356
5795
|
// src/cli.ts
|
|
5357
5796
|
if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
|