@quatronic/qore 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/cli.js +313 -78
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ qore version # CLI + portal version info
|
|
|
23
23
|
|
|
24
24
|
qore env pull # write this module's local secrets into an env file
|
|
25
25
|
qore run -- <command> # run a command with those secrets in its environment
|
|
26
|
+
qore roles list # roles in your code, on local, and yours there
|
|
27
|
+
qore roles use <role…> # announce your code's roles for local and take these
|
|
26
28
|
|
|
27
29
|
qore errors --env stag # recent unresolved errors
|
|
28
30
|
qore errors FRONTEND-1A --env prod --with trace,logs # one error, its trace and logs
|
|
@@ -145,6 +147,17 @@ not in the interactive conflict prompt, not in `--verbose` logs, and not in
|
|
|
145
147
|
the portal's own audit trail, which records key names only (see
|
|
146
148
|
`app.local_env.fetched`).
|
|
147
149
|
|
|
150
|
+
#### `qore roles`
|
|
151
|
+
|
|
152
|
+
Run in the app's frontend module. `qore roles use inspector` reads the roles in
|
|
153
|
+
`src/lib/permissions.ts`, tells the portal they exist on `local` (only there —
|
|
154
|
+
staging and production learn roles from the code that environment deploys:
|
|
155
|
+
`main` for staging, the `production` branch for production), and makes exactly
|
|
156
|
+
the named roles yours on `local`. Sign out and back in to the local app
|
|
157
|
+
afterwards. `qore roles list` shows the roles in your code, the ones `local`
|
|
158
|
+
knows, and yours. Needs write on the module, like `qore env pull`. An announced
|
|
159
|
+
role stays on `local`; nothing removes it automatically.
|
|
160
|
+
|
|
148
161
|
#### `qore run`
|
|
149
162
|
|
|
150
163
|
Runs a command with this module's local secrets injected into its
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2eacac78-c916-5f6b-825a-d5af2c955f32")}catch(e){}}();
|
|
4
4
|
import { createRequire as __qoreCreateRequire } from "node:module"; const require = __qoreCreateRequire(import.meta.url);
|
|
5
5
|
import {
|
|
6
6
|
addSecretValues,
|
|
@@ -655,7 +655,7 @@ var QoreApiClient = class {
|
|
|
655
655
|
this.o = o;
|
|
656
656
|
}
|
|
657
657
|
o;
|
|
658
|
-
async once(method,
|
|
658
|
+
async once(method, path2, body, force) {
|
|
659
659
|
const token = await this.o.token(force);
|
|
660
660
|
const headers = {
|
|
661
661
|
authorization: `Bearer ${token}`,
|
|
@@ -667,7 +667,7 @@ var QoreApiClient = class {
|
|
|
667
667
|
if (body !== void 0) headers["content-type"] = "application/json";
|
|
668
668
|
let res;
|
|
669
669
|
try {
|
|
670
|
-
res = await this.o.fetch(`${this.o.apiOrigin}${
|
|
670
|
+
res = await this.o.fetch(`${this.o.apiOrigin}${path2}`, {
|
|
671
671
|
method,
|
|
672
672
|
headers,
|
|
673
673
|
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
@@ -676,13 +676,13 @@ var QoreApiClient = class {
|
|
|
676
676
|
throw fetchError(e, new URL(this.o.apiOrigin).host);
|
|
677
677
|
}
|
|
678
678
|
const rid = res.headers.get("x-request-id");
|
|
679
|
-
this.o.onRequest?.(`${method} ${
|
|
679
|
+
this.o.onRequest?.(`${method} ${path2} \u2192 ${res.status}${rid ? ` (${rid})` : ""}`);
|
|
680
680
|
return res;
|
|
681
681
|
}
|
|
682
|
-
async request(method,
|
|
683
|
-
let res = await this.once(method,
|
|
682
|
+
async request(method, path2, body, parse2 = (r) => r.json()) {
|
|
683
|
+
let res = await this.once(method, path2, body, false);
|
|
684
684
|
if (res.status === 401) {
|
|
685
|
-
res = await this.once(method,
|
|
685
|
+
res = await this.once(method, path2, body, true);
|
|
686
686
|
if (res.status === 401) {
|
|
687
687
|
const skew = clockSkewSeconds(res.headers.get("date"), (this.o.now ?? (() => /* @__PURE__ */ new Date()))());
|
|
688
688
|
if (skew !== null && Math.abs(skew) > 60) {
|
|
@@ -703,14 +703,14 @@ var QoreApiClient = class {
|
|
|
703
703
|
if (!res.ok) throw await problemToError(res);
|
|
704
704
|
return parse2(res);
|
|
705
705
|
}
|
|
706
|
-
get(
|
|
707
|
-
return this.request("GET",
|
|
706
|
+
get(path2) {
|
|
707
|
+
return this.request("GET", path2);
|
|
708
708
|
}
|
|
709
|
-
getText(
|
|
710
|
-
return this.request("GET",
|
|
709
|
+
getText(path2) {
|
|
710
|
+
return this.request("GET", path2, void 0, (r) => r.text());
|
|
711
711
|
}
|
|
712
|
-
post(
|
|
713
|
-
return this.request("POST",
|
|
712
|
+
post(path2, body) {
|
|
713
|
+
return this.request("POST", path2, body);
|
|
714
714
|
}
|
|
715
715
|
};
|
|
716
716
|
|
|
@@ -978,7 +978,7 @@ function repoKey(url) {
|
|
|
978
978
|
if (raw.startsWith("git+")) raw = raw.slice(4);
|
|
979
979
|
if (!raw || raw.includes("\\")) return null;
|
|
980
980
|
let host;
|
|
981
|
-
let
|
|
981
|
+
let path2;
|
|
982
982
|
if (raw.includes("://")) {
|
|
983
983
|
if (hasDotSegment(raw.slice(raw.indexOf("://") + 3).split("/").slice(1))) return null;
|
|
984
984
|
let u;
|
|
@@ -989,14 +989,14 @@ function repoKey(url) {
|
|
|
989
989
|
}
|
|
990
990
|
if (!SCHEMES.has(u.protocol.toLowerCase())) return null;
|
|
991
991
|
host = u.hostname;
|
|
992
|
-
|
|
992
|
+
path2 = u.pathname;
|
|
993
993
|
} else {
|
|
994
994
|
const m = SCP.exec(raw);
|
|
995
995
|
if (!m) return null;
|
|
996
996
|
host = m[1];
|
|
997
|
-
|
|
997
|
+
path2 = m[2];
|
|
998
998
|
}
|
|
999
|
-
const segments =
|
|
999
|
+
const segments = path2.split("/").filter(Boolean);
|
|
1000
1000
|
if (!host || segments.length !== 2 || hasDotSegment(segments)) return null;
|
|
1001
1001
|
const owner = segments[0];
|
|
1002
1002
|
let repo = segments[1];
|
|
@@ -1359,10 +1359,10 @@ async function scanRoot(root) {
|
|
|
1359
1359
|
const out = [];
|
|
1360
1360
|
for (const tenant of (await subdirs(root)).sort()) {
|
|
1361
1361
|
for (const app of (await subdirs(join6(root, tenant))).sort()) {
|
|
1362
|
-
const
|
|
1362
|
+
const path2 = join6(root, tenant, app);
|
|
1363
1363
|
try {
|
|
1364
|
-
const ws = await readWorkspace(
|
|
1365
|
-
if (ws) out.push({ kind: "workspace", path, portal: ws.portal, tenantSlug: tenant, appId: ws.appId, appSlug: app });
|
|
1364
|
+
const ws = await readWorkspace(path2);
|
|
1365
|
+
if (ws) out.push({ kind: "workspace", path: path2, portal: ws.portal, tenantSlug: tenant, appId: ws.appId, appSlug: app });
|
|
1366
1366
|
} catch {
|
|
1367
1367
|
}
|
|
1368
1368
|
}
|
|
@@ -1440,8 +1440,8 @@ function trimSeparator(p) {
|
|
|
1440
1440
|
while (end2 > root.length && (p[end2 - 1] === "/" || p[end2 - 1] === "\\")) end2--;
|
|
1441
1441
|
return p.slice(0, end2);
|
|
1442
1442
|
}
|
|
1443
|
-
async function underAFile(
|
|
1444
|
-
for (let p = dirname3(
|
|
1443
|
+
async function underAFile(path2) {
|
|
1444
|
+
for (let p = dirname3(path2); ; p = dirname3(p)) {
|
|
1445
1445
|
const s = await stat2(p).catch((e) => e.code === "ENOENT" ? void 0 : null);
|
|
1446
1446
|
if (s === null) return false;
|
|
1447
1447
|
if (s) return !s.isDirectory();
|
|
@@ -1456,8 +1456,8 @@ function scannedBy(root, e) {
|
|
|
1456
1456
|
if (e.kind !== "workspace" || !isInside(root, e.path)) return false;
|
|
1457
1457
|
return relative2(root, e.path).split(sep).filter(Boolean).length === 2;
|
|
1458
1458
|
}
|
|
1459
|
-
async function writableError(
|
|
1460
|
-
const probe = join8(
|
|
1459
|
+
async function writableError(path2) {
|
|
1460
|
+
const probe = join8(path2, `.qore-write-test-${randomBytes3(6).toString("hex")}`);
|
|
1461
1461
|
try {
|
|
1462
1462
|
await writeFile3(probe, "");
|
|
1463
1463
|
await rm2(probe, { force: true });
|
|
@@ -1466,42 +1466,42 @@ async function writableError(path) {
|
|
|
1466
1466
|
return e;
|
|
1467
1467
|
}
|
|
1468
1468
|
}
|
|
1469
|
-
var invalid2 = (
|
|
1470
|
-
var notWritable = (
|
|
1469
|
+
var invalid2 = (path2, why) => new QoreError("QORE_ROOT_INVALID", `${path2} cannot be the projects root: ${why}`, { hint: "Pick another folder: qore init --root <path>." });
|
|
1470
|
+
var notWritable = (path2, e) => new QoreError("QORE_ROOT_NOT_WRITABLE", `${path2} is not writable (${e.code ?? e.message})`, {
|
|
1471
1471
|
hint: "Fix its permissions, or pick another folder: qore init --root <path>.",
|
|
1472
1472
|
cause: e
|
|
1473
1473
|
});
|
|
1474
1474
|
async function checkRoot(deps2, input2) {
|
|
1475
|
-
const
|
|
1476
|
-
if (dirname3(
|
|
1475
|
+
const path2 = expandPath(deps2, input2);
|
|
1476
|
+
if (dirname3(path2) === path2) throw invalid2(path2, "it is the filesystem root");
|
|
1477
1477
|
try {
|
|
1478
|
-
if (!(await stat2(
|
|
1478
|
+
if (!(await stat2(path2)).isDirectory()) throw invalid2(path2, "it is a file");
|
|
1479
1479
|
} catch (e) {
|
|
1480
1480
|
if (e instanceof QoreError) throw e;
|
|
1481
1481
|
const err = e;
|
|
1482
|
-
if (err.code === "ENOTDIR") throw invalid2(
|
|
1483
|
-
if (NOT_WRITABLE.has(err.code ?? "")) throw notWritable(
|
|
1482
|
+
if (err.code === "ENOTDIR") throw invalid2(path2, "a parent of it is a file");
|
|
1483
|
+
if (NOT_WRITABLE.has(err.code ?? "")) throw notWritable(path2, err);
|
|
1484
1484
|
if (err.code !== "ENOENT") throw e;
|
|
1485
|
-
if (await underAFile(
|
|
1485
|
+
if (await underAFile(path2)) throw invalid2(path2, "a parent of it is a file");
|
|
1486
1486
|
}
|
|
1487
|
-
const repo = await findRepoRoot(
|
|
1488
|
-
if (await findWorkspace(
|
|
1489
|
-
throw invalid2(
|
|
1487
|
+
const repo = await findRepoRoot(path2);
|
|
1488
|
+
if (await findWorkspace(path2, deps2.home) || repo && !isInside(repo, deps2.home)) {
|
|
1489
|
+
throw invalid2(path2, "it lies inside a project (an app workspace or a git repository)");
|
|
1490
1490
|
}
|
|
1491
|
-
return
|
|
1491
|
+
return path2;
|
|
1492
1492
|
}
|
|
1493
|
-
async function createRoot(
|
|
1493
|
+
async function createRoot(path2) {
|
|
1494
1494
|
try {
|
|
1495
|
-
await mkdir3(
|
|
1495
|
+
await mkdir3(path2, { recursive: true });
|
|
1496
1496
|
} catch (e) {
|
|
1497
1497
|
const err2 = e;
|
|
1498
|
-
if (NOT_WRITABLE.has(err2.code ?? "")) throw notWritable(
|
|
1499
|
-
if (err2.code === "ENOTDIR" || err2.code === "EEXIST") throw invalid2(
|
|
1498
|
+
if (NOT_WRITABLE.has(err2.code ?? "")) throw notWritable(path2, err2);
|
|
1499
|
+
if (err2.code === "ENOTDIR" || err2.code === "EEXIST") throw invalid2(path2, "a parent of it is a file");
|
|
1500
1500
|
throw e;
|
|
1501
1501
|
}
|
|
1502
|
-
const err = await writableError(
|
|
1503
|
-
if (err) throw notWritable(
|
|
1504
|
-
return realpath3(
|
|
1502
|
+
const err = await writableError(path2);
|
|
1503
|
+
if (err) throw notWritable(path2, err);
|
|
1504
|
+
return realpath3(path2);
|
|
1505
1505
|
}
|
|
1506
1506
|
async function validateRoot(deps2, input2) {
|
|
1507
1507
|
return createRoot(await checkRoot(deps2, input2));
|
|
@@ -1519,15 +1519,15 @@ async function quietly(fn) {
|
|
|
1519
1519
|
}
|
|
1520
1520
|
async function remember(deps2, e) {
|
|
1521
1521
|
await quietly(async () => {
|
|
1522
|
-
const
|
|
1523
|
-
const entry = { ...e, path };
|
|
1522
|
+
const path2 = await realpath4(e.path);
|
|
1523
|
+
const entry = { ...e, path: path2 };
|
|
1524
1524
|
const cfg = await deps2.config.read();
|
|
1525
1525
|
const root = cfg.projectsRoot ? await realpath4(cfg.projectsRoot).catch(() => cfg.projectsRoot) : void 0;
|
|
1526
1526
|
if (root && scannedBy(root, entry)) return;
|
|
1527
|
-
const current = cfg.projects?.find((p) => p.path ===
|
|
1527
|
+
const current = cfg.projects?.find((p) => p.path === path2);
|
|
1528
1528
|
if (current && same(current, entry)) return;
|
|
1529
1529
|
await deps2.withLock(LOCK, () => deps2.config.update((c) => {
|
|
1530
|
-
c.projects = [...(c.projects ?? []).filter((p) => p.path !==
|
|
1530
|
+
c.projects = [...(c.projects ?? []).filter((p) => p.path !== path2), entry];
|
|
1531
1531
|
}).then(() => void 0));
|
|
1532
1532
|
});
|
|
1533
1533
|
}
|
|
@@ -1825,12 +1825,12 @@ function registerConfig(program, ctx) {
|
|
|
1825
1825
|
io2.out(describeConsent(consent, built));
|
|
1826
1826
|
if (state && consent.source !== "config") io2.err(`${consent.source} in your environment overrides this setting.`);
|
|
1827
1827
|
});
|
|
1828
|
-
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 (
|
|
1828
|
+
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 (path2, f) => {
|
|
1829
1829
|
const { deps: deps2, io: io2 } = ctx;
|
|
1830
|
-
if (
|
|
1830
|
+
if (path2 !== void 0 && f.unset) {
|
|
1831
1831
|
throw new QoreError("QORE_CONTEXT_INVALID", "Pass a path or --unset, not both", { hint: "qore config root <path> | qore config root --unset" });
|
|
1832
1832
|
}
|
|
1833
|
-
if (
|
|
1833
|
+
if (path2 !== void 0) await setRoot(deps2, await validateRoot(deps2, path2));
|
|
1834
1834
|
if (f.unset) await setRoot(deps2, void 0);
|
|
1835
1835
|
const d = await describeRoot(deps2);
|
|
1836
1836
|
if (ctx.opts().json) {
|
|
@@ -2761,8 +2761,8 @@ async function serveErd(o) {
|
|
|
2761
2761
|
const host = req.headers.host;
|
|
2762
2762
|
if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`) return end(res, 403);
|
|
2763
2763
|
if (req.method !== "GET" && req.method !== "HEAD") return end(res, 405);
|
|
2764
|
-
const
|
|
2765
|
-
const produce = o.routes[
|
|
2764
|
+
const path2 = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
2765
|
+
const produce = o.routes[path2];
|
|
2766
2766
|
if (produce) {
|
|
2767
2767
|
try {
|
|
2768
2768
|
const body = JSON.stringify(await produce());
|
|
@@ -2779,7 +2779,7 @@ async function serveErd(o) {
|
|
|
2779
2779
|
return;
|
|
2780
2780
|
}
|
|
2781
2781
|
}
|
|
2782
|
-
const file = resolve6(root, `.${
|
|
2782
|
+
const file = resolve6(root, `.${path2 === "/" ? "/index.html" : path2}`);
|
|
2783
2783
|
if (file !== root && !file.startsWith(root + sep3)) return end(res, 404);
|
|
2784
2784
|
try {
|
|
2785
2785
|
const body = await readFile6(file);
|
|
@@ -3011,10 +3011,10 @@ async function projectCtx(ctx, o) {
|
|
|
3011
3011
|
const m = await choose(ctx, known.map((project) => ({ project })), what, false);
|
|
3012
3012
|
return relocate(ctx, await pathFor(ctx, m, o.needModule));
|
|
3013
3013
|
}
|
|
3014
|
-
function relocate(ctx,
|
|
3015
|
-
if (
|
|
3016
|
-
if (!ctx.opts().json) ctx.io.err(`\u2192 in ${
|
|
3017
|
-
return withCwd(ctx,
|
|
3014
|
+
function relocate(ctx, path2) {
|
|
3015
|
+
if (path2 === ctx.deps.cwd) return ctx;
|
|
3016
|
+
if (!ctx.opts().json) ctx.io.err(`\u2192 in ${path2}`);
|
|
3017
|
+
return withCwd(ctx, path2);
|
|
3018
3018
|
}
|
|
3019
3019
|
async function rememberContext(ctx, mc, app) {
|
|
3020
3020
|
const tenantSlug = mc.module.tenant.slug;
|
|
@@ -4036,16 +4036,16 @@ async function writeRunFile(deps2, root, rec) {
|
|
|
4036
4036
|
await writeJsonAtomic(dir2, name, rec);
|
|
4037
4037
|
}
|
|
4038
4038
|
async function removeRunFile(deps2, root) {
|
|
4039
|
-
const { path } = await runFilePath(deps2, root);
|
|
4040
|
-
await unlink2(
|
|
4039
|
+
const { path: path2 } = await runFilePath(deps2, root);
|
|
4040
|
+
await unlink2(path2).catch(() => {
|
|
4041
4041
|
});
|
|
4042
4042
|
}
|
|
4043
4043
|
async function leftoverRun(deps2, root, o = {}) {
|
|
4044
|
-
const { path } = await runFilePath(deps2, root);
|
|
4045
|
-
const rec = await readJson(
|
|
4044
|
+
const { path: path2 } = await runFilePath(deps2, root);
|
|
4045
|
+
const rec = await readJson(path2).catch(() => null);
|
|
4046
4046
|
if (!rec || rec.schemaVersion !== 1) return null;
|
|
4047
4047
|
if (deps2.launch.isAlive(rec.qorePid)) return rec;
|
|
4048
|
-
if (o.prune !== false) await unlink2(
|
|
4048
|
+
if (o.prune !== false) await unlink2(path2).catch(() => {
|
|
4049
4049
|
});
|
|
4050
4050
|
return null;
|
|
4051
4051
|
}
|
|
@@ -6705,6 +6705,179 @@ function registerLogout(program, ctx) {
|
|
|
6705
6705
|
});
|
|
6706
6706
|
}
|
|
6707
6707
|
|
|
6708
|
+
// src/core/roles/api.ts
|
|
6709
|
+
var CAPABILITY = "module.local-roles";
|
|
6710
|
+
var path = (mc, app) => `/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/local-roles?app_id=${encodeURIComponent(app.id)}`;
|
|
6711
|
+
async function fetchLocalRoles(deps2, mc, app) {
|
|
6712
|
+
await ensureCapability(deps2, mc.portal.apiOrigin, CAPABILITY);
|
|
6713
|
+
return explained(mc, () => mc.session.api.get(path(mc, app)));
|
|
6714
|
+
}
|
|
6715
|
+
async function useLocalRoles(deps2, mc, app, roles, use) {
|
|
6716
|
+
await ensureCapability(deps2, mc.portal.apiOrigin, CAPABILITY);
|
|
6717
|
+
return explained(mc, () => mc.session.api.post(path(mc, app), { roles, use }));
|
|
6718
|
+
}
|
|
6719
|
+
async function explained(mc, call) {
|
|
6720
|
+
try {
|
|
6721
|
+
return await call();
|
|
6722
|
+
} catch (e) {
|
|
6723
|
+
if (!(e instanceof QoreError)) throw e;
|
|
6724
|
+
if (e.serverCode === "app.access.denied") {
|
|
6725
|
+
throw withDetails(
|
|
6726
|
+
e,
|
|
6727
|
+
{ required: "write", have: mc.module.access },
|
|
6728
|
+
`You have ${mc.module.access} on ${mc.module.slug}; local roles need write. Ask a maintainer of the app's dev team for write access.`
|
|
6729
|
+
);
|
|
6730
|
+
}
|
|
6731
|
+
if (e.serverCode === "membership.permission.denied") {
|
|
6732
|
+
throw withDetails(e, {}, "Taking roles on this app needs write on one of its dev teams.");
|
|
6733
|
+
}
|
|
6734
|
+
if (e.serverCode === "auth.scope.insufficient") throw withDetails(e, {}, "Run `qore login` again.");
|
|
6735
|
+
if (e.serverCode === "app.no_platform_identity") {
|
|
6736
|
+
throw new QoreError("QORE_CONTEXT_UNSUPPORTED", "This app manages its own users; the portal hands out no roles for it", {
|
|
6737
|
+
hint: "Give yourself the role in the app's own user admin.",
|
|
6738
|
+
serverCode: e.serverCode,
|
|
6739
|
+
requestId: e.requestId
|
|
6740
|
+
});
|
|
6741
|
+
}
|
|
6742
|
+
throw e;
|
|
6743
|
+
}
|
|
6744
|
+
}
|
|
6745
|
+
|
|
6746
|
+
// src/core/roles/read.ts
|
|
6747
|
+
import { join as join26 } from "path";
|
|
6748
|
+
|
|
6749
|
+
// src/core/roles/extract.ts
|
|
6750
|
+
var KNOWN = { user: "User", admin: "App admin" };
|
|
6751
|
+
function defaultLabel(slug) {
|
|
6752
|
+
return KNOWN[slug] ?? slug.charAt(0).toUpperCase() + slug.slice(1);
|
|
6753
|
+
}
|
|
6754
|
+
async function extractRoles(source) {
|
|
6755
|
+
const { default: ts } = await import("typescript");
|
|
6756
|
+
const sf = ts.createSourceFile("permissions.ts", source, ts.ScriptTarget.Latest, true);
|
|
6757
|
+
let obj;
|
|
6758
|
+
const visit = (node) => {
|
|
6759
|
+
const first = ts.isCallExpression(node) ? node.arguments[0] : void 0;
|
|
6760
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "definePermissions" && first !== void 0 && ts.isObjectLiteralExpression(first)) {
|
|
6761
|
+
obj = first;
|
|
6762
|
+
}
|
|
6763
|
+
if (!obj) ts.forEachChild(node, visit);
|
|
6764
|
+
};
|
|
6765
|
+
visit(sf);
|
|
6766
|
+
if (!obj) return null;
|
|
6767
|
+
const slugs = [];
|
|
6768
|
+
for (const prop of obj.properties) {
|
|
6769
|
+
if (ts.isPropertyAssignment(prop) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))) {
|
|
6770
|
+
slugs.push(prop.name.text);
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
return slugs.map((slug) => ({ slug, label: defaultLabel(slug) }));
|
|
6774
|
+
}
|
|
6775
|
+
|
|
6776
|
+
// src/core/roles/read.ts
|
|
6777
|
+
var PERMISSIONS_FILE = "src/lib/permissions.ts";
|
|
6778
|
+
var SLUG = /^[a-z0-9][a-z0-9_-]*$/;
|
|
6779
|
+
var MAX_SLUG = 64;
|
|
6780
|
+
var MAX_ROLES = 100;
|
|
6781
|
+
async function readCodeRoles(repoRoot) {
|
|
6782
|
+
const source = await readOrNull(join26(repoRoot, PERMISSIONS_FILE));
|
|
6783
|
+
if (source === null) {
|
|
6784
|
+
throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `No ${PERMISSIONS_FILE} in this module`, {
|
|
6785
|
+
hint: "App roles live in the app's frontend module; run this there."
|
|
6786
|
+
});
|
|
6787
|
+
}
|
|
6788
|
+
const roles = await extractRoles(source);
|
|
6789
|
+
if (!roles?.length) {
|
|
6790
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `${PERMISSIONS_FILE} declares no roles in a definePermissions({ ... }) object`, {
|
|
6791
|
+
hint: "The portal reads only an object literal passed straight to definePermissions \u2014 no variable, no spread, no shorthand ({ admin, user })."
|
|
6792
|
+
});
|
|
6793
|
+
}
|
|
6794
|
+
const invalid4 = roles.filter((r) => !SLUG.test(r.slug) || r.slug.length > MAX_SLUG).map((r) => r.slug);
|
|
6795
|
+
if (invalid4.length) {
|
|
6796
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `The portal refuses these role names: ${invalid4.join(", ")}`, {
|
|
6797
|
+
hint: `Use lowercase letters, digits, - and _, starting with a letter or digit, at most ${MAX_SLUG} characters.`,
|
|
6798
|
+
details: { invalid: invalid4 }
|
|
6799
|
+
});
|
|
6800
|
+
}
|
|
6801
|
+
const unique = [...new Map(roles.map((r) => [r.slug, r])).values()];
|
|
6802
|
+
if (unique.length > MAX_ROLES) {
|
|
6803
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `The portal refuses more than ${MAX_ROLES} roles; ${PERMISSIONS_FILE} declares ${unique.length}`, {
|
|
6804
|
+
hint: "Merge roles that grant the same permissions.",
|
|
6805
|
+
details: { count: unique.length, max: MAX_ROLES }
|
|
6806
|
+
});
|
|
6807
|
+
}
|
|
6808
|
+
return unique;
|
|
6809
|
+
}
|
|
6810
|
+
|
|
6811
|
+
// src/cli/commands/roles.ts
|
|
6812
|
+
async function rolesContext(ctx, f, command) {
|
|
6813
|
+
const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true, command });
|
|
6814
|
+
const mc = await moduleContextFor(c, f, command);
|
|
6815
|
+
if (!mc.repoRoot) {
|
|
6816
|
+
throw new QoreError("QORE_CONTEXT_REQUIRED", `${command} reads the roles in a module repository`, {
|
|
6817
|
+
hint: "cd into the app's frontend module."
|
|
6818
|
+
});
|
|
6819
|
+
}
|
|
6820
|
+
const app = await appFor(c, mc, f.app);
|
|
6821
|
+
await rememberContext(c, mc, app);
|
|
6822
|
+
return { c, mc, app, code: await readCodeRoles(mc.repoRoot) };
|
|
6823
|
+
}
|
|
6824
|
+
function withFlags(cmd) {
|
|
6825
|
+
return cmd.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, from anywhere (see qore init)");
|
|
6826
|
+
}
|
|
6827
|
+
function registerRoles(program, ctx) {
|
|
6828
|
+
const roles = program.command("roles").description("App roles on your own machine (the local environment)");
|
|
6829
|
+
withFlags(roles.command("list").description("Roles in your code, roles local knows, and yours there")).action(async (f) => {
|
|
6830
|
+
const { c, mc, app, code } = await rolesContext(ctx, f, "roles list");
|
|
6831
|
+
const local = await fetchLocalRoles(c.deps, mc, app);
|
|
6832
|
+
if (c.opts().json) {
|
|
6833
|
+
renderJson(c.io, {
|
|
6834
|
+
context: contextJson(mc, app),
|
|
6835
|
+
code: code.map((r) => r.slug),
|
|
6836
|
+
local: local.roles,
|
|
6837
|
+
direct: local.direct,
|
|
6838
|
+
effective: local.effective
|
|
6839
|
+
});
|
|
6840
|
+
return;
|
|
6841
|
+
}
|
|
6842
|
+
const known = new Set(local.roles.map((r) => r.slug));
|
|
6843
|
+
const fresh = code.filter((r) => !known.has(r.slug)).map((r) => r.slug);
|
|
6844
|
+
c.io.err(contextLine(mc, app));
|
|
6845
|
+
c.io.out(`In your code ${code.map((r) => r.slug).join(", ")}`);
|
|
6846
|
+
c.io.out(`Known on local ${[...known].join(", ") || "\u2014"}`);
|
|
6847
|
+
c.io.out(`Yours on local ${local.effective.join(", ") || "\u2014"}`);
|
|
6848
|
+
if (fresh.length) {
|
|
6849
|
+
c.io.out(`New in your code: ${fresh.join(", ")} \u2014 \`qore roles use <role...>\` announces them and makes them yours on local.`);
|
|
6850
|
+
}
|
|
6851
|
+
});
|
|
6852
|
+
withFlags(roles.command("use").description("Announce your code's roles for local and make exactly these yours there").argument("<role...>", "the roles you want on local")).action(async (wanted, f) => {
|
|
6853
|
+
const { c, mc, app, code } = await rolesContext(ctx, f, "roles use");
|
|
6854
|
+
const inCode = new Set(code.map((r) => r.slug));
|
|
6855
|
+
const unknown = wanted.filter((r) => !inCode.has(r));
|
|
6856
|
+
if (unknown.length) {
|
|
6857
|
+
throw new QoreError("QORE_CONTEXT_INVALID", `Not a role in ${PERMISSIONS_FILE}: ${unknown.join(", ")}`, {
|
|
6858
|
+
details: { unknown, known: [...inCode] }
|
|
6859
|
+
});
|
|
6860
|
+
}
|
|
6861
|
+
const before = await fetchLocalRoles(c.deps, mc, app);
|
|
6862
|
+
const res = await useLocalRoles(c.deps, mc, app, code, [...new Set(wanted)]);
|
|
6863
|
+
const changed = !sameSet(before.direct, res.direct);
|
|
6864
|
+
const viaGroups = res.effective.filter((r) => !res.direct.includes(r));
|
|
6865
|
+
if (c.opts().json) {
|
|
6866
|
+
renderJson(c.io, { context: contextJson(mc, app), added: res.added, direct: res.direct, effective: res.effective });
|
|
6867
|
+
return;
|
|
6868
|
+
}
|
|
6869
|
+
c.io.err(contextLine(mc, app));
|
|
6870
|
+
c.io.out(`\u2713 Your roles on local: ${res.direct.join(", ")}`);
|
|
6871
|
+
if (res.added.length) c.io.out(` New on local: ${res.added.join(", ")} \u2014 teammates can pick it in the Access tab too.`);
|
|
6872
|
+
if (viaGroups.length) c.io.err(`! You also hold ${viaGroups.join(", ")} on local through a group.`);
|
|
6873
|
+
if (changed) c.io.err("! Sign out and back in to your local app: your current session still has your old roles.");
|
|
6874
|
+
});
|
|
6875
|
+
}
|
|
6876
|
+
function sameSet(a, b) {
|
|
6877
|
+
const s = new Set(a);
|
|
6878
|
+
return s.size === new Set(b).size && b.every((x) => s.has(x));
|
|
6879
|
+
}
|
|
6880
|
+
|
|
6708
6881
|
// src/cli/commands/status.ts
|
|
6709
6882
|
var HEAD = ["ENV", "DEPLOY", "COMMIT", "DEPLOYED", "APP HEALTH"];
|
|
6710
6883
|
var envRow = (e, now) => [e.name, e.paused ? "paused" : e.status, e.commit?.sha ?? "\u2013", ago(e.commit?.deployedAt ?? null, now), uptime(e.health)];
|
|
@@ -6779,7 +6952,7 @@ function registerStatus(program, ctx) {
|
|
|
6779
6952
|
}
|
|
6780
6953
|
|
|
6781
6954
|
// src/cli/commands/sync.ts
|
|
6782
|
-
import { join as
|
|
6955
|
+
import { join as join27, relative as relative9 } from "path";
|
|
6783
6956
|
import { Option as Option2 } from "commander";
|
|
6784
6957
|
function asFailure2(slug, e) {
|
|
6785
6958
|
if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
|
|
@@ -6811,7 +6984,7 @@ function registerSync(program, ctx) {
|
|
|
6811
6984
|
continue;
|
|
6812
6985
|
}
|
|
6813
6986
|
try {
|
|
6814
|
-
dirs[m.slug] = await deps2.git.inspect(
|
|
6987
|
+
dirs[m.slug] = await deps2.git.inspect(join27(ws.root, m.slug));
|
|
6815
6988
|
modulesForPlan.push(m);
|
|
6816
6989
|
} catch (e) {
|
|
6817
6990
|
earlyFailures.push(asFailure2(m.slug, e));
|
|
@@ -7007,6 +7180,7 @@ var COMMANDS = [
|
|
|
7007
7180
|
registerTraces,
|
|
7008
7181
|
registerErrors,
|
|
7009
7182
|
registerEnv,
|
|
7183
|
+
registerRoles,
|
|
7010
7184
|
registerRun,
|
|
7011
7185
|
registerDev,
|
|
7012
7186
|
registerDb,
|
|
@@ -7151,7 +7325,7 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
|
|
|
7151
7325
|
|
|
7152
7326
|
// src/core/config.ts
|
|
7153
7327
|
import { readFile as readFile8 } from "fs/promises";
|
|
7154
|
-
import { join as
|
|
7328
|
+
import { join as join28 } from "path";
|
|
7155
7329
|
var empty = () => ({ schemaVersion: 1, profiles: {} });
|
|
7156
7330
|
var ConfigStore = class {
|
|
7157
7331
|
constructor(dir2) {
|
|
@@ -7159,7 +7333,7 @@ var ConfigStore = class {
|
|
|
7159
7333
|
}
|
|
7160
7334
|
dir;
|
|
7161
7335
|
get file() {
|
|
7162
|
-
return
|
|
7336
|
+
return join28(this.dir, "config.json");
|
|
7163
7337
|
}
|
|
7164
7338
|
async read() {
|
|
7165
7339
|
let raw;
|
|
@@ -7192,7 +7366,7 @@ var ConfigStore = class {
|
|
|
7192
7366
|
import { execFile, execFileSync } from "child_process";
|
|
7193
7367
|
import { mkdir as mkdir4, readdir as readdir4, stat as stat7 } from "fs/promises";
|
|
7194
7368
|
import { homedir } from "os";
|
|
7195
|
-
import { dirname as dirname9, join as
|
|
7369
|
+
import { dirname as dirname9, join as join29 } from "path";
|
|
7196
7370
|
function parseRemotes(stdout) {
|
|
7197
7371
|
const seen = /* @__PURE__ */ new Set();
|
|
7198
7372
|
const out = [];
|
|
@@ -7304,12 +7478,12 @@ async function status(repoRoot) {
|
|
|
7304
7478
|
if (r.code !== 0) return { ok: false, stderr: r.stderr, command };
|
|
7305
7479
|
return parseStatus(r.stdout);
|
|
7306
7480
|
}
|
|
7307
|
-
async function statOrThrow(
|
|
7481
|
+
async function statOrThrow(path2) {
|
|
7308
7482
|
try {
|
|
7309
|
-
return await stat7(
|
|
7483
|
+
return await stat7(path2);
|
|
7310
7484
|
} catch (err) {
|
|
7311
7485
|
if (err.code === "ENOENT") return void 0;
|
|
7312
|
-
throw new QoreError("QORE_INTERNAL", `could not stat ${
|
|
7486
|
+
throw new QoreError("QORE_INTERNAL", `could not stat ${path2}`, { cause: err });
|
|
7313
7487
|
}
|
|
7314
7488
|
}
|
|
7315
7489
|
async function inspect(dir2) {
|
|
@@ -7323,7 +7497,7 @@ async function inspect(dir2) {
|
|
|
7323
7497
|
throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
|
|
7324
7498
|
}
|
|
7325
7499
|
const empty2 = entries.length === 0;
|
|
7326
|
-
const isRepo = await statOrThrow(
|
|
7500
|
+
const isRepo = await statOrThrow(join29(dir2, ".git")) !== void 0;
|
|
7327
7501
|
const dirRemotes = isRepo ? await remotes(dir2) : [];
|
|
7328
7502
|
return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
|
|
7329
7503
|
}
|
|
@@ -7356,8 +7530,64 @@ function classifyKeychainError(e, platform) {
|
|
|
7356
7530
|
cause: e
|
|
7357
7531
|
});
|
|
7358
7532
|
}
|
|
7359
|
-
var
|
|
7360
|
-
|
|
7533
|
+
var MAX_ENTRY_CHARS = 1200;
|
|
7534
|
+
var HEAD2 = /^qore-chunked:v1:(\d+):(\d+)$/;
|
|
7535
|
+
var ChunkedKeychain = class {
|
|
7536
|
+
constructor(store, maxChars = MAX_ENTRY_CHARS) {
|
|
7537
|
+
this.store = store;
|
|
7538
|
+
this.maxChars = maxChars;
|
|
7539
|
+
}
|
|
7540
|
+
store;
|
|
7541
|
+
maxChars;
|
|
7542
|
+
head(raw) {
|
|
7543
|
+
const m = raw === null ? null : HEAD2.exec(raw);
|
|
7544
|
+
return m ? { gen: Number(m[1]), n: Number(m[2]) } : null;
|
|
7545
|
+
}
|
|
7546
|
+
split(value) {
|
|
7547
|
+
const parts = [];
|
|
7548
|
+
for (let i = 0; i < value.length; ) {
|
|
7549
|
+
let end2 = Math.min(i + this.maxChars, value.length);
|
|
7550
|
+
if (end2 < value.length && /[\uD800-\uDBFF]/.test(value[end2 - 1])) end2--;
|
|
7551
|
+
parts.push(value.slice(i, end2));
|
|
7552
|
+
i = end2;
|
|
7553
|
+
}
|
|
7554
|
+
return parts;
|
|
7555
|
+
}
|
|
7556
|
+
async deleteChunks(account, h) {
|
|
7557
|
+
for (let i = 1; i <= h.n; i++) await this.store.delete(`${account}#${h.gen}.${i}`);
|
|
7558
|
+
}
|
|
7559
|
+
async get(account) {
|
|
7560
|
+
const raw = await this.store.get(account);
|
|
7561
|
+
const h = this.head(raw);
|
|
7562
|
+
if (!h) return raw;
|
|
7563
|
+
let value = "";
|
|
7564
|
+
for (let i = 1; i <= h.n; i++) {
|
|
7565
|
+
const part = await this.store.get(`${account}#${h.gen}.${i}`);
|
|
7566
|
+
if (part === null) return null;
|
|
7567
|
+
value += part;
|
|
7568
|
+
}
|
|
7569
|
+
return value;
|
|
7570
|
+
}
|
|
7571
|
+
async set(account, value) {
|
|
7572
|
+
const old = this.head(await this.store.get(account));
|
|
7573
|
+
if (value.length <= this.maxChars) {
|
|
7574
|
+
await this.store.set(account, value);
|
|
7575
|
+
} else {
|
|
7576
|
+
const gen = (old?.gen ?? 0) + 1;
|
|
7577
|
+
const parts = this.split(value);
|
|
7578
|
+
for (const [i, part] of parts.entries()) await this.store.set(`${account}#${gen}.${i + 1}`, part);
|
|
7579
|
+
await this.store.set(account, `qore-chunked:v1:${gen}:${parts.length}`);
|
|
7580
|
+
}
|
|
7581
|
+
if (old) await this.deleteChunks(account, old);
|
|
7582
|
+
}
|
|
7583
|
+
async delete(account) {
|
|
7584
|
+
const old = this.head(await this.store.get(account));
|
|
7585
|
+
await this.store.delete(account);
|
|
7586
|
+
if (old) await this.deleteChunks(account, old);
|
|
7587
|
+
}
|
|
7588
|
+
};
|
|
7589
|
+
var EntryKeychain = class {
|
|
7590
|
+
constructor(platform) {
|
|
7361
7591
|
this.platform = platform;
|
|
7362
7592
|
}
|
|
7363
7593
|
platform;
|
|
@@ -7382,16 +7612,21 @@ var OsKeychain = class {
|
|
|
7382
7612
|
});
|
|
7383
7613
|
}
|
|
7384
7614
|
};
|
|
7615
|
+
var OsKeychain = class extends ChunkedKeychain {
|
|
7616
|
+
constructor(platform = process.platform) {
|
|
7617
|
+
super(new EntryKeychain(platform));
|
|
7618
|
+
}
|
|
7619
|
+
};
|
|
7385
7620
|
|
|
7386
7621
|
// src/platform/lock.ts
|
|
7387
7622
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
7388
|
-
import { join as
|
|
7623
|
+
import { join as join30 } from "path";
|
|
7389
7624
|
import lockfile from "proper-lockfile";
|
|
7390
7625
|
function fileLock(dir2) {
|
|
7391
7626
|
return async (name, fn) => {
|
|
7392
|
-
const locks =
|
|
7627
|
+
const locks = join30(dir2, "locks");
|
|
7393
7628
|
await mkdir5(locks, { recursive: true, mode: 448 });
|
|
7394
|
-
const target =
|
|
7629
|
+
const target = join30(locks, `${name}.lock`);
|
|
7395
7630
|
await writeFile5(target, "", { flag: "a" });
|
|
7396
7631
|
let release;
|
|
7397
7632
|
let compromised;
|
|
@@ -7750,7 +7985,7 @@ function systemProxy(platform = process.platform) {
|
|
|
7750
7985
|
}
|
|
7751
7986
|
|
|
7752
7987
|
// src/version.ts
|
|
7753
|
-
var VERSION = true ? "0.1.
|
|
7988
|
+
var VERSION = true ? "0.1.10" : "0.0.0-dev";
|
|
7754
7989
|
|
|
7755
7990
|
// src/cli.ts
|
|
7756
7991
|
if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
@@ -7830,4 +8065,4 @@ if (!drained) {
|
|
|
7830
8065
|
process.exit();
|
|
7831
8066
|
}
|
|
7832
8067
|
//# sourceMappingURL=cli.js.map
|
|
7833
|
-
//# debugId=
|
|
8068
|
+
//# debugId=2eacac78-c916-5f6b-825a-d5af2c955f32
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quatronic/qore",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Qore developer CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"picocolors": "^1.1.1",
|
|
48
48
|
"proper-lockfile": "^4.1.2",
|
|
49
49
|
"semver": "^7.8.5",
|
|
50
|
+
"typescript": "^5.9.2",
|
|
50
51
|
"undici": "^7.29.1"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
@@ -74,7 +75,6 @@
|
|
|
74
75
|
"react": "19.3.0",
|
|
75
76
|
"react-dom": "19.3.0",
|
|
76
77
|
"tsup": "^8.5.0",
|
|
77
|
-
"typescript": "^5.9.2",
|
|
78
78
|
"typescript-eslint": "^8.40.0",
|
|
79
79
|
"vite": "8.3.4",
|
|
80
80
|
"vitest": "^3.2.4"
|