@quatronic/qore 0.1.3 → 0.1.5
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 +52 -0
- package/dist/chunk-QYBDINUC.js +121 -0
- package/dist/cli.js +851 -206
- package/dist/sentry-B3MDCAOI.js +11262 -0
- package/package.json +3 -2
- package/dist/cli.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a70250c5-daf4-5ff3-b490-5556ac377549")}catch(e){}}();
|
|
4
|
+
import { createRequire as __qoreCreateRequire } from "node:module"; const require = __qoreCreateRequire(import.meta.url);
|
|
5
|
+
import {
|
|
6
|
+
addSecretValues,
|
|
7
|
+
buildScrubContext
|
|
8
|
+
} from "./chunk-QYBDINUC.js";
|
|
9
|
+
|
|
3
10
|
// src/cli.ts
|
|
4
11
|
import { homedir as homedir2 } from "os";
|
|
5
12
|
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
|
|
6
13
|
|
|
7
14
|
// src/cli/io.ts
|
|
8
|
-
import { confirm, select } from "@inquirer/prompts";
|
|
15
|
+
import { confirm, search, select } from "@inquirer/prompts";
|
|
9
16
|
import { createInterface } from "readline";
|
|
10
17
|
|
|
11
18
|
// src/platform/browser.ts
|
|
@@ -129,9 +136,17 @@ function processIo(interactive, stderrTTY = Boolean(process.stderr.isTTY)) {
|
|
|
129
136
|
throw e;
|
|
130
137
|
}
|
|
131
138
|
},
|
|
132
|
-
async
|
|
139
|
+
async search(message, source) {
|
|
140
|
+
try {
|
|
141
|
+
return await search({ message, source: (term) => source(term ?? "") });
|
|
142
|
+
} catch (e) {
|
|
143
|
+
if (e.name === "ExitPromptError") throw new CancelledError();
|
|
144
|
+
throw e;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
async confirm(message, signal, o) {
|
|
133
148
|
try {
|
|
134
|
-
return await confirm({ message, default: false }, { signal });
|
|
149
|
+
return await confirm({ message, default: o?.default ?? false }, { signal });
|
|
135
150
|
} catch (e) {
|
|
136
151
|
if (e.name === "ExitPromptError" || e.name === "AbortPromptError") throw new CancelledError();
|
|
137
152
|
throw e;
|
|
@@ -312,7 +327,7 @@ function renderDoctorJson(io2, report) {
|
|
|
312
327
|
return true;
|
|
313
328
|
}
|
|
314
329
|
var SYMBOL = { ok: "\u2713", warn: "!", fail: "\u2717" };
|
|
315
|
-
var TOOL_IDS = ["qore", "node", "git", "github-ssh", "docker"];
|
|
330
|
+
var TOOL_IDS = ["qore", "node", "git", "github-ssh", "docker", "telemetry"];
|
|
316
331
|
var ROW_WIDTH = 80;
|
|
317
332
|
var COL_B = 36;
|
|
318
333
|
function rawLine(r) {
|
|
@@ -395,6 +410,23 @@ function argvBeforeSeparator(argv2) {
|
|
|
395
410
|
function jsonFromArgv(argv2) {
|
|
396
411
|
return argvBeforeSeparator(argv2).some((a) => a === "--json" || a.startsWith("--json="));
|
|
397
412
|
}
|
|
413
|
+
var DOCTOR_CODES = /* @__PURE__ */ new Set([
|
|
414
|
+
"QORE_DOCKER_MISSING",
|
|
415
|
+
"QORE_DOCKER_NOT_RUNNING",
|
|
416
|
+
"QORE_DOCKER_COMPOSE_MISSING",
|
|
417
|
+
"QORE_NODE_VERSION",
|
|
418
|
+
"QORE_PACKAGE_MANAGER",
|
|
419
|
+
"QORE_INSTALL_FAILED",
|
|
420
|
+
"QORE_PORT_IN_USE",
|
|
421
|
+
"QORE_SERVICE_UNHEALTHY"
|
|
422
|
+
]);
|
|
423
|
+
var machineFailure = (e) => DOCTOR_CODES.has(e.code) && !(e.code === "QORE_PORT_IN_USE" && e.details?.owner === "qore");
|
|
424
|
+
function needsDoctor(e) {
|
|
425
|
+
if (!(e instanceof QoreError)) return false;
|
|
426
|
+
const problems = e.details?.problems;
|
|
427
|
+
if (Array.isArray(problems)) return problems.some((p) => machineFailure(p));
|
|
428
|
+
return machineFailure(e);
|
|
429
|
+
}
|
|
398
430
|
function requestLog(ctx) {
|
|
399
431
|
return ctx.opts().verbose ? (line2) => ctx.io.err(line2) : void 0;
|
|
400
432
|
}
|
|
@@ -410,7 +442,16 @@ function buildProgram(deps2, io2, signal, register, setExitCode) {
|
|
|
410
442
|
for (const r of register) r(program, ctx);
|
|
411
443
|
return program;
|
|
412
444
|
}
|
|
413
|
-
|
|
445
|
+
function commandPath(action) {
|
|
446
|
+
const names = [];
|
|
447
|
+
for (let c = action; c?.parent; c = c.parent) names.unshift(c.name());
|
|
448
|
+
return names.join(" ");
|
|
449
|
+
}
|
|
450
|
+
function isCrash(e) {
|
|
451
|
+
if (e instanceof QoreError || e instanceof CancelledError || e instanceof CommanderError) return false;
|
|
452
|
+
return !(e instanceof Error && (e.name === "CancelledError" || e.name === "QoreError"));
|
|
453
|
+
}
|
|
454
|
+
async function run(argv2, deps2, io2, signal, register, hooks = {}) {
|
|
414
455
|
const json2 = jsonFromArgv(argv2);
|
|
415
456
|
const parseIo = json2 ? { ...io2, err: () => {
|
|
416
457
|
} } : io2;
|
|
@@ -418,6 +459,13 @@ async function run(argv2, deps2, io2, signal, register) {
|
|
|
418
459
|
const program = buildProgram(deps2, parseIo, signal, register, (code) => {
|
|
419
460
|
state.exitCode = code;
|
|
420
461
|
});
|
|
462
|
+
program.hook("preAction", async (_program, action) => {
|
|
463
|
+
state.command = action.name();
|
|
464
|
+
try {
|
|
465
|
+
await hooks.onCommand?.(commandPath(action));
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
});
|
|
421
469
|
try {
|
|
422
470
|
await program.parseAsync(argv2, { from: "user" });
|
|
423
471
|
return state.exitCode ?? 0;
|
|
@@ -428,8 +476,15 @@ async function run(argv2, deps2, io2, signal, register) {
|
|
|
428
476
|
if (json2) io2.out(JSON.stringify({ ok: false, error: { code: "QORE_CONTEXT_INVALID", message: e.message } }));
|
|
429
477
|
return 2;
|
|
430
478
|
}
|
|
479
|
+
if (isCrash(e)) {
|
|
480
|
+
try {
|
|
481
|
+
hooks.onCrash?.(e);
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
}
|
|
431
485
|
const opts = program.opts();
|
|
432
486
|
renderError(io2, e, Boolean(opts.json), pc2.isColorSupported && opts.color !== false);
|
|
487
|
+
if (!opts.json && needsDoctor(e) && state.command !== "doctor") io2.err("Run qore doctor for a full check of your machine.");
|
|
433
488
|
return exitCodeFor(e);
|
|
434
489
|
}
|
|
435
490
|
}
|
|
@@ -513,6 +568,9 @@ async function loadMeta(deps2, apiOrigin, refresh) {
|
|
|
513
568
|
});
|
|
514
569
|
return { meta, cached: false };
|
|
515
570
|
}
|
|
571
|
+
async function metaFor(deps2, apiOrigin) {
|
|
572
|
+
return (await loadMeta(deps2, apiOrigin, false)).meta;
|
|
573
|
+
}
|
|
516
574
|
async function ensureCapability(deps2, apiOrigin, capability) {
|
|
517
575
|
const first = await loadMeta(deps2, apiOrigin, false);
|
|
518
576
|
let meta = first.meta;
|
|
@@ -754,8 +812,7 @@ async function openSession(deps2, portal, command, onRequest) {
|
|
|
754
812
|
// src/core/workspace/apps.ts
|
|
755
813
|
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
756
814
|
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 };
|
|
815
|
+
async function resolveTenant(deps2, session, tenantFlag, whatFor) {
|
|
759
816
|
const me = await session.api.get("/api/cli/v1/me");
|
|
760
817
|
let tenant;
|
|
761
818
|
if (tenantFlag) {
|
|
@@ -779,11 +836,16 @@ async function resolveAppRef(deps2, session, arg, tenantFlag) {
|
|
|
779
836
|
} else if (me.tenants.length === 1) {
|
|
780
837
|
tenant = { id: me.tenants[0].id, slug: me.tenants[0].slug };
|
|
781
838
|
} else {
|
|
782
|
-
throw new QoreError("QORE_CONTEXT_REQUIRED", `Which tenant is "${
|
|
839
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", `Which tenant is "${whatFor}" in?`, {
|
|
783
840
|
hint: "Run `qore tenant use <slug>`, or pass --tenant <slug>.",
|
|
784
841
|
candidates: tenantCandidates(me.tenants)
|
|
785
842
|
});
|
|
786
843
|
}
|
|
844
|
+
return { tenant, me };
|
|
845
|
+
}
|
|
846
|
+
async function resolveAppRef(deps2, session, arg, tenantFlag) {
|
|
847
|
+
if (UUID.test(arg)) return { appId: arg, tenant: null };
|
|
848
|
+
const { tenant } = await resolveTenant(deps2, session, tenantFlag, arg);
|
|
787
849
|
try {
|
|
788
850
|
const { id } = await session.api.get(
|
|
789
851
|
`/api/cli/v1/tenants/${encodeURIComponent(tenant.id)}/apps/resolve?slug=${encodeURIComponent(arg)}`
|
|
@@ -806,7 +868,7 @@ async function resolveAppRef(deps2, session, arg, tenantFlag) {
|
|
|
806
868
|
}
|
|
807
869
|
|
|
808
870
|
// src/core/workspace/apply.ts
|
|
809
|
-
import { join as join4
|
|
871
|
+
import { join as join4 } from "path";
|
|
810
872
|
|
|
811
873
|
// src/core/context/cache.ts
|
|
812
874
|
import { join as join2 } from "path";
|
|
@@ -956,7 +1018,7 @@ async function trust(deps2, root, moduleId) {
|
|
|
956
1018
|
}
|
|
957
1019
|
|
|
958
1020
|
// src/core/workspace/plan.ts
|
|
959
|
-
import { join as join3 } from "path";
|
|
1021
|
+
import { join as join3, relative } from "path";
|
|
960
1022
|
var SEGMENT = /^[A-Za-z0-9_.-]+$/;
|
|
961
1023
|
function isSupportedRepoUrl(repoUrl) {
|
|
962
1024
|
let u;
|
|
@@ -981,6 +1043,13 @@ function cloneUrl(repoUrl, protocol) {
|
|
|
981
1043
|
const u = new URL(repoUrl);
|
|
982
1044
|
return `git@${u.hostname}:${u.pathname.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/, "")}.git`;
|
|
983
1045
|
}
|
|
1046
|
+
function conflictText(cwd, action) {
|
|
1047
|
+
const dir2 = relative(cwd, action.dir) || ".";
|
|
1048
|
+
const notRepo = action.reason === "not a git repository";
|
|
1049
|
+
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";
|
|
1050
|
+
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`;
|
|
1051
|
+
return { message, hint: `Move ${dir2} aside (${what}), then run \`qore sync\`.` };
|
|
1052
|
+
}
|
|
984
1053
|
function planWorkspace(root, modules, dirs, strayRepos2) {
|
|
985
1054
|
const actions = modules.map((module) => {
|
|
986
1055
|
const dir2 = join3(root, module.slug);
|
|
@@ -997,6 +1066,10 @@ function planWorkspace(root, modules, dirs, strayRepos2) {
|
|
|
997
1066
|
const slugs = new Set(modules.map((m) => m.slug));
|
|
998
1067
|
return { actions, removed: strayRepos2.filter((s) => !slugs.has(s)) };
|
|
999
1068
|
}
|
|
1069
|
+
function cloneProtocol(dirs) {
|
|
1070
|
+
const origins = Object.values(dirs).flatMap((d) => d.remotes.filter((r) => r.name === "origin").map((r) => r.url));
|
|
1071
|
+
return origins.some((u) => /^(git@|ssh:\/\/)/.test(u)) ? "ssh" : "https";
|
|
1072
|
+
}
|
|
1000
1073
|
|
|
1001
1074
|
// src/core/workspace/diagnose.ts
|
|
1002
1075
|
var ACCESS_CODES = /* @__PURE__ */ new Set(["QORE_GITHUB_NOT_LINKED", "QORE_REPO_NO_ACCESS", "QORE_REPO_ACCESS_PENDING"]);
|
|
@@ -1072,15 +1145,8 @@ function toFailure(slug, e) {
|
|
|
1072
1145
|
throw e;
|
|
1073
1146
|
}
|
|
1074
1147
|
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
|
-
};
|
|
1148
|
+
const { message, hint } = conflictText(deps2.cwd, action);
|
|
1149
|
+
return { slug: action.module.slug, status: "conflict", reason: action.reason, error: { code: "QORE_WORKSPACE_CONFLICT", message, hint } };
|
|
1084
1150
|
}
|
|
1085
1151
|
async function applyActions(deps2, o) {
|
|
1086
1152
|
const cache = new ResolutionCache(join4(deps2.config.dir, "cache"), deps2.now);
|
|
@@ -1221,8 +1287,8 @@ async function readAt(dir2) {
|
|
|
1221
1287
|
async function readWorkspace(dir2) {
|
|
1222
1288
|
return readAt(dir2);
|
|
1223
1289
|
}
|
|
1224
|
-
async function findWorkspace(
|
|
1225
|
-
let dir2 = resolve(
|
|
1290
|
+
async function findWorkspace(start2, home) {
|
|
1291
|
+
let dir2 = resolve(start2);
|
|
1226
1292
|
const stop = resolve(home);
|
|
1227
1293
|
for (; ; ) {
|
|
1228
1294
|
const ws = await readAt(dir2);
|
|
@@ -1382,8 +1448,34 @@ async function resolveApp(deps2, io2, session, appArg, tenantFlag) {
|
|
|
1382
1448
|
return resolveAppRef(deps2, session, appArg, slug);
|
|
1383
1449
|
}
|
|
1384
1450
|
}
|
|
1451
|
+
async function pickApp(deps2, io2, session, tenantFlag) {
|
|
1452
|
+
if (!io2.interactive) {
|
|
1453
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", "An app is required", {
|
|
1454
|
+
hint: "Pass an app slug: `qore clone <app>`. Interactively, omitting it opens a picker."
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
let tenant, me;
|
|
1458
|
+
try {
|
|
1459
|
+
({ tenant, me } = await resolveTenant(deps2, session, tenantFlag, "the app you want to clone"));
|
|
1460
|
+
} catch (e) {
|
|
1461
|
+
if (!(e instanceof QoreError) || e.code !== "QORE_CONTEXT_REQUIRED" && e.code !== "QORE_TENANT_ACCESS_LOST" || !e.candidates?.length) throw e;
|
|
1462
|
+
const slug = await io2.select("Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.slug })));
|
|
1463
|
+
({ tenant, me } = await resolveTenant(deps2, session, slug, "the app you want to clone"));
|
|
1464
|
+
}
|
|
1465
|
+
const apps = me.tenants.find((t) => t.id === tenant.id)?.apps ?? [];
|
|
1466
|
+
if (apps.length === 0) {
|
|
1467
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", `No apps in ${tenant.slug} to clone`, {
|
|
1468
|
+
hint: "Run `qore whoami` to see what you have access to."
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
const app = await io2.search("Select an app", (term) => {
|
|
1472
|
+
const q = term.toLowerCase();
|
|
1473
|
+
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 }));
|
|
1474
|
+
});
|
|
1475
|
+
return { tenant: tenant.slug, app };
|
|
1476
|
+
}
|
|
1385
1477
|
function registerClone(program, ctx) {
|
|
1386
|
-
program.command("clone").description("Clone an app's modules into a new workspace directory").argument("
|
|
1478
|
+
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
1479
|
const { deps: deps2, io: io2 } = ctx;
|
|
1388
1480
|
const g = ctx.opts();
|
|
1389
1481
|
const cfg = await deps2.config.read();
|
|
@@ -1391,7 +1483,9 @@ function registerClone(program, ctx) {
|
|
|
1391
1483
|
const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
|
|
1392
1484
|
const session = await openSession(deps2, portal, "clone", onRequest);
|
|
1393
1485
|
await ensureCapability(deps2, portal.apiOrigin, "app.modules");
|
|
1394
|
-
const
|
|
1486
|
+
const picked = appArg === void 0 ? await pickApp(deps2, io2, session, flags.tenant) : void 0;
|
|
1487
|
+
const appSlug = picked?.app ?? appArg;
|
|
1488
|
+
const ref = await resolveApp(deps2, io2, session, appSlug, picked?.tenant ?? flags.tenant);
|
|
1395
1489
|
const app = await session.api.get(`/api/cli/v1/apps/${encodeURIComponent(ref.appId)}/modules`);
|
|
1396
1490
|
if (dirArg === void 0 && !isSafeSlug(app.app.slug)) throw unsafeSlugError(app.app.slug);
|
|
1397
1491
|
const dirLabel = dirArg ?? app.app.slug;
|
|
@@ -1401,7 +1495,7 @@ function registerClone(program, ctx) {
|
|
|
1401
1495
|
const ws = await readWorkspace(root);
|
|
1402
1496
|
if (!ws || ws.appId !== app.app.id || ws.portal !== portal.apiOrigin) {
|
|
1403
1497
|
throw new QoreError("QORE_WORKSPACE_CONFLICT", `${root} exists and is not the workspace of ${app.app.slug}`, {
|
|
1404
|
-
hint: `Pick another directory: qore clone ${
|
|
1498
|
+
hint: `Pick another directory: qore clone ${appSlug} <dir>.`
|
|
1405
1499
|
});
|
|
1406
1500
|
}
|
|
1407
1501
|
}
|
|
@@ -1438,11 +1532,53 @@ function registerClone(program, ctx) {
|
|
|
1438
1532
|
});
|
|
1439
1533
|
}
|
|
1440
1534
|
|
|
1535
|
+
// src/core/telemetry/consent.ts
|
|
1536
|
+
var ON = /* @__PURE__ */ new Set(["1", "true", "on", "yes"]);
|
|
1537
|
+
var DO_NOT_TRACK_EXEMPT = /* @__PURE__ */ new Set(["0", "false"]);
|
|
1538
|
+
var norm = (v) => (v ?? "").trim().toLowerCase();
|
|
1539
|
+
function resolveConsent(env, cfg) {
|
|
1540
|
+
const dnt = norm(env.DO_NOT_TRACK);
|
|
1541
|
+
if (dnt !== "" && !DO_NOT_TRACK_EXEMPT.has(dnt)) return { enabled: false, source: "DO_NOT_TRACK" };
|
|
1542
|
+
const q = norm(env.QORE_TELEMETRY);
|
|
1543
|
+
if (q !== "") return { enabled: ON.has(q), source: "QORE_TELEMETRY" };
|
|
1544
|
+
if (typeof cfg.telemetry?.enabled === "boolean") return { enabled: cfg.telemetry.enabled, source: "config" };
|
|
1545
|
+
return { enabled: true, source: "default" };
|
|
1546
|
+
}
|
|
1547
|
+
function needsNotice(cfg, consent) {
|
|
1548
|
+
return consent.enabled && consent.source === "default" && !cfg.telemetry?.noticeShownAt;
|
|
1549
|
+
}
|
|
1550
|
+
function describeConsent(consent, built) {
|
|
1551
|
+
const state = `crash reports ${consent.enabled ? "on" : "off"} (${consent.source})`;
|
|
1552
|
+
return consent.enabled && !built ? `${state} \xB7 this build has no DSN, nothing is sent` : state;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// src/cli/commands/config.ts
|
|
1556
|
+
function registerConfig(program, ctx) {
|
|
1557
|
+
const config = program.command("config").description("Change qore's own settings");
|
|
1558
|
+
config.command("telemetry").description("Show or set crash reporting").argument("[state]", "on or off").action(async (state) => {
|
|
1559
|
+
const { deps: deps2, io: io2 } = ctx;
|
|
1560
|
+
if (state !== void 0 && state !== "on" && state !== "off") {
|
|
1561
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `Unknown telemetry state: ${state}`, { hint: "Use: qore config telemetry on|off" });
|
|
1562
|
+
}
|
|
1563
|
+
if (state) await deps2.config.update((c) => {
|
|
1564
|
+
c.telemetry = { ...c.telemetry, enabled: state === "on" };
|
|
1565
|
+
});
|
|
1566
|
+
const consent = resolveConsent(deps2.env, await deps2.config.read());
|
|
1567
|
+
const built = deps2.sentryDsn !== "";
|
|
1568
|
+
if (ctx.opts().json) {
|
|
1569
|
+
renderJson(io2, { telemetry: { ...consent, built } });
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
io2.out(describeConsent(consent, built));
|
|
1573
|
+
if (state && consent.source !== "config") io2.err(`${consent.source} in your environment overrides this setting.`);
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1441
1577
|
// src/cli/commands/dev.ts
|
|
1442
1578
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
1443
1579
|
import { realpath as realpath5 } from "fs/promises";
|
|
1444
1580
|
import { constants as constants3 } from "os";
|
|
1445
|
-
import { relative as relative6 } from "path";
|
|
1581
|
+
import { dirname as dirname6, join as join18, relative as relative6 } from "path";
|
|
1446
1582
|
import { StringDecoder } from "string_decoder";
|
|
1447
1583
|
|
|
1448
1584
|
// src/core/context/repoConfig.ts
|
|
@@ -1515,8 +1651,8 @@ async function readRepoConfig(root) {
|
|
|
1515
1651
|
// src/core/context/repoRoot.ts
|
|
1516
1652
|
import { stat } from "fs/promises";
|
|
1517
1653
|
import { dirname as dirname2, join as join8, resolve as resolve3 } from "path";
|
|
1518
|
-
async function findRepoRoot(
|
|
1519
|
-
let dir2 = resolve3(
|
|
1654
|
+
async function findRepoRoot(start2) {
|
|
1655
|
+
let dir2 = resolve3(start2);
|
|
1520
1656
|
for (; ; ) {
|
|
1521
1657
|
try {
|
|
1522
1658
|
await stat(join8(dir2, ".git"));
|
|
@@ -1801,7 +1937,7 @@ async function resolveModuleContext(deps2, o) {
|
|
|
1801
1937
|
});
|
|
1802
1938
|
}
|
|
1803
1939
|
var appCandidates = (apps) => apps.map((a) => ({ kind: "app", id: a.id, slug: a.slug, name: a.name }));
|
|
1804
|
-
function
|
|
1940
|
+
function pickApp2(mc, app) {
|
|
1805
1941
|
const apps = mc.module.apps;
|
|
1806
1942
|
if (app) {
|
|
1807
1943
|
const found = apps.find((a) => a.id === app || a.slug === app);
|
|
@@ -1821,14 +1957,14 @@ function pickApp(mc, app) {
|
|
|
1821
1957
|
|
|
1822
1958
|
// src/core/diagnostics/node.ts
|
|
1823
1959
|
var KEEP_LINES = 50;
|
|
1824
|
-
function
|
|
1825
|
-
const
|
|
1826
|
-
const missing =
|
|
1960
|
+
function exitHint(tail, h) {
|
|
1961
|
+
const text2 = tail.join("\n");
|
|
1962
|
+
const missing = text2.match(/Cannot find (?:module|package) '([^']+)'/);
|
|
1827
1963
|
if (missing) {
|
|
1828
|
-
return { reason: "missing-module", hint: `A dependency is missing (${missing[1]}). Run \`${
|
|
1964
|
+
return { reason: "missing-module", hint: `A dependency is missing (${missing[1]}). Run \`${h.installCommand}\`, then qore dev again.` };
|
|
1829
1965
|
}
|
|
1830
|
-
if (/ZodError:/.test(
|
|
1831
|
-
const keys = [...
|
|
1966
|
+
if (/ZodError:/.test(text2)) {
|
|
1967
|
+
const keys = [...text2.matchAll(/"path":\s*\[\s*"([^"]+)"/g)].map((m) => m[1]);
|
|
1832
1968
|
if (keys.length > 0) {
|
|
1833
1969
|
return {
|
|
1834
1970
|
reason: "invalid-env",
|
|
@@ -1837,7 +1973,7 @@ function earlyExitHint(tail, o) {
|
|
|
1837
1973
|
};
|
|
1838
1974
|
}
|
|
1839
1975
|
}
|
|
1840
|
-
const addrInUse =
|
|
1976
|
+
const addrInUse = text2.match(/listen EADDRINUSE: address already in use (?:\S*:)?(\d+)/);
|
|
1841
1977
|
if (addrInUse) {
|
|
1842
1978
|
const port = Number(addrInUse[1]);
|
|
1843
1979
|
return {
|
|
@@ -1846,7 +1982,45 @@ function earlyExitHint(tail, o) {
|
|
|
1846
1982
|
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
1983
|
};
|
|
1848
1984
|
}
|
|
1849
|
-
if (/
|
|
1985
|
+
if (/ECONNREFUSED/.test(text2)) {
|
|
1986
|
+
const m = text2.match(/ECONNREFUSED\s+\S*:(\d+)/) ?? text2.match(/code: 'ECONNREFUSED'[^}]*?port: (\d+)/);
|
|
1987
|
+
if (!m) return { reason: "conn-refused", hint: "A connection was refused (ECONNREFUSED) \u2014 a service this module calls is not running." };
|
|
1988
|
+
const port = Number(m[1]);
|
|
1989
|
+
const role = h.ports.get(port);
|
|
1990
|
+
if (role?.kind === "database") {
|
|
1991
|
+
const up = h.runCommand("db:up");
|
|
1992
|
+
return {
|
|
1993
|
+
reason: "conn-refused",
|
|
1994
|
+
port,
|
|
1995
|
+
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.")
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
if (role?.kind === "provider") {
|
|
1999
|
+
return {
|
|
2000
|
+
reason: "conn-refused",
|
|
2001
|
+
port,
|
|
2002
|
+
hint: `${role.key} points at :${port}, where nothing listens (ECONNREFUSED). Start that module \u2014 qore dev in the workspace root starts providers first.`
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
return { reason: "conn-refused", port, hint: `Nothing listens on :${port} (ECONNREFUSED) \u2014 a service this module calls is not running.` };
|
|
2006
|
+
}
|
|
2007
|
+
const auth = text2.match(/password authentication failed for user "([^"]+)"/);
|
|
2008
|
+
if (auth) {
|
|
2009
|
+
const reset = h.runCommand("db:reset");
|
|
2010
|
+
return {
|
|
2011
|
+
reason: "db-auth",
|
|
2012
|
+
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.` : "")
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
const relation = text2.match(/relation "([^"]+)" does not exist/);
|
|
2016
|
+
if (relation) {
|
|
2017
|
+
const reset = h.runCommand("db:reset");
|
|
2018
|
+
return {
|
|
2019
|
+
reason: "db-schema",
|
|
2020
|
+
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).` : "")
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
if (/ENOSPC: System limit for number of file watchers reached/.test(text2)) {
|
|
1850
2024
|
return {
|
|
1851
2025
|
reason: "watcher-limit",
|
|
1852
2026
|
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"
|
|
@@ -1862,9 +2036,11 @@ import { dirname as dirname5, join as join12 } from "path";
|
|
|
1862
2036
|
async function fetchLocalEnv(deps2, mc, app) {
|
|
1863
2037
|
await ensureCapability(deps2, mc.portal.apiOrigin, "module.local-env");
|
|
1864
2038
|
try {
|
|
1865
|
-
|
|
2039
|
+
const local = await mc.session.api.get(
|
|
1866
2040
|
`/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/local-env?app_id=${encodeURIComponent(app.id)}`
|
|
1867
2041
|
);
|
|
2042
|
+
deps2.registerSecrets(Object.values(local.env));
|
|
2043
|
+
return local;
|
|
1868
2044
|
} catch (e) {
|
|
1869
2045
|
if (!(e instanceof QoreError) || e.code !== "QORE_ACCESS_DENIED") throw e;
|
|
1870
2046
|
if (e.serverCode === "app.access.denied") {
|
|
@@ -1947,12 +2123,12 @@ function renderEnv(original, merged) {
|
|
|
1947
2123
|
for (const [key, value] of merged) {
|
|
1948
2124
|
if (!seen.has(key)) appended.push(`${key}=${value}`);
|
|
1949
2125
|
}
|
|
1950
|
-
let
|
|
2126
|
+
let text2 = out.join("\n");
|
|
1951
2127
|
if (appended.length) {
|
|
1952
|
-
if (
|
|
1953
|
-
|
|
2128
|
+
if (text2.length && !text2.endsWith("\n")) text2 += "\n";
|
|
2129
|
+
text2 += appended.join("\n") + "\n";
|
|
1954
2130
|
}
|
|
1955
|
-
return
|
|
2131
|
+
return text2;
|
|
1956
2132
|
}
|
|
1957
2133
|
|
|
1958
2134
|
// src/core/env/pull.ts
|
|
@@ -2034,12 +2210,12 @@ async function readOrNull(p) {
|
|
|
2034
2210
|
throw e;
|
|
2035
2211
|
}
|
|
2036
2212
|
}
|
|
2037
|
-
async function writeSecurely(target,
|
|
2213
|
+
async function writeSecurely(target, text2) {
|
|
2038
2214
|
const base = basename2(target);
|
|
2039
2215
|
const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes3(6).toString("hex")}.local` : `${base}.qore-${randomBytes3(6).toString("hex")}.tmp`;
|
|
2040
2216
|
const tmp = join10(dirname4(target), tmpName);
|
|
2041
2217
|
try {
|
|
2042
|
-
await writeFile3(tmp,
|
|
2218
|
+
await writeFile3(tmp, text2, { mode: 384, flag: "wx" });
|
|
2043
2219
|
await chmod2(tmp, 384);
|
|
2044
2220
|
await rename3(tmp, target);
|
|
2045
2221
|
} finally {
|
|
@@ -2124,16 +2300,117 @@ async function devEnv(deps2, o) {
|
|
|
2124
2300
|
cause = e;
|
|
2125
2301
|
}
|
|
2126
2302
|
}
|
|
2127
|
-
const
|
|
2128
|
-
if (
|
|
2303
|
+
const text2 = await readOrNull(target);
|
|
2304
|
+
if (text2 === null || !await isTrusted(deps2, o.root, o.moduleId)) {
|
|
2129
2305
|
throw cause ?? new QoreError("QORE_API_UNREACHABLE", "The portal is unreachable", {
|
|
2130
2306
|
hint: "The portal is unreachable and there is no env file to start from yet."
|
|
2131
2307
|
});
|
|
2132
2308
|
}
|
|
2133
|
-
const env = Object.fromEntries(parseEnv(
|
|
2309
|
+
const env = Object.fromEntries(parseEnv(text2));
|
|
2134
2310
|
return { env, offline: true, envFile: target, port: envPort(env, "PORT"), dbPort: envPort(env, "DB_PORT"), created: false, kept: [] };
|
|
2135
2311
|
}
|
|
2136
2312
|
|
|
2313
|
+
// src/core/workspace/access.ts
|
|
2314
|
+
function devTeamsOf(am, m) {
|
|
2315
|
+
const own = m.devTeamIds?.length ? am.devTeams.filter((t) => m.devTeamIds.includes(t.id)) : [];
|
|
2316
|
+
return own.length ? own : am.devTeams;
|
|
2317
|
+
}
|
|
2318
|
+
function askMaintainer(am, m) {
|
|
2319
|
+
const teams = devTeamsOf(am, m);
|
|
2320
|
+
const names = teams.map((t) => t.name);
|
|
2321
|
+
const maintainers = [...new Set(teams.flatMap((t) => t.maintainers))];
|
|
2322
|
+
const who = names.length ? names.join(", ") : "the app's dev team";
|
|
2323
|
+
return { teams: names, maintainers, text: `ask a maintainer of ${who}${maintainers.length ? ` (${maintainers.join(", ")})` : ""}` };
|
|
2324
|
+
}
|
|
2325
|
+
function notClonedReason(m, am) {
|
|
2326
|
+
if (am && !m.githubLinked) return `GitHub account not linked (${am.accountUrl})`;
|
|
2327
|
+
if (am && m.repoAccess === "none") {
|
|
2328
|
+
const names = devTeamsOf(am, m).map((t) => t.name);
|
|
2329
|
+
return names.length ? `no repository access (dev team ${names.join(", ")})` : "no repository access (ask your tenant owner)";
|
|
2330
|
+
}
|
|
2331
|
+
return "run qore sync";
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// src/core/dev/findings.ts
|
|
2335
|
+
var plural = (n, one, many) => n === 1 ? one : many;
|
|
2336
|
+
function modulesMissing(o) {
|
|
2337
|
+
const actionFor = new Map(o.actions.map((a) => [a.module.slug, a]));
|
|
2338
|
+
const findings = [];
|
|
2339
|
+
const fixable = [];
|
|
2340
|
+
const blocked = [];
|
|
2341
|
+
const reasons = {};
|
|
2342
|
+
const own = (m, reason, rest) => {
|
|
2343
|
+
reasons[m.slug] = reason;
|
|
2344
|
+
return { code: "dev.modules-missing", severity: "warn", module: m.slug, modules: [m.slug], message: `${m.slug} is not cloned \u2014 ${reason}`, ...rest };
|
|
2345
|
+
};
|
|
2346
|
+
for (const m of o.missing) {
|
|
2347
|
+
const a = actionFor.get(m.slug);
|
|
2348
|
+
if (a && a.kind !== "clone") {
|
|
2349
|
+
if (a.kind === "conflict") {
|
|
2350
|
+
const c = conflictText(o.cwd, a);
|
|
2351
|
+
blocked.push(own(m, c.message, { action: { command: c.hint } }));
|
|
2352
|
+
} else if (a.kind === "unsupported") {
|
|
2353
|
+
blocked.push(own(m, "its repository URL is not supported", { action: { command: unsupportedRepoUrlError(m.slug, "").hint } }));
|
|
2354
|
+
} else {
|
|
2355
|
+
blocked.push(own(m, a.reason ?? "no repository yet", { why: "It will not run in this session." }));
|
|
2356
|
+
}
|
|
2357
|
+
} else if (o.am !== null && (!m.githubLinked || m.repoAccess === "none")) {
|
|
2358
|
+
const am = o.am;
|
|
2359
|
+
blocked.push(own(m, notClonedReason(m, am), { action: { command: !m.githubLinked ? `Link your GitHub account: ${am.accountUrl}` : askMaintainer(am, m).text } }));
|
|
2360
|
+
} else fixable.push(m);
|
|
2361
|
+
}
|
|
2362
|
+
if (fixable.length > 0) {
|
|
2363
|
+
const slugs = fixable.map((m) => m.slug);
|
|
2364
|
+
const deps2 = slugs.flatMap((s) => (o.consumers[s] ?? []).map((c) => `${c} depends on ${s}`));
|
|
2365
|
+
findings.push({
|
|
2366
|
+
code: "dev.modules-missing",
|
|
2367
|
+
severity: "warn",
|
|
2368
|
+
message: `${fixable.length} ${plural(fixable.length, "module of this app is", "modules of this app are")} not cloned: ${slugs.join(", ")}`,
|
|
2369
|
+
why: deps2.length > 0 ? `${deps2.join("; ")}.` : `${plural(fixable.length, "It", "They")} will not run in this session.`,
|
|
2370
|
+
modules: slugs,
|
|
2371
|
+
action: o.am ? { command: "qore sync", fix: "sync" } : { command: "qore sync" }
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
return { findings: [...findings, ...blocked], fixable, reasons };
|
|
2375
|
+
}
|
|
2376
|
+
function strayClone(name, appName) {
|
|
2377
|
+
return { code: "dev.stray-clone", severity: "info", module: name, message: `${name} is no longer part of ${appName} \u2014 you can remove the directory.` };
|
|
2378
|
+
}
|
|
2379
|
+
function repoBehind(slug, s, o) {
|
|
2380
|
+
if (!s.ok || !s.upstream || !s.behind) return null;
|
|
2381
|
+
return {
|
|
2382
|
+
code: "dev.repo-behind",
|
|
2383
|
+
severity: "warn",
|
|
2384
|
+
module: slug,
|
|
2385
|
+
message: `${slug} is ${s.behind} ${plural(s.behind, "commit", "commits")} behind ${s.upstreamRef ?? "its upstream"} (as of your last fetch).`,
|
|
2386
|
+
action: { command: o.workspace ? "qore sync --pull" : "git pull" }
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
function servicesMissing(slug, env, recipe, o) {
|
|
2390
|
+
const db = envPort(env, "DB_PORT");
|
|
2391
|
+
if (db === null || recipe.services.length > 0 || !o.servicesFlag || o.configured) return null;
|
|
2392
|
+
return {
|
|
2393
|
+
code: "dev.services-missing",
|
|
2394
|
+
severity: "warn",
|
|
2395
|
+
module: slug,
|
|
2396
|
+
message: `The portal gives ${slug} a database (DB_PORT=${db}), but ${slug} has no db:up script \u2014 no database will be started.`,
|
|
2397
|
+
why: "This repository is probably older than its template.",
|
|
2398
|
+
action: { command: 'add a "db:up" script (see the current app template), or set dev.services in .qore.json' }
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
function providerDown(o) {
|
|
2402
|
+
const single = o.providers.length === 1 && o.keys.length === 1;
|
|
2403
|
+
return {
|
|
2404
|
+
code: "dev.provider-down",
|
|
2405
|
+
severity: "warn",
|
|
2406
|
+
module: o.slug,
|
|
2407
|
+
modules: o.providers,
|
|
2408
|
+
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(", ")}).`,
|
|
2409
|
+
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.`,
|
|
2410
|
+
action: { command: o.fix }
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2137
2414
|
// src/core/dev/project.ts
|
|
2138
2415
|
import { access, readFile as readFile5 } from "fs/promises";
|
|
2139
2416
|
import { join as join13 } from "path";
|
|
@@ -2252,10 +2529,10 @@ function classifyComposeVersion(r) {
|
|
|
2252
2529
|
// src/core/diagnostics/postgres.ts
|
|
2253
2530
|
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
2531
|
function classifyComposeLogs(logs, o) {
|
|
2255
|
-
const
|
|
2256
|
-
if (!INCOMPATIBLE.test(
|
|
2532
|
+
const text2 = logs.join("\n");
|
|
2533
|
+
if (!INCOMPATIBLE.test(text2)) return null;
|
|
2257
2534
|
const details = { reason: "pg-data-version" };
|
|
2258
|
-
const m =
|
|
2535
|
+
const m = text2.match(/initialized by PostgreSQL version (\d+).*this version (\d+)/);
|
|
2259
2536
|
if (m) {
|
|
2260
2537
|
details.found = m[1];
|
|
2261
2538
|
details.expected = m[2];
|
|
@@ -2345,6 +2622,7 @@ function nodeVersionError(found, range, slug) {
|
|
|
2345
2622
|
// src/core/dev/preflight.ts
|
|
2346
2623
|
var pmMajor = (v) => v ? v.split(".")[0] : "latest";
|
|
2347
2624
|
var GRACE_MS = 1e4;
|
|
2625
|
+
var secs = (ms) => `${Math.round(ms / 1e3)} s`;
|
|
2348
2626
|
async function runTool(deps2, cmd, args, o) {
|
|
2349
2627
|
if (o.signal.aborted) throw new CancelledError();
|
|
2350
2628
|
const tail = [];
|
|
@@ -2401,11 +2679,16 @@ async function checkNode(deps2, r, pkg, slug, signal) {
|
|
|
2401
2679
|
return found;
|
|
2402
2680
|
}
|
|
2403
2681
|
async function installIfNeeded(deps2, r, slug, onLine, report, signal) {
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2682
|
+
if (!r.install) {
|
|
2683
|
+
report({ module: slug, step: "install", status: "skip", detail: "disabled" });
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
if (!await needsInstall(r.root, r.pm)) {
|
|
2687
|
+
report({ module: slug, step: "install", status: "skip", detail: "up to date" });
|
|
2407
2688
|
return;
|
|
2408
2689
|
}
|
|
2690
|
+
report({ module: slug, step: "install", status: "start" });
|
|
2691
|
+
const began = deps2.now().getTime();
|
|
2409
2692
|
const { cmd, args } = installArgs(r.pm);
|
|
2410
2693
|
const res = await runTool(deps2, cmd, args, { cwd: r.root, env: deps2.env, onLine, signal });
|
|
2411
2694
|
if (res.code !== 0) {
|
|
@@ -2414,7 +2697,7 @@ async function installIfNeeded(deps2, r, slug, onLine, report, signal) {
|
|
|
2414
2697
|
if (err.details?.reason === "outdated-lockfile") throw err;
|
|
2415
2698
|
throw new QoreError("QORE_INSTALL_FAILED", `${command} failed in ${slug}`, { hint: err.hint, details: err.details });
|
|
2416
2699
|
}
|
|
2417
|
-
report({ module: slug, step: "install", status: "done" });
|
|
2700
|
+
report({ module: slug, step: "install", status: "done", detail: secs(deps2.now().getTime() - began) });
|
|
2418
2701
|
}
|
|
2419
2702
|
async function portInUseError(deps2, o) {
|
|
2420
2703
|
const owner = await deps2.ports.owner(o.port);
|
|
@@ -2477,7 +2760,12 @@ async function composeFailure(deps2, r, slug, env, signal, message, command, std
|
|
|
2477
2760
|
});
|
|
2478
2761
|
}
|
|
2479
2762
|
async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine, report) {
|
|
2480
|
-
|
|
2763
|
+
if (r.services.length === 0) {
|
|
2764
|
+
report({ module: slug, step: "services", status: "skip" });
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
report({ module: slug, step: "services", status: "start", ...dbPort !== null ? { detail: `:${dbPort}` } : {} });
|
|
2768
|
+
const began = deps2.now().getTime();
|
|
2481
2769
|
if (r.dockerServices) {
|
|
2482
2770
|
const res = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], { cwd: r.root, env, onLine, signal });
|
|
2483
2771
|
const dockerErr = classifyDockerInfo(res, deps2.platform);
|
|
@@ -2497,7 +2785,7 @@ async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine
|
|
|
2497
2785
|
throw await composeFailure(deps2, r, slug, env, signal, `${script} failed in ${slug}`, `${cmd} ${args.join(" ")}`, res.tail);
|
|
2498
2786
|
}
|
|
2499
2787
|
}
|
|
2500
|
-
if (dbPort !== null
|
|
2788
|
+
if (dbPort !== null) {
|
|
2501
2789
|
const ok2 = await deps2.ports.waitFor(dbPort, waitMs, signal);
|
|
2502
2790
|
if (signal.aborted) throw new CancelledError();
|
|
2503
2791
|
if (!ok2) {
|
|
@@ -2513,7 +2801,8 @@ async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine
|
|
|
2513
2801
|
);
|
|
2514
2802
|
}
|
|
2515
2803
|
}
|
|
2516
|
-
|
|
2804
|
+
const took = secs(deps2.now().getTime() - began);
|
|
2805
|
+
report({ module: slug, step: "services", status: "done", detail: dbPort !== null ? `:${dbPort} (${took})` : `(${took})` });
|
|
2517
2806
|
}
|
|
2518
2807
|
async function checkPort(deps2, o) {
|
|
2519
2808
|
if (o.port === null) return;
|
|
@@ -2568,6 +2857,44 @@ async function leftoverOwnsPort(deps2, run4, port) {
|
|
|
2568
2857
|
return false;
|
|
2569
2858
|
}
|
|
2570
2859
|
|
|
2860
|
+
// src/core/dev/providers.ts
|
|
2861
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
2862
|
+
function connPorts(env) {
|
|
2863
|
+
const out = [];
|
|
2864
|
+
for (const key of Object.keys(env).sort()) {
|
|
2865
|
+
const m = /^CONN_(.+)_URL$/.exec(key);
|
|
2866
|
+
if (!m) continue;
|
|
2867
|
+
let url;
|
|
2868
|
+
try {
|
|
2869
|
+
url = new URL(env[key].trim().replace(/^["']|["']$/g, ""));
|
|
2870
|
+
} catch {
|
|
2871
|
+
continue;
|
|
2872
|
+
}
|
|
2873
|
+
const port = Number(url.port);
|
|
2874
|
+
if (port > 0 && LOCAL_HOSTS.has(url.hostname)) out.push({ key, alias: m[1], port });
|
|
2875
|
+
}
|
|
2876
|
+
return out;
|
|
2877
|
+
}
|
|
2878
|
+
function deadAliases(env, listening) {
|
|
2879
|
+
return connPorts(env).filter((c) => !listening.has(c.port)).map((c) => c.key);
|
|
2880
|
+
}
|
|
2881
|
+
function hintContext(recipe, pkg, env) {
|
|
2882
|
+
const { cmd, args } = installArgs(recipe.pm);
|
|
2883
|
+
const ports = /* @__PURE__ */ new Map();
|
|
2884
|
+
for (const c of connPorts(env)) ports.set(c.port, { kind: "provider", key: c.key });
|
|
2885
|
+
const db = envPort(env, "DB_PORT");
|
|
2886
|
+
if (db !== null) ports.set(db, { kind: "database" });
|
|
2887
|
+
return {
|
|
2888
|
+
installCommand: [cmd, ...args].join(" "),
|
|
2889
|
+
runCommand: (script) => {
|
|
2890
|
+
if (pkg.scripts?.[script] === void 0) return null;
|
|
2891
|
+
const r = runArgs(recipe.pm, script);
|
|
2892
|
+
return [r.cmd, ...r.args].join(" ");
|
|
2893
|
+
},
|
|
2894
|
+
ports
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2571
2898
|
// src/core/dev/runFile.ts
|
|
2572
2899
|
import { unlink as unlink2 } from "fs/promises";
|
|
2573
2900
|
import { join as join15 } from "path";
|
|
@@ -2621,11 +2948,11 @@ async function moduleContextFor(ctx, flags, command) {
|
|
|
2621
2948
|
}
|
|
2622
2949
|
async function appFor(ctx, mc, app) {
|
|
2623
2950
|
try {
|
|
2624
|
-
return
|
|
2951
|
+
return pickApp2(mc, app);
|
|
2625
2952
|
} catch (e) {
|
|
2626
2953
|
if (!(e instanceof QoreError) || e.code !== "QORE_APP_AMBIGUOUS" || !ctx.io.interactive) throw e;
|
|
2627
2954
|
const id = await ctx.io.select("Select an app", mc.module.apps.map((a) => ({ name: a.name, value: a.id })));
|
|
2628
|
-
return
|
|
2955
|
+
return pickApp2(mc, id);
|
|
2629
2956
|
}
|
|
2630
2957
|
}
|
|
2631
2958
|
function sourceText(mc) {
|
|
@@ -2649,13 +2976,32 @@ function contextJson(mc, app) {
|
|
|
2649
2976
|
};
|
|
2650
2977
|
}
|
|
2651
2978
|
|
|
2652
|
-
// src/cli/
|
|
2653
|
-
|
|
2654
|
-
var STOP_WAIT_MS = 1e4;
|
|
2655
|
-
function canRecover(ctx) {
|
|
2979
|
+
// src/cli/findings.ts
|
|
2980
|
+
function canOffer(ctx) {
|
|
2656
2981
|
const g = ctx.opts();
|
|
2657
2982
|
return ctx.io.interactive && !g.json && g.input !== false;
|
|
2658
2983
|
}
|
|
2984
|
+
function findingEvent(f) {
|
|
2985
|
+
const { severity, ...rest } = f;
|
|
2986
|
+
return { type: severity === "info" ? "info" : "warning", ...rest };
|
|
2987
|
+
}
|
|
2988
|
+
function renderFinding(ctx, f) {
|
|
2989
|
+
if (ctx.opts().json) {
|
|
2990
|
+
ctx.io.out(JSON.stringify(findingEvent(f)));
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
ctx.io.err(`${f.severity === "info" ? "\u2192" : "!"} ${f.message}`);
|
|
2994
|
+
if (f.why) ctx.io.err(` ${f.why}`);
|
|
2995
|
+
if (f.action) ctx.io.err(` Fix: ${f.action.command}`);
|
|
2996
|
+
}
|
|
2997
|
+
async function offerFix(ctx, question, o) {
|
|
2998
|
+
if (!canOffer(ctx)) return false;
|
|
2999
|
+
return ctx.io.confirm(question, o.signal, { default: o.default });
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
// src/cli/devRecover.ts
|
|
3003
|
+
var DOCKER_WAIT = { timeoutMs: 12e4, pollMs: 2e3 };
|
|
3004
|
+
var STOP_WAIT_MS = 1e4;
|
|
2659
3005
|
var daemonDown = (e) => e instanceof QoreError && e.code === "QORE_DOCKER_NOT_RUNNING" && e.details?.reason === "daemon";
|
|
2660
3006
|
function dockerRecovery(ctx, signal) {
|
|
2661
3007
|
let answer;
|
|
@@ -2663,8 +3009,9 @@ function dockerRecovery(ctx, signal) {
|
|
|
2663
3009
|
try {
|
|
2664
3010
|
await fn();
|
|
2665
3011
|
} catch (e) {
|
|
2666
|
-
if (!daemonDown(e) || !
|
|
3012
|
+
if (!daemonDown(e) || !canOffer(ctx)) throw e;
|
|
2667
3013
|
answer ??= (async () => {
|
|
3014
|
+
if (!await offerFix(ctx, "Docker is not running. Wait for it to start?", { default: true, signal })) return false;
|
|
2668
3015
|
ctx.io.err("Waiting for Docker\u2026 (Ctrl+C to stop)");
|
|
2669
3016
|
const ok2 = await waitForDocker(ctx.deps, { ...at, signal, ...DOCKER_WAIT });
|
|
2670
3017
|
if (ok2) ctx.io.err("\u2713 Docker is running");
|
|
@@ -2680,7 +3027,7 @@ async function stopLeftover(ctx, e, runs, signal) {
|
|
|
2680
3027
|
const port = e.details?.port;
|
|
2681
3028
|
const run4 = runs.find((r) => r.qorePid === e.details?.pid);
|
|
2682
3029
|
if (!run4 || typeof port !== "number" || !await leftoverOwnsPort(deps2, run4, port)) return false;
|
|
2683
|
-
if (!await
|
|
3030
|
+
if (!await offerFix(ctx, `Stop the previous qore dev (pid ${run4.qorePid})?`, { default: false, signal })) return false;
|
|
2684
3031
|
if (!deps2.launch.signal(run4.qorePid, "SIGTERM")) {
|
|
2685
3032
|
io2.err(`! pid ${run4.qorePid} could not be signalled`);
|
|
2686
3033
|
return false;
|
|
@@ -2693,7 +3040,7 @@ async function checkPortOrStopLeftover(ctx, o, signal) {
|
|
|
2693
3040
|
try {
|
|
2694
3041
|
await checkPort(ctx.deps, o);
|
|
2695
3042
|
} catch (e) {
|
|
2696
|
-
if (!(e instanceof QoreError) || e.code !== "QORE_PORT_IN_USE" || e.details?.owner !== "qore" || !
|
|
3043
|
+
if (!(e instanceof QoreError) || e.code !== "QORE_PORT_IN_USE" || e.details?.owner !== "qore" || !canOffer(ctx)) throw e;
|
|
2697
3044
|
const freed = await stopLeftover(ctx, e, o.runs, signal);
|
|
2698
3045
|
if (signal.aborted) throw new CancelledError();
|
|
2699
3046
|
if (!freed) throw e;
|
|
@@ -2701,6 +3048,42 @@ async function checkPortOrStopLeftover(ctx, o, signal) {
|
|
|
2701
3048
|
}
|
|
2702
3049
|
}
|
|
2703
3050
|
|
|
3051
|
+
// src/cli/progress.ts
|
|
3052
|
+
function text(e, o) {
|
|
3053
|
+
switch (`${e.step}:${e.status}`) {
|
|
3054
|
+
case "pm:done":
|
|
3055
|
+
return `\u2713 ${e.detail}`;
|
|
3056
|
+
case "install:skip":
|
|
3057
|
+
return e.detail === "up to date" ? "\u2713 dependencies up to date" : `- install skipped (${o.installFlag ? "dev.install is false in .qore.json" : "--no-install"})`;
|
|
3058
|
+
case "install:start":
|
|
3059
|
+
return "\u22EF installing dependencies\u2026";
|
|
3060
|
+
case "install:done":
|
|
3061
|
+
return `\u2713 dependencies installed (${e.detail})`;
|
|
3062
|
+
case "env:done":
|
|
3063
|
+
return `\u2713 env ${e.detail}`;
|
|
3064
|
+
case "services:skip":
|
|
3065
|
+
return o.servicesFlag ? null : "- services skipped (--no-services)";
|
|
3066
|
+
case "services:start":
|
|
3067
|
+
return e.detail ? `\u22EF database waiting for ${e.detail}\u2026` : "\u22EF starting services\u2026";
|
|
3068
|
+
case "services:done":
|
|
3069
|
+
return e.detail?.startsWith(":") ? `\u2713 database ready on ${e.detail}` : `\u2713 services up ${e.detail}`;
|
|
3070
|
+
case "ports:done":
|
|
3071
|
+
return `\u2713 port ${e.detail} free`;
|
|
3072
|
+
default:
|
|
3073
|
+
return null;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
function progressReporter(ctx, o) {
|
|
3077
|
+
return (e) => {
|
|
3078
|
+
if (ctx.opts().json) {
|
|
3079
|
+
ctx.io.out(JSON.stringify({ type: "step", ...e }));
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
const line2 = text(e, o);
|
|
3083
|
+
if (line2 !== null) ctx.io.err(o.prefix ? o.prefix(e.module, line2) : line2);
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
|
|
2704
3087
|
// src/cli/termination.ts
|
|
2705
3088
|
var preflightHook = {};
|
|
2706
3089
|
function preflightTermination(abort, raise) {
|
|
@@ -2764,7 +3147,7 @@ async function ensureTrusted(ctx, mc, app, o) {
|
|
|
2764
3147
|
// src/cli/commands/devWorkspace.ts
|
|
2765
3148
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
2766
3149
|
import { realpath as realpath4 } from "fs/promises";
|
|
2767
|
-
import { join as
|
|
3150
|
+
import { join as join17, relative as relative5 } from "path";
|
|
2768
3151
|
|
|
2769
3152
|
// src/core/dev/order.ts
|
|
2770
3153
|
function selectSkips(modules, sel) {
|
|
@@ -2869,8 +3252,8 @@ function planDev(modules, sel) {
|
|
|
2869
3252
|
}
|
|
2870
3253
|
const order = sccs.map((_, i) => i).sort((a, b) => minSlug[a].localeCompare(minSlug[b]));
|
|
2871
3254
|
for (const i of order) visit(i);
|
|
2872
|
-
const
|
|
2873
|
-
for (const i of orderedSccIndices)
|
|
3255
|
+
const start2 = [];
|
|
3256
|
+
for (const i of orderedSccIndices) start2.push(...sccSlugs[i]);
|
|
2874
3257
|
const waitFor = {};
|
|
2875
3258
|
for (const m of starting) {
|
|
2876
3259
|
const mySccId = sccOf.get(m.id);
|
|
@@ -2885,7 +3268,7 @@ function planDev(modules, sel) {
|
|
|
2885
3268
|
missingProviders[m.slug] = [...new Set(missing)].sort();
|
|
2886
3269
|
}
|
|
2887
3270
|
}
|
|
2888
|
-
return { start, waitFor, cycles, skipped, missingProviders };
|
|
3271
|
+
return { start: start2, waitFor, cycles, skipped, missingProviders };
|
|
2889
3272
|
}
|
|
2890
3273
|
|
|
2891
3274
|
// src/core/dev/supervisor.ts
|
|
@@ -2986,14 +3369,14 @@ function supervise(o) {
|
|
|
2986
3369
|
const early = !!e.spawnError || now() - startedAt < EARLY_MS;
|
|
2987
3370
|
const rawCode = e.spawnError ? null : e.code;
|
|
2988
3371
|
const rawSignal = e.spawnError ? null : e.signal;
|
|
2989
|
-
const hint =
|
|
3372
|
+
const hint = stopCode === null && rawSignal === null && rawCode !== null && rawCode !== 0 ? exitHint(tails.get(m.slug) ?? [], m.hint) ?? void 0 : void 0;
|
|
2990
3373
|
emit({ type: "exit", module: m.slug, code: rawCode, signal: rawSignal, early, hint });
|
|
2991
3374
|
if (code !== 0 && firstFailure === null) firstFailure = code;
|
|
2992
3375
|
if (o.killOthers) beginStop("kill-others", code);
|
|
2993
3376
|
checkDone();
|
|
2994
3377
|
});
|
|
2995
3378
|
};
|
|
2996
|
-
const
|
|
3379
|
+
const start2 = async (m) => {
|
|
2997
3380
|
const providers = m.waitFor.filter((p) => readiness.has(p));
|
|
2998
3381
|
const results = await Promise.all(providers.map((p) => readiness.get(p).promise));
|
|
2999
3382
|
if (stopCode !== null) return;
|
|
@@ -3003,7 +3386,7 @@ function supervise(o) {
|
|
|
3003
3386
|
});
|
|
3004
3387
|
launch(m);
|
|
3005
3388
|
};
|
|
3006
|
-
for (const m of o.modules) void
|
|
3389
|
+
for (const m of o.modules) void start2(m);
|
|
3007
3390
|
checkDone();
|
|
3008
3391
|
return {
|
|
3009
3392
|
done,
|
|
@@ -3019,25 +3402,26 @@ function supervise(o) {
|
|
|
3019
3402
|
};
|
|
3020
3403
|
}
|
|
3021
3404
|
|
|
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)";
|
|
3405
|
+
// src/core/workspace/stray.ts
|
|
3406
|
+
import { lstat as lstat2, readdir } from "fs/promises";
|
|
3407
|
+
import { join as join16 } from "path";
|
|
3408
|
+
async function strayRepos(root, slugs) {
|
|
3409
|
+
let entries;
|
|
3410
|
+
try {
|
|
3411
|
+
entries = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3412
|
+
} catch {
|
|
3413
|
+
return [];
|
|
3039
3414
|
}
|
|
3040
|
-
|
|
3415
|
+
const names = [];
|
|
3416
|
+
for (const name of entries) {
|
|
3417
|
+
if (slugs.has(name)) continue;
|
|
3418
|
+
try {
|
|
3419
|
+
await lstat2(join16(root, name, ".git"));
|
|
3420
|
+
names.push(name);
|
|
3421
|
+
} catch {
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
return names.sort();
|
|
3041
3425
|
}
|
|
3042
3426
|
|
|
3043
3427
|
// src/cli/devOutput.ts
|
|
@@ -3135,18 +3519,6 @@ async function workspaceContext(ctx, ws) {
|
|
|
3135
3519
|
return { am: { app: last.app, accountUrl: "", devTeams: [], modules }, online: null, offlineCause: e };
|
|
3136
3520
|
}
|
|
3137
3521
|
}
|
|
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
3522
|
async function pool(items, n, fn) {
|
|
3151
3523
|
let next = 0;
|
|
3152
3524
|
const results = await Promise.allSettled(Array.from({ length: Math.min(n, items.length) }, async () => {
|
|
@@ -3157,7 +3529,7 @@ async function pool(items, n, fn) {
|
|
|
3157
3529
|
}
|
|
3158
3530
|
async function workspaceDev(ctx, ws, f, waitTimeout) {
|
|
3159
3531
|
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);
|
|
3532
|
+
await superviseAll(ctx, ws, ready.units, ready.waitFor, ready.o, ready.notRunning);
|
|
3161
3533
|
}
|
|
3162
3534
|
async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
3163
3535
|
const { deps: deps2, io: io2 } = ctx;
|
|
@@ -3202,22 +3574,48 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3202
3574
|
continue;
|
|
3203
3575
|
}
|
|
3204
3576
|
try {
|
|
3205
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
3577
|
+
dirs[m.slug] = await deps2.git.inspect(join17(ws.root, m.slug));
|
|
3206
3578
|
safe.push(m);
|
|
3207
3579
|
} catch (e) {
|
|
3208
3580
|
if (!(e instanceof QoreError) || e.code === "QORE_GIT_UNAVAILABLE") throw e;
|
|
3209
3581
|
problems.push(withDetails(e, { module: m.slug }));
|
|
3210
3582
|
}
|
|
3211
3583
|
}
|
|
3212
|
-
const present = new Map(planWorkspace(ws.root, safe, dirs, []).actions.filter((a) => a.kind === "present").map((a) => [a.module.slug, a.dir]));
|
|
3213
3584
|
const bySlug = new Map(safe.map((m) => [m.slug, m]));
|
|
3214
|
-
const
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3585
|
+
const computePlan = () => {
|
|
3586
|
+
const present2 = new Map(planWorkspace(ws.root, safe, dirs, []).actions.filter((a) => a.kind === "present").map((a) => [a.module.slug, a.dir]));
|
|
3587
|
+
const plan2 = planDev(safe.map((m) => ({
|
|
3588
|
+
id: m.id,
|
|
3589
|
+
slug: m.slug,
|
|
3590
|
+
present: present2.has(m.slug),
|
|
3591
|
+
canWrite: m.access === "write" || m.access === "maintain",
|
|
3592
|
+
dependsOn: m.dependsOn
|
|
3593
|
+
})), { only: slugList(f.only), skip: slugList(f.skip) });
|
|
3594
|
+
return { present: present2, plan: plan2 };
|
|
3595
|
+
};
|
|
3596
|
+
let { present, plan } = computePlan();
|
|
3597
|
+
const missingFindings = () => {
|
|
3598
|
+
const missing2 = plan.skipped.filter((s) => s.reason === "not-cloned").map((s) => bySlug.get(s.slug));
|
|
3599
|
+
const consumers = {};
|
|
3600
|
+
for (const [consumer, providers] of Object.entries(plan.missingProviders)) {
|
|
3601
|
+
for (const p2 of providers) (consumers[p2] ??= []).push(consumer);
|
|
3602
|
+
}
|
|
3603
|
+
const actions = planWorkspace(ws.root, missing2, dirs, []).actions;
|
|
3604
|
+
return modulesMissing({ missing: missing2, actions, cwd: deps2.cwd, consumers, am: online ? am : null });
|
|
3605
|
+
};
|
|
3606
|
+
const missing = missingFindings();
|
|
3607
|
+
for (const finding of missing.findings) renderFinding(ctx, finding);
|
|
3608
|
+
const canSync = online !== null && missing.findings.some((x) => x.action?.fix === "sync");
|
|
3609
|
+
if (canSync && await offerFix(ctx, `Clone ${missing.fixable.map((m) => m.slug).join(", ")} now with qore sync?`, { default: true, signal: ctx.signal })) {
|
|
3610
|
+
const actions = planWorkspace(ws.root, missing.fixable, dirs, []).actions;
|
|
3611
|
+
const results = await applyActions(deps2, { portal: online.portal, app: am, actions, protocol: cloneProtocol(dirs), onResult: () => {
|
|
3612
|
+
} });
|
|
3613
|
+
renderResults(io2, results, false);
|
|
3614
|
+
for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(join17(ws.root, r.slug));
|
|
3615
|
+
({ present, plan } = computePlan());
|
|
3616
|
+
}
|
|
3617
|
+
for (const name of await strayRepos(ws.root, new Set(am.modules.map((m) => m.slug)))) renderFinding(ctx, strayClone(name, am.app.name));
|
|
3618
|
+
if (ctx.signal.aborted) throw new CancelledError();
|
|
3221
3619
|
for (const s of plan.skipped) {
|
|
3222
3620
|
const m = bySlug.get(s.slug);
|
|
3223
3621
|
if (s.reason === "no-write") {
|
|
@@ -3226,8 +3624,10 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3226
3624
|
`- ${s.slug} no write access \u2014 local secrets need write; ${ask.text}`,
|
|
3227
3625
|
{ type: "skip", module: s.slug, reason: s.reason, devTeams: ask.teams, maintainers: ask.maintainers }
|
|
3228
3626
|
);
|
|
3627
|
+
} else if (s.reason === "not-cloned") {
|
|
3628
|
+
if (json2) io2.out(JSON.stringify({ type: "skip", module: s.slug, reason: s.reason }));
|
|
3229
3629
|
} else {
|
|
3230
|
-
note(`- ${s.slug}
|
|
3630
|
+
note(`- ${s.slug} excluded`, { type: "skip", module: s.slug, reason: s.reason });
|
|
3231
3631
|
}
|
|
3232
3632
|
}
|
|
3233
3633
|
for (const c of plan.cycles) warn(`${c.join(", ")} depend on each other \u2014 they start together, without waiting`, { modules: c });
|
|
@@ -3262,8 +3662,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3262
3662
|
else if (l.stream === "stdout") io2.out(prefix(slug, l.line));
|
|
3263
3663
|
else io2.err(prefix(slug, l.line));
|
|
3264
3664
|
};
|
|
3265
|
-
const report = (
|
|
3266
|
-
};
|
|
3665
|
+
const report = progressReporter(ctx, { prefix: json2 ? void 0 : prefix, installFlag: f.install, servicesFlag: f.services });
|
|
3267
3666
|
for (const u of units) {
|
|
3268
3667
|
await attempt(u.m.slug, async () => {
|
|
3269
3668
|
u.cfg = await readRepoConfig(u.dir);
|
|
@@ -3285,13 +3684,27 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3285
3684
|
const withRecipe = units.filter((u) => u.recipe && u.trusted);
|
|
3286
3685
|
for (const name of [...new Set(withRecipe.map((u) => u.recipe.pm.name))]) {
|
|
3287
3686
|
const users = withRecipe.filter((u) => u.recipe.pm.name === name);
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3687
|
+
let version = null;
|
|
3688
|
+
const pmOk = await attempt(users[0].m.slug, async () => {
|
|
3689
|
+
version = await checkPackageManager(deps2, users[0].recipe, () => {
|
|
3690
|
+
}, p.signal);
|
|
3691
|
+
});
|
|
3692
|
+
for (const u of users) {
|
|
3693
|
+
u.pmOk = pmOk;
|
|
3694
|
+
u.pmVersion = version;
|
|
3695
|
+
}
|
|
3291
3696
|
}
|
|
3292
3697
|
for (const u of withRecipe) {
|
|
3293
|
-
const nodeOk = await attempt(u.m.slug, () =>
|
|
3698
|
+
const nodeOk = await attempt(u.m.slug, async () => {
|
|
3699
|
+
u.node = await checkNode(deps2, u.recipe, u.pkg, u.m.slug, p.signal);
|
|
3700
|
+
});
|
|
3294
3701
|
u.pmOk = u.pmOk && nodeOk;
|
|
3702
|
+
if (u.pmOk) report({
|
|
3703
|
+
module: u.m.slug,
|
|
3704
|
+
step: "pm",
|
|
3705
|
+
status: "done",
|
|
3706
|
+
detail: `${u.recipe.pm.name}${u.pmVersion ? ` ${u.pmVersion}` : ""}${u.node ? ` \xB7 node ${u.node}` : ""}`
|
|
3707
|
+
});
|
|
3295
3708
|
}
|
|
3296
3709
|
await pool(units.filter((u) => u.pmOk && u.trusted), INSTALL_PARALLELISM, async (u) => {
|
|
3297
3710
|
u.installed = await attempt(u.m.slug, () => installIfNeeded(deps2, u.recipe, u.m.slug, onLine(u.m.slug), report, p.signal));
|
|
@@ -3337,11 +3750,21 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3337
3750
|
{ module: slug, keys: dev.kept }
|
|
3338
3751
|
);
|
|
3339
3752
|
}
|
|
3753
|
+
report({ module: slug, step: "env", status: "done", detail: relative5(rootReal, dev.envFile) });
|
|
3754
|
+
const services = servicesMissing(slug, dev.env, u.recipe, { servicesFlag: f.services, configured: u.cfg.dev?.services !== void 0 });
|
|
3755
|
+
if (services) renderFinding(ctx, services);
|
|
3756
|
+
try {
|
|
3757
|
+
const behind = repoBehind(slug, await deps2.git.status(u.dir), { workspace: true });
|
|
3758
|
+
if (behind) renderFinding(ctx, behind);
|
|
3759
|
+
} catch {
|
|
3760
|
+
}
|
|
3340
3761
|
if (u.installed) {
|
|
3341
3762
|
const env = { ...deps2.env, ...dev.env };
|
|
3342
3763
|
await attempt(slug, () => withDocker({ cwd: u.dir, env }, () => startServices(deps2, u.recipe, slug, env, dev.dbPort, waitTimeout * 1e3, p.signal, onLine(slug), report)));
|
|
3343
3764
|
}
|
|
3344
|
-
await attempt(slug, () => checkPortOrStopLeftover(ctx, { slug, port: dev.port, key: "PORT", runs }, p.signal))
|
|
3765
|
+
if (await attempt(slug, () => checkPortOrStopLeftover(ctx, { slug, port: dev.port, key: "PORT", runs }, p.signal)) && dev.port !== null) {
|
|
3766
|
+
report({ module: slug, step: "ports", status: "done", detail: String(dev.port) });
|
|
3767
|
+
}
|
|
3345
3768
|
}
|
|
3346
3769
|
if (p.signal.aborted) throw new CancelledError();
|
|
3347
3770
|
const byPort = /* @__PURE__ */ new Map();
|
|
@@ -3369,7 +3792,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3369
3792
|
if (online && units.every((u) => !u.dev.offline)) {
|
|
3370
3793
|
const envFile = (m) => {
|
|
3371
3794
|
const u = units.find((x) => x.m.id === m.id);
|
|
3372
|
-
return u ? relative5(rootReal, u.dev.envFile) :
|
|
3795
|
+
return u ? relative5(rootReal, u.dev.envFile) : join17(m.slug, moduleDefaults(m.type).envFile);
|
|
3373
3796
|
};
|
|
3374
3797
|
await saveLastStart(deps2, ws.root, {
|
|
3375
3798
|
schemaVersion: 1,
|
|
@@ -3390,9 +3813,14 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3390
3813
|
});
|
|
3391
3814
|
}
|
|
3392
3815
|
if (p.signal.aborted) throw new CancelledError();
|
|
3393
|
-
|
|
3816
|
+
const { reasons } = missingFindings();
|
|
3817
|
+
const notRunning = plan.skipped.filter((s) => s.reason !== "excluded").map((s) => {
|
|
3818
|
+
const m = bySlug.get(s.slug);
|
|
3819
|
+
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"}` };
|
|
3820
|
+
});
|
|
3821
|
+
return { units, waitFor: plan.waitFor, notRunning, o: { waitTimeout, killOthers: Boolean(f.killOthers), json: json2, color, prefix } };
|
|
3394
3822
|
}
|
|
3395
|
-
async function superviseAll(ctx, ws, units, waitFor, o) {
|
|
3823
|
+
async function superviseAll(ctx, ws, units, waitFor, o, notRunning) {
|
|
3396
3824
|
const { deps: deps2, io: io2 } = ctx;
|
|
3397
3825
|
const startedAt = deps2.now().toISOString();
|
|
3398
3826
|
const began = /* @__PURE__ */ new Map();
|
|
@@ -3414,9 +3842,9 @@ async function superviseAll(ctx, ws, units, waitFor, o) {
|
|
|
3414
3842
|
case "line":
|
|
3415
3843
|
return e.stream === "stdout" ? io2.out(o.prefix(e.module, e.line)) : io2.err(o.prefix(e.module, e.line));
|
|
3416
3844
|
case "start":
|
|
3417
|
-
return io2.err(`\u25B6 ${e.module}${e.pid === null ? "" : ` (pid ${e.pid})`}`);
|
|
3845
|
+
return io2.err(`\u25B6 starting ${e.module}${e.pid === null ? "" : ` (pid ${e.pid})`}`);
|
|
3418
3846
|
case "ready":
|
|
3419
|
-
return io2.err(`\u2713 ${e.module}
|
|
3847
|
+
return io2.err(`\u2713 ${e.module} ready`);
|
|
3420
3848
|
case "provider-missing":
|
|
3421
3849
|
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
3850
|
case "stopping":
|
|
@@ -3428,23 +3856,45 @@ async function superviseAll(ctx, ws, units, waitFor, o) {
|
|
|
3428
3856
|
const how = e.signal ? `was ended by ${e.signal}` : `exited with code ${e.code}`;
|
|
3429
3857
|
io2.err(`${e.code === 0 ? "\u25A0" : "\u2717"} ${e.module} ${how}${after}`);
|
|
3430
3858
|
if (e.hint) io2.err(` \u2192 ${e.hint.hint}`);
|
|
3859
|
+
if (e.code !== 0 && !o.killOthers && (ref.sup?.children().length ?? 0) > 0) {
|
|
3860
|
+
io2.err(` Restart it on its own: qore dev --only ${e.module} (in another terminal; the others keep running)`);
|
|
3861
|
+
}
|
|
3431
3862
|
return;
|
|
3432
3863
|
}
|
|
3433
3864
|
}
|
|
3434
3865
|
};
|
|
3866
|
+
const withPort = new Set(units.filter((u) => u.dev.port !== null).map((u) => u.m.slug));
|
|
3867
|
+
const readyPorts = /* @__PURE__ */ new Map();
|
|
3868
|
+
let summarized = false;
|
|
3869
|
+
const summarize = () => {
|
|
3870
|
+
if (summarized || o.json || readyPorts.size < withPort.size) return;
|
|
3871
|
+
summarized = true;
|
|
3872
|
+
const width = Math.max(0, ...[...readyPorts.keys(), ...notRunning.map((n) => n.slug)].map((s2) => s2.length));
|
|
3873
|
+
io2.err(notRunning.length === 0 ? "\u2714 All modules running:" : "\u2714 Running:");
|
|
3874
|
+
for (const slug of [...readyPorts.keys()].sort()) io2.err(` ${slug.padEnd(width)} http://localhost:${readyPorts.get(slug)}`);
|
|
3875
|
+
if (notRunning.length > 0) {
|
|
3876
|
+
io2.err("! Not running:");
|
|
3877
|
+
for (const n of notRunning) io2.err(` ${n.slug.padEnd(width)} ${n.reason}`);
|
|
3878
|
+
}
|
|
3879
|
+
};
|
|
3880
|
+
const crashed = /* @__PURE__ */ new Map();
|
|
3435
3881
|
const onEvent = (e) => {
|
|
3436
3882
|
if (e.type === "start") {
|
|
3437
3883
|
began.set(e.module, deps2.now().getTime());
|
|
3438
3884
|
writeRun();
|
|
3439
3885
|
}
|
|
3440
3886
|
if (e.type === "stopping") stopping = true;
|
|
3887
|
+
if (e.type === "ready") readyPorts.set(e.module, e.port);
|
|
3888
|
+
if (e.type === "exit" && !stopping && (e.code !== 0 || e.signal !== null)) {
|
|
3889
|
+
crashed.set(e.module, { code: e.code, signal: e.signal, ms: deps2.now().getTime() - (began.get(e.module) ?? deps2.now().getTime()) });
|
|
3890
|
+
}
|
|
3441
3891
|
if (o.json) io2.out(JSON.stringify(e));
|
|
3442
3892
|
else render(e);
|
|
3893
|
+
if (e.type === "ready") summarize();
|
|
3443
3894
|
};
|
|
3444
3895
|
const s = ref.sup = supervise({
|
|
3445
3896
|
modules: units.map((u) => {
|
|
3446
3897
|
const { cmd, args } = runArgs(u.recipe.pm, u.recipe.script);
|
|
3447
|
-
const { cmd: installCmd, args: installArgv } = installArgs(u.recipe.pm);
|
|
3448
3898
|
return {
|
|
3449
3899
|
slug: u.m.slug,
|
|
3450
3900
|
cmd,
|
|
@@ -3452,7 +3902,7 @@ async function superviseAll(ctx, ws, units, waitFor, o) {
|
|
|
3452
3902
|
cwd: u.dir,
|
|
3453
3903
|
port: u.dev.port,
|
|
3454
3904
|
waitFor: waitFor[u.m.slug] ?? [],
|
|
3455
|
-
|
|
3905
|
+
hint: hintContext(u.recipe, u.pkg, u.dev.env),
|
|
3456
3906
|
env: { ...deps2.env, ...u.dev.env, ...o.color ? { FORCE_COLOR: "1" } : {} }
|
|
3457
3907
|
};
|
|
3458
3908
|
}),
|
|
@@ -3474,6 +3924,11 @@ async function superviseAll(ctx, ws, units, waitFor, o) {
|
|
|
3474
3924
|
if (ctx.signal.aborted) onAbort();
|
|
3475
3925
|
writeRun();
|
|
3476
3926
|
ctx.setExitCode(await s.done);
|
|
3927
|
+
if (!o.json && crashed.size > 0) {
|
|
3928
|
+
const stopped = units.map((u) => u.m.slug).filter((s2) => !crashed.has(s2)).sort();
|
|
3929
|
+
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(", ");
|
|
3930
|
+
io2.err(`${stopped.length ? `\u25A0 stopped: ${stopped.join(", ")} ` : ""}\u2717 crashed: ${what}`);
|
|
3931
|
+
}
|
|
3477
3932
|
} finally {
|
|
3478
3933
|
deps2.signals.off("SIGTERM", onTerm);
|
|
3479
3934
|
deps2.signals.off("SIGHUP", onHup);
|
|
@@ -3543,6 +3998,48 @@ function parseWaitTimeout(raw) {
|
|
|
3543
3998
|
return n;
|
|
3544
3999
|
}
|
|
3545
4000
|
var str2 = (v) => typeof v === "string" && v.length > 0;
|
|
4001
|
+
var SHELL_SAFE = /^[A-Za-z0-9_./,:@%+=-]*$/;
|
|
4002
|
+
var shellQuote = (s) => SHELL_SAFE.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
|
|
4003
|
+
function cdAndDev(home, dir2) {
|
|
4004
|
+
if (/^[A-Za-z]:[\\/]|^\\\\/.test(dir2)) return `cd ${/\s/.test(dir2) ? `"${dir2}"` : dir2} && qore dev`;
|
|
4005
|
+
const h = home.length > 1 ? home.replace(/\/+$/, "") : home;
|
|
4006
|
+
let target = shellQuote(dir2);
|
|
4007
|
+
if (h && dir2 === h) target = "~";
|
|
4008
|
+
else if (h && dir2.startsWith(h + "/")) target = `~/${shellQuote(dir2.slice(h.length + 1))}`;
|
|
4009
|
+
return `cd ${target} && qore dev`;
|
|
4010
|
+
}
|
|
4011
|
+
async function providerFindings(ctx, root, mc, app, env) {
|
|
4012
|
+
const { deps: deps2 } = ctx;
|
|
4013
|
+
try {
|
|
4014
|
+
if (!(await metaFor(deps2, mc.portal.apiOrigin)).capabilities.includes("app.modules")) return null;
|
|
4015
|
+
const am = await mc.session.api.get(`/api/cli/v1/apps/${encodeURIComponent(app.id)}/modules`);
|
|
4016
|
+
const self = am.modules.find((m) => m.id === mc.module.id);
|
|
4017
|
+
const providers = am.modules.filter((m) => self?.dependsOn.includes(m.id));
|
|
4018
|
+
if (providers.length === 0) return null;
|
|
4019
|
+
const conns = connPorts(env);
|
|
4020
|
+
const listening = /* @__PURE__ */ new Set();
|
|
4021
|
+
for (const c of conns) if (await deps2.ports.inUse(c.port)) listening.add(c.port);
|
|
4022
|
+
const keys = deadAliases(env, listening);
|
|
4023
|
+
if (keys.length === 0) return null;
|
|
4024
|
+
const ws = await findWorkspace(root, deps2.home);
|
|
4025
|
+
let fix = `qore clone ${app.slug}`;
|
|
4026
|
+
if (ws && ws.appId === app.id) fix = cdAndDev(deps2.home, ws.root);
|
|
4027
|
+
else if (providers.length === 1) {
|
|
4028
|
+
const sibling = join18(dirname6(root), providers[0].slug);
|
|
4029
|
+
if ((await deps2.git.inspect(sibling)).isRepo) fix = cdAndDev(deps2.home, sibling);
|
|
4030
|
+
}
|
|
4031
|
+
return providerDown({
|
|
4032
|
+
slug: mc.module.slug,
|
|
4033
|
+
providers: providers.map((p) => p.slug),
|
|
4034
|
+
keys,
|
|
4035
|
+
ports: new Map(conns.map((c) => [c.key, c.port])),
|
|
4036
|
+
fix
|
|
4037
|
+
});
|
|
4038
|
+
} catch (e) {
|
|
4039
|
+
if (e instanceof QoreError) return null;
|
|
4040
|
+
throw e;
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
3546
4043
|
function usableLast2(last) {
|
|
3547
4044
|
if (!last || last.kind !== "module") return false;
|
|
3548
4045
|
const m = last.module;
|
|
@@ -3567,8 +4064,9 @@ async function devContext(ctx, f, root) {
|
|
|
3567
4064
|
}
|
|
3568
4065
|
async function moduleDev(ctx, f, waitTimeout) {
|
|
3569
4066
|
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));
|
|
4067
|
+
const { root, slug, recipe, env, port, pkg, devEnv: devEnv2 } = await inPreflight(deps2.signals, ctx.signal, (p) => prepareModule(ctx, f, waitTimeout, p));
|
|
3571
4068
|
const { cmd, args } = runArgs(recipe.pm, recipe.script);
|
|
4069
|
+
io2.err(`\u25B6 starting ${slug}${port === null ? "" : ` \u2014 http://localhost:${port}`}`);
|
|
3572
4070
|
const startedAt = deps2.now();
|
|
3573
4071
|
await writeRunFile(deps2, root, {
|
|
3574
4072
|
schemaVersion: 1,
|
|
@@ -3589,10 +4087,9 @@ async function moduleDev(ctx, f, waitTimeout) {
|
|
|
3589
4087
|
await removeRunFile(deps2, root);
|
|
3590
4088
|
}
|
|
3591
4089
|
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(" ") });
|
|
4090
|
+
if (exit.code !== 0 && exit.signal === null) {
|
|
4091
|
+
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}.`);
|
|
4092
|
+
const hint = exitHint(tee.lines(), hintContext(recipe, pkg, devEnv2));
|
|
3596
4093
|
if (hint) io2.err(` \u2192 ${hint.hint}`);
|
|
3597
4094
|
}
|
|
3598
4095
|
if (exit.signal) {
|
|
@@ -3637,11 +4134,16 @@ async function prepareModule(ctx, f, waitTimeout, p) {
|
|
|
3637
4134
|
});
|
|
3638
4135
|
const runs = [await leftoverRun(deps2, root)].filter((r) => r !== null);
|
|
3639
4136
|
const onLine = (l) => l.stream === "stdout" ? io2.out(l.line) : io2.err(l.line);
|
|
3640
|
-
const report = (
|
|
3641
|
-
|
|
3642
|
-
await checkPackageManager(deps2, recipe, () => {
|
|
4137
|
+
const report = progressReporter(ctx, { installFlag: f.install, servicesFlag: f.services });
|
|
4138
|
+
const pmVersion = await checkPackageManager(deps2, recipe, () => {
|
|
3643
4139
|
}, p.signal);
|
|
3644
|
-
await checkNode(deps2, recipe, pkg, slug, p.signal);
|
|
4140
|
+
const nodeVersion = await checkNode(deps2, recipe, pkg, slug, p.signal);
|
|
4141
|
+
report({
|
|
4142
|
+
module: slug,
|
|
4143
|
+
step: "pm",
|
|
4144
|
+
status: "done",
|
|
4145
|
+
detail: `${recipe.pm.name}${pmVersion ? ` ${pmVersion}` : ""}${nodeVersion ? ` \xB7 node ${nodeVersion}` : ""}`
|
|
4146
|
+
});
|
|
3645
4147
|
await installIfNeeded(deps2, recipe, slug, onLine, report, p.signal);
|
|
3646
4148
|
let dev;
|
|
3647
4149
|
try {
|
|
@@ -3663,9 +4165,22 @@ async function prepareModule(ctx, f, waitTimeout, p) {
|
|
|
3663
4165
|
if (dev.kept.length) {
|
|
3664
4166
|
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
4167
|
}
|
|
4168
|
+
report({ module: slug, step: "env", status: "done", detail: dev.offline ? `${envFile} (as last written \u2014 offline)` : envFile });
|
|
3666
4169
|
const env = { ...deps2.env, ...dev.env };
|
|
3667
4170
|
await dockerRecovery(ctx, p.signal)({ cwd: root, env }, () => startServices(deps2, recipe, slug, env, dev.dbPort, waitTimeout * 1e3, p.signal, onLine, report));
|
|
3668
4171
|
await checkPortOrStopLeftover(ctx, { slug, port: dev.port, key: "PORT", runs }, p.signal);
|
|
4172
|
+
if (dev.port !== null) report({ module: slug, step: "ports", status: "done", detail: String(dev.port) });
|
|
4173
|
+
if (c.online && !dev.offline) {
|
|
4174
|
+
const provider = await providerFindings(ctx, root, c.online.mc, c.online.app, dev.env);
|
|
4175
|
+
if (provider) renderFinding(ctx, provider);
|
|
4176
|
+
}
|
|
4177
|
+
const services = servicesMissing(slug, dev.env, recipe, { servicesFlag: f.services, configured: cfg.dev?.services !== void 0 });
|
|
4178
|
+
if (services) renderFinding(ctx, services);
|
|
4179
|
+
try {
|
|
4180
|
+
const behind = repoBehind(slug, await deps2.git.status(root), { workspace: false });
|
|
4181
|
+
if (behind) renderFinding(ctx, behind);
|
|
4182
|
+
} catch {
|
|
4183
|
+
}
|
|
3669
4184
|
if (c.online && !dev.offline) {
|
|
3670
4185
|
const { mc, app } = c.online;
|
|
3671
4186
|
const { id, name, type, access: access2, tenant, apps } = mc.module;
|
|
@@ -3688,7 +4203,15 @@ async function prepareModule(ctx, f, waitTimeout, p) {
|
|
|
3688
4203
|
});
|
|
3689
4204
|
}
|
|
3690
4205
|
if (p.signal.aborted) throw new CancelledError();
|
|
3691
|
-
return { root, slug, recipe, env, port: dev.port };
|
|
4206
|
+
return { root, slug, recipe, env, port: dev.port, pkg, devEnv: dev.env };
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
// src/cli/commands/debugTelemetry.ts
|
|
4210
|
+
function registerDebugTelemetry(program, ctx) {
|
|
4211
|
+
program.command("debug-telemetry", { hidden: true }).description("Send a test crash report").option("--crash", "throw an unexpected error").action((o) => {
|
|
4212
|
+
if (o.crash) throw new Error(`qore debug-telemetry crash in ${ctx.deps.home} token=debug-telemetry-secret`);
|
|
4213
|
+
if (ctx.opts().json) renderJson(ctx.io, { crash: false });
|
|
4214
|
+
});
|
|
3692
4215
|
}
|
|
3693
4216
|
|
|
3694
4217
|
// src/cli/commands/doctor.ts
|
|
@@ -3696,14 +4219,14 @@ import pc3 from "picocolors";
|
|
|
3696
4219
|
|
|
3697
4220
|
// src/core/doctor/checks.ts
|
|
3698
4221
|
import { realpath as realpath6 } from "fs/promises";
|
|
3699
|
-
import { basename as basename3, join as
|
|
4222
|
+
import { basename as basename3, join as join19 } from "path";
|
|
3700
4223
|
|
|
3701
4224
|
// src/core/diagnostics/ssh.ts
|
|
3702
4225
|
var SSH_TEST_ARGS = ["-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "git@github.com"];
|
|
3703
4226
|
var COMMAND = "ssh -T git@github.com";
|
|
3704
4227
|
function classifySshTest(r) {
|
|
3705
|
-
const
|
|
3706
|
-
const hi = /Hi ([^!\s]+)! You've successfully authenticated/.exec(
|
|
4228
|
+
const text2 = r.tail.join("\n");
|
|
4229
|
+
const hi = /Hi ([^!\s]+)! You've successfully authenticated/.exec(text2);
|
|
3707
4230
|
if (hi) return { ok: true, user: hi[1] };
|
|
3708
4231
|
const details = { protocol: "ssh" };
|
|
3709
4232
|
if (r.spawnError) {
|
|
@@ -3712,13 +4235,13 @@ function classifySshTest(r) {
|
|
|
3712
4235
|
details: { ...details, reason: "ssh-missing" }
|
|
3713
4236
|
}) };
|
|
3714
4237
|
}
|
|
3715
|
-
if (/Permission denied \(publickey\)/.test(
|
|
4238
|
+
if (/Permission denied \(publickey\)/.test(text2)) {
|
|
3716
4239
|
return { ok: false, error: new QoreError("QORE_GIT_AUTH", "GitHub refused your SSH key", {
|
|
3717
4240
|
hint: "Add your key to GitHub (https://github.com/settings/keys) and your ssh agent, or keep cloning over https.",
|
|
3718
4241
|
details
|
|
3719
4242
|
}) };
|
|
3720
4243
|
}
|
|
3721
|
-
if (/Host key verification failed/.test(
|
|
4244
|
+
if (/Host key verification failed/.test(text2)) {
|
|
3722
4245
|
return { ok: false, error: new QoreError("QORE_GIT_AUTH", "ssh could not verify github.com's host key", {
|
|
3723
4246
|
hint: `Run \`${COMMAND}\` once to accept its host key (or to see why it changed).`,
|
|
3724
4247
|
details: { ...details, reason: "host-key" }
|
|
@@ -3893,9 +4416,9 @@ async function moduleBody(deps2, m, runs, signal) {
|
|
|
3893
4416
|
let env = null;
|
|
3894
4417
|
await part(async () => {
|
|
3895
4418
|
const target = await resolveTarget(m.root, cfg.envFile ?? moduleDefaults(m.type).envFile);
|
|
3896
|
-
const
|
|
3897
|
-
if (
|
|
3898
|
-
env = Object.fromEntries(parseEnv(
|
|
4419
|
+
const text2 = await readOrNull(target);
|
|
4420
|
+
if (text2 === null) return { status: "warn", label: "port unknown (run qore env pull)" };
|
|
4421
|
+
env = Object.fromEntries(parseEnv(text2));
|
|
3899
4422
|
const port = envPort(env, "PORT");
|
|
3900
4423
|
if (port === null) return { status: "warn", label: `port unknown (no PORT in ${basename3(target)})` };
|
|
3901
4424
|
if (!await deps2.ports.inUse(port)) return { status: "ok", label: `port ${port} free` };
|
|
@@ -3976,6 +4499,12 @@ async function resolveRepoModule(deps2, session, portal, meta, root) {
|
|
|
3976
4499
|
}
|
|
3977
4500
|
return session.api.get(`/api/cli/v1/modules/${encodeURIComponent(ids[0])}`);
|
|
3978
4501
|
}
|
|
4502
|
+
function checkTelemetry(deps2, cfg) {
|
|
4503
|
+
const built = deps2.sentryDsn !== "";
|
|
4504
|
+
if (!cfg) return ok("crash reports off (config unreadable)", { enabled: false, source: "config", reason: "unreadable", built });
|
|
4505
|
+
const consent = resolveConsent(deps2.env, cfg);
|
|
4506
|
+
return ok(describeConsent(consent, built), { enabled: consent.enabled, source: consent.source, built });
|
|
4507
|
+
}
|
|
3979
4508
|
async function runDoctor(deps2, o = {}) {
|
|
3980
4509
|
const signal = o.signal ?? new AbortController().signal;
|
|
3981
4510
|
const toolMs = o.toolTimeoutMs ?? TOOL_TIMEOUT_MS;
|
|
@@ -3995,12 +4524,14 @@ async function runDoctor(deps2, o = {}) {
|
|
|
3995
4524
|
return r;
|
|
3996
4525
|
};
|
|
3997
4526
|
let cfg;
|
|
4527
|
+
let cfgReadable = true;
|
|
3998
4528
|
try {
|
|
3999
4529
|
cfg = await deps2.config.read();
|
|
4000
4530
|
} catch (e) {
|
|
4001
4531
|
if (isCancel(e, signal)) throw e;
|
|
4002
4532
|
await push("config", "tools", () => withError("fail", errorLabel(e), e));
|
|
4003
4533
|
cfg = { schemaVersion: 1, profiles: {} };
|
|
4534
|
+
cfgReadable = false;
|
|
4004
4535
|
}
|
|
4005
4536
|
let ws = null;
|
|
4006
4537
|
let wsError = null;
|
|
@@ -4022,6 +4553,7 @@ async function runDoctor(deps2, o = {}) {
|
|
|
4022
4553
|
await push("git", "tools", () => checkGit(deps2, signal, toolMs));
|
|
4023
4554
|
await push("github-ssh", "tools", () => checkGithubSsh(deps2, signal, toolMs));
|
|
4024
4555
|
await push("docker", "tools", () => checkDocker(deps2, signal, toolMs));
|
|
4556
|
+
await push("telemetry", "tools", () => checkTelemetry(deps2, cfgReadable ? cfg : null));
|
|
4025
4557
|
const sessions = /* @__PURE__ */ new Map();
|
|
4026
4558
|
let loggedIn = [];
|
|
4027
4559
|
try {
|
|
@@ -4114,7 +4646,7 @@ async function workspaceModule(deps2, ws, am, m, runs, signal, push, perModule)
|
|
|
4114
4646
|
await push(`module:${m.slug}`, "workspace", () => ({ status: "warn", label: "not a safe directory name \u2014 skipped" }), m.slug);
|
|
4115
4647
|
return;
|
|
4116
4648
|
}
|
|
4117
|
-
const mod = { root:
|
|
4649
|
+
const mod = { root: join19(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
|
|
4118
4650
|
let prepared = null;
|
|
4119
4651
|
await push(`module:${m.slug}`, "workspace", async () => {
|
|
4120
4652
|
const info = await deps2.git.inspect(mod.root);
|
|
@@ -4146,7 +4678,7 @@ function registerDoctor(program, ctx) {
|
|
|
4146
4678
|
// src/cli/commands/env.ts
|
|
4147
4679
|
import { randomBytes as randomBytes6 } from "crypto";
|
|
4148
4680
|
import { chmod as chmod3, realpath as realpath7 } from "fs/promises";
|
|
4149
|
-
import { dirname as
|
|
4681
|
+
import { dirname as dirname7, join as join20, relative as relative7 } from "path";
|
|
4150
4682
|
var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.length} chars)`;
|
|
4151
4683
|
function registerEnv(program, ctx) {
|
|
4152
4684
|
const env = program.command("env").description("Local environment");
|
|
@@ -4163,7 +4695,7 @@ function registerEnv(program, ctx) {
|
|
|
4163
4695
|
const target = f.target ? await resolveTarget(mc.repoRoot, f.target, deps2.cwd) : await resolveTarget(mc.repoRoot, cfg.envFile ?? defaults.envFile);
|
|
4164
4696
|
await ensureTrusted(ctx, mc, app, { yes: f.yes });
|
|
4165
4697
|
const existing = await readOrNull(target);
|
|
4166
|
-
const example = existing === null ? await readOrNull(
|
|
4698
|
+
const example = existing === null ? await readOrNull(join20(dirname7(target), ".env.example")) : null;
|
|
4167
4699
|
const local = await fetchLocalEnv(deps2, mc, app);
|
|
4168
4700
|
const inputs = {
|
|
4169
4701
|
existing,
|
|
@@ -4300,9 +4832,9 @@ async function createOidc(opts) {
|
|
|
4300
4832
|
throw mapOidcError(e, host);
|
|
4301
4833
|
}
|
|
4302
4834
|
},
|
|
4303
|
-
async pollDevice(
|
|
4835
|
+
async pollDevice(start2, signal) {
|
|
4304
4836
|
try {
|
|
4305
|
-
const t = await oc.pollDeviceAuthorizationGrant(config,
|
|
4837
|
+
const t = await oc.pollDeviceAuthorizationGrant(config, start2.handle, void 0, { signal });
|
|
4306
4838
|
return toTokenSet(t);
|
|
4307
4839
|
} catch (e) {
|
|
4308
4840
|
throw mapOidcError(e, host);
|
|
@@ -4329,14 +4861,14 @@ async function createOidc(opts) {
|
|
|
4329
4861
|
// src/core/auth/login.ts
|
|
4330
4862
|
async function deviceLogin(opts) {
|
|
4331
4863
|
await opts.store.getRefresh();
|
|
4332
|
-
const
|
|
4864
|
+
const start2 = await opts.oidc.startDevice(LOGIN_SCOPE);
|
|
4333
4865
|
const stopPoll = new AbortController();
|
|
4334
4866
|
const settled = new AbortController();
|
|
4335
|
-
const poll = opts.oidc.pollDevice(
|
|
4867
|
+
const poll = opts.oidc.pollDevice(start2, AbortSignal.any([opts.signal, stopPoll.signal])).finally(() => settled.abort());
|
|
4336
4868
|
poll.catch(() => {
|
|
4337
4869
|
});
|
|
4338
4870
|
try {
|
|
4339
|
-
await opts.onCode(
|
|
4871
|
+
await opts.onCode(start2, settled.signal);
|
|
4340
4872
|
} catch (e) {
|
|
4341
4873
|
stopPoll.abort();
|
|
4342
4874
|
const late = await poll.catch(() => null);
|
|
@@ -4562,8 +5094,7 @@ function registerStatus(program, ctx) {
|
|
|
4562
5094
|
}
|
|
4563
5095
|
|
|
4564
5096
|
// src/cli/commands/sync.ts
|
|
4565
|
-
import {
|
|
4566
|
-
import { join as join19, relative as relative8 } from "path";
|
|
5097
|
+
import { join as join21, relative as relative8 } from "path";
|
|
4567
5098
|
import { Option as Option2 } from "commander";
|
|
4568
5099
|
function asFailure2(slug, e) {
|
|
4569
5100
|
if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
|
|
@@ -4571,24 +5102,6 @@ function asFailure2(slug, e) {
|
|
|
4571
5102
|
}
|
|
4572
5103
|
throw e;
|
|
4573
5104
|
}
|
|
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
5105
|
function registerSync(program, ctx) {
|
|
4593
5106
|
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
5107
|
const { deps: deps2, io: io2 } = ctx;
|
|
@@ -4613,7 +5126,7 @@ function registerSync(program, ctx) {
|
|
|
4613
5126
|
continue;
|
|
4614
5127
|
}
|
|
4615
5128
|
try {
|
|
4616
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
5129
|
+
dirs[m.slug] = await deps2.git.inspect(join21(ws.root, m.slug));
|
|
4617
5130
|
modulesForPlan.push(m);
|
|
4618
5131
|
} catch (e) {
|
|
4619
5132
|
earlyFailures.push(asFailure2(m.slug, e));
|
|
@@ -4753,9 +5266,111 @@ var COMMANDS = [
|
|
|
4753
5266
|
registerClone,
|
|
4754
5267
|
registerSync,
|
|
4755
5268
|
registerVersion,
|
|
4756
|
-
|
|
5269
|
+
registerConfig,
|
|
5270
|
+
registerDoctor,
|
|
5271
|
+
registerDebugTelemetry
|
|
4757
5272
|
];
|
|
4758
5273
|
|
|
5274
|
+
// src/cli/telemetry/notice.ts
|
|
5275
|
+
var NOTICE = [
|
|
5276
|
+
"qore sends anonymous crash reports and a per-run health record to Quatronic, to find and fix bugs.",
|
|
5277
|
+
"Tokens, env values and your home path are removed; no user or tenant is attached.",
|
|
5278
|
+
"Turn it off with `qore config telemetry off`, QORE_TELEMETRY=0 or DO_NOT_TRACK=1. See the Privacy section of the README."
|
|
5279
|
+
];
|
|
5280
|
+
async function showNoticeOnce(deps2, io2, o) {
|
|
5281
|
+
if (o.json || !io2.interactive || !deps2.sentryDsn) return;
|
|
5282
|
+
try {
|
|
5283
|
+
const cfg = await deps2.config.read();
|
|
5284
|
+
if (!needsNotice(cfg, resolveConsent(deps2.env, cfg))) return;
|
|
5285
|
+
await deps2.config.update((c) => {
|
|
5286
|
+
c.telemetry = { ...c.telemetry, noticeShownAt: deps2.now().toISOString() };
|
|
5287
|
+
});
|
|
5288
|
+
} catch {
|
|
5289
|
+
return;
|
|
5290
|
+
}
|
|
5291
|
+
for (const line2 of NOTICE) io2.err(line2);
|
|
5292
|
+
}
|
|
5293
|
+
|
|
5294
|
+
// src/cli/telemetry/index.ts
|
|
5295
|
+
var CRASH_FLUSH_MS = 2e3;
|
|
5296
|
+
var CLEAN_FLUSH_MS = 200;
|
|
5297
|
+
var loadSentry = async () => (await import("./sentry-B3MDCAOI.js")).createSdk;
|
|
5298
|
+
function createTelemetry(deps2, o) {
|
|
5299
|
+
const load = o.load ?? loadSentry;
|
|
5300
|
+
const flushMs = o.flushMs ?? { crash: CRASH_FLUSH_MS, clean: CLEAN_FLUSH_MS };
|
|
5301
|
+
let sdk = null;
|
|
5302
|
+
let crashed = null;
|
|
5303
|
+
const secrets = [];
|
|
5304
|
+
let scrub = null;
|
|
5305
|
+
return {
|
|
5306
|
+
begin(command) {
|
|
5307
|
+
if (sdk || !deps2.sentryDsn) return;
|
|
5308
|
+
sdk = start(deps2, o.argv, command, load, (ctx) => {
|
|
5309
|
+
scrub = ctx;
|
|
5310
|
+
addSecretValues(ctx, secrets);
|
|
5311
|
+
}).catch(() => null);
|
|
5312
|
+
},
|
|
5313
|
+
crash(e) {
|
|
5314
|
+
crashed ??= { e };
|
|
5315
|
+
},
|
|
5316
|
+
registerSecrets(values) {
|
|
5317
|
+
secrets.push(...values);
|
|
5318
|
+
if (scrub) addSecretValues(scrub, values);
|
|
5319
|
+
},
|
|
5320
|
+
async end(signal) {
|
|
5321
|
+
if (!sdk) return { drained: true };
|
|
5322
|
+
const ms = crashed ? flushMs.crash : flushMs.clean;
|
|
5323
|
+
const work = sdk.then(async (s) => {
|
|
5324
|
+
if (!s) return true;
|
|
5325
|
+
if (crashed) s.crash(crashed.e);
|
|
5326
|
+
return s.end(ms);
|
|
5327
|
+
}).catch(() => false);
|
|
5328
|
+
return { drained: await withDeadline(work, ms, signal) };
|
|
5329
|
+
}
|
|
5330
|
+
};
|
|
5331
|
+
}
|
|
5332
|
+
async function start(deps2, argv2, command, load, onScrub) {
|
|
5333
|
+
const cfg = await deps2.config.read().catch(() => null);
|
|
5334
|
+
if (!cfg) return null;
|
|
5335
|
+
if (!resolveConsent(deps2.env, cfg).enabled) return null;
|
|
5336
|
+
const create = await load();
|
|
5337
|
+
const scrub = buildScrubContext({ home: deps2.home, platform: deps2.platform, env: deps2.env, argv: argv2, commandWords: command.split(" ") });
|
|
5338
|
+
onScrub(scrub);
|
|
5339
|
+
const sdk = create({
|
|
5340
|
+
dsn: deps2.sentryDsn,
|
|
5341
|
+
environment: deps2.sentryEnvironment,
|
|
5342
|
+
release: deps2.version,
|
|
5343
|
+
tags: { command, "cli.version": deps2.version, "node.version": deps2.nodeVersion, os: deps2.platform, ci: deps2.env.CI ? "true" : "false" },
|
|
5344
|
+
scrub
|
|
5345
|
+
});
|
|
5346
|
+
sdk.start();
|
|
5347
|
+
return sdk;
|
|
5348
|
+
}
|
|
5349
|
+
function withDeadline(work, ms, signal) {
|
|
5350
|
+
if (signal?.aborted) return Promise.resolve(false);
|
|
5351
|
+
return new Promise((resolve5) => {
|
|
5352
|
+
const done = (v) => {
|
|
5353
|
+
clearTimeout(timer);
|
|
5354
|
+
signal?.removeEventListener("abort", onAbort);
|
|
5355
|
+
resolve5(v);
|
|
5356
|
+
};
|
|
5357
|
+
const onAbort = () => done(false);
|
|
5358
|
+
const timer = setTimeout(() => done(false), ms);
|
|
5359
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
5360
|
+
work.then(done, () => done(false));
|
|
5361
|
+
});
|
|
5362
|
+
}
|
|
5363
|
+
function telemetryHooks(deps2, io2, t, o) {
|
|
5364
|
+
return {
|
|
5365
|
+
onCommand: async (command) => {
|
|
5366
|
+
if (command === "config telemetry") return;
|
|
5367
|
+
await showNoticeOnce(deps2, io2, o);
|
|
5368
|
+
t.begin(command);
|
|
5369
|
+
},
|
|
5370
|
+
onCrash: (e) => t.crash(e)
|
|
5371
|
+
};
|
|
5372
|
+
}
|
|
5373
|
+
|
|
4759
5374
|
// src/cli/updateCheck.ts
|
|
4760
5375
|
var REGISTRY_URL = "https://registry.npmjs.org/@quatronic%2Fqore/latest";
|
|
4761
5376
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
@@ -4789,7 +5404,7 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
|
|
|
4789
5404
|
|
|
4790
5405
|
// src/core/config.ts
|
|
4791
5406
|
import { readFile as readFile6 } from "fs/promises";
|
|
4792
|
-
import { join as
|
|
5407
|
+
import { join as join22 } from "path";
|
|
4793
5408
|
var empty = () => ({ schemaVersion: 1, profiles: {} });
|
|
4794
5409
|
var ConfigStore = class {
|
|
4795
5410
|
constructor(dir2) {
|
|
@@ -4797,7 +5412,7 @@ var ConfigStore = class {
|
|
|
4797
5412
|
}
|
|
4798
5413
|
dir;
|
|
4799
5414
|
get file() {
|
|
4800
|
-
return
|
|
5415
|
+
return join22(this.dir, "config.json");
|
|
4801
5416
|
}
|
|
4802
5417
|
async read() {
|
|
4803
5418
|
let raw;
|
|
@@ -4830,7 +5445,7 @@ var ConfigStore = class {
|
|
|
4830
5445
|
import { execFile, execFileSync } from "child_process";
|
|
4831
5446
|
import { mkdir as mkdir3, readdir as readdir2, stat as stat3 } from "fs/promises";
|
|
4832
5447
|
import { homedir } from "os";
|
|
4833
|
-
import { dirname as
|
|
5448
|
+
import { dirname as dirname8, join as join23 } from "path";
|
|
4834
5449
|
function parseRemotes(stdout) {
|
|
4835
5450
|
const seen = /* @__PURE__ */ new Set();
|
|
4836
5451
|
const out = [];
|
|
@@ -4842,6 +5457,30 @@ function parseRemotes(stdout) {
|
|
|
4842
5457
|
}
|
|
4843
5458
|
return out;
|
|
4844
5459
|
}
|
|
5460
|
+
function parseStatus(stdout) {
|
|
5461
|
+
const s = { ok: true, dirty: false, branch: null, upstream: false };
|
|
5462
|
+
for (const raw of stdout.split("\n")) {
|
|
5463
|
+
const line2 = raw.replace(/\r$/, "");
|
|
5464
|
+
if (line2.startsWith("# branch.head ")) {
|
|
5465
|
+
const name = line2.slice("# branch.head ".length).trim();
|
|
5466
|
+
s.branch = name === "(detached)" ? null : name;
|
|
5467
|
+
} else if (line2.startsWith("# branch.upstream ")) {
|
|
5468
|
+
s.upstream = true;
|
|
5469
|
+
s.upstreamRef = line2.slice("# branch.upstream ".length).trim();
|
|
5470
|
+
} else if (line2.startsWith("# branch.ab ")) {
|
|
5471
|
+
const m = /^# branch\.ab \+(\d+) -(\d+)$/.exec(line2.trim());
|
|
5472
|
+
if (m) {
|
|
5473
|
+
s.ahead = Number(m[1]);
|
|
5474
|
+
s.behind = Number(m[2]);
|
|
5475
|
+
}
|
|
5476
|
+
} else if (line2.startsWith("#")) {
|
|
5477
|
+
continue;
|
|
5478
|
+
} else if (line2.trim().length > 0) {
|
|
5479
|
+
s.dirty = true;
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
return s;
|
|
5483
|
+
}
|
|
4845
5484
|
var configuredSshCommand = /* @__PURE__ */ new Map();
|
|
4846
5485
|
function hasConfiguredSshCommand(repoDir) {
|
|
4847
5486
|
const key = repoDir ?? "";
|
|
@@ -4900,7 +5539,7 @@ async function remotes(repoRoot) {
|
|
|
4900
5539
|
return parseRemotes(r.stdout);
|
|
4901
5540
|
}
|
|
4902
5541
|
async function clone(url, dir2) {
|
|
4903
|
-
await mkdir3(
|
|
5542
|
+
await mkdir3(dirname8(dir2), { recursive: true });
|
|
4904
5543
|
const r = await runGit(["clone", "--", url, dir2]);
|
|
4905
5544
|
return toResult(r, "git clone");
|
|
4906
5545
|
}
|
|
@@ -4916,22 +5555,7 @@ async function status(repoRoot) {
|
|
|
4916
5555
|
const command = "git status --porcelain=v2 --branch";
|
|
4917
5556
|
const r = await runGit(["--no-optional-locks", "-C", repoRoot, "status", "--porcelain=v2", "--branch"], repoRoot);
|
|
4918
5557
|
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 };
|
|
5558
|
+
return parseStatus(r.stdout);
|
|
4935
5559
|
}
|
|
4936
5560
|
async function statOrThrow(path) {
|
|
4937
5561
|
try {
|
|
@@ -4952,7 +5576,7 @@ async function inspect(dir2) {
|
|
|
4952
5576
|
throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
|
|
4953
5577
|
}
|
|
4954
5578
|
const empty2 = entries.length === 0;
|
|
4955
|
-
const isRepo = await statOrThrow(
|
|
5579
|
+
const isRepo = await statOrThrow(join23(dir2, ".git")) !== void 0;
|
|
4956
5580
|
const dirRemotes = isRepo ? await remotes(dir2) : [];
|
|
4957
5581
|
return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
|
|
4958
5582
|
}
|
|
@@ -5014,13 +5638,13 @@ var OsKeychain = class {
|
|
|
5014
5638
|
|
|
5015
5639
|
// src/platform/lock.ts
|
|
5016
5640
|
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
5017
|
-
import { join as
|
|
5641
|
+
import { join as join24 } from "path";
|
|
5018
5642
|
import lockfile from "proper-lockfile";
|
|
5019
5643
|
function fileLock(dir2) {
|
|
5020
5644
|
return async (name, fn) => {
|
|
5021
|
-
const locks =
|
|
5645
|
+
const locks = join24(dir2, "locks");
|
|
5022
5646
|
await mkdir4(locks, { recursive: true, mode: 448 });
|
|
5023
|
-
const target =
|
|
5647
|
+
const target = join24(locks, `${name}.lock`);
|
|
5024
5648
|
await writeFile4(target, "", { flag: "a" });
|
|
5025
5649
|
let release;
|
|
5026
5650
|
let compromised;
|
|
@@ -5121,6 +5745,10 @@ var crossSpawner = makeSpawner(spawn, process);
|
|
|
5121
5745
|
|
|
5122
5746
|
// src/platform/launch.ts
|
|
5123
5747
|
import spawn2 from "cross-spawn";
|
|
5748
|
+
var RESET_OR_CLEAR = /\x1Bc|\x1B\[[23]J/g;
|
|
5749
|
+
function stripScreenClears(line2) {
|
|
5750
|
+
return line2.includes("\x1B") ? line2.replace(RESET_OR_CLEAR, "") : line2;
|
|
5751
|
+
}
|
|
5124
5752
|
function lineSplitter(stream, onLine) {
|
|
5125
5753
|
let buf = "";
|
|
5126
5754
|
return {
|
|
@@ -5128,13 +5756,13 @@ function lineSplitter(stream, onLine) {
|
|
|
5128
5756
|
buf += chunk;
|
|
5129
5757
|
let i;
|
|
5130
5758
|
while ((i = buf.indexOf("\n")) >= 0) {
|
|
5131
|
-
onLine({ stream, line: buf.slice(0, i).replace(/\r$/, "") });
|
|
5759
|
+
onLine({ stream, line: stripScreenClears(buf.slice(0, i).replace(/\r$/, "")) });
|
|
5132
5760
|
buf = buf.slice(i + 1);
|
|
5133
5761
|
}
|
|
5134
5762
|
},
|
|
5135
5763
|
flush() {
|
|
5136
5764
|
if (buf) {
|
|
5137
|
-
onLine({ stream, line: buf.replace(/\r$/, "") });
|
|
5765
|
+
onLine({ stream, line: stripScreenClears(buf.replace(/\r$/, "")) });
|
|
5138
5766
|
buf = "";
|
|
5139
5767
|
}
|
|
5140
5768
|
}
|
|
@@ -5289,6 +5917,10 @@ var systemPorts = {
|
|
|
5289
5917
|
}
|
|
5290
5918
|
};
|
|
5291
5919
|
|
|
5920
|
+
// src/buildInfo.ts
|
|
5921
|
+
var SENTRY_DSN = true ? "https://50b592efb53dbc1a31cb10d3945c6d52@o4512129906049024.ingest.de.sentry.io/4512174341750864" : "";
|
|
5922
|
+
var SENTRY_ENVIRONMENT = true ? "production" : "development";
|
|
5923
|
+
|
|
5292
5924
|
// src/platform/proxy.ts
|
|
5293
5925
|
import { execFile as execFile3 } from "child_process";
|
|
5294
5926
|
|
|
@@ -5351,7 +5983,7 @@ function systemProxy(platform = process.platform) {
|
|
|
5351
5983
|
}
|
|
5352
5984
|
|
|
5353
5985
|
// src/version.ts
|
|
5354
|
-
var VERSION = true ? "0.1.
|
|
5986
|
+
var VERSION = true ? "0.1.5" : "0.0.0-dev";
|
|
5355
5987
|
|
|
5356
5988
|
// src/cli.ts
|
|
5357
5989
|
if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
@@ -5394,6 +6026,11 @@ var deps = {
|
|
|
5394
6026
|
process.kill(process.pid, signal);
|
|
5395
6027
|
},
|
|
5396
6028
|
pid: process.pid,
|
|
6029
|
+
registerSecrets: (values) => {
|
|
6030
|
+
telemetry.registerSecrets(values);
|
|
6031
|
+
},
|
|
6032
|
+
sentryDsn: SENTRY_DSN,
|
|
6033
|
+
sentryEnvironment: SENTRY_ENVIRONMENT,
|
|
5397
6034
|
systemProxy: () => systemProxy(process.platform)
|
|
5398
6035
|
};
|
|
5399
6036
|
var termination = preflightTermination(() => ac.abort(), deps.raise);
|
|
@@ -5404,16 +6041,24 @@ var io = processIo(isInteractive({
|
|
|
5404
6041
|
noInput: preSeparatorArgv.includes("--no-input"),
|
|
5405
6042
|
env: process.env
|
|
5406
6043
|
}));
|
|
5407
|
-
|
|
5408
|
-
await termination.reRaise(async () => {
|
|
6044
|
+
var flushStd = async () => {
|
|
5409
6045
|
await Promise.all([process.stdout, process.stderr].map((s) => new Promise((r) => {
|
|
5410
6046
|
s.write("", () => r());
|
|
5411
6047
|
})));
|
|
5412
|
-
}
|
|
6048
|
+
};
|
|
6049
|
+
var telemetry = createTelemetry(deps, { argv });
|
|
6050
|
+
process.exitCode = await run(argv, deps, io, ac.signal, COMMANDS, telemetryHooks(deps, io, telemetry, { json }));
|
|
6051
|
+
var { drained } = await telemetry.end(ac.signal);
|
|
6052
|
+
await termination.reRaise(flushStd);
|
|
5413
6053
|
if (!ac.signal.aborted) {
|
|
5414
6054
|
try {
|
|
5415
6055
|
await maybeNotifyUpdate(deps, io, { json });
|
|
5416
6056
|
} catch {
|
|
5417
6057
|
}
|
|
5418
6058
|
}
|
|
5419
|
-
|
|
6059
|
+
if (!drained) {
|
|
6060
|
+
await flushStd();
|
|
6061
|
+
process.exit();
|
|
6062
|
+
}
|
|
6063
|
+
//# sourceMappingURL=cli.js.map
|
|
6064
|
+
//# debugId=a70250c5-daf4-5ff3-b490-5556ac377549
|