@quatronic/qore 0.1.4 → 0.1.6
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 +93 -4
- package/dist/chunk-OAU6HJHZ.js +140 -0
- package/dist/cli.js +1102 -371
- package/dist/sentry-GMM4X3FT.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]="28c91dc8-dbb9-5c0a-91e3-f368931a4ed3")}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-OAU6HJHZ.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, search, select } from "@inquirer/prompts";
|
|
15
|
+
import { confirm, input, search, select } from "@inquirer/prompts";
|
|
9
16
|
import { createInterface } from "readline";
|
|
10
17
|
|
|
11
18
|
// src/platform/browser.ts
|
|
@@ -44,6 +51,10 @@ var EXIT = {
|
|
|
44
51
|
QORE_REPO_ACCESS_PENDING: 5,
|
|
45
52
|
QORE_GIT_AUTH: 5,
|
|
46
53
|
QORE_WORKSPACE_CONFLICT: 2,
|
|
54
|
+
QORE_ROOT_NOT_SET: 2,
|
|
55
|
+
QORE_ROOT_INVALID: 2,
|
|
56
|
+
QORE_ROOT_NOT_WRITABLE: 1,
|
|
57
|
+
QORE_PROJECT_NOT_FOUND: 2,
|
|
47
58
|
QORE_AUTH_UNAVAILABLE: 6,
|
|
48
59
|
QORE_AUTH_BUSY: 6,
|
|
49
60
|
QORE_API_UNREACHABLE: 6,
|
|
@@ -145,13 +156,21 @@ function processIo(interactive, stderrTTY = Boolean(process.stderr.isTTY)) {
|
|
|
145
156
|
throw e;
|
|
146
157
|
}
|
|
147
158
|
},
|
|
159
|
+
async input(message, o) {
|
|
160
|
+
try {
|
|
161
|
+
return await input({ message, default: o?.default });
|
|
162
|
+
} catch (e) {
|
|
163
|
+
if (e.name === "ExitPromptError") throw new CancelledError();
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
166
|
+
},
|
|
148
167
|
waitForEnter: createWaitForEnter(process.stdin, process.stderr),
|
|
149
168
|
openUrl: openBrowser
|
|
150
169
|
};
|
|
151
170
|
}
|
|
152
|
-
function createWaitForEnter(
|
|
153
|
-
return (message, signal) => new Promise((
|
|
154
|
-
const rl = createInterface({ input, output, terminal: true });
|
|
171
|
+
function createWaitForEnter(input2, output) {
|
|
172
|
+
return (message, signal) => new Promise((resolve6, reject) => {
|
|
173
|
+
const rl = createInterface({ input: input2, output, terminal: true });
|
|
155
174
|
const onAbort = () => {
|
|
156
175
|
rl.close();
|
|
157
176
|
reject(new CancelledError());
|
|
@@ -159,7 +178,7 @@ function createWaitForEnter(input, output) {
|
|
|
159
178
|
const done = () => {
|
|
160
179
|
signal.removeEventListener("abort", onAbort);
|
|
161
180
|
rl.close();
|
|
162
|
-
|
|
181
|
+
resolve6();
|
|
163
182
|
};
|
|
164
183
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
165
184
|
rl.on("SIGINT", () => {
|
|
@@ -176,7 +195,7 @@ import { Command, CommanderError } from "commander";
|
|
|
176
195
|
|
|
177
196
|
// src/core/api/http.ts
|
|
178
197
|
function abortableFetch(fetch3, signal) {
|
|
179
|
-
return (
|
|
198
|
+
return (input2, init) => fetch3(input2, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal });
|
|
180
199
|
}
|
|
181
200
|
var TLS = /* @__PURE__ */ new Set([
|
|
182
201
|
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
@@ -320,7 +339,7 @@ function renderDoctorJson(io2, report) {
|
|
|
320
339
|
return true;
|
|
321
340
|
}
|
|
322
341
|
var SYMBOL = { ok: "\u2713", warn: "!", fail: "\u2717" };
|
|
323
|
-
var TOOL_IDS = ["qore", "node", "git", "github-ssh", "docker"];
|
|
342
|
+
var TOOL_IDS = ["qore", "node", "git", "github-ssh", "docker", "telemetry"];
|
|
324
343
|
var ROW_WIDTH = 80;
|
|
325
344
|
var COL_B = 36;
|
|
326
345
|
function rawLine(r) {
|
|
@@ -435,7 +454,16 @@ function buildProgram(deps2, io2, signal, register, setExitCode) {
|
|
|
435
454
|
for (const r of register) r(program, ctx);
|
|
436
455
|
return program;
|
|
437
456
|
}
|
|
438
|
-
|
|
457
|
+
function commandPath(action) {
|
|
458
|
+
const names = [];
|
|
459
|
+
for (let c = action; c?.parent; c = c.parent) names.unshift(c.name());
|
|
460
|
+
return names.join(" ");
|
|
461
|
+
}
|
|
462
|
+
function isCrash(e) {
|
|
463
|
+
if (e instanceof QoreError || e instanceof CancelledError || e instanceof CommanderError) return false;
|
|
464
|
+
return !(e instanceof Error && (e.name === "CancelledError" || e.name === "QoreError"));
|
|
465
|
+
}
|
|
466
|
+
async function run(argv2, deps2, io2, signal, register, hooks = {}) {
|
|
439
467
|
const json2 = jsonFromArgv(argv2);
|
|
440
468
|
const parseIo = json2 ? { ...io2, err: () => {
|
|
441
469
|
} } : io2;
|
|
@@ -443,8 +471,12 @@ async function run(argv2, deps2, io2, signal, register) {
|
|
|
443
471
|
const program = buildProgram(deps2, parseIo, signal, register, (code) => {
|
|
444
472
|
state.exitCode = code;
|
|
445
473
|
});
|
|
446
|
-
program.hook("preAction", (_program, action) => {
|
|
474
|
+
program.hook("preAction", async (_program, action) => {
|
|
447
475
|
state.command = action.name();
|
|
476
|
+
try {
|
|
477
|
+
await hooks.onCommand?.(commandPath(action));
|
|
478
|
+
} catch {
|
|
479
|
+
}
|
|
448
480
|
});
|
|
449
481
|
try {
|
|
450
482
|
await program.parseAsync(argv2, { from: "user" });
|
|
@@ -456,6 +488,12 @@ async function run(argv2, deps2, io2, signal, register) {
|
|
|
456
488
|
if (json2) io2.out(JSON.stringify({ ok: false, error: { code: "QORE_CONTEXT_INVALID", message: e.message } }));
|
|
457
489
|
return 2;
|
|
458
490
|
}
|
|
491
|
+
if (isCrash(e)) {
|
|
492
|
+
try {
|
|
493
|
+
hooks.onCrash?.(e);
|
|
494
|
+
} catch {
|
|
495
|
+
}
|
|
496
|
+
}
|
|
459
497
|
const opts = program.opts();
|
|
460
498
|
renderError(io2, e, Boolean(opts.json), pc2.isColorSupported && opts.color !== false);
|
|
461
499
|
if (!opts.json && needsDoctor(e) && state.command !== "doctor") io2.err("Run qore doctor for a full check of your machine.");
|
|
@@ -464,7 +502,7 @@ async function run(argv2, deps2, io2, signal, register) {
|
|
|
464
502
|
}
|
|
465
503
|
|
|
466
504
|
// src/cli/commands/clone.ts
|
|
467
|
-
import { join as
|
|
505
|
+
import { join as join9, relative as relative3, resolve as resolve4 } from "path";
|
|
468
506
|
import { Option } from "commander";
|
|
469
507
|
|
|
470
508
|
// src/core/api/meta.ts
|
|
@@ -918,8 +956,8 @@ var ResolutionCache = class {
|
|
|
918
956
|
const f = await this.load();
|
|
919
957
|
const hits = [];
|
|
920
958
|
for (const [k, e] of Object.entries(f.entries)) {
|
|
921
|
-
const
|
|
922
|
-
if (keys.includes(k.slice(
|
|
959
|
+
const sep3 = k.lastIndexOf("|");
|
|
960
|
+
if (keys.includes(k.slice(sep3 + 1)) && this.fresh(e)) hits.push({ origin: k.slice(0, sep3), at: new Date(e.resolvedAt).getTime() });
|
|
923
961
|
}
|
|
924
962
|
hits.sort((a, b) => b.at - a.at);
|
|
925
963
|
return [...new Set(hits.map((h) => h.origin))];
|
|
@@ -1225,6 +1263,13 @@ async function syncPresent(deps2, results, actions, pull, ctx) {
|
|
|
1225
1263
|
}
|
|
1226
1264
|
}
|
|
1227
1265
|
|
|
1266
|
+
// src/core/projects/registry.ts
|
|
1267
|
+
import { realpath as realpath4 } from "fs/promises";
|
|
1268
|
+
|
|
1269
|
+
// src/core/projects/discover.ts
|
|
1270
|
+
import { readdir, realpath as realpath2 } from "fs/promises";
|
|
1271
|
+
import { join as join6 } from "path";
|
|
1272
|
+
|
|
1228
1273
|
// src/core/workspace/file.ts
|
|
1229
1274
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1230
1275
|
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
@@ -1261,8 +1306,8 @@ async function readAt(dir2) {
|
|
|
1261
1306
|
async function readWorkspace(dir2) {
|
|
1262
1307
|
return readAt(dir2);
|
|
1263
1308
|
}
|
|
1264
|
-
async function findWorkspace(
|
|
1265
|
-
let dir2 = resolve(
|
|
1309
|
+
async function findWorkspace(start2, home) {
|
|
1310
|
+
let dir2 = resolve(start2);
|
|
1266
1311
|
const stop = resolve(home);
|
|
1267
1312
|
for (; ; ) {
|
|
1268
1313
|
const ws = await readAt(dir2);
|
|
@@ -1286,17 +1331,225 @@ async function writeWorkspace(root, o) {
|
|
|
1286
1331
|
}
|
|
1287
1332
|
async function workspacePortal(deps2, ws) {
|
|
1288
1333
|
const portal = resolvePortal(ws.portal, void 0);
|
|
1289
|
-
const
|
|
1334
|
+
const label3 = portal.builtin ?? portal.apiOrigin;
|
|
1290
1335
|
const profile = (await deps2.config.read()).profiles[portal.apiOrigin];
|
|
1291
1336
|
const loggedIn = profile && await new TokenStore(deps2.keychain, profileHash(profile.apiOrigin, profile.issuer)).getRefresh();
|
|
1292
1337
|
if (!loggedIn) {
|
|
1293
|
-
throw new QoreError("QORE_NOT_LOGGED_IN", `This workspace belongs to ${
|
|
1294
|
-
hint: `Run \`qore login --portal ${
|
|
1338
|
+
throw new QoreError("QORE_NOT_LOGGED_IN", `This workspace belongs to ${label3}, which you are not logged in to`, {
|
|
1339
|
+
hint: `Run \`qore login --portal ${label3}\`.`
|
|
1295
1340
|
});
|
|
1296
1341
|
}
|
|
1297
1342
|
return portal;
|
|
1298
1343
|
}
|
|
1299
1344
|
|
|
1345
|
+
// src/core/projects/discover.ts
|
|
1346
|
+
var SKIP = (name) => name.startsWith(".") || name === "node_modules";
|
|
1347
|
+
async function subdirs(dir2) {
|
|
1348
|
+
try {
|
|
1349
|
+
return (await readdir(dir2, { withFileTypes: true })).filter((d) => d.isDirectory() && !SKIP(d.name)).map((d) => d.name);
|
|
1350
|
+
} catch {
|
|
1351
|
+
return [];
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
async function scanRoot(root) {
|
|
1355
|
+
const out = [];
|
|
1356
|
+
for (const tenant of (await subdirs(root)).sort()) {
|
|
1357
|
+
for (const app of (await subdirs(join6(root, tenant))).sort()) {
|
|
1358
|
+
const path = join6(root, tenant, app);
|
|
1359
|
+
try {
|
|
1360
|
+
const ws = await readWorkspace(path);
|
|
1361
|
+
if (ws) out.push({ kind: "workspace", path, portal: ws.portal, tenantSlug: tenant, appId: ws.appId, appSlug: app });
|
|
1362
|
+
} catch {
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return out;
|
|
1367
|
+
}
|
|
1368
|
+
var GONE = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
|
|
1369
|
+
async function stillValid(e) {
|
|
1370
|
+
try {
|
|
1371
|
+
const real = await realpath2(e.path);
|
|
1372
|
+
if (e.kind === "workspace") {
|
|
1373
|
+
const ws = await readWorkspace(real);
|
|
1374
|
+
return ws && ws.appId === e.appId ? real : "gone";
|
|
1375
|
+
}
|
|
1376
|
+
return real;
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
if (err instanceof CancelledError) throw err;
|
|
1379
|
+
return GONE.has(err.code ?? "") ? "gone" : "unknown";
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
async function knownProjects(deps2, o = {}) {
|
|
1383
|
+
const cfg = await deps2.config.read();
|
|
1384
|
+
const root = cfg.projectsRoot ? await realpath2(cfg.projectsRoot).catch(() => cfg.projectsRoot) : void 0;
|
|
1385
|
+
const out = root ? (await scanRoot(root)).map((p) => ({ ...p, source: "root" })) : [];
|
|
1386
|
+
const seen = new Set(out.map((p) => p.path));
|
|
1387
|
+
const stale = [];
|
|
1388
|
+
for (const e of cfg.projects ?? []) {
|
|
1389
|
+
const real = await stillValid(e);
|
|
1390
|
+
if (real === "gone") {
|
|
1391
|
+
stale.push(e.path);
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
if (real === "unknown") continue;
|
|
1395
|
+
if (seen.has(real)) continue;
|
|
1396
|
+
seen.add(real);
|
|
1397
|
+
out.push({ ...e, path: real, source: "registry" });
|
|
1398
|
+
}
|
|
1399
|
+
if (stale.length > 0) await forget(deps2, stale);
|
|
1400
|
+
return o.portal ? out.filter((p) => p.portal === o.portal) : out;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// src/core/projects/root.ts
|
|
1404
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1405
|
+
import { mkdir as mkdir3, realpath as realpath3, rm as rm2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
|
|
1406
|
+
import { dirname as dirname3, isAbsolute, join as join8, parse, relative as relative2, resolve as resolve3, sep } from "path";
|
|
1407
|
+
|
|
1408
|
+
// src/core/context/repoRoot.ts
|
|
1409
|
+
import { stat } from "fs/promises";
|
|
1410
|
+
import { dirname as dirname2, join as join7, resolve as resolve2 } from "path";
|
|
1411
|
+
async function findRepoRoot(start2) {
|
|
1412
|
+
let dir2 = resolve2(start2);
|
|
1413
|
+
for (; ; ) {
|
|
1414
|
+
try {
|
|
1415
|
+
await stat(join7(dir2, ".git"));
|
|
1416
|
+
return dir2;
|
|
1417
|
+
} catch {
|
|
1418
|
+
}
|
|
1419
|
+
const up = dirname2(dir2);
|
|
1420
|
+
if (up === dir2) return null;
|
|
1421
|
+
dir2 = up;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// src/core/projects/root.ts
|
|
1426
|
+
var NOT_WRITABLE = /* @__PURE__ */ new Set(["EACCES", "EPERM", "EROFS"]);
|
|
1427
|
+
function expandPath(deps2, input2) {
|
|
1428
|
+
const s = input2.trim();
|
|
1429
|
+
if (s === "~") return deps2.home;
|
|
1430
|
+
if (s.startsWith("~/") || s.startsWith("~\\")) return trimSeparator(join8(deps2.home, s.slice(2)));
|
|
1431
|
+
return resolve3(deps2.cwd, s);
|
|
1432
|
+
}
|
|
1433
|
+
function trimSeparator(p) {
|
|
1434
|
+
const root = parse(p).root;
|
|
1435
|
+
let end = p.length;
|
|
1436
|
+
while (end > root.length && (p[end - 1] === "/" || p[end - 1] === "\\")) end--;
|
|
1437
|
+
return p.slice(0, end);
|
|
1438
|
+
}
|
|
1439
|
+
async function underAFile(path) {
|
|
1440
|
+
for (let p = dirname3(path); ; p = dirname3(p)) {
|
|
1441
|
+
const s = await stat2(p).catch((e) => e.code === "ENOENT" ? void 0 : null);
|
|
1442
|
+
if (s === null) return false;
|
|
1443
|
+
if (s) return !s.isDirectory();
|
|
1444
|
+
if (dirname3(p) === p) return false;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
function isInside(parent, child) {
|
|
1448
|
+
const rel = relative2(parent, child);
|
|
1449
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1450
|
+
}
|
|
1451
|
+
function scannedBy(root, e) {
|
|
1452
|
+
if (e.kind !== "workspace" || !isInside(root, e.path)) return false;
|
|
1453
|
+
return relative2(root, e.path).split(sep).filter(Boolean).length === 2;
|
|
1454
|
+
}
|
|
1455
|
+
async function writableError(path) {
|
|
1456
|
+
const probe = join8(path, `.qore-write-test-${randomBytes3(6).toString("hex")}`);
|
|
1457
|
+
try {
|
|
1458
|
+
await writeFile3(probe, "");
|
|
1459
|
+
await rm2(probe, { force: true });
|
|
1460
|
+
return null;
|
|
1461
|
+
} catch (e) {
|
|
1462
|
+
return e;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
var invalid2 = (path, why) => new QoreError("QORE_ROOT_INVALID", `${path} cannot be the projects root: ${why}`, { hint: "Pick another folder: qore init --root <path>." });
|
|
1466
|
+
var notWritable = (path, e) => new QoreError("QORE_ROOT_NOT_WRITABLE", `${path} is not writable (${e.code ?? e.message})`, {
|
|
1467
|
+
hint: "Fix its permissions, or pick another folder: qore init --root <path>.",
|
|
1468
|
+
cause: e
|
|
1469
|
+
});
|
|
1470
|
+
async function checkRoot(deps2, input2) {
|
|
1471
|
+
const path = expandPath(deps2, input2);
|
|
1472
|
+
if (dirname3(path) === path) throw invalid2(path, "it is the filesystem root");
|
|
1473
|
+
try {
|
|
1474
|
+
if (!(await stat2(path)).isDirectory()) throw invalid2(path, "it is a file");
|
|
1475
|
+
} catch (e) {
|
|
1476
|
+
if (e instanceof QoreError) throw e;
|
|
1477
|
+
const err = e;
|
|
1478
|
+
if (err.code === "ENOTDIR") throw invalid2(path, "a parent of it is a file");
|
|
1479
|
+
if (NOT_WRITABLE.has(err.code ?? "")) throw notWritable(path, err);
|
|
1480
|
+
if (err.code !== "ENOENT") throw e;
|
|
1481
|
+
if (await underAFile(path)) throw invalid2(path, "a parent of it is a file");
|
|
1482
|
+
}
|
|
1483
|
+
const repo = await findRepoRoot(path);
|
|
1484
|
+
if (await findWorkspace(path, deps2.home) || repo && !isInside(repo, deps2.home)) {
|
|
1485
|
+
throw invalid2(path, "it lies inside a project (an app workspace or a git repository)");
|
|
1486
|
+
}
|
|
1487
|
+
return path;
|
|
1488
|
+
}
|
|
1489
|
+
async function createRoot(path) {
|
|
1490
|
+
try {
|
|
1491
|
+
await mkdir3(path, { recursive: true });
|
|
1492
|
+
} catch (e) {
|
|
1493
|
+
const err2 = e;
|
|
1494
|
+
if (NOT_WRITABLE.has(err2.code ?? "")) throw notWritable(path, err2);
|
|
1495
|
+
if (err2.code === "ENOTDIR" || err2.code === "EEXIST") throw invalid2(path, "a parent of it is a file");
|
|
1496
|
+
throw e;
|
|
1497
|
+
}
|
|
1498
|
+
const err = await writableError(path);
|
|
1499
|
+
if (err) throw notWritable(path, err);
|
|
1500
|
+
return realpath3(path);
|
|
1501
|
+
}
|
|
1502
|
+
async function validateRoot(deps2, input2) {
|
|
1503
|
+
return createRoot(await checkRoot(deps2, input2));
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// src/core/projects/registry.ts
|
|
1507
|
+
var LOCK = "projects";
|
|
1508
|
+
var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
1509
|
+
async function quietly(fn) {
|
|
1510
|
+
try {
|
|
1511
|
+
await fn();
|
|
1512
|
+
} catch (e) {
|
|
1513
|
+
if (e instanceof CancelledError) throw e;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
async function remember(deps2, e) {
|
|
1517
|
+
await quietly(async () => {
|
|
1518
|
+
const path = await realpath4(e.path);
|
|
1519
|
+
const entry = { ...e, path };
|
|
1520
|
+
const cfg = await deps2.config.read();
|
|
1521
|
+
const root = cfg.projectsRoot ? await realpath4(cfg.projectsRoot).catch(() => cfg.projectsRoot) : void 0;
|
|
1522
|
+
if (root && scannedBy(root, entry)) return;
|
|
1523
|
+
const current = cfg.projects?.find((p) => p.path === path);
|
|
1524
|
+
if (current && same(current, entry)) return;
|
|
1525
|
+
await deps2.withLock(LOCK, () => deps2.config.update((c) => {
|
|
1526
|
+
c.projects = [...(c.projects ?? []).filter((p) => p.path !== path), entry];
|
|
1527
|
+
}).then(() => void 0));
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
async function forget(deps2, paths) {
|
|
1531
|
+
const drop = new Set(paths);
|
|
1532
|
+
await quietly(() => deps2.withLock(LOCK, () => deps2.config.update((c) => {
|
|
1533
|
+
if (c.projects) c.projects = c.projects.filter((p) => !drop.has(p.path));
|
|
1534
|
+
}).then(() => void 0)));
|
|
1535
|
+
}
|
|
1536
|
+
async function setRoot(deps2, next) {
|
|
1537
|
+
let registered = 0;
|
|
1538
|
+
await deps2.withLock(LOCK, async () => {
|
|
1539
|
+
const old = (await deps2.config.read()).projectsRoot;
|
|
1540
|
+
const carried = old && old !== next ? await scanRoot(old) : [];
|
|
1541
|
+
const keep = carried.filter((p) => !next || !scannedBy(next, p));
|
|
1542
|
+
registered = keep.length;
|
|
1543
|
+
await deps2.config.update((c) => {
|
|
1544
|
+
c.projectsRoot = next;
|
|
1545
|
+
if (next === void 0) delete c.projectsRoot;
|
|
1546
|
+
const rest = (c.projects ?? []).filter((p) => !keep.some((k) => k.path === p.path));
|
|
1547
|
+
if (rest.length + keep.length > 0) c.projects = [...rest, ...keep];
|
|
1548
|
+
});
|
|
1549
|
+
});
|
|
1550
|
+
return { registered };
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1300
1553
|
// src/core/workspace/safety.ts
|
|
1301
1554
|
var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/;
|
|
1302
1555
|
var WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/;
|
|
@@ -1347,9 +1600,9 @@ function firstSentenceLower(hint) {
|
|
|
1347
1600
|
return sentence.length ? sentence[0].toLowerCase() + sentence.slice(1) : sentence;
|
|
1348
1601
|
}
|
|
1349
1602
|
function failedLine(r) {
|
|
1350
|
-
const
|
|
1603
|
+
const label3 = FAILURE_LABELS[r.error?.code ?? ""] ?? "failed";
|
|
1351
1604
|
const detail2 = firstSentenceLower(r.error?.hint);
|
|
1352
|
-
return `\u2717 ${r.slug} ${
|
|
1605
|
+
return `\u2717 ${r.slug} ${label3}${detail2 ? ` \u2014 ${detail2}` : ""}`;
|
|
1353
1606
|
}
|
|
1354
1607
|
function line(r, conflictCount) {
|
|
1355
1608
|
switch (r.status) {
|
|
@@ -1448,85 +1701,164 @@ async function pickApp(deps2, io2, session, tenantFlag) {
|
|
|
1448
1701
|
});
|
|
1449
1702
|
return { tenant: tenant.slug, app };
|
|
1450
1703
|
}
|
|
1704
|
+
async function cloneApp(ctx, appArg, dirArg, flags) {
|
|
1705
|
+
const { deps: deps2, io: io2 } = ctx;
|
|
1706
|
+
const g = ctx.opts();
|
|
1707
|
+
const cfg = await deps2.config.read();
|
|
1708
|
+
const portal = resolvePortal(g.portal, cfg.defaultProfile);
|
|
1709
|
+
const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
|
|
1710
|
+
const session = await openSession(deps2, portal, "clone", onRequest);
|
|
1711
|
+
await ensureCapability(deps2, portal.apiOrigin, "app.modules");
|
|
1712
|
+
const picked = appArg === void 0 ? await pickApp(deps2, io2, session, flags.tenant) : void 0;
|
|
1713
|
+
const appSlug = picked?.app ?? appArg;
|
|
1714
|
+
const ref = await resolveApp(deps2, io2, session, appSlug, picked?.tenant ?? flags.tenant);
|
|
1715
|
+
const app = await session.api.get(`/api/cli/v1/apps/${encodeURIComponent(ref.appId)}/modules`);
|
|
1716
|
+
const projectsRoot = cfg.projectsRoot;
|
|
1717
|
+
if (dirArg === void 0 && !isSafeSlug(app.app.slug)) throw unsafeSlugError(app.app.slug);
|
|
1718
|
+
if (dirArg === void 0 && projectsRoot && !isSafeSlug(app.app.tenant.slug)) throw unsafeSlugError(app.app.tenant.slug);
|
|
1719
|
+
const root = dirArg !== void 0 ? resolve4(deps2.cwd, dirArg) : projectsRoot ? join9(projectsRoot, app.app.tenant.slug, app.app.slug) : resolve4(deps2.cwd, app.app.slug);
|
|
1720
|
+
const dirLabel = dirArg ?? (projectsRoot ? root : app.app.slug);
|
|
1721
|
+
const info = await deps2.git.inspect(root);
|
|
1722
|
+
if (info.exists && !info.empty) {
|
|
1723
|
+
const ws = await readWorkspace(root);
|
|
1724
|
+
if (!ws || ws.appId !== app.app.id || ws.portal !== portal.apiOrigin) {
|
|
1725
|
+
throw new QoreError("QORE_WORKSPACE_CONFLICT", `${root} exists and is not the workspace of ${app.app.slug}`, {
|
|
1726
|
+
hint: `Pick another directory: qore clone ${appSlug} <dir>.`
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
await writeWorkspace(root, { portal: portal.apiOrigin, appId: app.app.id });
|
|
1731
|
+
await remember(deps2, {
|
|
1732
|
+
kind: "workspace",
|
|
1733
|
+
path: root,
|
|
1734
|
+
portal: portal.apiOrigin,
|
|
1735
|
+
tenantSlug: app.app.tenant.slug,
|
|
1736
|
+
appId: app.app.id,
|
|
1737
|
+
appSlug: app.app.slug
|
|
1738
|
+
});
|
|
1739
|
+
const dirs = {};
|
|
1740
|
+
const modulesForPlan = [];
|
|
1741
|
+
const earlyFailures = [];
|
|
1742
|
+
for (const m of app.modules) {
|
|
1743
|
+
if (!isSafeSlug(m.slug)) {
|
|
1744
|
+
earlyFailures.push(asFailure(m.slug, unsafeSlugError(m.slug)));
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
try {
|
|
1748
|
+
dirs[m.slug] = await deps2.git.inspect(join9(root, m.slug));
|
|
1749
|
+
modulesForPlan.push(m);
|
|
1750
|
+
} catch (e) {
|
|
1751
|
+
earlyFailures.push(asFailure(m.slug, e));
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
const { actions } = planWorkspace(root, modulesForPlan, dirs, []);
|
|
1755
|
+
const applied = await applyActions(deps2, { portal, app, actions, protocol: flags.protocol, onResult: () => {
|
|
1756
|
+
} });
|
|
1757
|
+
for (const r of applied) {
|
|
1758
|
+
if (r.status === "cloned" && r.dir && isInside(deps2.cwd, r.dir)) r.dir = relative3(deps2.cwd, r.dir) || ".";
|
|
1759
|
+
}
|
|
1760
|
+
const results = [...earlyFailures, ...applied];
|
|
1761
|
+
const err = reportWorkspaceRun(io2, results, Boolean(g.json));
|
|
1762
|
+
if (!g.json) {
|
|
1763
|
+
io2.out(`\u2713 Wrote ${join9(dirLabel, "qore.workspace.json")}`);
|
|
1764
|
+
const first = results.find((r) => r.status === "cloned" || r.status === "present");
|
|
1765
|
+
if (first && !err) io2.out(`Next: cd ${join9(dirLabel, first.slug)} && qore env pull`);
|
|
1766
|
+
}
|
|
1767
|
+
if (!g.json && dirArg === void 0 && !projectsRoot) io2.err("Tip: run `qore init` to always clone into one projects folder.");
|
|
1768
|
+
if (err) throw err;
|
|
1769
|
+
return { root, appSlug: app.app.slug };
|
|
1770
|
+
}
|
|
1451
1771
|
function registerClone(program, ctx) {
|
|
1452
1772
|
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) => {
|
|
1773
|
+
await cloneApp(ctx, appArg, dirArg, flags);
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/core/telemetry/consent.ts
|
|
1778
|
+
var ON = /* @__PURE__ */ new Set(["1", "true", "on", "yes"]);
|
|
1779
|
+
var DO_NOT_TRACK_EXEMPT = /* @__PURE__ */ new Set(["0", "false"]);
|
|
1780
|
+
var norm = (v) => (v ?? "").trim().toLowerCase();
|
|
1781
|
+
function resolveConsent(env, cfg) {
|
|
1782
|
+
const dnt = norm(env.DO_NOT_TRACK);
|
|
1783
|
+
if (dnt !== "" && !DO_NOT_TRACK_EXEMPT.has(dnt)) return { enabled: false, source: "DO_NOT_TRACK" };
|
|
1784
|
+
const q = norm(env.QORE_TELEMETRY);
|
|
1785
|
+
if (q !== "") return { enabled: ON.has(q), source: "QORE_TELEMETRY" };
|
|
1786
|
+
if (typeof cfg.telemetry?.enabled === "boolean") return { enabled: cfg.telemetry.enabled, source: "config" };
|
|
1787
|
+
return { enabled: true, source: "default" };
|
|
1788
|
+
}
|
|
1789
|
+
function needsNotice(cfg, consent) {
|
|
1790
|
+
return consent.enabled && consent.source === "default" && !cfg.telemetry?.noticeShownAt;
|
|
1791
|
+
}
|
|
1792
|
+
function describeConsent(consent, built) {
|
|
1793
|
+
const state = `crash reports ${consent.enabled ? "on" : "off"} (${consent.source})`;
|
|
1794
|
+
return consent.enabled && !built ? `${state} \xB7 this build has no DSN, nothing is sent` : state;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// src/cli/commands/config.ts
|
|
1798
|
+
async function describeRoot(deps2) {
|
|
1799
|
+
const known = await knownProjects(deps2);
|
|
1800
|
+
return {
|
|
1801
|
+
root: (await deps2.config.read()).projectsRoot ?? null,
|
|
1802
|
+
projects: { inRoot: known.filter((p) => p.source === "root").length, registered: known.filter((p) => p.source === "registry").length }
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
function registerConfig(program, ctx) {
|
|
1806
|
+
const config = program.command("config").description("Change qore's own settings");
|
|
1807
|
+
config.command("telemetry").description("Show or set crash reporting").argument("[state]", "on or off").action(async (state) => {
|
|
1453
1808
|
const { deps: deps2, io: io2 } = ctx;
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
const portal = resolvePortal(g.portal, cfg.defaultProfile);
|
|
1457
|
-
const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
|
|
1458
|
-
const session = await openSession(deps2, portal, "clone", onRequest);
|
|
1459
|
-
await ensureCapability(deps2, portal.apiOrigin, "app.modules");
|
|
1460
|
-
const picked = appArg === void 0 ? await pickApp(deps2, io2, session, flags.tenant) : void 0;
|
|
1461
|
-
const appSlug = picked?.app ?? appArg;
|
|
1462
|
-
const ref = await resolveApp(deps2, io2, session, appSlug, picked?.tenant ?? flags.tenant);
|
|
1463
|
-
const app = await session.api.get(`/api/cli/v1/apps/${encodeURIComponent(ref.appId)}/modules`);
|
|
1464
|
-
if (dirArg === void 0 && !isSafeSlug(app.app.slug)) throw unsafeSlugError(app.app.slug);
|
|
1465
|
-
const dirLabel = dirArg ?? app.app.slug;
|
|
1466
|
-
const root = resolve2(deps2.cwd, dirLabel);
|
|
1467
|
-
const info = await deps2.git.inspect(root);
|
|
1468
|
-
if (info.exists && !info.empty) {
|
|
1469
|
-
const ws = await readWorkspace(root);
|
|
1470
|
-
if (!ws || ws.appId !== app.app.id || ws.portal !== portal.apiOrigin) {
|
|
1471
|
-
throw new QoreError("QORE_WORKSPACE_CONFLICT", `${root} exists and is not the workspace of ${app.app.slug}`, {
|
|
1472
|
-
hint: `Pick another directory: qore clone ${appSlug} <dir>.`
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1809
|
+
if (state !== void 0 && state !== "on" && state !== "off") {
|
|
1810
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `Unknown telemetry state: ${state}`, { hint: "Use: qore config telemetry on|off" });
|
|
1475
1811
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
}
|
|
1485
|
-
try {
|
|
1486
|
-
dirs[m.slug] = await deps2.git.inspect(join6(root, m.slug));
|
|
1487
|
-
modulesForPlan.push(m);
|
|
1488
|
-
} catch (e) {
|
|
1489
|
-
earlyFailures.push(asFailure(m.slug, e));
|
|
1490
|
-
}
|
|
1812
|
+
if (state) await deps2.config.update((c) => {
|
|
1813
|
+
c.telemetry = { ...c.telemetry, enabled: state === "on" };
|
|
1814
|
+
});
|
|
1815
|
+
const consent = resolveConsent(deps2.env, await deps2.config.read());
|
|
1816
|
+
const built = deps2.sentryDsn !== "";
|
|
1817
|
+
if (ctx.opts().json) {
|
|
1818
|
+
renderJson(io2, { telemetry: { ...consent, built } });
|
|
1819
|
+
return;
|
|
1491
1820
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1821
|
+
io2.out(describeConsent(consent, built));
|
|
1822
|
+
if (state && consent.source !== "config") io2.err(`${consent.source} in your environment overrides this setting.`);
|
|
1823
|
+
});
|
|
1824
|
+
config.command("root").description("Show or set the projects folder qore clones into (see qore init)").argument("[path]", "the new projects folder").option("--unset", "clear it; projects already there stay findable").action(async (path, f) => {
|
|
1825
|
+
const { deps: deps2, io: io2 } = ctx;
|
|
1826
|
+
if (path !== void 0 && f.unset) {
|
|
1827
|
+
throw new QoreError("QORE_CONTEXT_INVALID", "Pass a path or --unset, not both", { hint: "qore config root <path> | qore config root --unset" });
|
|
1497
1828
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1829
|
+
if (path !== void 0) await setRoot(deps2, await validateRoot(deps2, path));
|
|
1830
|
+
if (f.unset) await setRoot(deps2, void 0);
|
|
1831
|
+
const d = await describeRoot(deps2);
|
|
1832
|
+
if (ctx.opts().json) {
|
|
1833
|
+
renderJson(io2, d);
|
|
1834
|
+
return;
|
|
1504
1835
|
}
|
|
1505
|
-
|
|
1836
|
+
io2.out(d.root ? `Projects root: ${d.root}` : "Projects root: not set \u2014 run qore init");
|
|
1837
|
+
io2.out(`Known projects: ${d.projects.inRoot} in the root, ${d.projects.registered} elsewhere`);
|
|
1506
1838
|
});
|
|
1507
1839
|
}
|
|
1508
1840
|
|
|
1509
1841
|
// src/cli/commands/dev.ts
|
|
1510
|
-
import { randomBytes as
|
|
1511
|
-
import { realpath as
|
|
1842
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
1843
|
+
import { realpath as realpath8 } from "fs/promises";
|
|
1512
1844
|
import { constants as constants3 } from "os";
|
|
1513
|
-
import { dirname as
|
|
1845
|
+
import { dirname as dirname7, join as join21, relative as relative7 } from "path";
|
|
1514
1846
|
import { StringDecoder } from "string_decoder";
|
|
1515
1847
|
|
|
1516
1848
|
// src/core/context/repoConfig.ts
|
|
1517
1849
|
import { readFile as readFile3 } from "fs/promises";
|
|
1518
|
-
import { join as
|
|
1850
|
+
import { join as join10 } from "path";
|
|
1519
1851
|
var FORBIDDEN = ["portal", "api", "apiOrigin", "issuer", "tenant", "tenantId", "app", "appId", "module", "moduleId"];
|
|
1520
1852
|
var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1521
1853
|
var SCRIPT = /^[A-Za-z0-9:_.-]+$/;
|
|
1522
|
-
var
|
|
1854
|
+
var invalid3 = (msg, details) => new QoreError("QORE_CONTEXT_INVALID", `.qore.json: ${msg}`, { hint: "Fix .qore.json in the repository root; see the $schema it names.", details });
|
|
1523
1855
|
function moduleDefaults(type) {
|
|
1524
1856
|
return type === "frontend" ? { envFile: ".env.local", localSecrets: ["SESSION_COOKIE_SECRET"] } : { envFile: ".env", localSecrets: [] };
|
|
1525
1857
|
}
|
|
1526
1858
|
async function readRepoConfig(root) {
|
|
1527
1859
|
let raw;
|
|
1528
1860
|
try {
|
|
1529
|
-
raw = await readFile3(
|
|
1861
|
+
raw = await readFile3(join10(root, ".qore.json"), "utf8");
|
|
1530
1862
|
} catch (e) {
|
|
1531
1863
|
if (e.code === "ENOENT") return {};
|
|
1532
1864
|
throw e;
|
|
@@ -1535,44 +1867,44 @@ async function readRepoConfig(root) {
|
|
|
1535
1867
|
try {
|
|
1536
1868
|
doc = JSON.parse(raw);
|
|
1537
1869
|
} catch {
|
|
1538
|
-
throw
|
|
1870
|
+
throw invalid3("not valid JSON");
|
|
1539
1871
|
}
|
|
1540
|
-
if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw
|
|
1872
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw invalid3("must be an object");
|
|
1541
1873
|
const v = doc.schemaVersion === void 0 ? 1 : doc.schemaVersion;
|
|
1542
|
-
if (!Number.isInteger(v) || v < 1) throw
|
|
1874
|
+
if (!Number.isInteger(v) || v < 1) throw invalid3("schemaVersion must be a positive integer");
|
|
1543
1875
|
if (v > 1) {
|
|
1544
1876
|
throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `.qore.json schemaVersion ${v} is newer than this qore`, { hint: "Run: npm i -g @quatronic/qore" });
|
|
1545
1877
|
}
|
|
1546
|
-
for (const f of FORBIDDEN) if (f in doc) throw
|
|
1878
|
+
for (const f of FORBIDDEN) if (f in doc) throw invalid3(`"${f}" is not allowed \u2014 .qore.json never selects a portal or a resource`, { field: f });
|
|
1547
1879
|
const out = {};
|
|
1548
1880
|
if (doc.envFile !== void 0) {
|
|
1549
|
-
if (typeof doc.envFile !== "string" || !doc.envFile) throw
|
|
1881
|
+
if (typeof doc.envFile !== "string" || !doc.envFile) throw invalid3("envFile must be a non-empty string");
|
|
1550
1882
|
out.envFile = doc.envFile;
|
|
1551
1883
|
}
|
|
1552
1884
|
if (doc.localSecrets !== void 0) {
|
|
1553
1885
|
if (!Array.isArray(doc.localSecrets) || !doc.localSecrets.every((k) => typeof k === "string" && KEY.test(k))) {
|
|
1554
|
-
throw
|
|
1886
|
+
throw invalid3("localSecrets must be a list of env key names");
|
|
1555
1887
|
}
|
|
1556
|
-
if (new Set(doc.localSecrets).size !== doc.localSecrets.length) throw
|
|
1888
|
+
if (new Set(doc.localSecrets).size !== doc.localSecrets.length) throw invalid3("localSecrets must not contain duplicates");
|
|
1557
1889
|
out.localSecrets = doc.localSecrets;
|
|
1558
1890
|
}
|
|
1559
1891
|
if (doc.dev !== void 0) {
|
|
1560
1892
|
const d = doc.dev;
|
|
1561
|
-
if (!d || typeof d !== "object" || Array.isArray(d)) throw
|
|
1893
|
+
if (!d || typeof d !== "object" || Array.isArray(d)) throw invalid3("dev must be an object");
|
|
1562
1894
|
const dev = {};
|
|
1563
1895
|
if (d.install !== void 0) {
|
|
1564
|
-
if (typeof d.install !== "boolean") throw
|
|
1896
|
+
if (typeof d.install !== "boolean") throw invalid3("dev.install must be true or false");
|
|
1565
1897
|
dev.install = d.install;
|
|
1566
1898
|
}
|
|
1567
1899
|
if (d.services !== void 0) {
|
|
1568
1900
|
if (!Array.isArray(d.services) || !d.services.every((s) => typeof s === "string" && SCRIPT.test(s))) {
|
|
1569
|
-
throw
|
|
1901
|
+
throw invalid3("dev.services must be a list of script names");
|
|
1570
1902
|
}
|
|
1571
|
-
if (new Set(d.services).size !== d.services.length) throw
|
|
1903
|
+
if (new Set(d.services).size !== d.services.length) throw invalid3("dev.services must not contain duplicates");
|
|
1572
1904
|
dev.services = d.services;
|
|
1573
1905
|
}
|
|
1574
1906
|
if (d.script !== void 0) {
|
|
1575
|
-
if (typeof d.script !== "string" || !SCRIPT.test(d.script)) throw
|
|
1907
|
+
if (typeof d.script !== "string" || !SCRIPT.test(d.script)) throw invalid3("dev.script must be a script name");
|
|
1576
1908
|
dev.script = d.script;
|
|
1577
1909
|
}
|
|
1578
1910
|
out.dev = dev;
|
|
@@ -1580,25 +1912,8 @@ async function readRepoConfig(root) {
|
|
|
1580
1912
|
return out;
|
|
1581
1913
|
}
|
|
1582
1914
|
|
|
1583
|
-
// src/core/context/repoRoot.ts
|
|
1584
|
-
import { stat } from "fs/promises";
|
|
1585
|
-
import { dirname as dirname2, join as join8, resolve as resolve3 } from "path";
|
|
1586
|
-
async function findRepoRoot(start) {
|
|
1587
|
-
let dir2 = resolve3(start);
|
|
1588
|
-
for (; ; ) {
|
|
1589
|
-
try {
|
|
1590
|
-
await stat(join8(dir2, ".git"));
|
|
1591
|
-
return dir2;
|
|
1592
|
-
} catch {
|
|
1593
|
-
}
|
|
1594
|
-
const up = dirname2(dir2);
|
|
1595
|
-
if (up === dir2) return null;
|
|
1596
|
-
dir2 = up;
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
1915
|
// src/core/context/resolver.ts
|
|
1601
|
-
import { isAbsolute, join as
|
|
1916
|
+
import { isAbsolute as isAbsolute2, join as join11, relative as relative4 } from "path";
|
|
1602
1917
|
var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1603
1918
|
var HINT_FLAG = "cd into a module repository, or pass --module <slug|id>.";
|
|
1604
1919
|
var RESOLVE = "/api/cli/v1/repos/resolve";
|
|
@@ -1706,8 +2021,8 @@ async function byFlag(deps2, o, cache, ws) {
|
|
|
1706
2021
|
return { portal, session, module, source: overrides ? { kind: "flag", overrides } : { kind: "flag" }, repoRoot, workspace: ws };
|
|
1707
2022
|
}
|
|
1708
2023
|
var isBelow = (root, dir2) => {
|
|
1709
|
-
const rel =
|
|
1710
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
2024
|
+
const rel = relative4(root, dir2);
|
|
2025
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
|
|
1711
2026
|
};
|
|
1712
2027
|
async function inWorkspaceRoot(deps2, ws) {
|
|
1713
2028
|
if (!ws) return false;
|
|
@@ -1811,7 +2126,7 @@ async function anyLoggedIn(deps2, cfg) {
|
|
|
1811
2126
|
return false;
|
|
1812
2127
|
}
|
|
1813
2128
|
async function resolveModuleContext(deps2, o) {
|
|
1814
|
-
const cache = new ResolutionCache(
|
|
2129
|
+
const cache = new ResolutionCache(join11(deps2.config.dir, "cache"), deps2.now);
|
|
1815
2130
|
const ws = await findWorkspace(deps2.cwd, deps2.home);
|
|
1816
2131
|
if (o.module) return byFlag(deps2, o, cache, ws);
|
|
1817
2132
|
const cfg = await deps2.config.read();
|
|
@@ -1962,15 +2277,17 @@ function exitHint(tail, h) {
|
|
|
1962
2277
|
}
|
|
1963
2278
|
|
|
1964
2279
|
// src/core/dev/env.ts
|
|
1965
|
-
import { dirname as
|
|
2280
|
+
import { dirname as dirname6, join as join14 } from "path";
|
|
1966
2281
|
|
|
1967
2282
|
// src/core/env/fetch.ts
|
|
1968
2283
|
async function fetchLocalEnv(deps2, mc, app) {
|
|
1969
2284
|
await ensureCapability(deps2, mc.portal.apiOrigin, "module.local-env");
|
|
1970
2285
|
try {
|
|
1971
|
-
|
|
2286
|
+
const local = await mc.session.api.get(
|
|
1972
2287
|
`/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/local-env?app_id=${encodeURIComponent(app.id)}`
|
|
1973
2288
|
);
|
|
2289
|
+
deps2.registerSecrets(Object.values(local.env));
|
|
2290
|
+
return local;
|
|
1974
2291
|
} catch (e) {
|
|
1975
2292
|
if (!(e instanceof QoreError) || e.code !== "QORE_ACCESS_DENIED") throw e;
|
|
1976
2293
|
if (e.serverCode === "app.access.denied") {
|
|
@@ -2062,8 +2379,8 @@ function renderEnv(original, merged) {
|
|
|
2062
2379
|
}
|
|
2063
2380
|
|
|
2064
2381
|
// src/core/env/pull.ts
|
|
2065
|
-
import { lstat, realpath as
|
|
2066
|
-
import { basename, dirname as
|
|
2382
|
+
import { lstat, realpath as realpath5 } from "fs/promises";
|
|
2383
|
+
import { basename, dirname as dirname4, isAbsolute as isAbsolute3, relative as relative5, resolve as resolve5, sep as sep2 } from "path";
|
|
2067
2384
|
var outside = (target) => new QoreError("QORE_CONTEXT_INVALID", `The env file must be inside the repository (got ${target})`, {
|
|
2068
2385
|
hint: "Use a path inside the repository for --target or .qore.json envFile."
|
|
2069
2386
|
});
|
|
@@ -2071,29 +2388,29 @@ var symlinked = (target) => new QoreError("QORE_CONTEXT_INVALID", `The env file
|
|
|
2071
2388
|
hint: "Use a path inside the repository for --target or .qore.json envFile."
|
|
2072
2389
|
});
|
|
2073
2390
|
function inside(root, p) {
|
|
2074
|
-
const rel =
|
|
2075
|
-
return rel !== "" && rel !== ".." && !rel.startsWith(".." +
|
|
2391
|
+
const rel = relative5(root, p);
|
|
2392
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(".." + sep2) && !isAbsolute3(rel);
|
|
2076
2393
|
}
|
|
2077
2394
|
async function resolveTarget(root, target, base = root) {
|
|
2078
|
-
const realRoot = await
|
|
2079
|
-
const realBase = base === root ? realRoot : await
|
|
2080
|
-
const abs =
|
|
2081
|
-
if (!
|
|
2395
|
+
const realRoot = await realpath5(root);
|
|
2396
|
+
const realBase = base === root ? realRoot : await realpath5(base);
|
|
2397
|
+
const abs = resolve5(realBase, target);
|
|
2398
|
+
if (!isAbsolute3(target) && !inside(realRoot, abs)) throw outside(target);
|
|
2082
2399
|
let parent;
|
|
2083
2400
|
try {
|
|
2084
|
-
parent = await
|
|
2401
|
+
parent = await realpath5(dirname4(abs));
|
|
2085
2402
|
} catch (e) {
|
|
2086
2403
|
if (e.code !== "ENOENT") throw e;
|
|
2087
2404
|
throw new QoreError("QORE_CONTEXT_INVALID", `The directory for ${target} does not exist`, {
|
|
2088
2405
|
hint: "Create the parent directory, or point --target / .qore.json envFile at an existing one."
|
|
2089
2406
|
});
|
|
2090
2407
|
}
|
|
2091
|
-
const final =
|
|
2408
|
+
const final = resolve5(parent, basename(abs));
|
|
2092
2409
|
if (!inside(realRoot, final)) throw outside(target);
|
|
2093
2410
|
try {
|
|
2094
2411
|
const st = await lstat(final);
|
|
2095
2412
|
if (st.isSymbolicLink()) throw symlinked(target);
|
|
2096
|
-
const realFinal = await
|
|
2413
|
+
const realFinal = await realpath5(final);
|
|
2097
2414
|
if (!inside(realRoot, realFinal)) throw outside(target);
|
|
2098
2415
|
} catch (e) {
|
|
2099
2416
|
if (e instanceof QoreError) throw e;
|
|
@@ -2129,9 +2446,9 @@ function planPull(o) {
|
|
|
2129
2446
|
}
|
|
2130
2447
|
|
|
2131
2448
|
// src/core/env/write.ts
|
|
2132
|
-
import { chmod as chmod2, readFile as readFile4, rename as rename3, unlink, writeFile as
|
|
2133
|
-
import { randomBytes as
|
|
2134
|
-
import { basename as basename2, dirname as
|
|
2449
|
+
import { chmod as chmod2, readFile as readFile4, rename as rename3, unlink, writeFile as writeFile4 } from "fs/promises";
|
|
2450
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2451
|
+
import { basename as basename2, dirname as dirname5, join as join12 } from "path";
|
|
2135
2452
|
async function readOrNull(p) {
|
|
2136
2453
|
try {
|
|
2137
2454
|
return await readFile4(p, "utf8");
|
|
@@ -2142,10 +2459,10 @@ async function readOrNull(p) {
|
|
|
2142
2459
|
}
|
|
2143
2460
|
async function writeSecurely(target, text2) {
|
|
2144
2461
|
const base = basename2(target);
|
|
2145
|
-
const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${
|
|
2146
|
-
const tmp =
|
|
2462
|
+
const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes4(6).toString("hex")}.local` : `${base}.qore-${randomBytes4(6).toString("hex")}.tmp`;
|
|
2463
|
+
const tmp = join12(dirname5(target), tmpName);
|
|
2147
2464
|
try {
|
|
2148
|
-
await
|
|
2465
|
+
await writeFile4(tmp, text2, { mode: 384, flag: "wx" });
|
|
2149
2466
|
await chmod2(tmp, 384);
|
|
2150
2467
|
await rename3(tmp, target);
|
|
2151
2468
|
} finally {
|
|
@@ -2171,8 +2488,8 @@ async function syncEnvFile(o) {
|
|
|
2171
2488
|
|
|
2172
2489
|
// src/core/dev/lastStart.ts
|
|
2173
2490
|
import { createHash as createHash2 } from "crypto";
|
|
2174
|
-
import { realpath as
|
|
2175
|
-
import { join as
|
|
2491
|
+
import { realpath as realpath6 } from "fs/promises";
|
|
2492
|
+
import { join as join13 } from "path";
|
|
2176
2493
|
var NETWORK_REASONS = /* @__PURE__ */ new Set(["connect", "dns", "tls", "timeout"]);
|
|
2177
2494
|
function isOffline(e) {
|
|
2178
2495
|
if (!(e instanceof QoreError)) return false;
|
|
@@ -2181,14 +2498,14 @@ function isOffline(e) {
|
|
|
2181
2498
|
return (e.status ?? 0) >= 500 || NETWORK_REASONS.has(String(e.details?.reason));
|
|
2182
2499
|
}
|
|
2183
2500
|
async function stateKey(deps2, root) {
|
|
2184
|
-
const real = await
|
|
2501
|
+
const real = await realpath6(root).catch(() => root);
|
|
2185
2502
|
return createHash2("sha256").update(trustKey(real, deps2.platform)).digest("hex").slice(0, 16);
|
|
2186
2503
|
}
|
|
2187
2504
|
async function saveLastStart(deps2, root, rec) {
|
|
2188
|
-
await writeJsonAtomic(
|
|
2505
|
+
await writeJsonAtomic(join13(deps2.config.dir, "dev"), `${await stateKey(deps2, root)}.json`, rec);
|
|
2189
2506
|
}
|
|
2190
2507
|
async function loadLastStart(deps2, root) {
|
|
2191
|
-
const rec = await readJson(
|
|
2508
|
+
const rec = await readJson(join13(deps2.config.dir, "dev", `${await stateKey(deps2, root)}.json`)).catch(() => null);
|
|
2192
2509
|
return rec && rec.schemaVersion === 1 ? rec : null;
|
|
2193
2510
|
}
|
|
2194
2511
|
|
|
@@ -2208,7 +2525,7 @@ async function devEnv(deps2, o) {
|
|
|
2208
2525
|
if (o.mc && o.app) {
|
|
2209
2526
|
try {
|
|
2210
2527
|
const local = await fetchLocalEnv(deps2, o.mc, o.app);
|
|
2211
|
-
const example = await readOrNull(
|
|
2528
|
+
const example = await readOrNull(join14(dirname6(target), ".env.example"));
|
|
2212
2529
|
const w = await syncEnvFile({
|
|
2213
2530
|
target,
|
|
2214
2531
|
example,
|
|
@@ -2343,7 +2660,7 @@ function providerDown(o) {
|
|
|
2343
2660
|
|
|
2344
2661
|
// src/core/dev/project.ts
|
|
2345
2662
|
import { access, readFile as readFile5 } from "fs/promises";
|
|
2346
|
-
import { join as
|
|
2663
|
+
import { join as join15 } from "path";
|
|
2347
2664
|
var LEGACY_DEV_SCRIPTS = ["node --env-file=.env --import ./dist/instrumentation.js dist/index.js"];
|
|
2348
2665
|
var LOCKFILES = [["pnpm-lock.yaml", "pnpm"], ["package-lock.json", "npm"], ["yarn.lock", "yarn"]];
|
|
2349
2666
|
var exists = (p) => access(p).then(() => true, () => false);
|
|
@@ -2351,7 +2668,7 @@ var unsupported = (message, hint, details) => new QoreError("QORE_DEV_UNSUPPORTE
|
|
|
2351
2668
|
async function readPackageJson(root) {
|
|
2352
2669
|
let raw;
|
|
2353
2670
|
try {
|
|
2354
|
-
raw = await readFile5(
|
|
2671
|
+
raw = await readFile5(join15(root, "package.json"), "utf8");
|
|
2355
2672
|
} catch {
|
|
2356
2673
|
throw unsupported("This repository has no package.json", "qore dev starts Node repositories through their package.json scripts.");
|
|
2357
2674
|
}
|
|
@@ -2367,7 +2684,7 @@ async function detectPackageManager(root, pkg) {
|
|
|
2367
2684
|
if (!m) throw unsupported(`packageManager "${pkg.packageManager}" is not pnpm, npm or yarn`, "qore dev supports pnpm, npm and yarn.");
|
|
2368
2685
|
return { name: m[1], version: m[2].split("+")[0] };
|
|
2369
2686
|
}
|
|
2370
|
-
for (const [file, name] of LOCKFILES) if (await exists(
|
|
2687
|
+
for (const [file, name] of LOCKFILES) if (await exists(join15(root, file))) return { name, version: null };
|
|
2371
2688
|
throw unsupported("No package manager declared and no lockfile found", 'Add "packageManager": "pnpm@<version>" to package.json.');
|
|
2372
2689
|
}
|
|
2373
2690
|
function planRecipe(o) {
|
|
@@ -2410,21 +2727,21 @@ function installArgs(pm) {
|
|
|
2410
2727
|
}
|
|
2411
2728
|
|
|
2412
2729
|
// src/core/dev/install.ts
|
|
2413
|
-
import { stat as
|
|
2414
|
-
import { join as
|
|
2730
|
+
import { stat as stat3 } from "fs/promises";
|
|
2731
|
+
import { join as join16 } from "path";
|
|
2415
2732
|
var MARKERS = {
|
|
2416
2733
|
pnpm: [".modules.yaml"],
|
|
2417
2734
|
npm: [".package-lock.json"],
|
|
2418
2735
|
yarn: [".yarn-state.yml", ".yarn-integrity"]
|
|
2419
2736
|
};
|
|
2420
|
-
var
|
|
2421
|
-
var mtime = (p) =>
|
|
2737
|
+
var LOCK2 = { pnpm: "pnpm-lock.yaml", npm: "package-lock.json", yarn: "yarn.lock" };
|
|
2738
|
+
var mtime = (p) => stat3(p).then((s) => s.mtimeMs, () => null);
|
|
2422
2739
|
async function needsInstall(root, pm) {
|
|
2423
|
-
if (await mtime(
|
|
2740
|
+
if (await mtime(join16(root, "node_modules")) === null) return true;
|
|
2424
2741
|
let marker = null;
|
|
2425
|
-
for (const m of MARKERS[pm.name]) marker ??= await mtime(
|
|
2742
|
+
for (const m of MARKERS[pm.name]) marker ??= await mtime(join16(root, "node_modules", m));
|
|
2426
2743
|
if (marker === null) return true;
|
|
2427
|
-
const lock = await mtime(
|
|
2744
|
+
const lock = await mtime(join16(root, LOCK2[pm.name]));
|
|
2428
2745
|
return lock !== null && lock > marker;
|
|
2429
2746
|
}
|
|
2430
2747
|
|
|
@@ -2740,11 +3057,11 @@ async function checkPort(deps2, o) {
|
|
|
2740
3057
|
throw await portInUseError(deps2, { slug: o.slug, port: o.port, key: o.key, runs: o.runs });
|
|
2741
3058
|
}
|
|
2742
3059
|
function abortableDelay(signal) {
|
|
2743
|
-
return (ms) => new Promise((
|
|
3060
|
+
return (ms) => new Promise((resolve6) => {
|
|
2744
3061
|
const done = () => {
|
|
2745
3062
|
clearTimeout(t);
|
|
2746
3063
|
signal.removeEventListener("abort", done);
|
|
2747
|
-
|
|
3064
|
+
resolve6();
|
|
2748
3065
|
};
|
|
2749
3066
|
const t = setTimeout(done, ms);
|
|
2750
3067
|
signal.addEventListener("abort", done, { once: true });
|
|
@@ -2827,11 +3144,11 @@ function hintContext(recipe, pkg, env) {
|
|
|
2827
3144
|
|
|
2828
3145
|
// src/core/dev/runFile.ts
|
|
2829
3146
|
import { unlink as unlink2 } from "fs/promises";
|
|
2830
|
-
import { join as
|
|
3147
|
+
import { join as join17 } from "path";
|
|
2831
3148
|
async function runFilePath(deps2, root) {
|
|
2832
|
-
const dir2 =
|
|
3149
|
+
const dir2 = join17(deps2.config.dir, "run");
|
|
2833
3150
|
const name = `${await stateKey(deps2, root)}.json`;
|
|
2834
|
-
return { dir: dir2, name, path:
|
|
3151
|
+
return { dir: dir2, name, path: join17(dir2, name) };
|
|
2835
3152
|
}
|
|
2836
3153
|
async function writeRunFile(deps2, root, rec) {
|
|
2837
3154
|
const { dir: dir2, name } = await runFilePath(deps2, root);
|
|
@@ -2852,6 +3169,177 @@ async function leftoverRun(deps2, root, o = {}) {
|
|
|
2852
3169
|
return null;
|
|
2853
3170
|
}
|
|
2854
3171
|
|
|
3172
|
+
// src/cli/project.ts
|
|
3173
|
+
import { readdir as readdir2, stat as stat4 } from "fs/promises";
|
|
3174
|
+
import { join as join18 } from "path";
|
|
3175
|
+
|
|
3176
|
+
// src/core/projects/match.ts
|
|
3177
|
+
function readings(ref) {
|
|
3178
|
+
const s = ref.split("/").map((x) => x.trim());
|
|
3179
|
+
if (s.some((x) => x === "")) return [];
|
|
3180
|
+
if (s.length === 1) return [{ app: s[0] }];
|
|
3181
|
+
if (s.length === 2) return [{ app: s[0], module: s[1] }, { tenant: s[0], app: s[1] }];
|
|
3182
|
+
if (s.length === 3) return [{ tenant: s[0], app: s[1], module: s[2] }];
|
|
3183
|
+
return [];
|
|
3184
|
+
}
|
|
3185
|
+
function hit(p, w) {
|
|
3186
|
+
if (p.appSlug !== w.app || w.tenant !== void 0 && p.tenantSlug !== w.tenant) return null;
|
|
3187
|
+
if (p.kind === "module") return w.module === p.moduleSlug ? { project: p } : null;
|
|
3188
|
+
return w.module ? { project: p, module: w.module } : { project: p };
|
|
3189
|
+
}
|
|
3190
|
+
function matchProjects(projects, ref) {
|
|
3191
|
+
const out = [];
|
|
3192
|
+
for (const w of readings(ref)) {
|
|
3193
|
+
for (const p of projects) {
|
|
3194
|
+
const m = hit(p, w);
|
|
3195
|
+
if (m) out.push(m);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
return out;
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
// src/cli/project.ts
|
|
3202
|
+
async function loggedInAs(ctx) {
|
|
3203
|
+
const cfg = await ctx.deps.config.read();
|
|
3204
|
+
const portal = resolvePortal(ctx.opts().portal, cfg.defaultProfile);
|
|
3205
|
+
const p = cfg.profiles[portal.apiOrigin];
|
|
3206
|
+
if (!p || !await new TokenStore(ctx.deps.keychain, profileHash(p.apiOrigin, p.issuer)).getRefresh()) return null;
|
|
3207
|
+
return p.email ?? p.sub ?? portal.apiOrigin;
|
|
3208
|
+
}
|
|
3209
|
+
var isDir = async (p) => (await stat4(p).catch(() => null))?.isDirectory() ?? false;
|
|
3210
|
+
var label2 = (p) => `${p.tenantSlug} / ${p.appSlug}${p.kind === "module" ? ` / ${p.moduleSlug}` : ""}${p.source === "registry" ? ` (${p.path})` : ""}`;
|
|
3211
|
+
var candidate = (p) => p.kind === "module" ? { kind: "module", id: p.moduleId, slug: p.moduleSlug, tenant: p.tenantSlug, name: p.path } : { kind: "app", slug: p.appSlug, tenant: p.tenantSlug, name: p.path };
|
|
3212
|
+
var withCwd = (ctx, cwd) => ({ ...ctx, deps: { ...ctx.deps, cwd } });
|
|
3213
|
+
async function inProject(ctx) {
|
|
3214
|
+
if (await findWorkspace(ctx.deps.cwd, ctx.deps.home)) return true;
|
|
3215
|
+
const repo = await findRepoRoot(ctx.deps.cwd);
|
|
3216
|
+
return repo !== null && !isInside(repo, ctx.deps.home);
|
|
3217
|
+
}
|
|
3218
|
+
async function moduleDirs(wsPath) {
|
|
3219
|
+
const out = [];
|
|
3220
|
+
for (const d of await readdir2(wsPath, { withFileTypes: true }).catch(() => [])) {
|
|
3221
|
+
if (d.isDirectory() && !d.name.startsWith(".") && await stat4(join18(wsPath, d.name, ".git")).catch(() => null)) out.push(d.name);
|
|
3222
|
+
}
|
|
3223
|
+
return out.sort();
|
|
3224
|
+
}
|
|
3225
|
+
async function pathFor(ctx, m, needModule) {
|
|
3226
|
+
const p = m.project;
|
|
3227
|
+
if (p.kind === "module") return p.path;
|
|
3228
|
+
if (m.module) {
|
|
3229
|
+
const dir2 = join18(p.path, m.module);
|
|
3230
|
+
if (!await isDir(dir2)) {
|
|
3231
|
+
throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no checkout of module ${m.module} in ${p.path}`, {
|
|
3232
|
+
hint: `Run \`qore sync\` in ${p.path} to clone the app's missing modules.`
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
return dir2;
|
|
3236
|
+
}
|
|
3237
|
+
if (!needModule) return p.path;
|
|
3238
|
+
const dirs = await moduleDirs(p.path);
|
|
3239
|
+
if (dirs.length === 1) return join18(p.path, dirs[0]);
|
|
3240
|
+
if (dirs.length === 0) {
|
|
3241
|
+
throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no module checkouts in ${p.path}`, {
|
|
3242
|
+
hint: `Run \`qore sync\` in ${p.path}.`
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3245
|
+
if (!ctx.io.interactive) {
|
|
3246
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", `${p.appSlug} has several modules \u2014 which one?`, {
|
|
3247
|
+
hint: `Pass ${p.appSlug}/<module>.`,
|
|
3248
|
+
candidates: dirs.map((slug) => ({ kind: "module", slug, tenant: p.tenantSlug, name: join18(p.path, slug) }))
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
return join18(p.path, await ctx.io.select("Select a module", dirs.map((d) => ({ name: d, value: d }))));
|
|
3252
|
+
}
|
|
3253
|
+
async function liveMatches(matches) {
|
|
3254
|
+
const live = [];
|
|
3255
|
+
for (const m of matches) {
|
|
3256
|
+
if (m.project.kind === "workspace" && m.module && !await isDir(join18(m.project.path, m.module))) continue;
|
|
3257
|
+
live.push(m);
|
|
3258
|
+
}
|
|
3259
|
+
return live.length > 0 ? live : matches;
|
|
3260
|
+
}
|
|
3261
|
+
async function choose(ctx, matches, what, explicit) {
|
|
3262
|
+
if (explicit && matches.length === 1) return matches[0];
|
|
3263
|
+
if (!ctx.io.interactive) {
|
|
3264
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", what, {
|
|
3265
|
+
hint: "Pass a more specific reference: tenant/app or app/module.",
|
|
3266
|
+
candidates: matches.map((m) => candidate(m.project))
|
|
3267
|
+
});
|
|
3268
|
+
}
|
|
3269
|
+
const i = await ctx.io.select("Select a project", matches.map((m, idx) => ({
|
|
3270
|
+
name: m.module ? `${label2(m.project)} / ${m.module}` : label2(m.project),
|
|
3271
|
+
value: idx
|
|
3272
|
+
})));
|
|
3273
|
+
return matches[i];
|
|
3274
|
+
}
|
|
3275
|
+
async function projectCtx(ctx, o) {
|
|
3276
|
+
const g = ctx.opts();
|
|
3277
|
+
const cfg = await ctx.deps.config.read();
|
|
3278
|
+
const portal = g.portal ? resolvePortal(g.portal, cfg.defaultProfile).apiOrigin : void 0;
|
|
3279
|
+
if (o.ref) {
|
|
3280
|
+
const matches = matchProjects(await knownProjects(ctx.deps, { portal }), o.ref);
|
|
3281
|
+
if (matches.length === 0) {
|
|
3282
|
+
const parts = o.ref.split("/");
|
|
3283
|
+
const app = parts.length === 1 ? o.ref : parts.length === 3 ? parts[1] : "<app>";
|
|
3284
|
+
throw new QoreError("QORE_PROJECT_NOT_FOUND", `No local project "${o.ref}"`, {
|
|
3285
|
+
hint: cfg.projectsRoot ? `Run \`qore clone ${app}\`.` : `Run \`qore init\`, or \`qore clone ${app}\`.`
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
const m2 = await choose(ctx, await liveMatches(matches), `"${o.ref}" matches several projects`, true);
|
|
3289
|
+
return relocate(ctx, await pathFor(ctx, m2, o.needModule));
|
|
3290
|
+
}
|
|
3291
|
+
if (o.module || o.app || await inProject(ctx)) return ctx;
|
|
3292
|
+
const known = await knownProjects(ctx.deps, { portal });
|
|
3293
|
+
if (known.length === 0) {
|
|
3294
|
+
if (!cfg.projectsRoot) {
|
|
3295
|
+
if (!await loggedInAs(ctx)) return ctx;
|
|
3296
|
+
throw new QoreError("QORE_ROOT_NOT_SET", "Not inside a project, and qore knows no projects yet", {
|
|
3297
|
+
hint: "Run `qore init` to choose a projects folder, or cd into a project."
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
throw new QoreError("QORE_PROJECT_NOT_FOUND", `Not inside a project, and ${cfg.projectsRoot} holds none yet`, {
|
|
3301
|
+
hint: "Run `qore clone <app>`."
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
const m = await choose(ctx, known.map((project) => ({ project })), "Not inside a project \u2014 which one?", false);
|
|
3305
|
+
return relocate(ctx, await pathFor(ctx, m, o.needModule));
|
|
3306
|
+
}
|
|
3307
|
+
function relocate(ctx, path) {
|
|
3308
|
+
if (path === ctx.deps.cwd) return ctx;
|
|
3309
|
+
if (!ctx.opts().json) ctx.io.err(`\u2192 in ${path}`);
|
|
3310
|
+
return withCwd(ctx, path);
|
|
3311
|
+
}
|
|
3312
|
+
async function rememberContext(ctx, mc, app) {
|
|
3313
|
+
const tenantSlug = mc.module.tenant.slug;
|
|
3314
|
+
if (mc.workspace && mc.workspace.appId === app.id) {
|
|
3315
|
+
await remember(ctx.deps, {
|
|
3316
|
+
kind: "workspace",
|
|
3317
|
+
path: mc.workspace.root,
|
|
3318
|
+
portal: mc.workspace.portal,
|
|
3319
|
+
tenantSlug,
|
|
3320
|
+
appId: app.id,
|
|
3321
|
+
appSlug: app.slug
|
|
3322
|
+
});
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
if (mc.repoRoot) {
|
|
3326
|
+
await remember(ctx.deps, {
|
|
3327
|
+
kind: "module",
|
|
3328
|
+
path: mc.repoRoot,
|
|
3329
|
+
portal: mc.portal.apiOrigin,
|
|
3330
|
+
tenantSlug,
|
|
3331
|
+
appId: app.id,
|
|
3332
|
+
appSlug: app.slug,
|
|
3333
|
+
moduleId: mc.module.id,
|
|
3334
|
+
moduleSlug: mc.module.slug
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
async function rememberWorkspace(ctx, ws, app) {
|
|
3339
|
+
if (ws.appId !== app.id) return;
|
|
3340
|
+
await remember(ctx.deps, { kind: "workspace", path: ws.root, portal: ws.portal, tenantSlug: app.tenant.slug, appId: app.id, appSlug: app.slug });
|
|
3341
|
+
}
|
|
3342
|
+
|
|
2855
3343
|
// src/cli/context.ts
|
|
2856
3344
|
var allOfKind = (c, kind) => !!c && c.length > 0 && c.every((x) => x.kind === kind && typeof x.id === "string");
|
|
2857
3345
|
async function moduleContextFor(ctx, flags, command) {
|
|
@@ -2863,11 +3351,11 @@ async function moduleContextFor(ctx, flags, command) {
|
|
|
2863
3351
|
} catch (e) {
|
|
2864
3352
|
if (!(e instanceof QoreError) || e.code !== "QORE_CONTEXT_REQUIRED" || !ctx.io.interactive) throw e;
|
|
2865
3353
|
if (allOfKind(e.candidates, "module")) {
|
|
2866
|
-
const
|
|
3354
|
+
const choose2 = await ctx.io.select(
|
|
2867
3355
|
"Select a module",
|
|
2868
3356
|
e.candidates.map((c) => ({ name: `${c.tenant ? `${c.tenant} / ` : ""}${c.slug} (${c.remote})`, value: c.id }))
|
|
2869
3357
|
);
|
|
2870
|
-
return resolveModuleContext(ctx.deps, { ...base, choose });
|
|
3358
|
+
return resolveModuleContext(ctx.deps, { ...base, choose: choose2 });
|
|
2871
3359
|
}
|
|
2872
3360
|
if (allOfKind(e.candidates, "tenant")) {
|
|
2873
3361
|
const tenant = await ctx.io.select("Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.id })));
|
|
@@ -3075,9 +3563,9 @@ async function ensureTrusted(ctx, mc, app, o) {
|
|
|
3075
3563
|
}
|
|
3076
3564
|
|
|
3077
3565
|
// src/cli/commands/devWorkspace.ts
|
|
3078
|
-
import { randomBytes as
|
|
3079
|
-
import { realpath as
|
|
3080
|
-
import { join as
|
|
3566
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
3567
|
+
import { realpath as realpath7 } from "fs/promises";
|
|
3568
|
+
import { join as join20, relative as relative6 } from "path";
|
|
3081
3569
|
|
|
3082
3570
|
// src/core/dev/order.ts
|
|
3083
3571
|
function selectSkips(modules, sel) {
|
|
@@ -3182,8 +3670,8 @@ function planDev(modules, sel) {
|
|
|
3182
3670
|
}
|
|
3183
3671
|
const order = sccs.map((_, i) => i).sort((a, b) => minSlug[a].localeCompare(minSlug[b]));
|
|
3184
3672
|
for (const i of order) visit(i);
|
|
3185
|
-
const
|
|
3186
|
-
for (const i of orderedSccIndices)
|
|
3673
|
+
const start2 = [];
|
|
3674
|
+
for (const i of orderedSccIndices) start2.push(...sccSlugs[i]);
|
|
3187
3675
|
const waitFor = {};
|
|
3188
3676
|
for (const m of starting) {
|
|
3189
3677
|
const mySccId = sccOf.get(m.id);
|
|
@@ -3198,7 +3686,7 @@ function planDev(modules, sel) {
|
|
|
3198
3686
|
missingProviders[m.slug] = [...new Set(missing)].sort();
|
|
3199
3687
|
}
|
|
3200
3688
|
}
|
|
3201
|
-
return { start, waitFor, cycles, skipped, missingProviders };
|
|
3689
|
+
return { start: start2, waitFor, cycles, skipped, missingProviders };
|
|
3202
3690
|
}
|
|
3203
3691
|
|
|
3204
3692
|
// src/core/dev/supervisor.ts
|
|
@@ -3234,15 +3722,15 @@ function supervise(o) {
|
|
|
3234
3722
|
});
|
|
3235
3723
|
const readiness = /* @__PURE__ */ new Map();
|
|
3236
3724
|
for (const m of o.modules) {
|
|
3237
|
-
let
|
|
3725
|
+
let resolve6;
|
|
3238
3726
|
let settled = false;
|
|
3239
3727
|
const promise = new Promise((r) => {
|
|
3240
|
-
|
|
3728
|
+
resolve6 = r;
|
|
3241
3729
|
});
|
|
3242
3730
|
readiness.set(m.slug, { promise, settle: (r) => {
|
|
3243
3731
|
if (settled) return false;
|
|
3244
3732
|
settled = true;
|
|
3245
|
-
|
|
3733
|
+
resolve6(r);
|
|
3246
3734
|
return true;
|
|
3247
3735
|
} });
|
|
3248
3736
|
}
|
|
@@ -3306,7 +3794,7 @@ function supervise(o) {
|
|
|
3306
3794
|
checkDone();
|
|
3307
3795
|
});
|
|
3308
3796
|
};
|
|
3309
|
-
const
|
|
3797
|
+
const start2 = async (m) => {
|
|
3310
3798
|
const providers = m.waitFor.filter((p) => readiness.has(p));
|
|
3311
3799
|
const results = await Promise.all(providers.map((p) => readiness.get(p).promise));
|
|
3312
3800
|
if (stopCode !== null) return;
|
|
@@ -3316,7 +3804,7 @@ function supervise(o) {
|
|
|
3316
3804
|
});
|
|
3317
3805
|
launch(m);
|
|
3318
3806
|
};
|
|
3319
|
-
for (const m of o.modules) void
|
|
3807
|
+
for (const m of o.modules) void start2(m);
|
|
3320
3808
|
checkDone();
|
|
3321
3809
|
return {
|
|
3322
3810
|
done,
|
|
@@ -3333,12 +3821,12 @@ function supervise(o) {
|
|
|
3333
3821
|
}
|
|
3334
3822
|
|
|
3335
3823
|
// src/core/workspace/stray.ts
|
|
3336
|
-
import { lstat as lstat2, readdir } from "fs/promises";
|
|
3337
|
-
import { join as
|
|
3824
|
+
import { lstat as lstat2, readdir as readdir3 } from "fs/promises";
|
|
3825
|
+
import { join as join19 } from "path";
|
|
3338
3826
|
async function strayRepos(root, slugs) {
|
|
3339
3827
|
let entries;
|
|
3340
3828
|
try {
|
|
3341
|
-
entries = (await
|
|
3829
|
+
entries = (await readdir3(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3342
3830
|
} catch {
|
|
3343
3831
|
return [];
|
|
3344
3832
|
}
|
|
@@ -3346,7 +3834,7 @@ async function strayRepos(root, slugs) {
|
|
|
3346
3834
|
for (const name of entries) {
|
|
3347
3835
|
if (slugs.has(name)) continue;
|
|
3348
3836
|
try {
|
|
3349
|
-
await lstat2(
|
|
3837
|
+
await lstat2(join19(root, name, ".git"));
|
|
3350
3838
|
names.push(name);
|
|
3351
3839
|
} catch {
|
|
3352
3840
|
}
|
|
@@ -3386,16 +3874,18 @@ function prefixer(slugs, color) {
|
|
|
3386
3874
|
import { constants as constants2 } from "os";
|
|
3387
3875
|
var childRunning = { current: false };
|
|
3388
3876
|
function registerRun(program, ctx) {
|
|
3389
|
-
program.command("run").description("Run a command with this module's local secrets in its environment").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--yes", "trust this repository without asking").argument("<command>", "the command to run (after --)").argument("[args...]", "its arguments, passed verbatim").action(async (cmd, args, f) => {
|
|
3390
|
-
const { deps: deps2 } = ctx;
|
|
3877
|
+
program.command("run").description("Run a command with this module's local secrets in its environment").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "the project whose secrets to use, from anywhere (see qore init)").option("--yes", "trust this repository without asking").argument("<command>", "the command to run (after --)").argument("[args...]", "its arguments, passed verbatim").action(async (cmd, args, f) => {
|
|
3391
3878
|
if (ctx.opts().json) {
|
|
3392
3879
|
throw new QoreError("QORE_CONTEXT_INVALID", "run has no --json output", {
|
|
3393
3880
|
hint: "Drop --json; the child's own output passes through."
|
|
3394
3881
|
});
|
|
3395
3882
|
}
|
|
3396
|
-
const
|
|
3397
|
-
const
|
|
3398
|
-
|
|
3883
|
+
const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true });
|
|
3884
|
+
const { deps: deps2 } = c;
|
|
3885
|
+
const mc = await moduleContextFor(c, f, "run");
|
|
3886
|
+
const app = await appFor(c, mc, f.app);
|
|
3887
|
+
await rememberContext(c, mc, app);
|
|
3888
|
+
await ensureTrusted(c, mc, app, { yes: f.yes });
|
|
3399
3889
|
const local = await fetchLocalEnv(deps2, mc, app);
|
|
3400
3890
|
childRunning.current = true;
|
|
3401
3891
|
let exit;
|
|
@@ -3429,7 +3919,9 @@ async function workspaceContext(ctx, ws) {
|
|
|
3429
3919
|
const session = await openSession(deps2, portal, "dev", requestLog(ctx));
|
|
3430
3920
|
await ensureCapability(deps2, portal.apiOrigin, "app.modules");
|
|
3431
3921
|
await ensureCapability(deps2, portal.apiOrigin, "module.local-env");
|
|
3432
|
-
|
|
3922
|
+
const am = await workspaceAppModules(session, portal, ws);
|
|
3923
|
+
await rememberWorkspace(ctx, ws, am.app);
|
|
3924
|
+
return { am, online: { portal, session }, offlineCause: null };
|
|
3433
3925
|
} catch (e) {
|
|
3434
3926
|
if (!isOffline(e)) throw e;
|
|
3435
3927
|
const last = await loadLastStart(deps2, ws.root);
|
|
@@ -3504,7 +3996,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3504
3996
|
continue;
|
|
3505
3997
|
}
|
|
3506
3998
|
try {
|
|
3507
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
3999
|
+
dirs[m.slug] = await deps2.git.inspect(join20(ws.root, m.slug));
|
|
3508
4000
|
safe.push(m);
|
|
3509
4001
|
} catch (e) {
|
|
3510
4002
|
if (!(e instanceof QoreError) || e.code === "QORE_GIT_UNAVAILABLE") throw e;
|
|
@@ -3541,7 +4033,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3541
4033
|
const results = await applyActions(deps2, { portal: online.portal, app: am, actions, protocol: cloneProtocol(dirs), onResult: () => {
|
|
3542
4034
|
} });
|
|
3543
4035
|
renderResults(io2, results, false);
|
|
3544
|
-
for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(
|
|
4036
|
+
for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(join20(ws.root, r.slug));
|
|
3545
4037
|
({ present, plan } = computePlan());
|
|
3546
4038
|
}
|
|
3547
4039
|
for (const name of await strayRepos(ws.root, new Set(am.modules.map((m) => m.slug)))) renderFinding(ctx, strayClone(name, am.app.name));
|
|
@@ -3649,7 +4141,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3649
4141
|
workspace: ws,
|
|
3650
4142
|
module: { id: u.m.id, slug: u.m.slug, name: u.m.name, type: u.m.type, access: u.m.access, tenant: am.app.tenant, apps: [appRef] }
|
|
3651
4143
|
});
|
|
3652
|
-
const rootReal = await
|
|
4144
|
+
const rootReal = await realpath7(ws.root);
|
|
3653
4145
|
const withDocker = dockerRecovery(ctx, p.signal);
|
|
3654
4146
|
for (const u of units.filter((x) => x.recipe && x.trusted)) {
|
|
3655
4147
|
const slug = u.m.slug;
|
|
@@ -3662,7 +4154,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3662
4154
|
type: u.m.type,
|
|
3663
4155
|
moduleId: u.m.id,
|
|
3664
4156
|
cfg: u.cfg,
|
|
3665
|
-
random: () =>
|
|
4157
|
+
random: () => randomBytes5(32).toString("hex")
|
|
3666
4158
|
});
|
|
3667
4159
|
} catch (e) {
|
|
3668
4160
|
if (offlineCause && isOffline(e)) throw offlineCause;
|
|
@@ -3672,7 +4164,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3672
4164
|
const dev = u.dev;
|
|
3673
4165
|
if (!dev) continue;
|
|
3674
4166
|
if (online && dev.offline) {
|
|
3675
|
-
warn(`${slug}: the portal is unreachable \u2014 starting from ${
|
|
4167
|
+
warn(`${slug}: the portal is unreachable \u2014 starting from ${relative6(rootReal, dev.envFile)} as last written; values may be stale.`, { module: slug });
|
|
3676
4168
|
}
|
|
3677
4169
|
if (dev.kept.length) {
|
|
3678
4170
|
warn(
|
|
@@ -3680,7 +4172,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3680
4172
|
{ module: slug, keys: dev.kept }
|
|
3681
4173
|
);
|
|
3682
4174
|
}
|
|
3683
|
-
report({ module: slug, step: "env", status: "done", detail:
|
|
4175
|
+
report({ module: slug, step: "env", status: "done", detail: relative6(rootReal, dev.envFile) });
|
|
3684
4176
|
const services = servicesMissing(slug, dev.env, u.recipe, { servicesFlag: f.services, configured: u.cfg.dev?.services !== void 0 });
|
|
3685
4177
|
if (services) renderFinding(ctx, services);
|
|
3686
4178
|
try {
|
|
@@ -3722,7 +4214,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3722
4214
|
if (online && units.every((u) => !u.dev.offline)) {
|
|
3723
4215
|
const envFile = (m) => {
|
|
3724
4216
|
const u = units.find((x) => x.m.id === m.id);
|
|
3725
|
-
return u ?
|
|
4217
|
+
return u ? relative6(rootReal, u.dev.envFile) : join20(m.slug, moduleDefaults(m.type).envFile);
|
|
3726
4218
|
};
|
|
3727
4219
|
await saveLastStart(deps2, ws.root, {
|
|
3728
4220
|
schemaVersion: 1,
|
|
@@ -3911,11 +4403,17 @@ function makeStderrTee(sink) {
|
|
|
3911
4403
|
};
|
|
3912
4404
|
}
|
|
3913
4405
|
function registerDev(program, ctx) {
|
|
3914
|
-
program.command("dev").description("Start this module's dev server \u2014 or, in a workspace root, every module of the app").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--only <slugs>", "workspace: start only these modules (comma-separated)").option("--skip <slugs>", "workspace: start all but these modules (comma-separated)").option("--no-install", "do not run the package manager's install").option("--no-services", "do not start local services (db:up)").option("--kill-others", "workspace: the first module to exit stops the rest").option("--wait-timeout <s>", "workspace: seconds to wait for a provider's port", "60").option("--yes", "trust the repositories without asking").action(async (f) => {
|
|
4406
|
+
program.command("dev").description("Start this module's dev server \u2014 or, in a workspace root, every module of the app").argument("[project]", "app or app/module to start, from anywhere (see qore init)").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "same as the [project] argument").option("--only <slugs>", "workspace: start only these modules (comma-separated)").option("--skip <slugs>", "workspace: start all but these modules (comma-separated)").option("--no-install", "do not run the package manager's install").option("--no-services", "do not start local services (db:up)").option("--kill-others", "workspace: the first module to exit stops the rest").option("--wait-timeout <s>", "workspace: seconds to wait for a provider's port", "60").option("--yes", "trust the repositories without asking").action(async (project, f) => {
|
|
3915
4407
|
const waitTimeout = parseWaitTimeout(f.waitTimeout);
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
4408
|
+
if (project !== void 0 && f.project !== void 0 && project !== f.project) {
|
|
4409
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `Pass the project once: "${project}" and --project "${f.project}" differ`, {
|
|
4410
|
+
hint: "qore dev <project> | qore dev --project <project>"
|
|
4411
|
+
});
|
|
4412
|
+
}
|
|
4413
|
+
const c = await projectCtx(ctx, { ref: project ?? f.project, module: f.module, app: f.app, needModule: false });
|
|
4414
|
+
const ws = await workspaceRoot(c.deps);
|
|
4415
|
+
if (ws && f.module === void 0) return workspaceDev(c, ws, f, waitTimeout);
|
|
4416
|
+
return moduleDev(c, f, waitTimeout);
|
|
3919
4417
|
});
|
|
3920
4418
|
}
|
|
3921
4419
|
function parseWaitTimeout(raw) {
|
|
@@ -3955,7 +4453,7 @@ async function providerFindings(ctx, root, mc, app, env) {
|
|
|
3955
4453
|
let fix = `qore clone ${app.slug}`;
|
|
3956
4454
|
if (ws && ws.appId === app.id) fix = cdAndDev(deps2.home, ws.root);
|
|
3957
4455
|
else if (providers.length === 1) {
|
|
3958
|
-
const sibling =
|
|
4456
|
+
const sibling = join21(dirname7(root), providers[0].slug);
|
|
3959
4457
|
if ((await deps2.git.inspect(sibling)).isRepo) fix = cdAndDev(deps2.home, sibling);
|
|
3960
4458
|
}
|
|
3961
4459
|
return providerDown({
|
|
@@ -3989,6 +4487,7 @@ async function devContext(ctx, f, root) {
|
|
|
3989
4487
|
return { online: null, module: last.module, offlineCause: e };
|
|
3990
4488
|
}
|
|
3991
4489
|
const app = await appFor(ctx, mc, f.app);
|
|
4490
|
+
await rememberContext(ctx, mc, app);
|
|
3992
4491
|
await ensureTrusted(ctx, mc, app, { yes: f.yes });
|
|
3993
4492
|
return { online: { mc, app }, module: mc.module, offlineCause: null };
|
|
3994
4493
|
}
|
|
@@ -4084,13 +4583,13 @@ async function prepareModule(ctx, f, waitTimeout, p) {
|
|
|
4084
4583
|
type: c.module.type,
|
|
4085
4584
|
moduleId: c.module.id,
|
|
4086
4585
|
cfg,
|
|
4087
|
-
random: () =>
|
|
4586
|
+
random: () => randomBytes6(32).toString("hex")
|
|
4088
4587
|
});
|
|
4089
4588
|
} catch (e) {
|
|
4090
4589
|
if (c.offlineCause && isOffline(e)) throw c.offlineCause;
|
|
4091
4590
|
throw e;
|
|
4092
4591
|
}
|
|
4093
|
-
const envFile =
|
|
4592
|
+
const envFile = relative7(await realpath8(root), dev.envFile);
|
|
4094
4593
|
if (dev.offline) io2.err(`! The portal is unreachable \u2014 starting from ${envFile} as last written; values may be stale.`);
|
|
4095
4594
|
if (dev.kept.length) {
|
|
4096
4595
|
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`);
|
|
@@ -4136,12 +4635,20 @@ async function prepareModule(ctx, f, waitTimeout, p) {
|
|
|
4136
4635
|
return { root, slug, recipe, env, port: dev.port, pkg, devEnv: dev.env };
|
|
4137
4636
|
}
|
|
4138
4637
|
|
|
4638
|
+
// src/cli/commands/debugTelemetry.ts
|
|
4639
|
+
function registerDebugTelemetry(program, ctx) {
|
|
4640
|
+
program.command("debug-telemetry", { hidden: true }).description("Send a test crash report").option("--crash", "throw an unexpected error").action((o) => {
|
|
4641
|
+
if (o.crash) throw new Error(`qore debug-telemetry crash in ${ctx.deps.home} token=debug-telemetry-secret`);
|
|
4642
|
+
if (ctx.opts().json) renderJson(ctx.io, { crash: false });
|
|
4643
|
+
});
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4139
4646
|
// src/cli/commands/doctor.ts
|
|
4140
4647
|
import pc3 from "picocolors";
|
|
4141
4648
|
|
|
4142
4649
|
// src/core/doctor/checks.ts
|
|
4143
|
-
import { realpath as
|
|
4144
|
-
import { basename as basename3, join as
|
|
4650
|
+
import { realpath as realpath9, stat as stat5 } from "fs/promises";
|
|
4651
|
+
import { basename as basename3, join as join22 } from "path";
|
|
4145
4652
|
|
|
4146
4653
|
// src/core/diagnostics/ssh.ts
|
|
4147
4654
|
var SSH_TEST_ARGS = ["-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "git@github.com"];
|
|
@@ -4186,8 +4693,8 @@ var UPGRADE = "npm i -g @quatronic/qore";
|
|
|
4186
4693
|
var RANK = { ok: 0, warn: 1, fail: 2 };
|
|
4187
4694
|
var noop = () => {
|
|
4188
4695
|
};
|
|
4189
|
-
var ok = (
|
|
4190
|
-
var withError = (status2,
|
|
4696
|
+
var ok = (label3, details) => ({ status: "ok", label: label3, ...details ? { details } : {} });
|
|
4697
|
+
var withError = (status2, label3, e) => ({ status: status2, label: label3, error: toJsonError(e) });
|
|
4191
4698
|
var isCancel = (e, signal) => e instanceof CancelledError || signal.aborted;
|
|
4192
4699
|
var portalLabel = (p) => p.builtin ?? p.apiOrigin;
|
|
4193
4700
|
var ToolTimeout = class extends Error {
|
|
@@ -4323,7 +4830,7 @@ function ownerText(e) {
|
|
|
4323
4830
|
if (d.owner === "qore") return ` by a previous qore dev (pid ${String(d.pid)})`;
|
|
4324
4831
|
return "";
|
|
4325
4832
|
}
|
|
4326
|
-
var sameDir = async (a, b) => await
|
|
4833
|
+
var sameDir = async (a, b) => await realpath9(a).catch(() => a) === await realpath9(b).catch(() => b);
|
|
4327
4834
|
async function moduleBody(deps2, m, runs, signal) {
|
|
4328
4835
|
const parts = [];
|
|
4329
4836
|
const part = async (fn) => {
|
|
@@ -4421,6 +4928,21 @@ async function resolveRepoModule(deps2, session, portal, meta, root) {
|
|
|
4421
4928
|
}
|
|
4422
4929
|
return session.api.get(`/api/cli/v1/modules/${encodeURIComponent(ids[0])}`);
|
|
4423
4930
|
}
|
|
4931
|
+
async function checkProjectsRoot(cfg) {
|
|
4932
|
+
const root = cfg?.projectsRoot;
|
|
4933
|
+
if (!root) return ok("projects folder not set (optional \u2014 qore init)");
|
|
4934
|
+
const s = await stat5(root).catch(() => null);
|
|
4935
|
+
if (!s?.isDirectory()) return { status: "warn", label: `projects folder ${root} does not exist \u2014 qore init` };
|
|
4936
|
+
const e = await writableError(root);
|
|
4937
|
+
if (e) return { status: "fail", label: `projects folder ${root} is not writable (${e.code ?? e.message})` };
|
|
4938
|
+
return ok(`projects folder ${root}`);
|
|
4939
|
+
}
|
|
4940
|
+
function checkTelemetry(deps2, cfg) {
|
|
4941
|
+
const built = deps2.sentryDsn !== "";
|
|
4942
|
+
if (!cfg) return ok("crash reports off (config unreadable)", { enabled: false, source: "config", reason: "unreadable", built });
|
|
4943
|
+
const consent = resolveConsent(deps2.env, cfg);
|
|
4944
|
+
return ok(describeConsent(consent, built), { enabled: consent.enabled, source: consent.source, built });
|
|
4945
|
+
}
|
|
4424
4946
|
async function runDoctor(deps2, o = {}) {
|
|
4425
4947
|
const signal = o.signal ?? new AbortController().signal;
|
|
4426
4948
|
const toolMs = o.toolTimeoutMs ?? TOOL_TIMEOUT_MS;
|
|
@@ -4440,12 +4962,14 @@ async function runDoctor(deps2, o = {}) {
|
|
|
4440
4962
|
return r;
|
|
4441
4963
|
};
|
|
4442
4964
|
let cfg;
|
|
4965
|
+
let cfgReadable = true;
|
|
4443
4966
|
try {
|
|
4444
4967
|
cfg = await deps2.config.read();
|
|
4445
4968
|
} catch (e) {
|
|
4446
4969
|
if (isCancel(e, signal)) throw e;
|
|
4447
4970
|
await push("config", "tools", () => withError("fail", errorLabel(e), e));
|
|
4448
4971
|
cfg = { schemaVersion: 1, profiles: {} };
|
|
4972
|
+
cfgReadable = false;
|
|
4449
4973
|
}
|
|
4450
4974
|
let ws = null;
|
|
4451
4975
|
let wsError = null;
|
|
@@ -4467,6 +4991,8 @@ async function runDoctor(deps2, o = {}) {
|
|
|
4467
4991
|
await push("git", "tools", () => checkGit(deps2, signal, toolMs));
|
|
4468
4992
|
await push("github-ssh", "tools", () => checkGithubSsh(deps2, signal, toolMs));
|
|
4469
4993
|
await push("docker", "tools", () => checkDocker(deps2, signal, toolMs));
|
|
4994
|
+
await push("telemetry", "tools", () => checkTelemetry(deps2, cfgReadable ? cfg : null));
|
|
4995
|
+
await push("projects-root", "tools", () => checkProjectsRoot(cfgReadable ? cfg : null));
|
|
4470
4996
|
const sessions = /* @__PURE__ */ new Map();
|
|
4471
4997
|
let loggedIn = [];
|
|
4472
4998
|
try {
|
|
@@ -4559,7 +5085,7 @@ async function workspaceModule(deps2, ws, am, m, runs, signal, push, perModule)
|
|
|
4559
5085
|
await push(`module:${m.slug}`, "workspace", () => ({ status: "warn", label: "not a safe directory name \u2014 skipped" }), m.slug);
|
|
4560
5086
|
return;
|
|
4561
5087
|
}
|
|
4562
|
-
const mod = { root:
|
|
5088
|
+
const mod = { root: join22(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
|
|
4563
5089
|
let prepared = null;
|
|
4564
5090
|
await push(`module:${m.slug}`, "workspace", async () => {
|
|
4565
5091
|
const info = await deps2.git.inspect(mod.root);
|
|
@@ -4589,26 +5115,28 @@ function registerDoctor(program, ctx) {
|
|
|
4589
5115
|
}
|
|
4590
5116
|
|
|
4591
5117
|
// src/cli/commands/env.ts
|
|
4592
|
-
import { randomBytes as
|
|
4593
|
-
import { chmod as chmod3, realpath as
|
|
4594
|
-
import { dirname as
|
|
5118
|
+
import { randomBytes as randomBytes7 } from "crypto";
|
|
5119
|
+
import { chmod as chmod3, realpath as realpath10 } from "fs/promises";
|
|
5120
|
+
import { dirname as dirname8, join as join23, relative as relative8 } from "path";
|
|
4595
5121
|
var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.length} chars)`;
|
|
4596
5122
|
function registerEnv(program, ctx) {
|
|
4597
5123
|
const env = program.command("env").description("Local environment");
|
|
4598
|
-
env.command("pull").description("Write this module's local secrets into its env file").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--target <path>", "env file to write (inside the repository)").option("--use-portal", "take the portal's value for keys that differ locally").option("--yes", "trust this repository without asking").action(async (f) => {
|
|
4599
|
-
const {
|
|
4600
|
-
const
|
|
4601
|
-
const
|
|
5124
|
+
env.command("pull").description("Write this module's local secrets into its env file").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "the project to pull for, from anywhere (see qore init)").option("--target <path>", "env file to write (inside the repository)").option("--use-portal", "take the portal's value for keys that differ locally").option("--yes", "trust this repository without asking").action(async (f) => {
|
|
5125
|
+
const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true });
|
|
5126
|
+
const { deps: deps2, io: io2 } = c;
|
|
5127
|
+
const g = c.opts();
|
|
5128
|
+
const mc = await moduleContextFor(c, f, "env pull");
|
|
4602
5129
|
if (!mc.repoRoot) {
|
|
4603
5130
|
throw new QoreError("QORE_CONTEXT_REQUIRED", "env pull writes into a module repository", { hint: "cd into the module's repository." });
|
|
4604
5131
|
}
|
|
4605
|
-
const app = await appFor(
|
|
5132
|
+
const app = await appFor(c, mc, f.app);
|
|
5133
|
+
await rememberContext(c, mc, app);
|
|
4606
5134
|
const cfg = await readRepoConfig(mc.repoRoot);
|
|
4607
5135
|
const defaults = moduleDefaults(mc.module.type);
|
|
4608
5136
|
const target = f.target ? await resolveTarget(mc.repoRoot, f.target, deps2.cwd) : await resolveTarget(mc.repoRoot, cfg.envFile ?? defaults.envFile);
|
|
4609
|
-
await ensureTrusted(
|
|
5137
|
+
await ensureTrusted(c, mc, app, { yes: f.yes });
|
|
4610
5138
|
const existing = await readOrNull(target);
|
|
4611
|
-
const example = existing === null ? await readOrNull(
|
|
5139
|
+
const example = existing === null ? await readOrNull(join23(dirname8(target), ".env.example")) : null;
|
|
4612
5140
|
const local = await fetchLocalEnv(deps2, mc, app);
|
|
4613
5141
|
const inputs = {
|
|
4614
5142
|
existing,
|
|
@@ -4617,7 +5145,7 @@ function registerEnv(program, ctx) {
|
|
|
4617
5145
|
managed: local.managedKeys,
|
|
4618
5146
|
localSecrets: cfg.localSecrets ?? defaults.localSecrets,
|
|
4619
5147
|
defaultChoice: f.usePortal ? "use-portal" : "keep",
|
|
4620
|
-
random: () =>
|
|
5148
|
+
random: () => randomBytes7(32).toString("hex")
|
|
4621
5149
|
};
|
|
4622
5150
|
let plan = planPull(inputs);
|
|
4623
5151
|
if (plan.conflicts.length && io2.interactive && !f.usePortal) {
|
|
@@ -4649,7 +5177,7 @@ function registerEnv(program, ctx) {
|
|
|
4649
5177
|
} else {
|
|
4650
5178
|
await writeSecurely(target, plan.text);
|
|
4651
5179
|
}
|
|
4652
|
-
const rel =
|
|
5180
|
+
const rel = relative8(await realpath10(mc.repoRoot), target);
|
|
4653
5181
|
if (g.json) {
|
|
4654
5182
|
const names = {
|
|
4655
5183
|
created: plan.created,
|
|
@@ -4678,6 +5206,9 @@ function registerEnv(program, ctx) {
|
|
|
4678
5206
|
});
|
|
4679
5207
|
}
|
|
4680
5208
|
|
|
5209
|
+
// src/cli/commands/init.ts
|
|
5210
|
+
import { realpath as realpath11 } from "fs/promises";
|
|
5211
|
+
|
|
4681
5212
|
// src/core/auth/oidc.ts
|
|
4682
5213
|
import * as oc from "openid-client";
|
|
4683
5214
|
var LOGIN_SCOPE = "openid profile email offline_access qore:read qore:local-env";
|
|
@@ -4745,9 +5276,9 @@ async function createOidc(opts) {
|
|
|
4745
5276
|
throw mapOidcError(e, host);
|
|
4746
5277
|
}
|
|
4747
5278
|
},
|
|
4748
|
-
async pollDevice(
|
|
5279
|
+
async pollDevice(start2, signal) {
|
|
4749
5280
|
try {
|
|
4750
|
-
const t = await oc.pollDeviceAuthorizationGrant(config,
|
|
5281
|
+
const t = await oc.pollDeviceAuthorizationGrant(config, start2.handle, void 0, { signal });
|
|
4751
5282
|
return toTokenSet(t);
|
|
4752
5283
|
} catch (e) {
|
|
4753
5284
|
throw mapOidcError(e, host);
|
|
@@ -4774,14 +5305,14 @@ async function createOidc(opts) {
|
|
|
4774
5305
|
// src/core/auth/login.ts
|
|
4775
5306
|
async function deviceLogin(opts) {
|
|
4776
5307
|
await opts.store.getRefresh();
|
|
4777
|
-
const
|
|
5308
|
+
const start2 = await opts.oidc.startDevice(LOGIN_SCOPE);
|
|
4778
5309
|
const stopPoll = new AbortController();
|
|
4779
5310
|
const settled = new AbortController();
|
|
4780
|
-
const poll = opts.oidc.pollDevice(
|
|
5311
|
+
const poll = opts.oidc.pollDevice(start2, AbortSignal.any([opts.signal, stopPoll.signal])).finally(() => settled.abort());
|
|
4781
5312
|
poll.catch(() => {
|
|
4782
5313
|
});
|
|
4783
5314
|
try {
|
|
4784
|
-
await opts.onCode(
|
|
5315
|
+
await opts.onCode(start2, settled.signal);
|
|
4785
5316
|
} catch (e) {
|
|
4786
5317
|
stopPoll.abort();
|
|
4787
5318
|
const late = await poll.catch(() => null);
|
|
@@ -4809,78 +5340,201 @@ async function deviceLogin(opts) {
|
|
|
4809
5340
|
}
|
|
4810
5341
|
|
|
4811
5342
|
// src/cli/commands/login.ts
|
|
5343
|
+
var NO_TENANTS = "Your account is not a developer in any organisation. Ask an admin to invite you as developer.";
|
|
5344
|
+
async function loginFlow(ctx, cmd) {
|
|
5345
|
+
const { deps: deps2, io: io2, signal } = ctx;
|
|
5346
|
+
const g = ctx.opts();
|
|
5347
|
+
const cfg = await deps2.config.read();
|
|
5348
|
+
const portal = resolvePortal(g.portal, cfg.defaultProfile);
|
|
5349
|
+
const meta = await fetchMeta(deps2.fetch, portal.apiOrigin);
|
|
5350
|
+
const known = cfg.profiles[portal.apiOrigin]?.issuer;
|
|
5351
|
+
const pinned = { ...portal, pinnedIssuer: portal.pinnedIssuer ?? known ?? null };
|
|
5352
|
+
if (!pinned.pinnedIssuer && portal.builtin === null) {
|
|
5353
|
+
io2.err(`! Logging in to ${portal.apiOrigin}, identity provider ${meta.auth.issuer}`);
|
|
5354
|
+
}
|
|
5355
|
+
negotiate(meta, pinned, deps2.version);
|
|
5356
|
+
const issuer = pinned.pinnedIssuer ?? meta.auth.issuer;
|
|
5357
|
+
const hash = profileHash(portal.apiOrigin, issuer);
|
|
5358
|
+
const store = new TokenStore(deps2.keychain, hash);
|
|
5359
|
+
const t = await deviceLogin({
|
|
5360
|
+
oidc: await deps2.oidcFor(issuer),
|
|
5361
|
+
store,
|
|
5362
|
+
now: deps2.now,
|
|
5363
|
+
signal,
|
|
5364
|
+
withLock: deps2.withLock,
|
|
5365
|
+
lockName: hash,
|
|
5366
|
+
onCode: async (s, settled) => {
|
|
5367
|
+
const url = s.verificationUriComplete ?? s.verificationUri;
|
|
5368
|
+
io2.err(`! First copy your one-time code: ${s.userCode}`);
|
|
5369
|
+
if (io2.interactive && cmd.browser) {
|
|
5370
|
+
try {
|
|
5371
|
+
await io2.waitForEnter(` Press Enter to open ${url} (or open it yourself)`, AbortSignal.any([signal, settled]));
|
|
5372
|
+
} catch (e) {
|
|
5373
|
+
if (settled.aborted && !signal.aborted) return;
|
|
5374
|
+
throw e;
|
|
5375
|
+
}
|
|
5376
|
+
if (settled.aborted) return;
|
|
5377
|
+
if (!await io2.openUrl(url)) io2.err(` Open ${url} in a browser`);
|
|
5378
|
+
} else {
|
|
5379
|
+
io2.err(` Open ${url} in a browser`);
|
|
5380
|
+
}
|
|
5381
|
+
}
|
|
5382
|
+
});
|
|
5383
|
+
await deps2.config.update((c) => {
|
|
5384
|
+
c.profiles[portal.apiOrigin] = {
|
|
5385
|
+
...c.profiles[portal.apiOrigin],
|
|
5386
|
+
apiOrigin: portal.apiOrigin,
|
|
5387
|
+
issuer,
|
|
5388
|
+
sub: t.sub,
|
|
5389
|
+
email: t.email,
|
|
5390
|
+
meta: { fetchedAt: deps2.now().toISOString(), doc: meta }
|
|
5391
|
+
};
|
|
5392
|
+
if (g.portal || !c.defaultProfile) c.defaultProfile = portal.apiOrigin;
|
|
5393
|
+
});
|
|
5394
|
+
const { api } = await openSession(deps2, { ...portal, pinnedIssuer: issuer }, "login", requestLog(ctx));
|
|
5395
|
+
const me = await api.get("/api/cli/v1/me");
|
|
5396
|
+
const prev = cfg.profiles[portal.apiOrigin];
|
|
5397
|
+
let active = prev?.sub === t.sub && prev.activeTenant && me.tenants.some((x) => x.id === prev.activeTenant.id) ? prev.activeTenant : void 0;
|
|
5398
|
+
if (active) {
|
|
5399
|
+
} else if (me.tenants.length === 0) {
|
|
5400
|
+
io2.err(`! ${NO_TENANTS}`);
|
|
5401
|
+
} else if (me.tenants.length === 1) {
|
|
5402
|
+
active = { id: me.tenants[0].id, slug: me.tenants[0].slug };
|
|
5403
|
+
io2.err(`\u2713 Using tenant ${me.tenants[0].name}`);
|
|
5404
|
+
} else if (io2.interactive) {
|
|
5405
|
+
active = await io2.select("Select a tenant", me.tenants.map((x) => ({ name: `${x.name} ${x.role}`, value: { id: x.id, slug: x.slug } })));
|
|
5406
|
+
} else {
|
|
5407
|
+
io2.err(`! You belong to ${me.tenants.length} tenants; choose one with \`qore tenant use <slug>\``);
|
|
5408
|
+
}
|
|
5409
|
+
await deps2.config.update((c) => {
|
|
5410
|
+
c.profiles[portal.apiOrigin].activeTenant = active;
|
|
5411
|
+
});
|
|
5412
|
+
const label3 = portal.builtin ?? portal.apiOrigin;
|
|
5413
|
+
if (g.json) renderJson(io2, { portal: label3, email: me.email, activeTenant: active ?? null });
|
|
5414
|
+
else io2.out(`\u2713 Logged in as ${me.email} (${label3})`);
|
|
5415
|
+
}
|
|
4812
5416
|
function registerLogin(program, ctx) {
|
|
4813
|
-
program.command("login").description("Log in with your Qore account").option("--no-browser", "only print the URL").action(async (cmd) =>
|
|
4814
|
-
|
|
5417
|
+
program.command("login").description("Log in with your Qore account").option("--no-browser", "only print the URL").action(async (cmd) => loginFlow(ctx, cmd));
|
|
5418
|
+
}
|
|
5419
|
+
|
|
5420
|
+
// src/cli/commands/tenant.ts
|
|
5421
|
+
function registerTenant(program, ctx) {
|
|
5422
|
+
const tenant = program.command("tenant").description("Manage the active tenant");
|
|
5423
|
+
tenant.command("list").description("List tenants you have access to").action(async () => {
|
|
5424
|
+
const { deps: deps2, io: io2 } = ctx;
|
|
4815
5425
|
const g = ctx.opts();
|
|
4816
5426
|
const cfg = await deps2.config.read();
|
|
4817
5427
|
const portal = resolvePortal(g.portal, cfg.defaultProfile);
|
|
4818
|
-
const
|
|
4819
|
-
const known = cfg.profiles[portal.apiOrigin]?.issuer;
|
|
4820
|
-
const pinned = { ...portal, pinnedIssuer: portal.pinnedIssuer ?? known ?? null };
|
|
4821
|
-
if (!pinned.pinnedIssuer && portal.builtin === null) {
|
|
4822
|
-
io2.err(`! Logging in to ${portal.apiOrigin}, identity provider ${meta.auth.issuer}`);
|
|
4823
|
-
}
|
|
4824
|
-
negotiate(meta, pinned, deps2.version);
|
|
4825
|
-
const issuer = pinned.pinnedIssuer ?? meta.auth.issuer;
|
|
4826
|
-
const hash = profileHash(portal.apiOrigin, issuer);
|
|
4827
|
-
const store = new TokenStore(deps2.keychain, hash);
|
|
4828
|
-
const t = await deviceLogin({
|
|
4829
|
-
oidc: await deps2.oidcFor(issuer),
|
|
4830
|
-
store,
|
|
4831
|
-
now: deps2.now,
|
|
4832
|
-
signal,
|
|
4833
|
-
withLock: deps2.withLock,
|
|
4834
|
-
lockName: hash,
|
|
4835
|
-
onCode: async (s, settled) => {
|
|
4836
|
-
const url = s.verificationUriComplete ?? s.verificationUri;
|
|
4837
|
-
io2.err(`! First copy your one-time code: ${s.userCode}`);
|
|
4838
|
-
if (io2.interactive && cmd.browser) {
|
|
4839
|
-
try {
|
|
4840
|
-
await io2.waitForEnter(` Press Enter to open ${url} (or open it yourself)`, AbortSignal.any([signal, settled]));
|
|
4841
|
-
} catch (e) {
|
|
4842
|
-
if (settled.aborted && !signal.aborted) return;
|
|
4843
|
-
throw e;
|
|
4844
|
-
}
|
|
4845
|
-
if (settled.aborted) return;
|
|
4846
|
-
if (!await io2.openUrl(url)) io2.err(` Open ${url} in a browser`);
|
|
4847
|
-
} else {
|
|
4848
|
-
io2.err(` Open ${url} in a browser`);
|
|
4849
|
-
}
|
|
4850
|
-
}
|
|
4851
|
-
});
|
|
4852
|
-
await deps2.config.update((c) => {
|
|
4853
|
-
c.profiles[portal.apiOrigin] = {
|
|
4854
|
-
...c.profiles[portal.apiOrigin],
|
|
4855
|
-
apiOrigin: portal.apiOrigin,
|
|
4856
|
-
issuer,
|
|
4857
|
-
sub: t.sub,
|
|
4858
|
-
email: t.email,
|
|
4859
|
-
meta: { fetchedAt: deps2.now().toISOString(), doc: meta }
|
|
4860
|
-
};
|
|
4861
|
-
if (g.portal || !c.defaultProfile) c.defaultProfile = portal.apiOrigin;
|
|
4862
|
-
});
|
|
4863
|
-
const { api } = await openSession(deps2, { ...portal, pinnedIssuer: issuer }, "login", requestLog(ctx));
|
|
5428
|
+
const { profile, api } = await openSession(deps2, portal, "tenant list", requestLog(ctx));
|
|
4864
5429
|
const me = await api.get("/api/cli/v1/me");
|
|
4865
|
-
const
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
io2.err("! Your account is not a developer in any organisation. Ask an admin to invite you as developer.");
|
|
4870
|
-
} else if (me.tenants.length === 1) {
|
|
4871
|
-
active = { id: me.tenants[0].id, slug: me.tenants[0].slug };
|
|
4872
|
-
io2.err(`\u2713 Using tenant ${me.tenants[0].name}`);
|
|
4873
|
-
} else if (io2.interactive) {
|
|
4874
|
-
active = await io2.select("Select a tenant", me.tenants.map((x) => ({ name: `${x.name} ${x.role}`, value: { id: x.id, slug: x.slug } })));
|
|
4875
|
-
} else {
|
|
4876
|
-
io2.err(`! You belong to ${me.tenants.length} tenants; choose one with \`qore tenant use <slug>\``);
|
|
5430
|
+
const activeId = profile.activeTenant?.id;
|
|
5431
|
+
if (g.json) {
|
|
5432
|
+
renderJson(io2, { tenants: me.tenants.map((t) => ({ ...t, active: t.id === activeId })) });
|
|
5433
|
+
return;
|
|
4877
5434
|
}
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
5435
|
+
for (const t of me.tenants) {
|
|
5436
|
+
const mark = t.id === activeId ? "*" : " ";
|
|
5437
|
+
io2.out(`${mark} ${t.name} ${t.role}`);
|
|
5438
|
+
}
|
|
5439
|
+
});
|
|
5440
|
+
tenant.command("use").description("Set the active tenant").argument("[slug]", "tenant slug").action(async (slug) => {
|
|
5441
|
+
const chosen = await useTenant(ctx, slug);
|
|
5442
|
+
if (ctx.opts().json) renderJson(ctx.io, { activeTenant: chosen });
|
|
5443
|
+
else ctx.io.out(`\u2713 Using tenant ${chosen.slug}`);
|
|
5444
|
+
});
|
|
5445
|
+
}
|
|
5446
|
+
async function useTenant(ctx, slug, known) {
|
|
5447
|
+
const { deps: deps2, io: io2 } = ctx;
|
|
5448
|
+
const g = ctx.opts();
|
|
5449
|
+
const cfg = await deps2.config.read();
|
|
5450
|
+
const portal = resolvePortal(g.portal, cfg.defaultProfile);
|
|
5451
|
+
const me = known ?? await (await openSession(deps2, portal, "tenant use", requestLog(ctx))).api.get("/api/cli/v1/me");
|
|
5452
|
+
const candidates = me.tenants.map((t) => ({ kind: "tenant", id: t.id, slug: t.slug, name: t.name }));
|
|
5453
|
+
let chosen;
|
|
5454
|
+
if (slug) {
|
|
5455
|
+
const found = me.tenants.find((t) => t.slug === slug);
|
|
5456
|
+
if (!found) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `No tenant "${slug}" for this account`, { candidates });
|
|
5457
|
+
chosen = { id: found.id, slug: found.slug };
|
|
5458
|
+
} else if (io2.interactive) {
|
|
5459
|
+
chosen = await io2.select("Select a tenant", me.tenants.map((t) => ({ name: `${t.name} ${t.role}`, value: { id: t.id, slug: t.slug } })));
|
|
5460
|
+
} else {
|
|
5461
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", "Which tenant? Pass a slug or run interactively.", { candidates });
|
|
5462
|
+
}
|
|
5463
|
+
await deps2.config.update((c) => {
|
|
5464
|
+
c.profiles[portal.apiOrigin].activeTenant = chosen;
|
|
5465
|
+
});
|
|
5466
|
+
return chosen;
|
|
5467
|
+
}
|
|
5468
|
+
|
|
5469
|
+
// src/cli/commands/init.ts
|
|
5470
|
+
var step = (status2, detail2) => detail2 === void 0 ? { status: status2 } : { status: status2, detail: detail2 };
|
|
5471
|
+
async function rootStep(ctx, f) {
|
|
5472
|
+
const { deps: deps2, io: io2 } = ctx;
|
|
5473
|
+
const current = (await deps2.config.read()).projectsRoot;
|
|
5474
|
+
let input2 = f.root;
|
|
5475
|
+
if (input2 === void 0) {
|
|
5476
|
+
if (!io2.interactive) {
|
|
5477
|
+
if (current) return step("already", current);
|
|
5478
|
+
throw new QoreError("QORE_ROOT_NOT_SET", "No projects folder chosen", { hint: "Pass one: qore init --root <path>." });
|
|
5479
|
+
}
|
|
5480
|
+
input2 = await io2.input("Projects folder", { default: current ?? "~/qore" }) || current || "~/qore";
|
|
5481
|
+
}
|
|
5482
|
+
const wanted = await checkRoot(deps2, input2);
|
|
5483
|
+
const asked = await realpath11(wanted).catch(() => wanted);
|
|
5484
|
+
if (current && asked !== current && io2.interactive && !await io2.confirm(`Change the projects folder from ${current} to ${asked}? Nothing is moved.`, ctx.signal)) {
|
|
5485
|
+
return step("skipped", current);
|
|
5486
|
+
}
|
|
5487
|
+
const next = await createRoot(wanted);
|
|
5488
|
+
if (next === current) return step("already", current);
|
|
5489
|
+
await setRoot(deps2, next);
|
|
5490
|
+
return step("done", next);
|
|
5491
|
+
}
|
|
5492
|
+
async function activeTenant(ctx) {
|
|
5493
|
+
const cfg = await ctx.deps.config.read();
|
|
5494
|
+
return cfg.profiles[resolvePortal(ctx.opts().portal, cfg.defaultProfile).apiOrigin]?.activeTenant?.slug ?? null;
|
|
5495
|
+
}
|
|
5496
|
+
async function tenantStep(ctx) {
|
|
5497
|
+
const cfg = await ctx.deps.config.read();
|
|
5498
|
+
const portal = resolvePortal(ctx.opts().portal, cfg.defaultProfile);
|
|
5499
|
+
const { api } = await openSession(ctx.deps, portal, "init", requestLog(ctx));
|
|
5500
|
+
const me = await api.get("/api/cli/v1/me");
|
|
5501
|
+
if (me.tenants.length === 0) return step("skipped", NO_TENANTS);
|
|
5502
|
+
return step("done", (await useTenant(ctx, me.tenants.length === 1 ? me.tenants[0].slug : void 0, me)).slug);
|
|
5503
|
+
}
|
|
5504
|
+
function registerInit(program, ctx) {
|
|
5505
|
+
program.command("init").description("Set up qore: a projects folder, login, tenant and a first app").option("--root <path>", "the projects folder (default: ask, ~/qore)").option("--clone <app>", "clone this app into the projects folder").option("--tenant <slug>", "tenant the --clone app belongs to").action(async (f) => {
|
|
5506
|
+
const { io: io2 } = ctx;
|
|
5507
|
+
const json2 = Boolean(ctx.opts().json);
|
|
5508
|
+
const sub = json2 ? { ...ctx, io: { ...io2, out: io2.err }, opts: () => ({ ...ctx.opts(), json: false }) } : ctx;
|
|
5509
|
+
const steps = {
|
|
5510
|
+
root: await rootStep(ctx, f),
|
|
5511
|
+
login: step("skipped"),
|
|
5512
|
+
tenant: step("skipped"),
|
|
5513
|
+
clone: step("skipped")
|
|
5514
|
+
};
|
|
5515
|
+
const who = await loggedInAs(ctx);
|
|
5516
|
+
if (who) steps.login = step("already", who);
|
|
5517
|
+
else if (io2.interactive) {
|
|
5518
|
+
await loginFlow(sub, { browser: true });
|
|
5519
|
+
steps.login = step("done", await loggedInAs(ctx) ?? void 0);
|
|
5520
|
+
} else steps.login = step("skipped", "run qore login");
|
|
5521
|
+
const tenant = await activeTenant(ctx);
|
|
5522
|
+
if (tenant) steps.tenant = step(steps.login.status === "done" ? "done" : "already", tenant);
|
|
5523
|
+
else if (steps.login.status !== "skipped" && io2.interactive) steps.tenant = await tenantStep(sub);
|
|
5524
|
+
else steps.tenant = step("skipped", "run qore tenant use <slug>");
|
|
5525
|
+
if (f.clone) {
|
|
5526
|
+
steps.clone = step("done", (await cloneApp(sub, f.clone, void 0, { tenant: f.tenant, protocol: "https" })).root);
|
|
5527
|
+
} else if (io2.interactive && steps.login.status !== "skipped" && steps.tenant.status !== "skipped" && await io2.confirm("Clone an app now?", ctx.signal)) {
|
|
5528
|
+
steps.clone = step("done", (await cloneApp(sub, void 0, void 0, { tenant: f.tenant, protocol: "https" })).root);
|
|
5529
|
+
}
|
|
5530
|
+
const projectsRoot = (await ctx.deps.config.read()).projectsRoot ?? null;
|
|
5531
|
+
if (json2) {
|
|
5532
|
+
renderJson(io2, { projectsRoot, steps });
|
|
5533
|
+
return;
|
|
5534
|
+
}
|
|
5535
|
+
const mark = (s) => s.status === "skipped" ? "\u2013" : "\u2713";
|
|
5536
|
+
for (const [name, s] of Object.entries(steps)) io2.out(`${mark(s)} ${name.padEnd(6)} ${s.status}${s.detail ? ` ${s.detail}` : ""}`);
|
|
5537
|
+
if (projectsRoot) io2.out(`Next: qore dev <app> works from any directory.`);
|
|
4884
5538
|
});
|
|
4885
5539
|
}
|
|
4886
5540
|
|
|
@@ -4948,6 +5602,7 @@ async function appStatus(ctx, ws, appFlag) {
|
|
|
4948
5602
|
await ensureCapability(deps2, portal.apiOrigin, "app.modules");
|
|
4949
5603
|
await ensureCapability(deps2, portal.apiOrigin, "module.status");
|
|
4950
5604
|
const am = await workspaceAppModules(session, portal, ws);
|
|
5605
|
+
await rememberWorkspace(ctx, ws, am.app);
|
|
4951
5606
|
const { tenant, ...app } = am.app;
|
|
4952
5607
|
if (appFlag && appFlag !== app.id && appFlag !== app.slug) {
|
|
4953
5608
|
throw new QoreError("QORE_RESOURCE_NOT_FOUND", `This workspace is for ${app.slug}, not "${appFlag}"`, {
|
|
@@ -4975,19 +5630,21 @@ async function appStatus(ctx, ws, appFlag) {
|
|
|
4975
5630
|
for (const line2 of table(rows)) io2.out(line2);
|
|
4976
5631
|
}
|
|
4977
5632
|
function registerStatus(program, ctx) {
|
|
4978
|
-
program.command("status").description("Deployment and health per environment for this module (or, in a workspace root, the app)").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").action(async (flags) => {
|
|
4979
|
-
const {
|
|
4980
|
-
const
|
|
5633
|
+
program.command("status").description("Deployment and health per environment for this module (or, in a workspace root, the app)").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "the app or module, from anywhere (see qore init)").action(async (flags) => {
|
|
5634
|
+
const c = await projectCtx(ctx, { ref: flags.project, module: flags.module, app: flags.app, needModule: false });
|
|
5635
|
+
const { deps: deps2, io: io2 } = c;
|
|
5636
|
+
const g = c.opts();
|
|
4981
5637
|
if (!flags.module) {
|
|
4982
5638
|
const ws = await workspaceRoot(deps2);
|
|
4983
5639
|
if (ws) {
|
|
4984
|
-
await appStatus(
|
|
5640
|
+
await appStatus(c, ws, flags.app);
|
|
4985
5641
|
return;
|
|
4986
5642
|
}
|
|
4987
5643
|
}
|
|
4988
|
-
const mc = await moduleContextFor(
|
|
5644
|
+
const mc = await moduleContextFor(c, flags, "status");
|
|
4989
5645
|
await ensureCapability(deps2, mc.portal.apiOrigin, "module.status");
|
|
4990
|
-
const app = await appFor(
|
|
5646
|
+
const app = await appFor(c, mc, flags.app);
|
|
5647
|
+
await rememberContext(c, mc, app);
|
|
4991
5648
|
if (!g.json) io2.err(contextLine(mc, app));
|
|
4992
5649
|
const st = await mc.session.api.get(
|
|
4993
5650
|
`/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/status?app_id=${encodeURIComponent(app.id)}`
|
|
@@ -5007,7 +5664,7 @@ function registerStatus(program, ctx) {
|
|
|
5007
5664
|
}
|
|
5008
5665
|
|
|
5009
5666
|
// src/cli/commands/sync.ts
|
|
5010
|
-
import { join as
|
|
5667
|
+
import { join as join24, relative as relative9 } from "path";
|
|
5011
5668
|
import { Option as Option2 } from "commander";
|
|
5012
5669
|
function asFailure2(slug, e) {
|
|
5013
5670
|
if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
|
|
@@ -5039,7 +5696,7 @@ function registerSync(program, ctx) {
|
|
|
5039
5696
|
continue;
|
|
5040
5697
|
}
|
|
5041
5698
|
try {
|
|
5042
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
5699
|
+
dirs[m.slug] = await deps2.git.inspect(join24(ws.root, m.slug));
|
|
5043
5700
|
modulesForPlan.push(m);
|
|
5044
5701
|
} catch (e) {
|
|
5045
5702
|
earlyFailures.push(asFailure2(m.slug, e));
|
|
@@ -5051,7 +5708,7 @@ function registerSync(program, ctx) {
|
|
|
5051
5708
|
} });
|
|
5052
5709
|
await syncPresent(deps2, applied, actions, Boolean(flags.pull), { app, protocol: flags.protocol });
|
|
5053
5710
|
for (const r of applied) {
|
|
5054
|
-
if (r.status === "cloned" && r.dir) r.dir =
|
|
5711
|
+
if (r.status === "cloned" && r.dir) r.dir = relative9(deps2.cwd, r.dir) || ".";
|
|
5055
5712
|
}
|
|
5056
5713
|
const removedResults = removed.map((slug) => ({ slug, status: "removed", reason: "not a module of this app \u2014 left in place" }));
|
|
5057
5714
|
const results = [...earlyFailures, ...applied, ...removedResults];
|
|
@@ -5060,52 +5717,6 @@ function registerSync(program, ctx) {
|
|
|
5060
5717
|
});
|
|
5061
5718
|
}
|
|
5062
5719
|
|
|
5063
|
-
// src/cli/commands/tenant.ts
|
|
5064
|
-
function registerTenant(program, ctx) {
|
|
5065
|
-
const tenant = program.command("tenant").description("Manage the active tenant");
|
|
5066
|
-
tenant.command("list").description("List tenants you have access to").action(async () => {
|
|
5067
|
-
const { deps: deps2, io: io2 } = ctx;
|
|
5068
|
-
const g = ctx.opts();
|
|
5069
|
-
const cfg = await deps2.config.read();
|
|
5070
|
-
const portal = resolvePortal(g.portal, cfg.defaultProfile);
|
|
5071
|
-
const { profile, api } = await openSession(deps2, portal, "tenant list", requestLog(ctx));
|
|
5072
|
-
const me = await api.get("/api/cli/v1/me");
|
|
5073
|
-
const activeId = profile.activeTenant?.id;
|
|
5074
|
-
if (g.json) {
|
|
5075
|
-
renderJson(io2, { tenants: me.tenants.map((t) => ({ ...t, active: t.id === activeId })) });
|
|
5076
|
-
return;
|
|
5077
|
-
}
|
|
5078
|
-
for (const t of me.tenants) {
|
|
5079
|
-
const mark = t.id === activeId ? "*" : " ";
|
|
5080
|
-
io2.out(`${mark} ${t.name} ${t.role}`);
|
|
5081
|
-
}
|
|
5082
|
-
});
|
|
5083
|
-
tenant.command("use").description("Set the active tenant").argument("[slug]", "tenant slug").action(async (slug) => {
|
|
5084
|
-
const { deps: deps2, io: io2 } = ctx;
|
|
5085
|
-
const g = ctx.opts();
|
|
5086
|
-
const cfg = await deps2.config.read();
|
|
5087
|
-
const portal = resolvePortal(g.portal, cfg.defaultProfile);
|
|
5088
|
-
const { api } = await openSession(deps2, portal, "tenant use", requestLog(ctx));
|
|
5089
|
-
const me = await api.get("/api/cli/v1/me");
|
|
5090
|
-
const candidates = me.tenants.map((t) => ({ kind: "tenant", id: t.id, slug: t.slug, name: t.name }));
|
|
5091
|
-
let chosen;
|
|
5092
|
-
if (slug) {
|
|
5093
|
-
const found = me.tenants.find((t) => t.slug === slug);
|
|
5094
|
-
if (!found) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `No tenant "${slug}" for this account`, { candidates });
|
|
5095
|
-
chosen = { id: found.id, slug: found.slug };
|
|
5096
|
-
} else if (io2.interactive) {
|
|
5097
|
-
chosen = await io2.select("Select a tenant", me.tenants.map((t) => ({ name: `${t.name} ${t.role}`, value: { id: t.id, slug: t.slug } })));
|
|
5098
|
-
} else {
|
|
5099
|
-
throw new QoreError("QORE_CONTEXT_REQUIRED", "Which tenant? Pass a slug or run interactively.", { candidates });
|
|
5100
|
-
}
|
|
5101
|
-
await deps2.config.update((c) => {
|
|
5102
|
-
c.profiles[portal.apiOrigin].activeTenant = chosen;
|
|
5103
|
-
});
|
|
5104
|
-
if (g.json) renderJson(io2, { activeTenant: chosen });
|
|
5105
|
-
else io2.out(`\u2713 Using tenant ${chosen.slug}`);
|
|
5106
|
-
});
|
|
5107
|
-
}
|
|
5108
|
-
|
|
5109
5720
|
// src/cli/commands/version.ts
|
|
5110
5721
|
function registerVersion(program, ctx) {
|
|
5111
5722
|
program.command("version").description("Show the CLI and portal version").action(async () => {
|
|
@@ -5125,11 +5736,11 @@ function registerVersion(program, ctx) {
|
|
|
5125
5736
|
return;
|
|
5126
5737
|
}
|
|
5127
5738
|
io2.out(`qore ${deps2.version}`);
|
|
5128
|
-
const
|
|
5739
|
+
const label3 = portal.builtin ?? portal.apiOrigin;
|
|
5129
5740
|
if (portalInfo) {
|
|
5130
|
-
io2.out(`portal ${
|
|
5741
|
+
io2.out(`portal ${label3}: api ${portalInfo.apiVersions.join(",")}, min ${portalInfo.minCliVersion}, latest ${portalInfo.latestCliVersion}`);
|
|
5131
5742
|
} else {
|
|
5132
|
-
io2.out(`portal ${
|
|
5743
|
+
io2.out(`portal ${label3}: unreachable`);
|
|
5133
5744
|
}
|
|
5134
5745
|
});
|
|
5135
5746
|
}
|
|
@@ -5143,18 +5754,18 @@ function registerWhoami(program, ctx) {
|
|
|
5143
5754
|
const portal = resolvePortal(g.portal, cfg.defaultProfile);
|
|
5144
5755
|
const { profile, api } = await openSession(deps2, portal, "whoami", requestLog(ctx));
|
|
5145
5756
|
const me = await api.get("/api/cli/v1/me");
|
|
5146
|
-
const
|
|
5147
|
-
if (
|
|
5148
|
-
io2.err(`! You no longer have developer access to ${
|
|
5757
|
+
const activeTenant2 = profile.activeTenant;
|
|
5758
|
+
if (activeTenant2 && !me.tenants.some((t) => t.id === activeTenant2.id)) {
|
|
5759
|
+
io2.err(`! You no longer have developer access to ${activeTenant2.slug}`);
|
|
5149
5760
|
}
|
|
5150
|
-
const
|
|
5761
|
+
const label3 = portal.builtin ?? portal.apiOrigin;
|
|
5151
5762
|
if (g.json) {
|
|
5152
|
-
renderJson(io2, { portal:
|
|
5763
|
+
renderJson(io2, { portal: label3, activeTenant: activeTenant2 ?? null, me });
|
|
5153
5764
|
return;
|
|
5154
5765
|
}
|
|
5155
|
-
io2.out(`${me.email} (${
|
|
5766
|
+
io2.out(`${me.email} (${label3})`);
|
|
5156
5767
|
for (const t of me.tenants) {
|
|
5157
|
-
const mark =
|
|
5768
|
+
const mark = activeTenant2?.id === t.id ? "*" : " ";
|
|
5158
5769
|
io2.out(`${mark} ${t.name} ${t.role}`);
|
|
5159
5770
|
for (const a of t.apps) {
|
|
5160
5771
|
io2.out(` ${a.slug} ${a.access}`);
|
|
@@ -5168,6 +5779,7 @@ function registerWhoami(program, ctx) {
|
|
|
5168
5779
|
|
|
5169
5780
|
// src/cli/commands/index.ts
|
|
5170
5781
|
var COMMANDS = [
|
|
5782
|
+
registerInit,
|
|
5171
5783
|
registerLogin,
|
|
5172
5784
|
registerLogout,
|
|
5173
5785
|
registerWhoami,
|
|
@@ -5179,9 +5791,111 @@ var COMMANDS = [
|
|
|
5179
5791
|
registerClone,
|
|
5180
5792
|
registerSync,
|
|
5181
5793
|
registerVersion,
|
|
5182
|
-
|
|
5794
|
+
registerConfig,
|
|
5795
|
+
registerDoctor,
|
|
5796
|
+
registerDebugTelemetry
|
|
5183
5797
|
];
|
|
5184
5798
|
|
|
5799
|
+
// src/cli/telemetry/notice.ts
|
|
5800
|
+
var NOTICE = [
|
|
5801
|
+
"qore sends anonymous crash reports and a per-run health record to Quatronic, to find and fix bugs.",
|
|
5802
|
+
"Tokens, env values and your home path are removed; no user or tenant is attached.",
|
|
5803
|
+
"Turn it off with `qore config telemetry off`, QORE_TELEMETRY=0 or DO_NOT_TRACK=1. See the Privacy section of the README."
|
|
5804
|
+
];
|
|
5805
|
+
async function showNoticeOnce(deps2, io2, o) {
|
|
5806
|
+
if (o.json || !io2.interactive || !deps2.sentryDsn) return;
|
|
5807
|
+
try {
|
|
5808
|
+
const cfg = await deps2.config.read();
|
|
5809
|
+
if (!needsNotice(cfg, resolveConsent(deps2.env, cfg))) return;
|
|
5810
|
+
await deps2.config.update((c) => {
|
|
5811
|
+
c.telemetry = { ...c.telemetry, noticeShownAt: deps2.now().toISOString() };
|
|
5812
|
+
});
|
|
5813
|
+
} catch {
|
|
5814
|
+
return;
|
|
5815
|
+
}
|
|
5816
|
+
for (const line2 of NOTICE) io2.err(line2);
|
|
5817
|
+
}
|
|
5818
|
+
|
|
5819
|
+
// src/cli/telemetry/index.ts
|
|
5820
|
+
var CRASH_FLUSH_MS = 2e3;
|
|
5821
|
+
var CLEAN_FLUSH_MS = 200;
|
|
5822
|
+
var loadSentry = async () => (await import("./sentry-GMM4X3FT.js")).createSdk;
|
|
5823
|
+
function createTelemetry(deps2, o) {
|
|
5824
|
+
const load = o.load ?? loadSentry;
|
|
5825
|
+
const flushMs = o.flushMs ?? { crash: CRASH_FLUSH_MS, clean: CLEAN_FLUSH_MS };
|
|
5826
|
+
let sdk = null;
|
|
5827
|
+
let crashed = null;
|
|
5828
|
+
const secrets = [];
|
|
5829
|
+
let scrub = null;
|
|
5830
|
+
return {
|
|
5831
|
+
begin(command) {
|
|
5832
|
+
if (sdk || !deps2.sentryDsn) return;
|
|
5833
|
+
sdk = start(deps2, o.argv, command, load, (ctx) => {
|
|
5834
|
+
scrub = ctx;
|
|
5835
|
+
addSecretValues(ctx, secrets);
|
|
5836
|
+
}).catch(() => null);
|
|
5837
|
+
},
|
|
5838
|
+
crash(e) {
|
|
5839
|
+
crashed ??= { e };
|
|
5840
|
+
},
|
|
5841
|
+
registerSecrets(values) {
|
|
5842
|
+
secrets.push(...values);
|
|
5843
|
+
if (scrub) addSecretValues(scrub, values);
|
|
5844
|
+
},
|
|
5845
|
+
async end(signal) {
|
|
5846
|
+
if (!sdk) return { drained: true };
|
|
5847
|
+
const ms = crashed ? flushMs.crash : flushMs.clean;
|
|
5848
|
+
const work = sdk.then(async (s) => {
|
|
5849
|
+
if (!s) return true;
|
|
5850
|
+
if (crashed) s.crash(crashed.e);
|
|
5851
|
+
return s.end(ms);
|
|
5852
|
+
}).catch(() => false);
|
|
5853
|
+
return { drained: await withDeadline(work, ms, signal) };
|
|
5854
|
+
}
|
|
5855
|
+
};
|
|
5856
|
+
}
|
|
5857
|
+
async function start(deps2, argv2, command, load, onScrub) {
|
|
5858
|
+
const cfg = await deps2.config.read().catch(() => null);
|
|
5859
|
+
if (!cfg) return null;
|
|
5860
|
+
if (!resolveConsent(deps2.env, cfg).enabled) return null;
|
|
5861
|
+
const create = await load();
|
|
5862
|
+
const scrub = buildScrubContext({ home: deps2.home, platform: deps2.platform, env: deps2.env, argv: argv2, commandWords: command.split(" ") });
|
|
5863
|
+
onScrub(scrub);
|
|
5864
|
+
const sdk = create({
|
|
5865
|
+
dsn: deps2.sentryDsn,
|
|
5866
|
+
environment: deps2.sentryEnvironment,
|
|
5867
|
+
release: deps2.version,
|
|
5868
|
+
tags: { command, "cli.version": deps2.version, "node.version": deps2.nodeVersion, os: deps2.platform, ci: deps2.env.CI ? "true" : "false" },
|
|
5869
|
+
scrub
|
|
5870
|
+
});
|
|
5871
|
+
sdk.start();
|
|
5872
|
+
return sdk;
|
|
5873
|
+
}
|
|
5874
|
+
function withDeadline(work, ms, signal) {
|
|
5875
|
+
if (signal?.aborted) return Promise.resolve(false);
|
|
5876
|
+
return new Promise((resolve6) => {
|
|
5877
|
+
const done = (v) => {
|
|
5878
|
+
clearTimeout(timer);
|
|
5879
|
+
signal?.removeEventListener("abort", onAbort);
|
|
5880
|
+
resolve6(v);
|
|
5881
|
+
};
|
|
5882
|
+
const onAbort = () => done(false);
|
|
5883
|
+
const timer = setTimeout(() => done(false), ms);
|
|
5884
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
5885
|
+
work.then(done, () => done(false));
|
|
5886
|
+
});
|
|
5887
|
+
}
|
|
5888
|
+
function telemetryHooks(deps2, io2, t, o) {
|
|
5889
|
+
return {
|
|
5890
|
+
onCommand: async (command) => {
|
|
5891
|
+
if (command === "config telemetry") return;
|
|
5892
|
+
await showNoticeOnce(deps2, io2, o);
|
|
5893
|
+
t.begin(command);
|
|
5894
|
+
},
|
|
5895
|
+
onCrash: (e) => t.crash(e)
|
|
5896
|
+
};
|
|
5897
|
+
}
|
|
5898
|
+
|
|
5185
5899
|
// src/cli/updateCheck.ts
|
|
5186
5900
|
var REGISTRY_URL = "https://registry.npmjs.org/@quatronic%2Fqore/latest";
|
|
5187
5901
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
@@ -5215,7 +5929,7 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
|
|
|
5215
5929
|
|
|
5216
5930
|
// src/core/config.ts
|
|
5217
5931
|
import { readFile as readFile6 } from "fs/promises";
|
|
5218
|
-
import { join as
|
|
5932
|
+
import { join as join25 } from "path";
|
|
5219
5933
|
var empty = () => ({ schemaVersion: 1, profiles: {} });
|
|
5220
5934
|
var ConfigStore = class {
|
|
5221
5935
|
constructor(dir2) {
|
|
@@ -5223,7 +5937,7 @@ var ConfigStore = class {
|
|
|
5223
5937
|
}
|
|
5224
5938
|
dir;
|
|
5225
5939
|
get file() {
|
|
5226
|
-
return
|
|
5940
|
+
return join25(this.dir, "config.json");
|
|
5227
5941
|
}
|
|
5228
5942
|
async read() {
|
|
5229
5943
|
let raw;
|
|
@@ -5254,9 +5968,9 @@ var ConfigStore = class {
|
|
|
5254
5968
|
|
|
5255
5969
|
// src/platform/git.ts
|
|
5256
5970
|
import { execFile, execFileSync } from "child_process";
|
|
5257
|
-
import { mkdir as
|
|
5971
|
+
import { mkdir as mkdir4, readdir as readdir4, stat as stat6 } from "fs/promises";
|
|
5258
5972
|
import { homedir } from "os";
|
|
5259
|
-
import { dirname as
|
|
5973
|
+
import { dirname as dirname9, join as join26 } from "path";
|
|
5260
5974
|
function parseRemotes(stdout) {
|
|
5261
5975
|
const seen = /* @__PURE__ */ new Set();
|
|
5262
5976
|
const out = [];
|
|
@@ -5323,7 +6037,7 @@ function gitExecOptions(repoDir) {
|
|
|
5323
6037
|
return { cwd: homedir(), env, windowsHide: true, maxBuffer: 8 * 1024 * 1024 };
|
|
5324
6038
|
}
|
|
5325
6039
|
function runGit(args, repoDir) {
|
|
5326
|
-
return new Promise((
|
|
6040
|
+
return new Promise((resolve6, reject) => {
|
|
5327
6041
|
execFile("git", args, gitExecOptions(repoDir), (err, stdout, stderr) => {
|
|
5328
6042
|
if (err && err.code === "ENOENT") {
|
|
5329
6043
|
reject(new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
|
|
@@ -5332,7 +6046,7 @@ function runGit(args, repoDir) {
|
|
|
5332
6046
|
return;
|
|
5333
6047
|
}
|
|
5334
6048
|
const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
|
|
5335
|
-
|
|
6049
|
+
resolve6({ code, stdout: String(stdout), stderr: String(stderr), err: err ?? void 0 });
|
|
5336
6050
|
});
|
|
5337
6051
|
});
|
|
5338
6052
|
}
|
|
@@ -5350,7 +6064,7 @@ async function remotes(repoRoot) {
|
|
|
5350
6064
|
return parseRemotes(r.stdout);
|
|
5351
6065
|
}
|
|
5352
6066
|
async function clone(url, dir2) {
|
|
5353
|
-
await
|
|
6067
|
+
await mkdir4(dirname9(dir2), { recursive: true });
|
|
5354
6068
|
const r = await runGit(["clone", "--", url, dir2]);
|
|
5355
6069
|
return toResult(r, "git clone");
|
|
5356
6070
|
}
|
|
@@ -5370,7 +6084,7 @@ async function status(repoRoot) {
|
|
|
5370
6084
|
}
|
|
5371
6085
|
async function statOrThrow(path) {
|
|
5372
6086
|
try {
|
|
5373
|
-
return await
|
|
6087
|
+
return await stat6(path);
|
|
5374
6088
|
} catch (err) {
|
|
5375
6089
|
if (err.code === "ENOENT") return void 0;
|
|
5376
6090
|
throw new QoreError("QORE_INTERNAL", `could not stat ${path}`, { cause: err });
|
|
@@ -5382,12 +6096,12 @@ async function inspect(dir2) {
|
|
|
5382
6096
|
if (!st.isDirectory()) return { exists: true, empty: false, isRepo: false, remotes: [] };
|
|
5383
6097
|
let entries;
|
|
5384
6098
|
try {
|
|
5385
|
-
entries = await
|
|
6099
|
+
entries = await readdir4(dir2);
|
|
5386
6100
|
} catch (err) {
|
|
5387
6101
|
throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
|
|
5388
6102
|
}
|
|
5389
6103
|
const empty2 = entries.length === 0;
|
|
5390
|
-
const isRepo = await statOrThrow(
|
|
6104
|
+
const isRepo = await statOrThrow(join26(dir2, ".git")) !== void 0;
|
|
5391
6105
|
const dirRemotes = isRepo ? await remotes(dir2) : [];
|
|
5392
6106
|
return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
|
|
5393
6107
|
}
|
|
@@ -5448,15 +6162,15 @@ var OsKeychain = class {
|
|
|
5448
6162
|
};
|
|
5449
6163
|
|
|
5450
6164
|
// src/platform/lock.ts
|
|
5451
|
-
import { mkdir as
|
|
5452
|
-
import { join as
|
|
6165
|
+
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
6166
|
+
import { join as join27 } from "path";
|
|
5453
6167
|
import lockfile from "proper-lockfile";
|
|
5454
6168
|
function fileLock(dir2) {
|
|
5455
6169
|
return async (name, fn) => {
|
|
5456
|
-
const locks =
|
|
5457
|
-
await
|
|
5458
|
-
const target =
|
|
5459
|
-
await
|
|
6170
|
+
const locks = join27(dir2, "locks");
|
|
6171
|
+
await mkdir5(locks, { recursive: true, mode: 448 });
|
|
6172
|
+
const target = join27(locks, `${name}.lock`);
|
|
6173
|
+
await writeFile5(target, "", { flag: "a" });
|
|
5460
6174
|
let release;
|
|
5461
6175
|
let compromised;
|
|
5462
6176
|
try {
|
|
@@ -5499,7 +6213,7 @@ var STDERR_DRAIN_MS = 500;
|
|
|
5499
6213
|
function makeSpawner(spawnFn, proc) {
|
|
5500
6214
|
return {
|
|
5501
6215
|
run(cmd, args, env, o) {
|
|
5502
|
-
return new Promise((
|
|
6216
|
+
return new Promise((resolve6, reject) => {
|
|
5503
6217
|
let child;
|
|
5504
6218
|
try {
|
|
5505
6219
|
child = spawnFn(cmd, args, {
|
|
@@ -5533,7 +6247,7 @@ function makeSpawner(spawnFn, proc) {
|
|
|
5533
6247
|
cleanup();
|
|
5534
6248
|
const stderr = child.stderr;
|
|
5535
6249
|
if (!o?.onStderr || !stderr || stderr.readableEnded) {
|
|
5536
|
-
|
|
6250
|
+
resolve6({ code, signal });
|
|
5537
6251
|
return;
|
|
5538
6252
|
}
|
|
5539
6253
|
let settled = false;
|
|
@@ -5541,7 +6255,7 @@ function makeSpawner(spawnFn, proc) {
|
|
|
5541
6255
|
if (settled) return;
|
|
5542
6256
|
settled = true;
|
|
5543
6257
|
clearTimeout(timer);
|
|
5544
|
-
|
|
6258
|
+
resolve6({ code, signal });
|
|
5545
6259
|
};
|
|
5546
6260
|
const timer = setTimeout(finish, STDERR_DRAIN_MS);
|
|
5547
6261
|
timer.unref?.();
|
|
@@ -5596,13 +6310,13 @@ var systemLauncher = {
|
|
|
5596
6310
|
child.stdout?.on("data", (c) => out.push(c));
|
|
5597
6311
|
child.stderr?.on("data", (c) => err.push(c));
|
|
5598
6312
|
let closed = false;
|
|
5599
|
-
const exited = new Promise((
|
|
5600
|
-
child.once("error", (e) =>
|
|
6313
|
+
const exited = new Promise((resolve6) => {
|
|
6314
|
+
child.once("error", (e) => resolve6({ code: null, signal: null, spawnError: e.code ?? "error" }));
|
|
5601
6315
|
child.once("close", (code, signal) => {
|
|
5602
6316
|
closed = true;
|
|
5603
6317
|
out.flush();
|
|
5604
6318
|
err.flush();
|
|
5605
|
-
|
|
6319
|
+
resolve6({ code, signal });
|
|
5606
6320
|
});
|
|
5607
6321
|
});
|
|
5608
6322
|
return {
|
|
@@ -5648,11 +6362,11 @@ var systemLauncher = {
|
|
|
5648
6362
|
import { execFile as execFile2 } from "child_process";
|
|
5649
6363
|
import { connect } from "net";
|
|
5650
6364
|
function accepts(host, port) {
|
|
5651
|
-
return new Promise((
|
|
6365
|
+
return new Promise((resolve6) => {
|
|
5652
6366
|
const s = connect({ host, port });
|
|
5653
6367
|
const done = (v) => {
|
|
5654
6368
|
s.destroy();
|
|
5655
|
-
|
|
6369
|
+
resolve6(v);
|
|
5656
6370
|
};
|
|
5657
6371
|
s.setTimeout(500, () => done(false));
|
|
5658
6372
|
s.once("connect", () => done(true));
|
|
@@ -5660,12 +6374,12 @@ function accepts(host, port) {
|
|
|
5660
6374
|
});
|
|
5661
6375
|
}
|
|
5662
6376
|
function run2(cmd, args) {
|
|
5663
|
-
return new Promise((
|
|
6377
|
+
return new Promise((resolve6) => {
|
|
5664
6378
|
execFile2(
|
|
5665
6379
|
cmd,
|
|
5666
6380
|
args,
|
|
5667
6381
|
{ timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
|
|
5668
|
-
(e, stdout) =>
|
|
6382
|
+
(e, stdout) => resolve6(e && !stdout ? null : String(stdout))
|
|
5669
6383
|
);
|
|
5670
6384
|
});
|
|
5671
6385
|
}
|
|
@@ -5728,6 +6442,10 @@ var systemPorts = {
|
|
|
5728
6442
|
}
|
|
5729
6443
|
};
|
|
5730
6444
|
|
|
6445
|
+
// src/buildInfo.ts
|
|
6446
|
+
var SENTRY_DSN = true ? "https://50b592efb53dbc1a31cb10d3945c6d52@o4512129906049024.ingest.de.sentry.io/4512174341750864" : "";
|
|
6447
|
+
var SENTRY_ENVIRONMENT = true ? "production" : "development";
|
|
6448
|
+
|
|
5731
6449
|
// src/platform/proxy.ts
|
|
5732
6450
|
import { execFile as execFile3 } from "child_process";
|
|
5733
6451
|
|
|
@@ -5754,12 +6472,12 @@ function parseRegProxy(stdout) {
|
|
|
5754
6472
|
|
|
5755
6473
|
// src/platform/proxy.ts
|
|
5756
6474
|
function run3(cmd, args) {
|
|
5757
|
-
return new Promise((
|
|
6475
|
+
return new Promise((resolve6) => {
|
|
5758
6476
|
execFile3(
|
|
5759
6477
|
cmd,
|
|
5760
6478
|
args,
|
|
5761
6479
|
{ timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
|
|
5762
|
-
(e, stdout) =>
|
|
6480
|
+
(e, stdout) => resolve6(e && !stdout ? null : String(stdout))
|
|
5763
6481
|
);
|
|
5764
6482
|
});
|
|
5765
6483
|
}
|
|
@@ -5790,7 +6508,7 @@ function systemProxy(platform = process.platform) {
|
|
|
5790
6508
|
}
|
|
5791
6509
|
|
|
5792
6510
|
// src/version.ts
|
|
5793
|
-
var VERSION = true ? "0.1.
|
|
6511
|
+
var VERSION = true ? "0.1.6" : "0.0.0-dev";
|
|
5794
6512
|
|
|
5795
6513
|
// src/cli.ts
|
|
5796
6514
|
if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
@@ -5833,6 +6551,11 @@ var deps = {
|
|
|
5833
6551
|
process.kill(process.pid, signal);
|
|
5834
6552
|
},
|
|
5835
6553
|
pid: process.pid,
|
|
6554
|
+
registerSecrets: (values) => {
|
|
6555
|
+
telemetry.registerSecrets(values);
|
|
6556
|
+
},
|
|
6557
|
+
sentryDsn: SENTRY_DSN,
|
|
6558
|
+
sentryEnvironment: SENTRY_ENVIRONMENT,
|
|
5836
6559
|
systemProxy: () => systemProxy(process.platform)
|
|
5837
6560
|
};
|
|
5838
6561
|
var termination = preflightTermination(() => ac.abort(), deps.raise);
|
|
@@ -5843,16 +6566,24 @@ var io = processIo(isInteractive({
|
|
|
5843
6566
|
noInput: preSeparatorArgv.includes("--no-input"),
|
|
5844
6567
|
env: process.env
|
|
5845
6568
|
}));
|
|
5846
|
-
|
|
5847
|
-
await termination.reRaise(async () => {
|
|
6569
|
+
var flushStd = async () => {
|
|
5848
6570
|
await Promise.all([process.stdout, process.stderr].map((s) => new Promise((r) => {
|
|
5849
6571
|
s.write("", () => r());
|
|
5850
6572
|
})));
|
|
5851
|
-
}
|
|
6573
|
+
};
|
|
6574
|
+
var telemetry = createTelemetry(deps, { argv });
|
|
6575
|
+
process.exitCode = await run(argv, deps, io, ac.signal, COMMANDS, telemetryHooks(deps, io, telemetry, { json }));
|
|
6576
|
+
var { drained } = await telemetry.end(ac.signal);
|
|
6577
|
+
await termination.reRaise(flushStd);
|
|
5852
6578
|
if (!ac.signal.aborted) {
|
|
5853
6579
|
try {
|
|
5854
6580
|
await maybeNotifyUpdate(deps, io, { json });
|
|
5855
6581
|
} catch {
|
|
5856
6582
|
}
|
|
5857
6583
|
}
|
|
5858
|
-
|
|
6584
|
+
if (!drained) {
|
|
6585
|
+
await flushStd();
|
|
6586
|
+
process.exit();
|
|
6587
|
+
}
|
|
6588
|
+
//# sourceMappingURL=cli.js.map
|
|
6589
|
+
//# debugId=28c91dc8-dbb9-5c0a-91e3-f368931a4ed3
|