@quatronic/qore 0.1.5 → 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/dist/cli.js
CHANGED
|
@@ -1,18 +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]="
|
|
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
4
|
import { createRequire as __qoreCreateRequire } from "node:module"; const require = __qoreCreateRequire(import.meta.url);
|
|
5
5
|
import {
|
|
6
6
|
addSecretValues,
|
|
7
7
|
buildScrubContext
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-OAU6HJHZ.js";
|
|
9
9
|
|
|
10
10
|
// src/cli.ts
|
|
11
11
|
import { homedir as homedir2 } from "os";
|
|
12
12
|
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
|
|
13
13
|
|
|
14
14
|
// src/cli/io.ts
|
|
15
|
-
import { confirm, search, select } from "@inquirer/prompts";
|
|
15
|
+
import { confirm, input, search, select } from "@inquirer/prompts";
|
|
16
16
|
import { createInterface } from "readline";
|
|
17
17
|
|
|
18
18
|
// src/platform/browser.ts
|
|
@@ -51,6 +51,10 @@ var EXIT = {
|
|
|
51
51
|
QORE_REPO_ACCESS_PENDING: 5,
|
|
52
52
|
QORE_GIT_AUTH: 5,
|
|
53
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,
|
|
54
58
|
QORE_AUTH_UNAVAILABLE: 6,
|
|
55
59
|
QORE_AUTH_BUSY: 6,
|
|
56
60
|
QORE_API_UNREACHABLE: 6,
|
|
@@ -152,13 +156,21 @@ function processIo(interactive, stderrTTY = Boolean(process.stderr.isTTY)) {
|
|
|
152
156
|
throw e;
|
|
153
157
|
}
|
|
154
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
|
+
},
|
|
155
167
|
waitForEnter: createWaitForEnter(process.stdin, process.stderr),
|
|
156
168
|
openUrl: openBrowser
|
|
157
169
|
};
|
|
158
170
|
}
|
|
159
|
-
function createWaitForEnter(
|
|
160
|
-
return (message, signal) => new Promise((
|
|
161
|
-
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 });
|
|
162
174
|
const onAbort = () => {
|
|
163
175
|
rl.close();
|
|
164
176
|
reject(new CancelledError());
|
|
@@ -166,7 +178,7 @@ function createWaitForEnter(input, output) {
|
|
|
166
178
|
const done = () => {
|
|
167
179
|
signal.removeEventListener("abort", onAbort);
|
|
168
180
|
rl.close();
|
|
169
|
-
|
|
181
|
+
resolve6();
|
|
170
182
|
};
|
|
171
183
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
172
184
|
rl.on("SIGINT", () => {
|
|
@@ -183,7 +195,7 @@ import { Command, CommanderError } from "commander";
|
|
|
183
195
|
|
|
184
196
|
// src/core/api/http.ts
|
|
185
197
|
function abortableFetch(fetch3, signal) {
|
|
186
|
-
return (
|
|
198
|
+
return (input2, init) => fetch3(input2, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal });
|
|
187
199
|
}
|
|
188
200
|
var TLS = /* @__PURE__ */ new Set([
|
|
189
201
|
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
@@ -490,7 +502,7 @@ async function run(argv2, deps2, io2, signal, register, hooks = {}) {
|
|
|
490
502
|
}
|
|
491
503
|
|
|
492
504
|
// src/cli/commands/clone.ts
|
|
493
|
-
import { join as
|
|
505
|
+
import { join as join9, relative as relative3, resolve as resolve4 } from "path";
|
|
494
506
|
import { Option } from "commander";
|
|
495
507
|
|
|
496
508
|
// src/core/api/meta.ts
|
|
@@ -944,8 +956,8 @@ var ResolutionCache = class {
|
|
|
944
956
|
const f = await this.load();
|
|
945
957
|
const hits = [];
|
|
946
958
|
for (const [k, e] of Object.entries(f.entries)) {
|
|
947
|
-
const
|
|
948
|
-
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() });
|
|
949
961
|
}
|
|
950
962
|
hits.sort((a, b) => b.at - a.at);
|
|
951
963
|
return [...new Set(hits.map((h) => h.origin))];
|
|
@@ -1251,6 +1263,13 @@ async function syncPresent(deps2, results, actions, pull, ctx) {
|
|
|
1251
1263
|
}
|
|
1252
1264
|
}
|
|
1253
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
|
+
|
|
1254
1273
|
// src/core/workspace/file.ts
|
|
1255
1274
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1256
1275
|
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
@@ -1312,17 +1331,225 @@ async function writeWorkspace(root, o) {
|
|
|
1312
1331
|
}
|
|
1313
1332
|
async function workspacePortal(deps2, ws) {
|
|
1314
1333
|
const portal = resolvePortal(ws.portal, void 0);
|
|
1315
|
-
const
|
|
1334
|
+
const label3 = portal.builtin ?? portal.apiOrigin;
|
|
1316
1335
|
const profile = (await deps2.config.read()).profiles[portal.apiOrigin];
|
|
1317
1336
|
const loggedIn = profile && await new TokenStore(deps2.keychain, profileHash(profile.apiOrigin, profile.issuer)).getRefresh();
|
|
1318
1337
|
if (!loggedIn) {
|
|
1319
|
-
throw new QoreError("QORE_NOT_LOGGED_IN", `This workspace belongs to ${
|
|
1320
|
-
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}\`.`
|
|
1321
1340
|
});
|
|
1322
1341
|
}
|
|
1323
1342
|
return portal;
|
|
1324
1343
|
}
|
|
1325
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
|
+
|
|
1326
1553
|
// src/core/workspace/safety.ts
|
|
1327
1554
|
var SAFE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/;
|
|
1328
1555
|
var WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/;
|
|
@@ -1373,9 +1600,9 @@ function firstSentenceLower(hint) {
|
|
|
1373
1600
|
return sentence.length ? sentence[0].toLowerCase() + sentence.slice(1) : sentence;
|
|
1374
1601
|
}
|
|
1375
1602
|
function failedLine(r) {
|
|
1376
|
-
const
|
|
1603
|
+
const label3 = FAILURE_LABELS[r.error?.code ?? ""] ?? "failed";
|
|
1377
1604
|
const detail2 = firstSentenceLower(r.error?.hint);
|
|
1378
|
-
return `\u2717 ${r.slug} ${
|
|
1605
|
+
return `\u2717 ${r.slug} ${label3}${detail2 ? ` \u2014 ${detail2}` : ""}`;
|
|
1379
1606
|
}
|
|
1380
1607
|
function line(r, conflictCount) {
|
|
1381
1608
|
switch (r.status) {
|
|
@@ -1474,61 +1701,76 @@ async function pickApp(deps2, io2, session, tenantFlag) {
|
|
|
1474
1701
|
});
|
|
1475
1702
|
return { tenant: tenant.slug, app };
|
|
1476
1703
|
}
|
|
1477
|
-
function
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
await writeWorkspace(root, { portal: portal.apiOrigin, appId: app.app.id });
|
|
1503
|
-
const dirs = {};
|
|
1504
|
-
const modulesForPlan = [];
|
|
1505
|
-
const earlyFailures = [];
|
|
1506
|
-
for (const m of app.modules) {
|
|
1507
|
-
if (!isSafeSlug(m.slug)) {
|
|
1508
|
-
earlyFailures.push(asFailure(m.slug, unsafeSlugError(m.slug)));
|
|
1509
|
-
continue;
|
|
1510
|
-
}
|
|
1511
|
-
try {
|
|
1512
|
-
dirs[m.slug] = await deps2.git.inspect(join6(root, m.slug));
|
|
1513
|
-
modulesForPlan.push(m);
|
|
1514
|
-
} catch (e) {
|
|
1515
|
-
earlyFailures.push(asFailure(m.slug, e));
|
|
1516
|
-
}
|
|
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
|
+
});
|
|
1517
1728
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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;
|
|
1523
1746
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
if (first && !err) io2.out(`Next: cd ${join6(dirLabel, first.slug)} && qore env pull`);
|
|
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));
|
|
1530
1752
|
}
|
|
1531
|
-
|
|
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
|
+
}
|
|
1771
|
+
function registerClone(program, ctx) {
|
|
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);
|
|
1532
1774
|
});
|
|
1533
1775
|
}
|
|
1534
1776
|
|
|
@@ -1553,6 +1795,13 @@ function describeConsent(consent, built) {
|
|
|
1553
1795
|
}
|
|
1554
1796
|
|
|
1555
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
|
+
}
|
|
1556
1805
|
function registerConfig(program, ctx) {
|
|
1557
1806
|
const config = program.command("config").description("Change qore's own settings");
|
|
1558
1807
|
config.command("telemetry").description("Show or set crash reporting").argument("[state]", "on or off").action(async (state) => {
|
|
@@ -1572,29 +1821,44 @@ function registerConfig(program, ctx) {
|
|
|
1572
1821
|
io2.out(describeConsent(consent, built));
|
|
1573
1822
|
if (state && consent.source !== "config") io2.err(`${consent.source} in your environment overrides this setting.`);
|
|
1574
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" });
|
|
1828
|
+
}
|
|
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;
|
|
1835
|
+
}
|
|
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`);
|
|
1838
|
+
});
|
|
1575
1839
|
}
|
|
1576
1840
|
|
|
1577
1841
|
// src/cli/commands/dev.ts
|
|
1578
|
-
import { randomBytes as
|
|
1579
|
-
import { realpath as
|
|
1842
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
1843
|
+
import { realpath as realpath8 } from "fs/promises";
|
|
1580
1844
|
import { constants as constants3 } from "os";
|
|
1581
|
-
import { dirname as
|
|
1845
|
+
import { dirname as dirname7, join as join21, relative as relative7 } from "path";
|
|
1582
1846
|
import { StringDecoder } from "string_decoder";
|
|
1583
1847
|
|
|
1584
1848
|
// src/core/context/repoConfig.ts
|
|
1585
1849
|
import { readFile as readFile3 } from "fs/promises";
|
|
1586
|
-
import { join as
|
|
1850
|
+
import { join as join10 } from "path";
|
|
1587
1851
|
var FORBIDDEN = ["portal", "api", "apiOrigin", "issuer", "tenant", "tenantId", "app", "appId", "module", "moduleId"];
|
|
1588
1852
|
var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1589
1853
|
var SCRIPT = /^[A-Za-z0-9:_.-]+$/;
|
|
1590
|
-
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 });
|
|
1591
1855
|
function moduleDefaults(type) {
|
|
1592
1856
|
return type === "frontend" ? { envFile: ".env.local", localSecrets: ["SESSION_COOKIE_SECRET"] } : { envFile: ".env", localSecrets: [] };
|
|
1593
1857
|
}
|
|
1594
1858
|
async function readRepoConfig(root) {
|
|
1595
1859
|
let raw;
|
|
1596
1860
|
try {
|
|
1597
|
-
raw = await readFile3(
|
|
1861
|
+
raw = await readFile3(join10(root, ".qore.json"), "utf8");
|
|
1598
1862
|
} catch (e) {
|
|
1599
1863
|
if (e.code === "ENOENT") return {};
|
|
1600
1864
|
throw e;
|
|
@@ -1603,44 +1867,44 @@ async function readRepoConfig(root) {
|
|
|
1603
1867
|
try {
|
|
1604
1868
|
doc = JSON.parse(raw);
|
|
1605
1869
|
} catch {
|
|
1606
|
-
throw
|
|
1870
|
+
throw invalid3("not valid JSON");
|
|
1607
1871
|
}
|
|
1608
|
-
if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw
|
|
1872
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw invalid3("must be an object");
|
|
1609
1873
|
const v = doc.schemaVersion === void 0 ? 1 : doc.schemaVersion;
|
|
1610
|
-
if (!Number.isInteger(v) || v < 1) throw
|
|
1874
|
+
if (!Number.isInteger(v) || v < 1) throw invalid3("schemaVersion must be a positive integer");
|
|
1611
1875
|
if (v > 1) {
|
|
1612
1876
|
throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `.qore.json schemaVersion ${v} is newer than this qore`, { hint: "Run: npm i -g @quatronic/qore" });
|
|
1613
1877
|
}
|
|
1614
|
-
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 });
|
|
1615
1879
|
const out = {};
|
|
1616
1880
|
if (doc.envFile !== void 0) {
|
|
1617
|
-
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");
|
|
1618
1882
|
out.envFile = doc.envFile;
|
|
1619
1883
|
}
|
|
1620
1884
|
if (doc.localSecrets !== void 0) {
|
|
1621
1885
|
if (!Array.isArray(doc.localSecrets) || !doc.localSecrets.every((k) => typeof k === "string" && KEY.test(k))) {
|
|
1622
|
-
throw
|
|
1886
|
+
throw invalid3("localSecrets must be a list of env key names");
|
|
1623
1887
|
}
|
|
1624
|
-
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");
|
|
1625
1889
|
out.localSecrets = doc.localSecrets;
|
|
1626
1890
|
}
|
|
1627
1891
|
if (doc.dev !== void 0) {
|
|
1628
1892
|
const d = doc.dev;
|
|
1629
|
-
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");
|
|
1630
1894
|
const dev = {};
|
|
1631
1895
|
if (d.install !== void 0) {
|
|
1632
|
-
if (typeof d.install !== "boolean") throw
|
|
1896
|
+
if (typeof d.install !== "boolean") throw invalid3("dev.install must be true or false");
|
|
1633
1897
|
dev.install = d.install;
|
|
1634
1898
|
}
|
|
1635
1899
|
if (d.services !== void 0) {
|
|
1636
1900
|
if (!Array.isArray(d.services) || !d.services.every((s) => typeof s === "string" && SCRIPT.test(s))) {
|
|
1637
|
-
throw
|
|
1901
|
+
throw invalid3("dev.services must be a list of script names");
|
|
1638
1902
|
}
|
|
1639
|
-
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");
|
|
1640
1904
|
dev.services = d.services;
|
|
1641
1905
|
}
|
|
1642
1906
|
if (d.script !== void 0) {
|
|
1643
|
-
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");
|
|
1644
1908
|
dev.script = d.script;
|
|
1645
1909
|
}
|
|
1646
1910
|
out.dev = dev;
|
|
@@ -1648,25 +1912,8 @@ async function readRepoConfig(root) {
|
|
|
1648
1912
|
return out;
|
|
1649
1913
|
}
|
|
1650
1914
|
|
|
1651
|
-
// src/core/context/repoRoot.ts
|
|
1652
|
-
import { stat } from "fs/promises";
|
|
1653
|
-
import { dirname as dirname2, join as join8, resolve as resolve3 } from "path";
|
|
1654
|
-
async function findRepoRoot(start2) {
|
|
1655
|
-
let dir2 = resolve3(start2);
|
|
1656
|
-
for (; ; ) {
|
|
1657
|
-
try {
|
|
1658
|
-
await stat(join8(dir2, ".git"));
|
|
1659
|
-
return dir2;
|
|
1660
|
-
} catch {
|
|
1661
|
-
}
|
|
1662
|
-
const up = dirname2(dir2);
|
|
1663
|
-
if (up === dir2) return null;
|
|
1664
|
-
dir2 = up;
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
1915
|
// src/core/context/resolver.ts
|
|
1669
|
-
import { isAbsolute, join as
|
|
1916
|
+
import { isAbsolute as isAbsolute2, join as join11, relative as relative4 } from "path";
|
|
1670
1917
|
var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1671
1918
|
var HINT_FLAG = "cd into a module repository, or pass --module <slug|id>.";
|
|
1672
1919
|
var RESOLVE = "/api/cli/v1/repos/resolve";
|
|
@@ -1774,8 +2021,8 @@ async function byFlag(deps2, o, cache, ws) {
|
|
|
1774
2021
|
return { portal, session, module, source: overrides ? { kind: "flag", overrides } : { kind: "flag" }, repoRoot, workspace: ws };
|
|
1775
2022
|
}
|
|
1776
2023
|
var isBelow = (root, dir2) => {
|
|
1777
|
-
const rel =
|
|
1778
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
2024
|
+
const rel = relative4(root, dir2);
|
|
2025
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
|
|
1779
2026
|
};
|
|
1780
2027
|
async function inWorkspaceRoot(deps2, ws) {
|
|
1781
2028
|
if (!ws) return false;
|
|
@@ -1879,7 +2126,7 @@ async function anyLoggedIn(deps2, cfg) {
|
|
|
1879
2126
|
return false;
|
|
1880
2127
|
}
|
|
1881
2128
|
async function resolveModuleContext(deps2, o) {
|
|
1882
|
-
const cache = new ResolutionCache(
|
|
2129
|
+
const cache = new ResolutionCache(join11(deps2.config.dir, "cache"), deps2.now);
|
|
1883
2130
|
const ws = await findWorkspace(deps2.cwd, deps2.home);
|
|
1884
2131
|
if (o.module) return byFlag(deps2, o, cache, ws);
|
|
1885
2132
|
const cfg = await deps2.config.read();
|
|
@@ -2030,7 +2277,7 @@ function exitHint(tail, h) {
|
|
|
2030
2277
|
}
|
|
2031
2278
|
|
|
2032
2279
|
// src/core/dev/env.ts
|
|
2033
|
-
import { dirname as
|
|
2280
|
+
import { dirname as dirname6, join as join14 } from "path";
|
|
2034
2281
|
|
|
2035
2282
|
// src/core/env/fetch.ts
|
|
2036
2283
|
async function fetchLocalEnv(deps2, mc, app) {
|
|
@@ -2132,8 +2379,8 @@ function renderEnv(original, merged) {
|
|
|
2132
2379
|
}
|
|
2133
2380
|
|
|
2134
2381
|
// src/core/env/pull.ts
|
|
2135
|
-
import { lstat, realpath as
|
|
2136
|
-
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";
|
|
2137
2384
|
var outside = (target) => new QoreError("QORE_CONTEXT_INVALID", `The env file must be inside the repository (got ${target})`, {
|
|
2138
2385
|
hint: "Use a path inside the repository for --target or .qore.json envFile."
|
|
2139
2386
|
});
|
|
@@ -2141,29 +2388,29 @@ var symlinked = (target) => new QoreError("QORE_CONTEXT_INVALID", `The env file
|
|
|
2141
2388
|
hint: "Use a path inside the repository for --target or .qore.json envFile."
|
|
2142
2389
|
});
|
|
2143
2390
|
function inside(root, p) {
|
|
2144
|
-
const rel =
|
|
2145
|
-
return rel !== "" && rel !== ".." && !rel.startsWith(".." +
|
|
2391
|
+
const rel = relative5(root, p);
|
|
2392
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(".." + sep2) && !isAbsolute3(rel);
|
|
2146
2393
|
}
|
|
2147
2394
|
async function resolveTarget(root, target, base = root) {
|
|
2148
|
-
const realRoot = await
|
|
2149
|
-
const realBase = base === root ? realRoot : await
|
|
2150
|
-
const abs =
|
|
2151
|
-
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);
|
|
2152
2399
|
let parent;
|
|
2153
2400
|
try {
|
|
2154
|
-
parent = await
|
|
2401
|
+
parent = await realpath5(dirname4(abs));
|
|
2155
2402
|
} catch (e) {
|
|
2156
2403
|
if (e.code !== "ENOENT") throw e;
|
|
2157
2404
|
throw new QoreError("QORE_CONTEXT_INVALID", `The directory for ${target} does not exist`, {
|
|
2158
2405
|
hint: "Create the parent directory, or point --target / .qore.json envFile at an existing one."
|
|
2159
2406
|
});
|
|
2160
2407
|
}
|
|
2161
|
-
const final =
|
|
2408
|
+
const final = resolve5(parent, basename(abs));
|
|
2162
2409
|
if (!inside(realRoot, final)) throw outside(target);
|
|
2163
2410
|
try {
|
|
2164
2411
|
const st = await lstat(final);
|
|
2165
2412
|
if (st.isSymbolicLink()) throw symlinked(target);
|
|
2166
|
-
const realFinal = await
|
|
2413
|
+
const realFinal = await realpath5(final);
|
|
2167
2414
|
if (!inside(realRoot, realFinal)) throw outside(target);
|
|
2168
2415
|
} catch (e) {
|
|
2169
2416
|
if (e instanceof QoreError) throw e;
|
|
@@ -2199,9 +2446,9 @@ function planPull(o) {
|
|
|
2199
2446
|
}
|
|
2200
2447
|
|
|
2201
2448
|
// src/core/env/write.ts
|
|
2202
|
-
import { chmod as chmod2, readFile as readFile4, rename as rename3, unlink, writeFile as
|
|
2203
|
-
import { randomBytes as
|
|
2204
|
-
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";
|
|
2205
2452
|
async function readOrNull(p) {
|
|
2206
2453
|
try {
|
|
2207
2454
|
return await readFile4(p, "utf8");
|
|
@@ -2212,10 +2459,10 @@ async function readOrNull(p) {
|
|
|
2212
2459
|
}
|
|
2213
2460
|
async function writeSecurely(target, text2) {
|
|
2214
2461
|
const base = basename2(target);
|
|
2215
|
-
const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${
|
|
2216
|
-
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);
|
|
2217
2464
|
try {
|
|
2218
|
-
await
|
|
2465
|
+
await writeFile4(tmp, text2, { mode: 384, flag: "wx" });
|
|
2219
2466
|
await chmod2(tmp, 384);
|
|
2220
2467
|
await rename3(tmp, target);
|
|
2221
2468
|
} finally {
|
|
@@ -2241,8 +2488,8 @@ async function syncEnvFile(o) {
|
|
|
2241
2488
|
|
|
2242
2489
|
// src/core/dev/lastStart.ts
|
|
2243
2490
|
import { createHash as createHash2 } from "crypto";
|
|
2244
|
-
import { realpath as
|
|
2245
|
-
import { join as
|
|
2491
|
+
import { realpath as realpath6 } from "fs/promises";
|
|
2492
|
+
import { join as join13 } from "path";
|
|
2246
2493
|
var NETWORK_REASONS = /* @__PURE__ */ new Set(["connect", "dns", "tls", "timeout"]);
|
|
2247
2494
|
function isOffline(e) {
|
|
2248
2495
|
if (!(e instanceof QoreError)) return false;
|
|
@@ -2251,14 +2498,14 @@ function isOffline(e) {
|
|
|
2251
2498
|
return (e.status ?? 0) >= 500 || NETWORK_REASONS.has(String(e.details?.reason));
|
|
2252
2499
|
}
|
|
2253
2500
|
async function stateKey(deps2, root) {
|
|
2254
|
-
const real = await
|
|
2501
|
+
const real = await realpath6(root).catch(() => root);
|
|
2255
2502
|
return createHash2("sha256").update(trustKey(real, deps2.platform)).digest("hex").slice(0, 16);
|
|
2256
2503
|
}
|
|
2257
2504
|
async function saveLastStart(deps2, root, rec) {
|
|
2258
|
-
await writeJsonAtomic(
|
|
2505
|
+
await writeJsonAtomic(join13(deps2.config.dir, "dev"), `${await stateKey(deps2, root)}.json`, rec);
|
|
2259
2506
|
}
|
|
2260
2507
|
async function loadLastStart(deps2, root) {
|
|
2261
|
-
const rec = await readJson(
|
|
2508
|
+
const rec = await readJson(join13(deps2.config.dir, "dev", `${await stateKey(deps2, root)}.json`)).catch(() => null);
|
|
2262
2509
|
return rec && rec.schemaVersion === 1 ? rec : null;
|
|
2263
2510
|
}
|
|
2264
2511
|
|
|
@@ -2278,7 +2525,7 @@ async function devEnv(deps2, o) {
|
|
|
2278
2525
|
if (o.mc && o.app) {
|
|
2279
2526
|
try {
|
|
2280
2527
|
const local = await fetchLocalEnv(deps2, o.mc, o.app);
|
|
2281
|
-
const example = await readOrNull(
|
|
2528
|
+
const example = await readOrNull(join14(dirname6(target), ".env.example"));
|
|
2282
2529
|
const w = await syncEnvFile({
|
|
2283
2530
|
target,
|
|
2284
2531
|
example,
|
|
@@ -2413,7 +2660,7 @@ function providerDown(o) {
|
|
|
2413
2660
|
|
|
2414
2661
|
// src/core/dev/project.ts
|
|
2415
2662
|
import { access, readFile as readFile5 } from "fs/promises";
|
|
2416
|
-
import { join as
|
|
2663
|
+
import { join as join15 } from "path";
|
|
2417
2664
|
var LEGACY_DEV_SCRIPTS = ["node --env-file=.env --import ./dist/instrumentation.js dist/index.js"];
|
|
2418
2665
|
var LOCKFILES = [["pnpm-lock.yaml", "pnpm"], ["package-lock.json", "npm"], ["yarn.lock", "yarn"]];
|
|
2419
2666
|
var exists = (p) => access(p).then(() => true, () => false);
|
|
@@ -2421,7 +2668,7 @@ var unsupported = (message, hint, details) => new QoreError("QORE_DEV_UNSUPPORTE
|
|
|
2421
2668
|
async function readPackageJson(root) {
|
|
2422
2669
|
let raw;
|
|
2423
2670
|
try {
|
|
2424
|
-
raw = await readFile5(
|
|
2671
|
+
raw = await readFile5(join15(root, "package.json"), "utf8");
|
|
2425
2672
|
} catch {
|
|
2426
2673
|
throw unsupported("This repository has no package.json", "qore dev starts Node repositories through their package.json scripts.");
|
|
2427
2674
|
}
|
|
@@ -2437,7 +2684,7 @@ async function detectPackageManager(root, pkg) {
|
|
|
2437
2684
|
if (!m) throw unsupported(`packageManager "${pkg.packageManager}" is not pnpm, npm or yarn`, "qore dev supports pnpm, npm and yarn.");
|
|
2438
2685
|
return { name: m[1], version: m[2].split("+")[0] };
|
|
2439
2686
|
}
|
|
2440
|
-
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 };
|
|
2441
2688
|
throw unsupported("No package manager declared and no lockfile found", 'Add "packageManager": "pnpm@<version>" to package.json.');
|
|
2442
2689
|
}
|
|
2443
2690
|
function planRecipe(o) {
|
|
@@ -2480,21 +2727,21 @@ function installArgs(pm) {
|
|
|
2480
2727
|
}
|
|
2481
2728
|
|
|
2482
2729
|
// src/core/dev/install.ts
|
|
2483
|
-
import { stat as
|
|
2484
|
-
import { join as
|
|
2730
|
+
import { stat as stat3 } from "fs/promises";
|
|
2731
|
+
import { join as join16 } from "path";
|
|
2485
2732
|
var MARKERS = {
|
|
2486
2733
|
pnpm: [".modules.yaml"],
|
|
2487
2734
|
npm: [".package-lock.json"],
|
|
2488
2735
|
yarn: [".yarn-state.yml", ".yarn-integrity"]
|
|
2489
2736
|
};
|
|
2490
|
-
var
|
|
2491
|
-
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);
|
|
2492
2739
|
async function needsInstall(root, pm) {
|
|
2493
|
-
if (await mtime(
|
|
2740
|
+
if (await mtime(join16(root, "node_modules")) === null) return true;
|
|
2494
2741
|
let marker = null;
|
|
2495
|
-
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));
|
|
2496
2743
|
if (marker === null) return true;
|
|
2497
|
-
const lock = await mtime(
|
|
2744
|
+
const lock = await mtime(join16(root, LOCK2[pm.name]));
|
|
2498
2745
|
return lock !== null && lock > marker;
|
|
2499
2746
|
}
|
|
2500
2747
|
|
|
@@ -2810,11 +3057,11 @@ async function checkPort(deps2, o) {
|
|
|
2810
3057
|
throw await portInUseError(deps2, { slug: o.slug, port: o.port, key: o.key, runs: o.runs });
|
|
2811
3058
|
}
|
|
2812
3059
|
function abortableDelay(signal) {
|
|
2813
|
-
return (ms) => new Promise((
|
|
3060
|
+
return (ms) => new Promise((resolve6) => {
|
|
2814
3061
|
const done = () => {
|
|
2815
3062
|
clearTimeout(t);
|
|
2816
3063
|
signal.removeEventListener("abort", done);
|
|
2817
|
-
|
|
3064
|
+
resolve6();
|
|
2818
3065
|
};
|
|
2819
3066
|
const t = setTimeout(done, ms);
|
|
2820
3067
|
signal.addEventListener("abort", done, { once: true });
|
|
@@ -2897,11 +3144,11 @@ function hintContext(recipe, pkg, env) {
|
|
|
2897
3144
|
|
|
2898
3145
|
// src/core/dev/runFile.ts
|
|
2899
3146
|
import { unlink as unlink2 } from "fs/promises";
|
|
2900
|
-
import { join as
|
|
3147
|
+
import { join as join17 } from "path";
|
|
2901
3148
|
async function runFilePath(deps2, root) {
|
|
2902
|
-
const dir2 =
|
|
3149
|
+
const dir2 = join17(deps2.config.dir, "run");
|
|
2903
3150
|
const name = `${await stateKey(deps2, root)}.json`;
|
|
2904
|
-
return { dir: dir2, name, path:
|
|
3151
|
+
return { dir: dir2, name, path: join17(dir2, name) };
|
|
2905
3152
|
}
|
|
2906
3153
|
async function writeRunFile(deps2, root, rec) {
|
|
2907
3154
|
const { dir: dir2, name } = await runFilePath(deps2, root);
|
|
@@ -2922,6 +3169,177 @@ async function leftoverRun(deps2, root, o = {}) {
|
|
|
2922
3169
|
return null;
|
|
2923
3170
|
}
|
|
2924
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
|
+
|
|
2925
3343
|
// src/cli/context.ts
|
|
2926
3344
|
var allOfKind = (c, kind) => !!c && c.length > 0 && c.every((x) => x.kind === kind && typeof x.id === "string");
|
|
2927
3345
|
async function moduleContextFor(ctx, flags, command) {
|
|
@@ -2933,11 +3351,11 @@ async function moduleContextFor(ctx, flags, command) {
|
|
|
2933
3351
|
} catch (e) {
|
|
2934
3352
|
if (!(e instanceof QoreError) || e.code !== "QORE_CONTEXT_REQUIRED" || !ctx.io.interactive) throw e;
|
|
2935
3353
|
if (allOfKind(e.candidates, "module")) {
|
|
2936
|
-
const
|
|
3354
|
+
const choose2 = await ctx.io.select(
|
|
2937
3355
|
"Select a module",
|
|
2938
3356
|
e.candidates.map((c) => ({ name: `${c.tenant ? `${c.tenant} / ` : ""}${c.slug} (${c.remote})`, value: c.id }))
|
|
2939
3357
|
);
|
|
2940
|
-
return resolveModuleContext(ctx.deps, { ...base, choose });
|
|
3358
|
+
return resolveModuleContext(ctx.deps, { ...base, choose: choose2 });
|
|
2941
3359
|
}
|
|
2942
3360
|
if (allOfKind(e.candidates, "tenant")) {
|
|
2943
3361
|
const tenant = await ctx.io.select("Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.id })));
|
|
@@ -3145,9 +3563,9 @@ async function ensureTrusted(ctx, mc, app, o) {
|
|
|
3145
3563
|
}
|
|
3146
3564
|
|
|
3147
3565
|
// src/cli/commands/devWorkspace.ts
|
|
3148
|
-
import { randomBytes as
|
|
3149
|
-
import { realpath as
|
|
3150
|
-
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";
|
|
3151
3569
|
|
|
3152
3570
|
// src/core/dev/order.ts
|
|
3153
3571
|
function selectSkips(modules, sel) {
|
|
@@ -3304,15 +3722,15 @@ function supervise(o) {
|
|
|
3304
3722
|
});
|
|
3305
3723
|
const readiness = /* @__PURE__ */ new Map();
|
|
3306
3724
|
for (const m of o.modules) {
|
|
3307
|
-
let
|
|
3725
|
+
let resolve6;
|
|
3308
3726
|
let settled = false;
|
|
3309
3727
|
const promise = new Promise((r) => {
|
|
3310
|
-
|
|
3728
|
+
resolve6 = r;
|
|
3311
3729
|
});
|
|
3312
3730
|
readiness.set(m.slug, { promise, settle: (r) => {
|
|
3313
3731
|
if (settled) return false;
|
|
3314
3732
|
settled = true;
|
|
3315
|
-
|
|
3733
|
+
resolve6(r);
|
|
3316
3734
|
return true;
|
|
3317
3735
|
} });
|
|
3318
3736
|
}
|
|
@@ -3403,12 +3821,12 @@ function supervise(o) {
|
|
|
3403
3821
|
}
|
|
3404
3822
|
|
|
3405
3823
|
// src/core/workspace/stray.ts
|
|
3406
|
-
import { lstat as lstat2, readdir } from "fs/promises";
|
|
3407
|
-
import { join as
|
|
3824
|
+
import { lstat as lstat2, readdir as readdir3 } from "fs/promises";
|
|
3825
|
+
import { join as join19 } from "path";
|
|
3408
3826
|
async function strayRepos(root, slugs) {
|
|
3409
3827
|
let entries;
|
|
3410
3828
|
try {
|
|
3411
|
-
entries = (await
|
|
3829
|
+
entries = (await readdir3(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3412
3830
|
} catch {
|
|
3413
3831
|
return [];
|
|
3414
3832
|
}
|
|
@@ -3416,7 +3834,7 @@ async function strayRepos(root, slugs) {
|
|
|
3416
3834
|
for (const name of entries) {
|
|
3417
3835
|
if (slugs.has(name)) continue;
|
|
3418
3836
|
try {
|
|
3419
|
-
await lstat2(
|
|
3837
|
+
await lstat2(join19(root, name, ".git"));
|
|
3420
3838
|
names.push(name);
|
|
3421
3839
|
} catch {
|
|
3422
3840
|
}
|
|
@@ -3456,16 +3874,18 @@ function prefixer(slugs, color) {
|
|
|
3456
3874
|
import { constants as constants2 } from "os";
|
|
3457
3875
|
var childRunning = { current: false };
|
|
3458
3876
|
function registerRun(program, ctx) {
|
|
3459
|
-
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) => {
|
|
3460
|
-
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) => {
|
|
3461
3878
|
if (ctx.opts().json) {
|
|
3462
3879
|
throw new QoreError("QORE_CONTEXT_INVALID", "run has no --json output", {
|
|
3463
3880
|
hint: "Drop --json; the child's own output passes through."
|
|
3464
3881
|
});
|
|
3465
3882
|
}
|
|
3466
|
-
const
|
|
3467
|
-
const
|
|
3468
|
-
|
|
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 });
|
|
3469
3889
|
const local = await fetchLocalEnv(deps2, mc, app);
|
|
3470
3890
|
childRunning.current = true;
|
|
3471
3891
|
let exit;
|
|
@@ -3499,7 +3919,9 @@ async function workspaceContext(ctx, ws) {
|
|
|
3499
3919
|
const session = await openSession(deps2, portal, "dev", requestLog(ctx));
|
|
3500
3920
|
await ensureCapability(deps2, portal.apiOrigin, "app.modules");
|
|
3501
3921
|
await ensureCapability(deps2, portal.apiOrigin, "module.local-env");
|
|
3502
|
-
|
|
3922
|
+
const am = await workspaceAppModules(session, portal, ws);
|
|
3923
|
+
await rememberWorkspace(ctx, ws, am.app);
|
|
3924
|
+
return { am, online: { portal, session }, offlineCause: null };
|
|
3503
3925
|
} catch (e) {
|
|
3504
3926
|
if (!isOffline(e)) throw e;
|
|
3505
3927
|
const last = await loadLastStart(deps2, ws.root);
|
|
@@ -3574,7 +3996,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3574
3996
|
continue;
|
|
3575
3997
|
}
|
|
3576
3998
|
try {
|
|
3577
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
3999
|
+
dirs[m.slug] = await deps2.git.inspect(join20(ws.root, m.slug));
|
|
3578
4000
|
safe.push(m);
|
|
3579
4001
|
} catch (e) {
|
|
3580
4002
|
if (!(e instanceof QoreError) || e.code === "QORE_GIT_UNAVAILABLE") throw e;
|
|
@@ -3611,7 +4033,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3611
4033
|
const results = await applyActions(deps2, { portal: online.portal, app: am, actions, protocol: cloneProtocol(dirs), onResult: () => {
|
|
3612
4034
|
} });
|
|
3613
4035
|
renderResults(io2, results, false);
|
|
3614
|
-
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));
|
|
3615
4037
|
({ present, plan } = computePlan());
|
|
3616
4038
|
}
|
|
3617
4039
|
for (const name of await strayRepos(ws.root, new Set(am.modules.map((m) => m.slug)))) renderFinding(ctx, strayClone(name, am.app.name));
|
|
@@ -3719,7 +4141,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3719
4141
|
workspace: ws,
|
|
3720
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] }
|
|
3721
4143
|
});
|
|
3722
|
-
const rootReal = await
|
|
4144
|
+
const rootReal = await realpath7(ws.root);
|
|
3723
4145
|
const withDocker = dockerRecovery(ctx, p.signal);
|
|
3724
4146
|
for (const u of units.filter((x) => x.recipe && x.trusted)) {
|
|
3725
4147
|
const slug = u.m.slug;
|
|
@@ -3732,7 +4154,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3732
4154
|
type: u.m.type,
|
|
3733
4155
|
moduleId: u.m.id,
|
|
3734
4156
|
cfg: u.cfg,
|
|
3735
|
-
random: () =>
|
|
4157
|
+
random: () => randomBytes5(32).toString("hex")
|
|
3736
4158
|
});
|
|
3737
4159
|
} catch (e) {
|
|
3738
4160
|
if (offlineCause && isOffline(e)) throw offlineCause;
|
|
@@ -3742,7 +4164,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3742
4164
|
const dev = u.dev;
|
|
3743
4165
|
if (!dev) continue;
|
|
3744
4166
|
if (online && dev.offline) {
|
|
3745
|
-
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 });
|
|
3746
4168
|
}
|
|
3747
4169
|
if (dev.kept.length) {
|
|
3748
4170
|
warn(
|
|
@@ -3750,7 +4172,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3750
4172
|
{ module: slug, keys: dev.kept }
|
|
3751
4173
|
);
|
|
3752
4174
|
}
|
|
3753
|
-
report({ module: slug, step: "env", status: "done", detail:
|
|
4175
|
+
report({ module: slug, step: "env", status: "done", detail: relative6(rootReal, dev.envFile) });
|
|
3754
4176
|
const services = servicesMissing(slug, dev.env, u.recipe, { servicesFlag: f.services, configured: u.cfg.dev?.services !== void 0 });
|
|
3755
4177
|
if (services) renderFinding(ctx, services);
|
|
3756
4178
|
try {
|
|
@@ -3792,7 +4214,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
|
|
|
3792
4214
|
if (online && units.every((u) => !u.dev.offline)) {
|
|
3793
4215
|
const envFile = (m) => {
|
|
3794
4216
|
const u = units.find((x) => x.m.id === m.id);
|
|
3795
|
-
return u ?
|
|
4217
|
+
return u ? relative6(rootReal, u.dev.envFile) : join20(m.slug, moduleDefaults(m.type).envFile);
|
|
3796
4218
|
};
|
|
3797
4219
|
await saveLastStart(deps2, ws.root, {
|
|
3798
4220
|
schemaVersion: 1,
|
|
@@ -3981,11 +4403,17 @@ function makeStderrTee(sink) {
|
|
|
3981
4403
|
};
|
|
3982
4404
|
}
|
|
3983
4405
|
function registerDev(program, ctx) {
|
|
3984
|
-
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) => {
|
|
3985
4407
|
const waitTimeout = parseWaitTimeout(f.waitTimeout);
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
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);
|
|
3989
4417
|
});
|
|
3990
4418
|
}
|
|
3991
4419
|
function parseWaitTimeout(raw) {
|
|
@@ -4025,7 +4453,7 @@ async function providerFindings(ctx, root, mc, app, env) {
|
|
|
4025
4453
|
let fix = `qore clone ${app.slug}`;
|
|
4026
4454
|
if (ws && ws.appId === app.id) fix = cdAndDev(deps2.home, ws.root);
|
|
4027
4455
|
else if (providers.length === 1) {
|
|
4028
|
-
const sibling =
|
|
4456
|
+
const sibling = join21(dirname7(root), providers[0].slug);
|
|
4029
4457
|
if ((await deps2.git.inspect(sibling)).isRepo) fix = cdAndDev(deps2.home, sibling);
|
|
4030
4458
|
}
|
|
4031
4459
|
return providerDown({
|
|
@@ -4059,6 +4487,7 @@ async function devContext(ctx, f, root) {
|
|
|
4059
4487
|
return { online: null, module: last.module, offlineCause: e };
|
|
4060
4488
|
}
|
|
4061
4489
|
const app = await appFor(ctx, mc, f.app);
|
|
4490
|
+
await rememberContext(ctx, mc, app);
|
|
4062
4491
|
await ensureTrusted(ctx, mc, app, { yes: f.yes });
|
|
4063
4492
|
return { online: { mc, app }, module: mc.module, offlineCause: null };
|
|
4064
4493
|
}
|
|
@@ -4154,13 +4583,13 @@ async function prepareModule(ctx, f, waitTimeout, p) {
|
|
|
4154
4583
|
type: c.module.type,
|
|
4155
4584
|
moduleId: c.module.id,
|
|
4156
4585
|
cfg,
|
|
4157
|
-
random: () =>
|
|
4586
|
+
random: () => randomBytes6(32).toString("hex")
|
|
4158
4587
|
});
|
|
4159
4588
|
} catch (e) {
|
|
4160
4589
|
if (c.offlineCause && isOffline(e)) throw c.offlineCause;
|
|
4161
4590
|
throw e;
|
|
4162
4591
|
}
|
|
4163
|
-
const envFile =
|
|
4592
|
+
const envFile = relative7(await realpath8(root), dev.envFile);
|
|
4164
4593
|
if (dev.offline) io2.err(`! The portal is unreachable \u2014 starting from ${envFile} as last written; values may be stale.`);
|
|
4165
4594
|
if (dev.kept.length) {
|
|
4166
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`);
|
|
@@ -4218,8 +4647,8 @@ function registerDebugTelemetry(program, ctx) {
|
|
|
4218
4647
|
import pc3 from "picocolors";
|
|
4219
4648
|
|
|
4220
4649
|
// src/core/doctor/checks.ts
|
|
4221
|
-
import { realpath as
|
|
4222
|
-
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";
|
|
4223
4652
|
|
|
4224
4653
|
// src/core/diagnostics/ssh.ts
|
|
4225
4654
|
var SSH_TEST_ARGS = ["-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "git@github.com"];
|
|
@@ -4264,8 +4693,8 @@ var UPGRADE = "npm i -g @quatronic/qore";
|
|
|
4264
4693
|
var RANK = { ok: 0, warn: 1, fail: 2 };
|
|
4265
4694
|
var noop = () => {
|
|
4266
4695
|
};
|
|
4267
|
-
var ok = (
|
|
4268
|
-
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) });
|
|
4269
4698
|
var isCancel = (e, signal) => e instanceof CancelledError || signal.aborted;
|
|
4270
4699
|
var portalLabel = (p) => p.builtin ?? p.apiOrigin;
|
|
4271
4700
|
var ToolTimeout = class extends Error {
|
|
@@ -4401,7 +4830,7 @@ function ownerText(e) {
|
|
|
4401
4830
|
if (d.owner === "qore") return ` by a previous qore dev (pid ${String(d.pid)})`;
|
|
4402
4831
|
return "";
|
|
4403
4832
|
}
|
|
4404
|
-
var sameDir = async (a, b) => await
|
|
4833
|
+
var sameDir = async (a, b) => await realpath9(a).catch(() => a) === await realpath9(b).catch(() => b);
|
|
4405
4834
|
async function moduleBody(deps2, m, runs, signal) {
|
|
4406
4835
|
const parts = [];
|
|
4407
4836
|
const part = async (fn) => {
|
|
@@ -4499,6 +4928,15 @@ async function resolveRepoModule(deps2, session, portal, meta, root) {
|
|
|
4499
4928
|
}
|
|
4500
4929
|
return session.api.get(`/api/cli/v1/modules/${encodeURIComponent(ids[0])}`);
|
|
4501
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
|
+
}
|
|
4502
4940
|
function checkTelemetry(deps2, cfg) {
|
|
4503
4941
|
const built = deps2.sentryDsn !== "";
|
|
4504
4942
|
if (!cfg) return ok("crash reports off (config unreadable)", { enabled: false, source: "config", reason: "unreadable", built });
|
|
@@ -4554,6 +4992,7 @@ async function runDoctor(deps2, o = {}) {
|
|
|
4554
4992
|
await push("github-ssh", "tools", () => checkGithubSsh(deps2, signal, toolMs));
|
|
4555
4993
|
await push("docker", "tools", () => checkDocker(deps2, signal, toolMs));
|
|
4556
4994
|
await push("telemetry", "tools", () => checkTelemetry(deps2, cfgReadable ? cfg : null));
|
|
4995
|
+
await push("projects-root", "tools", () => checkProjectsRoot(cfgReadable ? cfg : null));
|
|
4557
4996
|
const sessions = /* @__PURE__ */ new Map();
|
|
4558
4997
|
let loggedIn = [];
|
|
4559
4998
|
try {
|
|
@@ -4646,7 +5085,7 @@ async function workspaceModule(deps2, ws, am, m, runs, signal, push, perModule)
|
|
|
4646
5085
|
await push(`module:${m.slug}`, "workspace", () => ({ status: "warn", label: "not a safe directory name \u2014 skipped" }), m.slug);
|
|
4647
5086
|
return;
|
|
4648
5087
|
}
|
|
4649
|
-
const mod = { root:
|
|
5088
|
+
const mod = { root: join22(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
|
|
4650
5089
|
let prepared = null;
|
|
4651
5090
|
await push(`module:${m.slug}`, "workspace", async () => {
|
|
4652
5091
|
const info = await deps2.git.inspect(mod.root);
|
|
@@ -4676,26 +5115,28 @@ function registerDoctor(program, ctx) {
|
|
|
4676
5115
|
}
|
|
4677
5116
|
|
|
4678
5117
|
// src/cli/commands/env.ts
|
|
4679
|
-
import { randomBytes as
|
|
4680
|
-
import { chmod as chmod3, realpath as
|
|
4681
|
-
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";
|
|
4682
5121
|
var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.length} chars)`;
|
|
4683
5122
|
function registerEnv(program, ctx) {
|
|
4684
5123
|
const env = program.command("env").description("Local environment");
|
|
4685
|
-
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) => {
|
|
4686
|
-
const {
|
|
4687
|
-
const
|
|
4688
|
-
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");
|
|
4689
5129
|
if (!mc.repoRoot) {
|
|
4690
5130
|
throw new QoreError("QORE_CONTEXT_REQUIRED", "env pull writes into a module repository", { hint: "cd into the module's repository." });
|
|
4691
5131
|
}
|
|
4692
|
-
const app = await appFor(
|
|
5132
|
+
const app = await appFor(c, mc, f.app);
|
|
5133
|
+
await rememberContext(c, mc, app);
|
|
4693
5134
|
const cfg = await readRepoConfig(mc.repoRoot);
|
|
4694
5135
|
const defaults = moduleDefaults(mc.module.type);
|
|
4695
5136
|
const target = f.target ? await resolveTarget(mc.repoRoot, f.target, deps2.cwd) : await resolveTarget(mc.repoRoot, cfg.envFile ?? defaults.envFile);
|
|
4696
|
-
await ensureTrusted(
|
|
5137
|
+
await ensureTrusted(c, mc, app, { yes: f.yes });
|
|
4697
5138
|
const existing = await readOrNull(target);
|
|
4698
|
-
const example = existing === null ? await readOrNull(
|
|
5139
|
+
const example = existing === null ? await readOrNull(join23(dirname8(target), ".env.example")) : null;
|
|
4699
5140
|
const local = await fetchLocalEnv(deps2, mc, app);
|
|
4700
5141
|
const inputs = {
|
|
4701
5142
|
existing,
|
|
@@ -4704,7 +5145,7 @@ function registerEnv(program, ctx) {
|
|
|
4704
5145
|
managed: local.managedKeys,
|
|
4705
5146
|
localSecrets: cfg.localSecrets ?? defaults.localSecrets,
|
|
4706
5147
|
defaultChoice: f.usePortal ? "use-portal" : "keep",
|
|
4707
|
-
random: () =>
|
|
5148
|
+
random: () => randomBytes7(32).toString("hex")
|
|
4708
5149
|
};
|
|
4709
5150
|
let plan = planPull(inputs);
|
|
4710
5151
|
if (plan.conflicts.length && io2.interactive && !f.usePortal) {
|
|
@@ -4736,7 +5177,7 @@ function registerEnv(program, ctx) {
|
|
|
4736
5177
|
} else {
|
|
4737
5178
|
await writeSecurely(target, plan.text);
|
|
4738
5179
|
}
|
|
4739
|
-
const rel =
|
|
5180
|
+
const rel = relative8(await realpath10(mc.repoRoot), target);
|
|
4740
5181
|
if (g.json) {
|
|
4741
5182
|
const names = {
|
|
4742
5183
|
created: plan.created,
|
|
@@ -4765,6 +5206,9 @@ function registerEnv(program, ctx) {
|
|
|
4765
5206
|
});
|
|
4766
5207
|
}
|
|
4767
5208
|
|
|
5209
|
+
// src/cli/commands/init.ts
|
|
5210
|
+
import { realpath as realpath11 } from "fs/promises";
|
|
5211
|
+
|
|
4768
5212
|
// src/core/auth/oidc.ts
|
|
4769
5213
|
import * as oc from "openid-client";
|
|
4770
5214
|
var LOGIN_SCOPE = "openid profile email offline_access qore:read qore:local-env";
|
|
@@ -4896,78 +5340,201 @@ async function deviceLogin(opts) {
|
|
|
4896
5340
|
}
|
|
4897
5341
|
|
|
4898
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
|
+
}
|
|
4899
5416
|
function registerLogin(program, ctx) {
|
|
4900
|
-
program.command("login").description("Log in with your Qore account").option("--no-browser", "only print the URL").action(async (cmd) =>
|
|
4901
|
-
|
|
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;
|
|
4902
5425
|
const g = ctx.opts();
|
|
4903
5426
|
const cfg = await deps2.config.read();
|
|
4904
5427
|
const portal = resolvePortal(g.portal, cfg.defaultProfile);
|
|
4905
|
-
const
|
|
4906
|
-
const known = cfg.profiles[portal.apiOrigin]?.issuer;
|
|
4907
|
-
const pinned = { ...portal, pinnedIssuer: portal.pinnedIssuer ?? known ?? null };
|
|
4908
|
-
if (!pinned.pinnedIssuer && portal.builtin === null) {
|
|
4909
|
-
io2.err(`! Logging in to ${portal.apiOrigin}, identity provider ${meta.auth.issuer}`);
|
|
4910
|
-
}
|
|
4911
|
-
negotiate(meta, pinned, deps2.version);
|
|
4912
|
-
const issuer = pinned.pinnedIssuer ?? meta.auth.issuer;
|
|
4913
|
-
const hash = profileHash(portal.apiOrigin, issuer);
|
|
4914
|
-
const store = new TokenStore(deps2.keychain, hash);
|
|
4915
|
-
const t = await deviceLogin({
|
|
4916
|
-
oidc: await deps2.oidcFor(issuer),
|
|
4917
|
-
store,
|
|
4918
|
-
now: deps2.now,
|
|
4919
|
-
signal,
|
|
4920
|
-
withLock: deps2.withLock,
|
|
4921
|
-
lockName: hash,
|
|
4922
|
-
onCode: async (s, settled) => {
|
|
4923
|
-
const url = s.verificationUriComplete ?? s.verificationUri;
|
|
4924
|
-
io2.err(`! First copy your one-time code: ${s.userCode}`);
|
|
4925
|
-
if (io2.interactive && cmd.browser) {
|
|
4926
|
-
try {
|
|
4927
|
-
await io2.waitForEnter(` Press Enter to open ${url} (or open it yourself)`, AbortSignal.any([signal, settled]));
|
|
4928
|
-
} catch (e) {
|
|
4929
|
-
if (settled.aborted && !signal.aborted) return;
|
|
4930
|
-
throw e;
|
|
4931
|
-
}
|
|
4932
|
-
if (settled.aborted) return;
|
|
4933
|
-
if (!await io2.openUrl(url)) io2.err(` Open ${url} in a browser`);
|
|
4934
|
-
} else {
|
|
4935
|
-
io2.err(` Open ${url} in a browser`);
|
|
4936
|
-
}
|
|
4937
|
-
}
|
|
4938
|
-
});
|
|
4939
|
-
await deps2.config.update((c) => {
|
|
4940
|
-
c.profiles[portal.apiOrigin] = {
|
|
4941
|
-
...c.profiles[portal.apiOrigin],
|
|
4942
|
-
apiOrigin: portal.apiOrigin,
|
|
4943
|
-
issuer,
|
|
4944
|
-
sub: t.sub,
|
|
4945
|
-
email: t.email,
|
|
4946
|
-
meta: { fetchedAt: deps2.now().toISOString(), doc: meta }
|
|
4947
|
-
};
|
|
4948
|
-
if (g.portal || !c.defaultProfile) c.defaultProfile = portal.apiOrigin;
|
|
4949
|
-
});
|
|
4950
|
-
const { api } = await openSession(deps2, { ...portal, pinnedIssuer: issuer }, "login", requestLog(ctx));
|
|
5428
|
+
const { profile, api } = await openSession(deps2, portal, "tenant list", requestLog(ctx));
|
|
4951
5429
|
const me = await api.get("/api/cli/v1/me");
|
|
4952
|
-
const
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
io2.err("! Your account is not a developer in any organisation. Ask an admin to invite you as developer.");
|
|
4957
|
-
} else if (me.tenants.length === 1) {
|
|
4958
|
-
active = { id: me.tenants[0].id, slug: me.tenants[0].slug };
|
|
4959
|
-
io2.err(`\u2713 Using tenant ${me.tenants[0].name}`);
|
|
4960
|
-
} else if (io2.interactive) {
|
|
4961
|
-
active = await io2.select("Select a tenant", me.tenants.map((x) => ({ name: `${x.name} ${x.role}`, value: { id: x.id, slug: x.slug } })));
|
|
4962
|
-
} else {
|
|
4963
|
-
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;
|
|
4964
5434
|
}
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
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.`);
|
|
4971
5538
|
});
|
|
4972
5539
|
}
|
|
4973
5540
|
|
|
@@ -5035,6 +5602,7 @@ async function appStatus(ctx, ws, appFlag) {
|
|
|
5035
5602
|
await ensureCapability(deps2, portal.apiOrigin, "app.modules");
|
|
5036
5603
|
await ensureCapability(deps2, portal.apiOrigin, "module.status");
|
|
5037
5604
|
const am = await workspaceAppModules(session, portal, ws);
|
|
5605
|
+
await rememberWorkspace(ctx, ws, am.app);
|
|
5038
5606
|
const { tenant, ...app } = am.app;
|
|
5039
5607
|
if (appFlag && appFlag !== app.id && appFlag !== app.slug) {
|
|
5040
5608
|
throw new QoreError("QORE_RESOURCE_NOT_FOUND", `This workspace is for ${app.slug}, not "${appFlag}"`, {
|
|
@@ -5062,19 +5630,21 @@ async function appStatus(ctx, ws, appFlag) {
|
|
|
5062
5630
|
for (const line2 of table(rows)) io2.out(line2);
|
|
5063
5631
|
}
|
|
5064
5632
|
function registerStatus(program, ctx) {
|
|
5065
|
-
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) => {
|
|
5066
|
-
const {
|
|
5067
|
-
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();
|
|
5068
5637
|
if (!flags.module) {
|
|
5069
5638
|
const ws = await workspaceRoot(deps2);
|
|
5070
5639
|
if (ws) {
|
|
5071
|
-
await appStatus(
|
|
5640
|
+
await appStatus(c, ws, flags.app);
|
|
5072
5641
|
return;
|
|
5073
5642
|
}
|
|
5074
5643
|
}
|
|
5075
|
-
const mc = await moduleContextFor(
|
|
5644
|
+
const mc = await moduleContextFor(c, flags, "status");
|
|
5076
5645
|
await ensureCapability(deps2, mc.portal.apiOrigin, "module.status");
|
|
5077
|
-
const app = await appFor(
|
|
5646
|
+
const app = await appFor(c, mc, flags.app);
|
|
5647
|
+
await rememberContext(c, mc, app);
|
|
5078
5648
|
if (!g.json) io2.err(contextLine(mc, app));
|
|
5079
5649
|
const st = await mc.session.api.get(
|
|
5080
5650
|
`/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/status?app_id=${encodeURIComponent(app.id)}`
|
|
@@ -5094,7 +5664,7 @@ function registerStatus(program, ctx) {
|
|
|
5094
5664
|
}
|
|
5095
5665
|
|
|
5096
5666
|
// src/cli/commands/sync.ts
|
|
5097
|
-
import { join as
|
|
5667
|
+
import { join as join24, relative as relative9 } from "path";
|
|
5098
5668
|
import { Option as Option2 } from "commander";
|
|
5099
5669
|
function asFailure2(slug, e) {
|
|
5100
5670
|
if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
|
|
@@ -5126,7 +5696,7 @@ function registerSync(program, ctx) {
|
|
|
5126
5696
|
continue;
|
|
5127
5697
|
}
|
|
5128
5698
|
try {
|
|
5129
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
5699
|
+
dirs[m.slug] = await deps2.git.inspect(join24(ws.root, m.slug));
|
|
5130
5700
|
modulesForPlan.push(m);
|
|
5131
5701
|
} catch (e) {
|
|
5132
5702
|
earlyFailures.push(asFailure2(m.slug, e));
|
|
@@ -5138,7 +5708,7 @@ function registerSync(program, ctx) {
|
|
|
5138
5708
|
} });
|
|
5139
5709
|
await syncPresent(deps2, applied, actions, Boolean(flags.pull), { app, protocol: flags.protocol });
|
|
5140
5710
|
for (const r of applied) {
|
|
5141
|
-
if (r.status === "cloned" && r.dir) r.dir =
|
|
5711
|
+
if (r.status === "cloned" && r.dir) r.dir = relative9(deps2.cwd, r.dir) || ".";
|
|
5142
5712
|
}
|
|
5143
5713
|
const removedResults = removed.map((slug) => ({ slug, status: "removed", reason: "not a module of this app \u2014 left in place" }));
|
|
5144
5714
|
const results = [...earlyFailures, ...applied, ...removedResults];
|
|
@@ -5147,52 +5717,6 @@ function registerSync(program, ctx) {
|
|
|
5147
5717
|
});
|
|
5148
5718
|
}
|
|
5149
5719
|
|
|
5150
|
-
// src/cli/commands/tenant.ts
|
|
5151
|
-
function registerTenant(program, ctx) {
|
|
5152
|
-
const tenant = program.command("tenant").description("Manage the active tenant");
|
|
5153
|
-
tenant.command("list").description("List tenants you have access to").action(async () => {
|
|
5154
|
-
const { deps: deps2, io: io2 } = ctx;
|
|
5155
|
-
const g = ctx.opts();
|
|
5156
|
-
const cfg = await deps2.config.read();
|
|
5157
|
-
const portal = resolvePortal(g.portal, cfg.defaultProfile);
|
|
5158
|
-
const { profile, api } = await openSession(deps2, portal, "tenant list", requestLog(ctx));
|
|
5159
|
-
const me = await api.get("/api/cli/v1/me");
|
|
5160
|
-
const activeId = profile.activeTenant?.id;
|
|
5161
|
-
if (g.json) {
|
|
5162
|
-
renderJson(io2, { tenants: me.tenants.map((t) => ({ ...t, active: t.id === activeId })) });
|
|
5163
|
-
return;
|
|
5164
|
-
}
|
|
5165
|
-
for (const t of me.tenants) {
|
|
5166
|
-
const mark = t.id === activeId ? "*" : " ";
|
|
5167
|
-
io2.out(`${mark} ${t.name} ${t.role}`);
|
|
5168
|
-
}
|
|
5169
|
-
});
|
|
5170
|
-
tenant.command("use").description("Set the active tenant").argument("[slug]", "tenant slug").action(async (slug) => {
|
|
5171
|
-
const { deps: deps2, io: io2 } = ctx;
|
|
5172
|
-
const g = ctx.opts();
|
|
5173
|
-
const cfg = await deps2.config.read();
|
|
5174
|
-
const portal = resolvePortal(g.portal, cfg.defaultProfile);
|
|
5175
|
-
const { api } = await openSession(deps2, portal, "tenant use", requestLog(ctx));
|
|
5176
|
-
const me = await api.get("/api/cli/v1/me");
|
|
5177
|
-
const candidates = me.tenants.map((t) => ({ kind: "tenant", id: t.id, slug: t.slug, name: t.name }));
|
|
5178
|
-
let chosen;
|
|
5179
|
-
if (slug) {
|
|
5180
|
-
const found = me.tenants.find((t) => t.slug === slug);
|
|
5181
|
-
if (!found) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `No tenant "${slug}" for this account`, { candidates });
|
|
5182
|
-
chosen = { id: found.id, slug: found.slug };
|
|
5183
|
-
} else if (io2.interactive) {
|
|
5184
|
-
chosen = await io2.select("Select a tenant", me.tenants.map((t) => ({ name: `${t.name} ${t.role}`, value: { id: t.id, slug: t.slug } })));
|
|
5185
|
-
} else {
|
|
5186
|
-
throw new QoreError("QORE_CONTEXT_REQUIRED", "Which tenant? Pass a slug or run interactively.", { candidates });
|
|
5187
|
-
}
|
|
5188
|
-
await deps2.config.update((c) => {
|
|
5189
|
-
c.profiles[portal.apiOrigin].activeTenant = chosen;
|
|
5190
|
-
});
|
|
5191
|
-
if (g.json) renderJson(io2, { activeTenant: chosen });
|
|
5192
|
-
else io2.out(`\u2713 Using tenant ${chosen.slug}`);
|
|
5193
|
-
});
|
|
5194
|
-
}
|
|
5195
|
-
|
|
5196
5720
|
// src/cli/commands/version.ts
|
|
5197
5721
|
function registerVersion(program, ctx) {
|
|
5198
5722
|
program.command("version").description("Show the CLI and portal version").action(async () => {
|
|
@@ -5212,11 +5736,11 @@ function registerVersion(program, ctx) {
|
|
|
5212
5736
|
return;
|
|
5213
5737
|
}
|
|
5214
5738
|
io2.out(`qore ${deps2.version}`);
|
|
5215
|
-
const
|
|
5739
|
+
const label3 = portal.builtin ?? portal.apiOrigin;
|
|
5216
5740
|
if (portalInfo) {
|
|
5217
|
-
io2.out(`portal ${
|
|
5741
|
+
io2.out(`portal ${label3}: api ${portalInfo.apiVersions.join(",")}, min ${portalInfo.minCliVersion}, latest ${portalInfo.latestCliVersion}`);
|
|
5218
5742
|
} else {
|
|
5219
|
-
io2.out(`portal ${
|
|
5743
|
+
io2.out(`portal ${label3}: unreachable`);
|
|
5220
5744
|
}
|
|
5221
5745
|
});
|
|
5222
5746
|
}
|
|
@@ -5230,18 +5754,18 @@ function registerWhoami(program, ctx) {
|
|
|
5230
5754
|
const portal = resolvePortal(g.portal, cfg.defaultProfile);
|
|
5231
5755
|
const { profile, api } = await openSession(deps2, portal, "whoami", requestLog(ctx));
|
|
5232
5756
|
const me = await api.get("/api/cli/v1/me");
|
|
5233
|
-
const
|
|
5234
|
-
if (
|
|
5235
|
-
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}`);
|
|
5236
5760
|
}
|
|
5237
|
-
const
|
|
5761
|
+
const label3 = portal.builtin ?? portal.apiOrigin;
|
|
5238
5762
|
if (g.json) {
|
|
5239
|
-
renderJson(io2, { portal:
|
|
5763
|
+
renderJson(io2, { portal: label3, activeTenant: activeTenant2 ?? null, me });
|
|
5240
5764
|
return;
|
|
5241
5765
|
}
|
|
5242
|
-
io2.out(`${me.email} (${
|
|
5766
|
+
io2.out(`${me.email} (${label3})`);
|
|
5243
5767
|
for (const t of me.tenants) {
|
|
5244
|
-
const mark =
|
|
5768
|
+
const mark = activeTenant2?.id === t.id ? "*" : " ";
|
|
5245
5769
|
io2.out(`${mark} ${t.name} ${t.role}`);
|
|
5246
5770
|
for (const a of t.apps) {
|
|
5247
5771
|
io2.out(` ${a.slug} ${a.access}`);
|
|
@@ -5255,6 +5779,7 @@ function registerWhoami(program, ctx) {
|
|
|
5255
5779
|
|
|
5256
5780
|
// src/cli/commands/index.ts
|
|
5257
5781
|
var COMMANDS = [
|
|
5782
|
+
registerInit,
|
|
5258
5783
|
registerLogin,
|
|
5259
5784
|
registerLogout,
|
|
5260
5785
|
registerWhoami,
|
|
@@ -5294,7 +5819,7 @@ async function showNoticeOnce(deps2, io2, o) {
|
|
|
5294
5819
|
// src/cli/telemetry/index.ts
|
|
5295
5820
|
var CRASH_FLUSH_MS = 2e3;
|
|
5296
5821
|
var CLEAN_FLUSH_MS = 200;
|
|
5297
|
-
var loadSentry = async () => (await import("./sentry-
|
|
5822
|
+
var loadSentry = async () => (await import("./sentry-GMM4X3FT.js")).createSdk;
|
|
5298
5823
|
function createTelemetry(deps2, o) {
|
|
5299
5824
|
const load = o.load ?? loadSentry;
|
|
5300
5825
|
const flushMs = o.flushMs ?? { crash: CRASH_FLUSH_MS, clean: CLEAN_FLUSH_MS };
|
|
@@ -5348,11 +5873,11 @@ async function start(deps2, argv2, command, load, onScrub) {
|
|
|
5348
5873
|
}
|
|
5349
5874
|
function withDeadline(work, ms, signal) {
|
|
5350
5875
|
if (signal?.aborted) return Promise.resolve(false);
|
|
5351
|
-
return new Promise((
|
|
5876
|
+
return new Promise((resolve6) => {
|
|
5352
5877
|
const done = (v) => {
|
|
5353
5878
|
clearTimeout(timer);
|
|
5354
5879
|
signal?.removeEventListener("abort", onAbort);
|
|
5355
|
-
|
|
5880
|
+
resolve6(v);
|
|
5356
5881
|
};
|
|
5357
5882
|
const onAbort = () => done(false);
|
|
5358
5883
|
const timer = setTimeout(() => done(false), ms);
|
|
@@ -5404,7 +5929,7 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
|
|
|
5404
5929
|
|
|
5405
5930
|
// src/core/config.ts
|
|
5406
5931
|
import { readFile as readFile6 } from "fs/promises";
|
|
5407
|
-
import { join as
|
|
5932
|
+
import { join as join25 } from "path";
|
|
5408
5933
|
var empty = () => ({ schemaVersion: 1, profiles: {} });
|
|
5409
5934
|
var ConfigStore = class {
|
|
5410
5935
|
constructor(dir2) {
|
|
@@ -5412,7 +5937,7 @@ var ConfigStore = class {
|
|
|
5412
5937
|
}
|
|
5413
5938
|
dir;
|
|
5414
5939
|
get file() {
|
|
5415
|
-
return
|
|
5940
|
+
return join25(this.dir, "config.json");
|
|
5416
5941
|
}
|
|
5417
5942
|
async read() {
|
|
5418
5943
|
let raw;
|
|
@@ -5443,9 +5968,9 @@ var ConfigStore = class {
|
|
|
5443
5968
|
|
|
5444
5969
|
// src/platform/git.ts
|
|
5445
5970
|
import { execFile, execFileSync } from "child_process";
|
|
5446
|
-
import { mkdir as
|
|
5971
|
+
import { mkdir as mkdir4, readdir as readdir4, stat as stat6 } from "fs/promises";
|
|
5447
5972
|
import { homedir } from "os";
|
|
5448
|
-
import { dirname as
|
|
5973
|
+
import { dirname as dirname9, join as join26 } from "path";
|
|
5449
5974
|
function parseRemotes(stdout) {
|
|
5450
5975
|
const seen = /* @__PURE__ */ new Set();
|
|
5451
5976
|
const out = [];
|
|
@@ -5512,7 +6037,7 @@ function gitExecOptions(repoDir) {
|
|
|
5512
6037
|
return { cwd: homedir(), env, windowsHide: true, maxBuffer: 8 * 1024 * 1024 };
|
|
5513
6038
|
}
|
|
5514
6039
|
function runGit(args, repoDir) {
|
|
5515
|
-
return new Promise((
|
|
6040
|
+
return new Promise((resolve6, reject) => {
|
|
5516
6041
|
execFile("git", args, gitExecOptions(repoDir), (err, stdout, stderr) => {
|
|
5517
6042
|
if (err && err.code === "ENOENT") {
|
|
5518
6043
|
reject(new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
|
|
@@ -5521,7 +6046,7 @@ function runGit(args, repoDir) {
|
|
|
5521
6046
|
return;
|
|
5522
6047
|
}
|
|
5523
6048
|
const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
|
|
5524
|
-
|
|
6049
|
+
resolve6({ code, stdout: String(stdout), stderr: String(stderr), err: err ?? void 0 });
|
|
5525
6050
|
});
|
|
5526
6051
|
});
|
|
5527
6052
|
}
|
|
@@ -5539,7 +6064,7 @@ async function remotes(repoRoot) {
|
|
|
5539
6064
|
return parseRemotes(r.stdout);
|
|
5540
6065
|
}
|
|
5541
6066
|
async function clone(url, dir2) {
|
|
5542
|
-
await
|
|
6067
|
+
await mkdir4(dirname9(dir2), { recursive: true });
|
|
5543
6068
|
const r = await runGit(["clone", "--", url, dir2]);
|
|
5544
6069
|
return toResult(r, "git clone");
|
|
5545
6070
|
}
|
|
@@ -5559,7 +6084,7 @@ async function status(repoRoot) {
|
|
|
5559
6084
|
}
|
|
5560
6085
|
async function statOrThrow(path) {
|
|
5561
6086
|
try {
|
|
5562
|
-
return await
|
|
6087
|
+
return await stat6(path);
|
|
5563
6088
|
} catch (err) {
|
|
5564
6089
|
if (err.code === "ENOENT") return void 0;
|
|
5565
6090
|
throw new QoreError("QORE_INTERNAL", `could not stat ${path}`, { cause: err });
|
|
@@ -5571,12 +6096,12 @@ async function inspect(dir2) {
|
|
|
5571
6096
|
if (!st.isDirectory()) return { exists: true, empty: false, isRepo: false, remotes: [] };
|
|
5572
6097
|
let entries;
|
|
5573
6098
|
try {
|
|
5574
|
-
entries = await
|
|
6099
|
+
entries = await readdir4(dir2);
|
|
5575
6100
|
} catch (err) {
|
|
5576
6101
|
throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
|
|
5577
6102
|
}
|
|
5578
6103
|
const empty2 = entries.length === 0;
|
|
5579
|
-
const isRepo = await statOrThrow(
|
|
6104
|
+
const isRepo = await statOrThrow(join26(dir2, ".git")) !== void 0;
|
|
5580
6105
|
const dirRemotes = isRepo ? await remotes(dir2) : [];
|
|
5581
6106
|
return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
|
|
5582
6107
|
}
|
|
@@ -5637,15 +6162,15 @@ var OsKeychain = class {
|
|
|
5637
6162
|
};
|
|
5638
6163
|
|
|
5639
6164
|
// src/platform/lock.ts
|
|
5640
|
-
import { mkdir as
|
|
5641
|
-
import { join as
|
|
6165
|
+
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
6166
|
+
import { join as join27 } from "path";
|
|
5642
6167
|
import lockfile from "proper-lockfile";
|
|
5643
6168
|
function fileLock(dir2) {
|
|
5644
6169
|
return async (name, fn) => {
|
|
5645
|
-
const locks =
|
|
5646
|
-
await
|
|
5647
|
-
const target =
|
|
5648
|
-
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" });
|
|
5649
6174
|
let release;
|
|
5650
6175
|
let compromised;
|
|
5651
6176
|
try {
|
|
@@ -5688,7 +6213,7 @@ var STDERR_DRAIN_MS = 500;
|
|
|
5688
6213
|
function makeSpawner(spawnFn, proc) {
|
|
5689
6214
|
return {
|
|
5690
6215
|
run(cmd, args, env, o) {
|
|
5691
|
-
return new Promise((
|
|
6216
|
+
return new Promise((resolve6, reject) => {
|
|
5692
6217
|
let child;
|
|
5693
6218
|
try {
|
|
5694
6219
|
child = spawnFn(cmd, args, {
|
|
@@ -5722,7 +6247,7 @@ function makeSpawner(spawnFn, proc) {
|
|
|
5722
6247
|
cleanup();
|
|
5723
6248
|
const stderr = child.stderr;
|
|
5724
6249
|
if (!o?.onStderr || !stderr || stderr.readableEnded) {
|
|
5725
|
-
|
|
6250
|
+
resolve6({ code, signal });
|
|
5726
6251
|
return;
|
|
5727
6252
|
}
|
|
5728
6253
|
let settled = false;
|
|
@@ -5730,7 +6255,7 @@ function makeSpawner(spawnFn, proc) {
|
|
|
5730
6255
|
if (settled) return;
|
|
5731
6256
|
settled = true;
|
|
5732
6257
|
clearTimeout(timer);
|
|
5733
|
-
|
|
6258
|
+
resolve6({ code, signal });
|
|
5734
6259
|
};
|
|
5735
6260
|
const timer = setTimeout(finish, STDERR_DRAIN_MS);
|
|
5736
6261
|
timer.unref?.();
|
|
@@ -5785,13 +6310,13 @@ var systemLauncher = {
|
|
|
5785
6310
|
child.stdout?.on("data", (c) => out.push(c));
|
|
5786
6311
|
child.stderr?.on("data", (c) => err.push(c));
|
|
5787
6312
|
let closed = false;
|
|
5788
|
-
const exited = new Promise((
|
|
5789
|
-
child.once("error", (e) =>
|
|
6313
|
+
const exited = new Promise((resolve6) => {
|
|
6314
|
+
child.once("error", (e) => resolve6({ code: null, signal: null, spawnError: e.code ?? "error" }));
|
|
5790
6315
|
child.once("close", (code, signal) => {
|
|
5791
6316
|
closed = true;
|
|
5792
6317
|
out.flush();
|
|
5793
6318
|
err.flush();
|
|
5794
|
-
|
|
6319
|
+
resolve6({ code, signal });
|
|
5795
6320
|
});
|
|
5796
6321
|
});
|
|
5797
6322
|
return {
|
|
@@ -5837,11 +6362,11 @@ var systemLauncher = {
|
|
|
5837
6362
|
import { execFile as execFile2 } from "child_process";
|
|
5838
6363
|
import { connect } from "net";
|
|
5839
6364
|
function accepts(host, port) {
|
|
5840
|
-
return new Promise((
|
|
6365
|
+
return new Promise((resolve6) => {
|
|
5841
6366
|
const s = connect({ host, port });
|
|
5842
6367
|
const done = (v) => {
|
|
5843
6368
|
s.destroy();
|
|
5844
|
-
|
|
6369
|
+
resolve6(v);
|
|
5845
6370
|
};
|
|
5846
6371
|
s.setTimeout(500, () => done(false));
|
|
5847
6372
|
s.once("connect", () => done(true));
|
|
@@ -5849,12 +6374,12 @@ function accepts(host, port) {
|
|
|
5849
6374
|
});
|
|
5850
6375
|
}
|
|
5851
6376
|
function run2(cmd, args) {
|
|
5852
|
-
return new Promise((
|
|
6377
|
+
return new Promise((resolve6) => {
|
|
5853
6378
|
execFile2(
|
|
5854
6379
|
cmd,
|
|
5855
6380
|
args,
|
|
5856
6381
|
{ timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
|
|
5857
|
-
(e, stdout) =>
|
|
6382
|
+
(e, stdout) => resolve6(e && !stdout ? null : String(stdout))
|
|
5858
6383
|
);
|
|
5859
6384
|
});
|
|
5860
6385
|
}
|
|
@@ -5947,12 +6472,12 @@ function parseRegProxy(stdout) {
|
|
|
5947
6472
|
|
|
5948
6473
|
// src/platform/proxy.ts
|
|
5949
6474
|
function run3(cmd, args) {
|
|
5950
|
-
return new Promise((
|
|
6475
|
+
return new Promise((resolve6) => {
|
|
5951
6476
|
execFile3(
|
|
5952
6477
|
cmd,
|
|
5953
6478
|
args,
|
|
5954
6479
|
{ timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
|
|
5955
|
-
(e, stdout) =>
|
|
6480
|
+
(e, stdout) => resolve6(e && !stdout ? null : String(stdout))
|
|
5956
6481
|
);
|
|
5957
6482
|
});
|
|
5958
6483
|
}
|
|
@@ -5983,7 +6508,7 @@ function systemProxy(platform = process.platform) {
|
|
|
5983
6508
|
}
|
|
5984
6509
|
|
|
5985
6510
|
// src/version.ts
|
|
5986
|
-
var VERSION = true ? "0.1.
|
|
6511
|
+
var VERSION = true ? "0.1.6" : "0.0.0-dev";
|
|
5987
6512
|
|
|
5988
6513
|
// src/cli.ts
|
|
5989
6514
|
if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
@@ -6061,4 +6586,4 @@ if (!drained) {
|
|
|
6061
6586
|
process.exit();
|
|
6062
6587
|
}
|
|
6063
6588
|
//# sourceMappingURL=cli.js.map
|
|
6064
|
-
//# debugId=
|
|
6589
|
+
//# debugId=28c91dc8-dbb9-5c0a-91e3-f368931a4ed3
|