@quatronic/qore 0.1.6 → 0.1.7

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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/dist/cli.js +694 -81
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,6 +23,11 @@ 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
+
27
+ qore errors --env stag # recent unresolved errors
28
+ qore errors FRONTEND-1A --env prod --with trace,logs # one error, its trace and logs
29
+ qore traces --env stag --slower-than 1s # slow requests
30
+ qore logs --env stag --level error # error logs
26
31
  ```
27
32
 
28
33
  ### Portals
@@ -57,6 +62,12 @@ current directory. `qore env pull --project` writes into the located module,
57
62
  and a relative `--target` then resolves against that project directory, not
58
63
  the directory you ran it from.
59
64
 
65
+ A repository that is no module counts as outside one: standing in some other
66
+ codebase, `qore dev` asks the portal about its remotes and, when they match no
67
+ module (or it has no remote Qore can match), offers your projects just the
68
+ same. A repository inside a known project is never asked about, and a portal
69
+ that cannot answer keeps today's error.
70
+
60
71
  Logged out with a projects folder set, the local lookup still runs first: a
61
72
  folder that holds no projects gives `QORE_PROJECT_NOT_FOUND`; otherwise the
62
73
  project is located exactly as when logged in (a match, a picker, or
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]="28c91dc8-dbb9-5c0a-91e3-f368931a4ed3")}catch(e){}}();
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]="bf813c0e-4054-532d-9e74-62fbe8673876")}catch(e){}}();
4
4
  import { createRequire as __qoreCreateRequire } from "node:module"; const require = __qoreCreateRequire(import.meta.url);
5
5
  import {
6
6
  addSecretValues,
@@ -538,8 +538,8 @@ async function fetchMetaWithDate(fetch3, apiOrigin) {
538
538
  }
539
539
  if (!res.ok) throw await problemToError(res);
540
540
  const header = res.headers.get("date");
541
- const ms = header ? Date.parse(header) : NaN;
542
- return { meta: await res.json(), date: Number.isNaN(ms) ? null : new Date(ms) };
541
+ const ms2 = header ? Date.parse(header) : NaN;
542
+ return { meta: await res.json(), date: Number.isNaN(ms2) ? null : new Date(ms2) };
543
543
  }
544
544
  function negotiate(meta, portal, cliVersion) {
545
545
  if (portal.pinnedIssuer && meta.auth.issuer !== portal.pinnedIssuer) {
@@ -678,7 +678,7 @@ var QoreApiClient = class {
678
678
  this.o.onRequest?.(`${method} ${path} \u2192 ${res.status}${rid ? ` (${rid})` : ""}`);
679
679
  return res;
680
680
  }
681
- async request(method, path, body) {
681
+ async request(method, path, body, parse2 = (r) => r.json()) {
682
682
  let res = await this.once(method, path, body, false);
683
683
  if (res.status === 401) {
684
684
  res = await this.once(method, path, body, true);
@@ -700,11 +700,14 @@ var QoreApiClient = class {
700
700
  }
701
701
  }
702
702
  if (!res.ok) throw await problemToError(res);
703
- return await res.json();
703
+ return parse2(res);
704
704
  }
705
705
  get(path) {
706
706
  return this.request("GET", path);
707
707
  }
708
+ getText(path) {
709
+ return this.request("GET", path, void 0, (r) => r.text());
710
+ }
708
711
  post(path, body) {
709
712
  return this.request("POST", path, body);
710
713
  }
@@ -720,13 +723,13 @@ var RefreshCoordinator = class {
720
723
  fresh(expiresAt) {
721
724
  return new Date(expiresAt).getTime() - this.o.now().getTime() > SKEW_MS;
722
725
  }
723
- async rotate(scope) {
726
+ async rotate(scope2) {
724
727
  let stored = await this.o.store.getRefresh();
725
728
  if (!stored) throw new QoreError("QORE_NOT_LOGGED_IN", "Not logged in", { hint: "Run `qore login`." });
726
729
  let t;
727
730
  for (let attempt = 0; ; attempt++) {
728
731
  try {
729
- t = await (await this.o.oidc()).refresh(stored.rt, scope);
732
+ t = await (await this.o.oidc()).refresh(stored.rt, scope2);
730
733
  break;
731
734
  } catch (e) {
732
735
  if (!(e instanceof QoreError && e.code === "QORE_SESSION_EXPIRED")) throw e;
@@ -742,7 +745,7 @@ var RefreshCoordinator = class {
742
745
  throw e;
743
746
  }
744
747
  }
745
- await this.o.store.setRefresh({ rt: t.refreshToken, sub: t.sub, scope: t.scope || (scope ?? stored.scope), obtainedAt: this.o.now().toISOString() });
748
+ await this.o.store.setRefresh({ rt: t.refreshToken, sub: t.sub, scope: t.scope || (scope2 ?? stored.scope), obtainedAt: this.o.now().toISOString() });
746
749
  return t;
747
750
  }
748
751
  async accessToken(opts = {}) {
@@ -2125,6 +2128,7 @@ async function anyLoggedIn(deps2, cfg) {
2125
2128
  }
2126
2129
  return false;
2127
2130
  }
2131
+ var isForeignRepo = (e) => e instanceof QoreError && e.code === "QORE_CONTEXT_REQUIRED" && e.details?.foreignRepo === true;
2128
2132
  async function resolveModuleContext(deps2, o) {
2129
2133
  const cache = new ResolutionCache(join11(deps2.config.dir, "cache"), deps2.now);
2130
2134
  const ws = await findWorkspace(deps2.cwd, deps2.home);
@@ -2143,7 +2147,7 @@ async function resolveModuleContext(deps2, o) {
2143
2147
  const said = gitError?.hint;
2144
2148
  throw new QoreError("QORE_CONTEXT_REQUIRED", "This repository has no remote Qore can match", {
2145
2149
  hint: said ? `${HINT_FLAG} git said: ${said}` : HINT_FLAG,
2146
- details: gitError ? { git: said ?? gitError.message } : void 0,
2150
+ details: gitError ? { git: said ?? gitError.message } : { foreignRepo: true },
2147
2151
  cause: gitError
2148
2152
  });
2149
2153
  }
@@ -2172,6 +2176,7 @@ async function resolveModuleContext(deps2, o) {
2172
2176
  }
2173
2177
  const details = { triedProfiles: tried };
2174
2178
  if (skipped.length > 0) details.skippedProfiles = skipped;
2179
+ if (answered.length > 0) details.foreignRepo = true;
2175
2180
  if (answered.length === 0) {
2176
2181
  const cause = skipErrors[0] ?? unsupported2;
2177
2182
  if (cause) throw withDetails(cause, details);
@@ -2579,7 +2584,7 @@ function notClonedReason(m, am) {
2579
2584
  }
2580
2585
 
2581
2586
  // src/core/dev/findings.ts
2582
- var plural = (n, one, many) => n === 1 ? one : many;
2587
+ var plural = (n, one2, many) => n === 1 ? one2 : many;
2583
2588
  function modulesMissing(o) {
2584
2589
  const actionFor = new Map(o.actions.map((a) => [a.module.slug, a]));
2585
2590
  const findings = [];
@@ -2869,7 +2874,7 @@ function nodeVersionError(found, range, slug) {
2869
2874
  // src/core/dev/preflight.ts
2870
2875
  var pmMajor = (v) => v ? v.split(".")[0] : "latest";
2871
2876
  var GRACE_MS = 1e4;
2872
- var secs = (ms) => `${Math.round(ms / 1e3)} s`;
2877
+ var secs = (ms2) => `${Math.round(ms2 / 1e3)} s`;
2873
2878
  async function runTool(deps2, cmd, args, o) {
2874
2879
  if (o.signal.aborted) throw new CancelledError();
2875
2880
  const tail = [];
@@ -3057,13 +3062,13 @@ async function checkPort(deps2, o) {
3057
3062
  throw await portInUseError(deps2, { slug: o.slug, port: o.port, key: o.key, runs: o.runs });
3058
3063
  }
3059
3064
  function abortableDelay(signal) {
3060
- return (ms) => new Promise((resolve6) => {
3065
+ return (ms2) => new Promise((resolve6) => {
3061
3066
  const done = () => {
3062
3067
  clearTimeout(t);
3063
3068
  signal.removeEventListener("abort", done);
3064
3069
  resolve6();
3065
3070
  };
3066
- const t = setTimeout(done, ms);
3071
+ const t = setTimeout(done, ms2);
3067
3072
  signal.addEventListener("abort", done, { once: true });
3068
3073
  });
3069
3074
  }
@@ -3210,10 +3215,20 @@ var isDir = async (p) => (await stat4(p).catch(() => null))?.isDirectory() ?? fa
3210
3215
  var label2 = (p) => `${p.tenantSlug} / ${p.appSlug}${p.kind === "module" ? ` / ${p.moduleSlug}` : ""}${p.source === "registry" ? ` (${p.path})` : ""}`;
3211
3216
  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
3217
  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;
3218
+ async function standingIn(ctx) {
3219
+ if (await findWorkspace(ctx.deps.cwd, ctx.deps.home)) return "workspace";
3215
3220
  const repo = await findRepoRoot(ctx.deps.cwd);
3216
- return repo !== null && !isInside(repo, ctx.deps.home);
3221
+ return repo !== null && !isInside(repo, ctx.deps.home) ? { repo } : null;
3222
+ }
3223
+ async function isForeign(ctx, command) {
3224
+ const g = ctx.opts();
3225
+ try {
3226
+ await resolveModuleContext(ctx.deps, { portal: g.portal, command, onRequest: g.verbose ? (l) => ctx.io.err(l) : void 0 });
3227
+ return false;
3228
+ } catch (e) {
3229
+ if (e instanceof CancelledError) throw e;
3230
+ return isForeignRepo(e);
3231
+ }
3217
3232
  }
3218
3233
  async function moduleDirs(wsPath) {
3219
3234
  const out = [];
@@ -3288,8 +3303,11 @@ async function projectCtx(ctx, o) {
3288
3303
  const m2 = await choose(ctx, await liveMatches(matches), `"${o.ref}" matches several projects`, true);
3289
3304
  return relocate(ctx, await pathFor(ctx, m2, o.needModule));
3290
3305
  }
3291
- if (o.module || o.app || await inProject(ctx)) return ctx;
3306
+ if (o.module || o.app) return ctx;
3307
+ const here = await standingIn(ctx);
3308
+ if (here === "workspace") return ctx;
3292
3309
  const known = await knownProjects(ctx.deps, { portal });
3310
+ if (here && (known.length === 0 || known.some((p) => isInside(p.path, here.repo)) || !await isForeign(ctx, o.command ?? "project"))) return ctx;
3293
3311
  if (known.length === 0) {
3294
3312
  if (!cfg.projectsRoot) {
3295
3313
  if (!await loggedInAs(ctx)) return ctx;
@@ -3301,7 +3319,8 @@ async function projectCtx(ctx, o) {
3301
3319
  hint: "Run `qore clone <app>`."
3302
3320
  });
3303
3321
  }
3304
- const m = await choose(ctx, known.map((project) => ({ project })), "Not inside a project \u2014 which one?", false);
3322
+ const what = here ? "This repository is not a Qore module \u2014 which project?" : "Not inside a project \u2014 which one?";
3323
+ const m = await choose(ctx, known.map((project) => ({ project })), what, false);
3305
3324
  return relocate(ctx, await pathFor(ctx, m, o.needModule));
3306
3325
  }
3307
3326
  function relocate(ctx, path) {
@@ -3342,6 +3361,10 @@ async function rememberWorkspace(ctx, ws, app) {
3342
3361
 
3343
3362
  // src/cli/context.ts
3344
3363
  var allOfKind = (c, kind) => !!c && c.length > 0 && c.every((x) => x.kind === kind && typeof x.id === "string");
3364
+ async function pick(ctx, err, message, choices) {
3365
+ if (!ctx.io.interactive) throw err;
3366
+ return ctx.io.select(message, choices);
3367
+ }
3345
3368
  async function moduleContextFor(ctx, flags, command) {
3346
3369
  const g = ctx.opts();
3347
3370
  const onRequest = g.verbose ? (l) => ctx.io.err(l) : void 0;
@@ -3351,14 +3374,16 @@ async function moduleContextFor(ctx, flags, command) {
3351
3374
  } catch (e) {
3352
3375
  if (!(e instanceof QoreError) || e.code !== "QORE_CONTEXT_REQUIRED" || !ctx.io.interactive) throw e;
3353
3376
  if (allOfKind(e.candidates, "module")) {
3354
- const choose2 = await ctx.io.select(
3377
+ const choose2 = await pick(
3378
+ ctx,
3379
+ e,
3355
3380
  "Select a module",
3356
3381
  e.candidates.map((c) => ({ name: `${c.tenant ? `${c.tenant} / ` : ""}${c.slug} (${c.remote})`, value: c.id }))
3357
3382
  );
3358
3383
  return resolveModuleContext(ctx.deps, { ...base, choose: choose2 });
3359
3384
  }
3360
3385
  if (allOfKind(e.candidates, "tenant")) {
3361
- const tenant = await ctx.io.select("Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.id })));
3386
+ const tenant = await pick(ctx, e, "Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.id })));
3362
3387
  return resolveModuleContext(ctx.deps, { ...base, tenant });
3363
3388
  }
3364
3389
  throw e;
@@ -3369,7 +3394,7 @@ async function appFor(ctx, mc, app) {
3369
3394
  return pickApp2(mc, app);
3370
3395
  } catch (e) {
3371
3396
  if (!(e instanceof QoreError) || e.code !== "QORE_APP_AMBIGUOUS" || !ctx.io.interactive) throw e;
3372
- const id = await ctx.io.select("Select an app", mc.module.apps.map((a) => ({ name: a.name, value: a.id })));
3397
+ const id = await pick(ctx, e, "Select an app", mc.module.apps.map((a) => ({ name: a.name, value: a.id })));
3373
3398
  return pickApp2(mc, id);
3374
3399
  }
3375
3400
  }
@@ -3701,8 +3726,8 @@ function exitCode(e) {
3701
3726
  }
3702
3727
  function supervise(o) {
3703
3728
  const now = o.now ?? Date.now;
3704
- const delay = o.delay ?? ((ms) => new Promise((r) => {
3705
- setTimeout(r, ms).unref();
3729
+ const delay = o.delay ?? ((ms2) => new Promise((r) => {
3730
+ setTimeout(r, ms2).unref();
3706
3731
  }));
3707
3732
  const abort = new AbortController();
3708
3733
  const emit = (e) => {
@@ -3880,7 +3905,7 @@ function registerRun(program, ctx) {
3880
3905
  hint: "Drop --json; the child's own output passes through."
3881
3906
  });
3882
3907
  }
3883
- const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true });
3908
+ const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true, command: "run" });
3884
3909
  const { deps: deps2 } = c;
3885
3910
  const mc = await moduleContextFor(c, f, "run");
3886
3911
  const app = await appFor(c, mc, f.app);
@@ -4410,7 +4435,7 @@ function registerDev(program, ctx) {
4410
4435
  hint: "qore dev <project> | qore dev --project <project>"
4411
4436
  });
4412
4437
  }
4413
- const c = await projectCtx(ctx, { ref: project ?? f.project, module: f.module, app: f.app, needModule: false });
4438
+ const c = await projectCtx(ctx, { ref: project ?? f.project, module: f.module, app: f.app, needModule: false, command: "dev" });
4414
4439
  const ws = await workspaceRoot(c.deps);
4415
4440
  if (ws && f.module === void 0) return workspaceDev(c, ws, f, waitTimeout);
4416
4441
  return moduleDev(c, f, waitTimeout);
@@ -4515,9 +4540,9 @@ async function moduleDev(ctx, f, waitTimeout) {
4515
4540
  childRunning.current = false;
4516
4541
  await removeRunFile(deps2, root);
4517
4542
  }
4518
- const ms = deps2.now().getTime() - startedAt.getTime();
4543
+ const ms2 = deps2.now().getTime() - startedAt.getTime();
4519
4544
  if (exit.code !== 0 && exit.signal === null) {
4520
- io2.err(ms < EARLY_EXIT_MS ? `\u2717 ${slug} exited after ${Math.round(ms / 1e3)} s with code ${exit.code ?? 1} \u2014 its output above says why.` : `\u2717 ${slug} exited with code ${exit.code ?? 1}.`);
4545
+ io2.err(ms2 < EARLY_EXIT_MS ? `\u2717 ${slug} exited after ${Math.round(ms2 / 1e3)} s with code ${exit.code ?? 1} \u2014 its output above says why.` : `\u2717 ${slug} exited with code ${exit.code ?? 1}.`);
4521
4546
  const hint = exitHint(tee.lines(), hintContext(recipe, pkg, devEnv2));
4522
4547
  if (hint) io2.err(` \u2192 ${hint.hint}`);
4523
4548
  }
@@ -4698,19 +4723,19 @@ var withError = (status2, label3, e) => ({ status: status2, label: label3, error
4698
4723
  var isCancel = (e, signal) => e instanceof CancelledError || signal.aborted;
4699
4724
  var portalLabel = (p) => p.builtin ?? p.apiOrigin;
4700
4725
  var ToolTimeout = class extends Error {
4701
- constructor(tool, ms) {
4702
- super(`${tool} did not answer within ${Math.round(ms / 1e3)} s`);
4726
+ constructor(tool, ms2) {
4727
+ super(`${tool} did not answer within ${Math.round(ms2 / 1e3)} s`);
4703
4728
  this.tool = tool;
4704
- this.ms = ms;
4729
+ this.ms = ms2;
4705
4730
  }
4706
4731
  tool;
4707
4732
  ms;
4708
4733
  };
4709
- async function timed(tool, signal, ms, fn) {
4734
+ async function timed(tool, signal, ms2, fn) {
4710
4735
  try {
4711
- return await fn(AbortSignal.any([signal, AbortSignal.timeout(ms)]));
4736
+ return await fn(AbortSignal.any([signal, AbortSignal.timeout(ms2)]));
4712
4737
  } catch (e) {
4713
- if (e instanceof CancelledError && !signal.aborted) throw new ToolTimeout(tool, ms);
4738
+ if (e instanceof CancelledError && !signal.aborted) throw new ToolTimeout(tool, ms2);
4714
4739
  throw e;
4715
4740
  }
4716
4741
  }
@@ -4741,8 +4766,8 @@ function checkOwnNode(deps2) {
4741
4766
  }
4742
4767
  return ok(`Node ${v} (\u226522 required)`);
4743
4768
  }
4744
- async function checkGit(deps2, signal, ms) {
4745
- const res = await timed("git", signal, ms, (s) => runTool(deps2, "git", ["--version"], { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
4769
+ async function checkGit(deps2, signal, ms2) {
4770
+ const res = await timed("git", signal, ms2, (s) => runTool(deps2, "git", ["--version"], { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
4746
4771
  if (res.spawnError === "ENOENT") {
4747
4772
  const e = new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
4748
4773
  hint: "Install git (https://git-scm.com/downloads) and open a new terminal."
@@ -4756,14 +4781,14 @@ async function checkGit(deps2, signal, ms) {
4756
4781
  const m = /git version (\d+)\.(\d+)/.exec(res.tail.join("\n"));
4757
4782
  return ok(m ? `git ${m[1]}.${m[2]}` : "git");
4758
4783
  }
4759
- async function checkGithubSsh(deps2, signal, ms) {
4760
- const res = await timed("ssh", signal, ms, (s) => runTool(deps2, "ssh", SSH_TEST_ARGS, { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
4784
+ async function checkGithubSsh(deps2, signal, ms2) {
4785
+ const res = await timed("ssh", signal, ms2, (s) => runTool(deps2, "ssh", SSH_TEST_ARGS, { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
4761
4786
  const r = classifySshTest(res);
4762
4787
  if (r.ok) return ok(`GitHub SSH ok (${r.user})`, { user: r.user });
4763
4788
  return withError("warn", `GitHub SSH: ${r.error.message} (https still works)`, r.error);
4764
4789
  }
4765
- async function checkDocker(deps2, signal, ms) {
4766
- return timed("docker", signal, ms, async (s) => {
4790
+ async function checkDocker(deps2, signal, ms2) {
4791
+ return timed("docker", signal, ms2, async (s) => {
4767
4792
  const o = { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s };
4768
4793
  const info = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], o);
4769
4794
  const infoErr = classifyDockerInfo(info, deps2.platform);
@@ -4895,16 +4920,16 @@ function runsAt(deps2, root) {
4895
4920
  return () => p ??= leftoverRun(deps2, root, { prune: false }).then((r) => r ? [r] : []);
4896
4921
  }
4897
4922
  var UNTRUSTED = { status: "warn", label: "not checked (repository not trusted)" };
4898
- async function checkRepoPm(deps2, m, recipe, signal, ms) {
4923
+ async function checkRepoPm(deps2, m, recipe, signal, ms2) {
4899
4924
  if (!await isTrusted(deps2, m.root, m.id)) return UNTRUSTED;
4900
- const found = await timed(recipe.pm.name, signal, ms, (s) => checkPackageManager(deps2, recipe, noop, s));
4925
+ const found = await timed(recipe.pm.name, signal, ms2, (s) => checkPackageManager(deps2, recipe, noop, s));
4901
4926
  const { name, version } = recipe.pm;
4902
4927
  if (found === null) return ok(`${name} (version unknown)`);
4903
4928
  return ok(version ? `${name} ${found} (matches)` : `${name} ${found}`, { found, ...version ? { pinned: version } : {} });
4904
4929
  }
4905
- async function checkRepoNode(deps2, m, recipe, pkg, signal, ms) {
4930
+ async function checkRepoNode(deps2, m, recipe, pkg, signal, ms2) {
4906
4931
  if (!await isTrusted(deps2, m.root, m.id)) return UNTRUSTED;
4907
- const found = await timed("node", signal, ms, (s) => checkNode(deps2, recipe, pkg, m.slug, s));
4932
+ const found = await timed("node", signal, ms2, (s) => checkNode(deps2, recipe, pkg, m.slug, s));
4908
4933
  return ok(`Node ${found ?? "?"} (engines ${pkg.engines.node})`);
4909
4934
  }
4910
4935
  async function loggedInPortals(deps2, profiles) {
@@ -5122,7 +5147,7 @@ var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.l
5122
5147
  function registerEnv(program, ctx) {
5123
5148
  const env = program.command("env").description("Local environment");
5124
5149
  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 });
5150
+ const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true, command: "env pull" });
5126
5151
  const { deps: deps2, io: io2 } = c;
5127
5152
  const g = c.opts();
5128
5153
  const mc = await moduleContextFor(c, f, "env pull");
@@ -5157,7 +5182,7 @@ function registerEnv(program, ctx) {
5157
5182
  choices[key] = bulk;
5158
5183
  continue;
5159
5184
  }
5160
- const pick = await io2.select(
5185
+ const pick2 = await io2.select(
5161
5186
  `${key} differs (local ${maskLen(current.get(key))}, portal ${maskLen(local.env[key])})`,
5162
5187
  [
5163
5188
  { name: "keep local", value: "keep" },
@@ -5166,9 +5191,9 @@ function registerEnv(program, ctx) {
5166
5191
  { name: "keep local for all", value: "skip" }
5167
5192
  ]
5168
5193
  );
5169
- if (pick === "all") bulk = "use-portal";
5170
- if (pick === "skip") bulk = "keep";
5171
- choices[key] = pick === "portal" || pick === "all" ? "use-portal" : "keep";
5194
+ if (pick2 === "all") bulk = "use-portal";
5195
+ if (pick2 === "skip") bulk = "keep";
5196
+ choices[key] = pick2 === "portal" || pick2 === "all" ? "use-portal" : "keep";
5172
5197
  }
5173
5198
  plan = planPull({ ...inputs, choices });
5174
5199
  }
@@ -5206,12 +5231,486 @@ function registerEnv(program, ctx) {
5206
5231
  });
5207
5232
  }
5208
5233
 
5234
+ // src/core/observability/compose.ts
5235
+ function pageWarnings(...pages) {
5236
+ return pages.some((p) => p.ingestPaused) ? ["ingest_paused"] : [];
5237
+ }
5238
+ function scope(q) {
5239
+ const s = { env: q.env };
5240
+ if (q.since !== void 0) s.since = q.since;
5241
+ if (q.start !== void 0) s.start = q.start;
5242
+ if (q.end !== void 0) s.end = q.end;
5243
+ return s;
5244
+ }
5245
+ async function errorDetail(c, id, q, withs) {
5246
+ const w = scope(q);
5247
+ const markdown = await c.issueMarkdown(id, w);
5248
+ const event = await c.issueEvent(id, w);
5249
+ const out = { issue: { markdown, event }, warnings: [] };
5250
+ if (withs.size === 0) return out;
5251
+ if (!event.traceId) {
5252
+ out.warnings.push("no_trace_id");
5253
+ return out;
5254
+ }
5255
+ if (withs.has("trace")) out.trace = await c.trace(event.traceId, w);
5256
+ if (withs.has("logs")) {
5257
+ const logs = await c.logs({ ...w, trace: event.traceId, limit: 100 });
5258
+ out.logs = logs.items;
5259
+ out.warnings.push(...pageWarnings(logs));
5260
+ }
5261
+ return out;
5262
+ }
5263
+ async function traceDetail(c, id, q, withs) {
5264
+ const w = scope(q);
5265
+ const out = { trace: await c.trace(id, w), warnings: [] };
5266
+ const pages = [];
5267
+ if (withs.has("logs")) {
5268
+ const p = await c.logs({ ...w, trace: id, limit: 100 });
5269
+ out.logs = p.items;
5270
+ pages.push(p);
5271
+ }
5272
+ if (withs.has("errors")) {
5273
+ const p = await c.issues({ ...w, trace: id, status: "all" });
5274
+ out.errors = p.items;
5275
+ pages.push(p);
5276
+ }
5277
+ out.warnings.push(...pageWarnings(...pages));
5278
+ return out;
5279
+ }
5280
+
5281
+ // src/core/observability/client.ts
5282
+ var MAX_RETRY_WAIT_S = 30;
5283
+ var NOT_PROVISIONED = "observability_not_provisioned";
5284
+ var wait = (ms2) => new Promise((r) => setTimeout(r, ms2));
5285
+ var ObsClient = class {
5286
+ constructor(api, moduleId, sleep = wait, slug = moduleId) {
5287
+ this.api = api;
5288
+ this.moduleId = moduleId;
5289
+ this.sleep = sleep;
5290
+ this.slug = slug;
5291
+ }
5292
+ api;
5293
+ moduleId;
5294
+ sleep;
5295
+ slug;
5296
+ path(resource, o, extra = {}) {
5297
+ const p = new URLSearchParams();
5298
+ const all = { env: o.env, since: o.since, start: o.start, end: o.end, q: o.q, limit: o.limit, ...extra };
5299
+ for (const [k, v] of Object.entries(all)) if (v !== void 0 && v !== "") p.set(k, String(v));
5300
+ return `/api/cli/v1/modules/${encodeURIComponent(this.moduleId)}/observability/${resource}?${p}`;
5301
+ }
5302
+ async call(fn) {
5303
+ try {
5304
+ return await this.retryOnce(fn);
5305
+ } catch (e) {
5306
+ if (e instanceof QoreError && e.code === "QORE_ACCESS_DENIED" && e.serverCode === "auth.scope.insufficient") {
5307
+ throw new QoreError("QORE_ACCESS_DENIED", "This login predates `qore errors/traces/logs`", {
5308
+ hint: "Run `qore login` again.",
5309
+ status: e.status,
5310
+ requestId: e.requestId,
5311
+ serverCode: e.serverCode
5312
+ });
5313
+ }
5314
+ if (e instanceof QoreError && e.serverCode === NOT_PROVISIONED) {
5315
+ throw new QoreError("QORE_RESOURCE_NOT_FOUND", `Module "${this.slug}" does not report to Sentry yet`, {
5316
+ hint: "Its Sentry project is not provisioned; pick another module with --module.",
5317
+ status: e.status,
5318
+ requestId: e.requestId,
5319
+ serverCode: e.serverCode,
5320
+ details: e.details
5321
+ });
5322
+ }
5323
+ throw e;
5324
+ }
5325
+ }
5326
+ async retryOnce(fn) {
5327
+ try {
5328
+ return await fn();
5329
+ } catch (e) {
5330
+ if (!(e instanceof QoreError) || e.code !== "QORE_RATE_LIMITED") throw e;
5331
+ const secs2 = typeof e.details?.retryAfter === "number" ? e.details.retryAfter : 1;
5332
+ await this.sleep(Math.min(secs2, MAX_RETRY_WAIT_S) * 1e3);
5333
+ return fn();
5334
+ }
5335
+ }
5336
+ issues(o) {
5337
+ return this.call(() => this.api.get(this.path("issues", o, { status: o.status, sort: o.sort, trace: o.trace })));
5338
+ }
5339
+ issueMarkdown(id, o) {
5340
+ return this.call(() => this.api.getText(this.path(`issues/${encodeURIComponent(id)}/markdown`, { env: o.env, since: o.since, start: o.start, end: o.end }, { event: "recommended" })));
5341
+ }
5342
+ issueEvent(id, o) {
5343
+ return this.call(() => this.api.get(this.path(`issues/${encodeURIComponent(id)}/events/recommended`, { env: o.env, since: o.since, start: o.start, end: o.end })));
5344
+ }
5345
+ logs(o) {
5346
+ return this.call(() => this.api.get(this.path("logs", o, { level: o.level, trace: o.trace })));
5347
+ }
5348
+ spans(o) {
5349
+ return this.call(() => this.api.get(this.path("spans", o, { sort: o.sort })));
5350
+ }
5351
+ trace(id, o) {
5352
+ return this.call(() => this.api.get(this.path(`traces/${encodeURIComponent(id)}`, { env: o.env, since: o.since, start: o.start, end: o.end })));
5353
+ }
5354
+ };
5355
+
5356
+ // src/core/observability/query.ts
5357
+ var ROOT_SPAN_FILTER = "is_transaction:true";
5358
+ var DURATION = /^\d{1,6}(ms|s|m)$/;
5359
+ var SINCE = /^(\d{1,4})([mhdw])$/;
5360
+ var SECONDS = { m: 60, h: 3600, d: 86400, w: 604800 };
5361
+ function parseDuration(s) {
5362
+ if (!DURATION.test(s)) throw new QoreError("QORE_CONTEXT_INVALID", `"${s}" is not a duration`, { hint: "Use e.g. 500ms, 1s or 2m." });
5363
+ return s;
5364
+ }
5365
+ function parseSince(s) {
5366
+ const m = SINCE.exec(s);
5367
+ if (!m || Number(m[1]) * SECONDS[m[2]] > 90 * 86400) {
5368
+ throw new QoreError("QORE_CONTEXT_INVALID", `"${s}" is not a time window`, { hint: "Use a count plus m, h, d or w, at most 90 days (e.g. 24h, 7d)." });
5369
+ }
5370
+ return s;
5371
+ }
5372
+ function joinQuery(...parts) {
5373
+ const q = parts.filter((p) => typeof p === "string" && p.length > 0).join(" ");
5374
+ return q || void 0;
5375
+ }
5376
+
5377
+ // src/cli/observability/context.ts
5378
+ var ALL = { kind: "module", slug: "all", name: "All modules" };
5379
+ var modCandidates = (mods, withAll) => [...mods.map((m) => ({ kind: "module", id: m.id, slug: m.slug, name: m.name })), ...withAll ? [ALL] : []];
5380
+ function withRepo(mods) {
5381
+ const r = mods.filter((m) => m.repoUrl !== null);
5382
+ if (r.length === 0) throw new QoreError("QORE_RESOURCE_NOT_FOUND", "No modules with a repository in this app");
5383
+ return r;
5384
+ }
5385
+ function windowOf(f) {
5386
+ if (Boolean(f.start) !== Boolean(f.end)) throw new QoreError("QORE_CONTEXT_INVALID", "--start and --end go together; pass both");
5387
+ if (f.start || f.end) return { start: f.start, end: f.end };
5388
+ return { since: parseSince(f.since ?? "24h") };
5389
+ }
5390
+ var windowText = (w) => w.since ? `last ${w.since}` : `${w.start ?? "\u2026"} \u2013 ${w.end ?? "now"}`;
5391
+ async function chooseModule(ctx, mods, flag, single) {
5392
+ if (flag === "all" && !single) return mods;
5393
+ if (flag && flag !== "all") {
5394
+ const m = mods.find((x) => x.slug === flag || x.id === flag);
5395
+ if (m) return [m];
5396
+ throw new QoreError("QORE_CONTEXT_REQUIRED", `No module "${flag}" in this app`, { candidates: modCandidates(mods, !single) });
5397
+ }
5398
+ const err = new QoreError(
5399
+ "QORE_CONTEXT_REQUIRED",
5400
+ single && flag === "all" ? "A detail view needs one module" : "Which module?",
5401
+ { candidates: modCandidates(mods, !single), hint: "Pass --module <slug>" + (single ? "." : " or --module all.") }
5402
+ );
5403
+ if (flag === "all") throw err;
5404
+ const choice = await pick(
5405
+ ctx,
5406
+ err,
5407
+ "Select a module",
5408
+ [...mods.map((m) => ({ name: m.slug, value: m.id })), ...single ? [] : [{ name: "All modules", value: "all" }]]
5409
+ );
5410
+ return choice === "all" ? mods : mods.filter((m) => m.id === choice);
5411
+ }
5412
+ async function chooseEnv(ctx, api, moduleId, appId, flag) {
5413
+ const st = await api.get(`/api/cli/v1/modules/${encodeURIComponent(moduleId)}/status?app_id=${encodeURIComponent(appId)}`);
5414
+ const names = st.environments.map((e) => e.name);
5415
+ if (names.length === 0) throw new QoreError("QORE_RESOURCE_NOT_FOUND", "No environments yet");
5416
+ const candidates = names.map((n) => ({ kind: "env", slug: n }));
5417
+ if (flag) {
5418
+ if (names.includes(flag)) return flag;
5419
+ throw new QoreError("QORE_CONTEXT_REQUIRED", `Unknown environment "${flag}"`, { candidates, hint: `Pass --env ${names.join("|")}.` });
5420
+ }
5421
+ const err = new QoreError("QORE_CONTEXT_REQUIRED", "Which environment?", { candidates, hint: `Pass --env ${names.join("|")}.` });
5422
+ return pick(ctx, err, "Select an environment", names.map((n) => ({ name: n, value: n })));
5423
+ }
5424
+ async function obsContext(ctx, flags, command, o) {
5425
+ const { deps: deps2, io: io2 } = ctx;
5426
+ const g = ctx.opts();
5427
+ const window = windowOf(flags);
5428
+ const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
5429
+ const ws = flags.module === void 0 || flags.module === "all" ? await workspaceRoot(deps2) : null;
5430
+ let api, tenant, app, mods, base, apiOrigin;
5431
+ if (ws) {
5432
+ const portal = await flagPortal(deps2, g.portal, ws);
5433
+ apiOrigin = portal.apiOrigin;
5434
+ const session = await openSession(deps2, portal, command, onRequest);
5435
+ await ensureCapability(deps2, apiOrigin, "module.observability");
5436
+ await ensureCapability(deps2, apiOrigin, "app.modules");
5437
+ const am = await workspaceAppModules(session, portal, ws);
5438
+ api = session.api;
5439
+ ({ tenant } = am.app);
5440
+ app = { id: am.app.id, slug: am.app.slug, name: am.app.name };
5441
+ mods = await chooseModule(ctx, withRepo(am.modules), flags.module, o.single);
5442
+ base = { portal: portal.builtin ?? portal.apiOrigin, tenant, app, source: WORKSPACE_FILE };
5443
+ } else {
5444
+ const mc = await moduleContextFor(ctx, { module: flags.module === "all" ? void 0 : flags.module }, command);
5445
+ apiOrigin = mc.portal.apiOrigin;
5446
+ await ensureCapability(deps2, apiOrigin, "module.observability");
5447
+ app = await appFor(ctx, mc, flags.app);
5448
+ api = mc.session.api;
5449
+ tenant = mc.module.tenant;
5450
+ if (flags.module === "all") {
5451
+ if (o.single) throw new QoreError(
5452
+ "QORE_CONTEXT_REQUIRED",
5453
+ "A detail view needs one module",
5454
+ { candidates: [{ kind: "module", id: mc.module.id, slug: mc.module.slug, name: mc.module.name }] }
5455
+ );
5456
+ await ensureCapability(deps2, apiOrigin, "app.modules");
5457
+ const am = await api.get(`/api/cli/v1/apps/${encodeURIComponent(app.id)}/modules`);
5458
+ mods = withRepo(am.modules);
5459
+ } else {
5460
+ mods = [mc.module];
5461
+ }
5462
+ base = contextJson(mc, app);
5463
+ delete base.module;
5464
+ }
5465
+ await ensureCapability(deps2, apiOrigin, "module.status");
5466
+ const env = await chooseEnv(ctx, api, mods[0].id, app.id, flags.env);
5467
+ const fanOut2 = mods.length > 1 || flags.module === "all";
5468
+ const targets = mods.map((m) => ({ module: { id: m.id, slug: m.slug, name: m.name, type: m.type, access: m.access }, client: new ObsClient(api, m.id, void 0, m.slug) }));
5469
+ const where = fanOut2 ? "all modules" : mods[0].slug;
5470
+ return {
5471
+ targets,
5472
+ fanOut: fanOut2,
5473
+ env,
5474
+ window,
5475
+ header: `\u2192 ${tenant.name} / ${app.name} / ${where} \xB7 ${env} \xB7 ${windowText(window)}`,
5476
+ json: { ...base, ...fanOut2 ? { modules: targets.map((t) => t.module) } : { module: targets[0].module }, env, window }
5477
+ };
5478
+ }
5479
+
5480
+ // src/cli/format.ts
5481
+ function ago(iso, now) {
5482
+ if (!iso) return "\u2013";
5483
+ const s = Math.max(0, Math.round((now.getTime() - new Date(iso).getTime()) / 1e3));
5484
+ if (s < 60) return "just now";
5485
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
5486
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
5487
+ return `${Math.floor(s / 86400)}d ago`;
5488
+ }
5489
+ var pct = (f) => String(Number((f * 100).toFixed(2)));
5490
+ function uptime(h) {
5491
+ if (!h) return "\u2013";
5492
+ const kind = h.cause && h.kind !== "up" ? `${h.kind}: ${h.cause}` : h.kind;
5493
+ return h.uptime24h === null ? kind : `${kind} (${pct(h.uptime24h)}% 24h)`;
5494
+ }
5495
+ function table(rows) {
5496
+ const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length)));
5497
+ return rows.map((r) => " " + r.map((c, i) => i === r.length - 1 ? c : c.padEnd(widths[i])).join(" "));
5498
+ }
5499
+
5500
+ // src/cli/observability/render.ts
5501
+ var CONTROL = /[\x00-\x08\x0b-\x1f\x7f\x80-\x9f]/g;
5502
+ var clean = (s) => s.replace(CONTROL, "");
5503
+ function fence(content) {
5504
+ const lines = content.map(clean);
5505
+ const longest = Math.max(0, ...lines.flatMap((l) => (l.match(/`+/g) ?? []).map((r) => r.length)));
5506
+ const ticks = "`".repeat(Math.max(3, longest + 1));
5507
+ return [`${ticks}untrusted-data`, ...lines, ticks];
5508
+ }
5509
+ function ms(n) {
5510
+ return n < 1e3 ? `${Math.round(n)}ms` : `${(n / 1e3).toFixed(1)}s`;
5511
+ }
5512
+ var oneLine = (s) => clean((s ?? "").replace(/\s+/g, " ")).trim();
5513
+ function issuesTable(items, now, withModule) {
5514
+ const head = ["ID", "LEVEL", "EVENTS", "USERS", "LAST SEEN", "TITLE"];
5515
+ const rows = items.map((i) => [oneLine(i.shortId), oneLine(i.level), String(i.count), String(i.userCount), ago(i.lastSeen, now), oneLine(i.title)]);
5516
+ return table(withModule ? [["MODULE", ...head], ...rows.map((r, n) => [oneLine(items[n].module.slug), ...r])] : [head, ...rows]);
5517
+ }
5518
+ function spansTable(items, now, withModule) {
5519
+ const head = ["TRACE", "DURATION", "ROOT", "WHEN"];
5520
+ const rows = items.map((s) => [oneLine(s.traceId), ms(s.durationMs), oneLine(s.transaction ?? s.description ?? s.name), ago(s.timestamp, now)]);
5521
+ return table(withModule ? [["MODULE", ...head], ...rows.map((r, n) => [oneLine(items[n].module.slug), ...r])] : [head, ...rows]);
5522
+ }
5523
+ function logLines(items, withModule) {
5524
+ return items.map((l) => [oneLine(l.timestamp), oneLine(l.severity).toUpperCase(), ...withModule ? [oneLine(l.module.slug)] : [], oneLine(l.message)].join(" "));
5525
+ }
5526
+ function spanTree(t) {
5527
+ const all = [];
5528
+ const walk = (n) => {
5529
+ all.push(n);
5530
+ n.children.forEach(walk);
5531
+ };
5532
+ t.roots.forEach(walk);
5533
+ const slowest = all.reduce((a, n) => !a || n.end - n.start > a.end - a.start ? n : a, null);
5534
+ const out = [];
5535
+ const line2 = (n, depth) => {
5536
+ const label3 = [oneLine(n.op), oneLine(n.description ?? n.transaction)].filter(Boolean).join(" ");
5537
+ const errs = n.errors.map((e) => `\u2716 ${oneLine(e.shortId ?? e.eventId)}`).join(" ");
5538
+ out.push([
5539
+ `${" ".repeat(depth)}${label3}`,
5540
+ ms((n.end - n.start) * 1e3),
5541
+ `[${oneLine(n.module.slug)}]`,
5542
+ ...n === slowest ? ["\u25C0 slowest"] : [],
5543
+ ...errs ? [errs] : []
5544
+ ].join(" "));
5545
+ n.children.forEach((c) => line2(c, depth + 1));
5546
+ };
5547
+ t.roots.forEach((r) => line2(r, 0));
5548
+ return out;
5549
+ }
5550
+ var WARN = {
5551
+ ingest_paused: "! This tenant's Sentry quota is reached; new events are not being recorded.",
5552
+ no_trace_id: "! This error has no trace id, so there is no trace or trace logs to show.",
5553
+ sort_unavailable: "! Sorting by duration is not available here; showing the slowest of the most recent matches.",
5554
+ module_not_provisioned: "! Skipped, not reporting to Sentry yet:"
5555
+ };
5556
+ function warningLines(w, skipped = []) {
5557
+ return [...new Set(w)].map((k) => k === "module_not_provisioned" ? `${WARN[k]} ${skipped.join(", ")}` : WARN[k]);
5558
+ }
5559
+
5560
+ // src/cli/observability/flags.ts
5561
+ function addObsFlags(cmd) {
5562
+ return cmd.option("--env <name>", "the environment (required; prompted when interactive)").option("--module <slug|all>", "the module (default: this repository); all = every module of the app").option("--app <slug|id>", "the app, when the module is part of several").option("--since <window>", "time window, e.g. 1h, 24h, 7d (default 24h)").option("--start <iso>", "window start (use with --end, instead of --since)").option("--end <iso>", "window end (use with --start, instead of --since)").option("--query <search>", "Sentry search syntax, appended to the filter").option("--limit <n>", "rows per module, 1-100 (default 25)");
5563
+ }
5564
+ function limitOf(s) {
5565
+ if (s === void 0) return 25;
5566
+ const n = Number(s);
5567
+ if (!Number.isInteger(n) || n < 1 || n > 100) throw new QoreError("QORE_CONTEXT_INVALID", `--limit must be 1-100, not "${s}"`);
5568
+ return n;
5569
+ }
5570
+ function withList(s, allowed) {
5571
+ if (!s) return /* @__PURE__ */ new Set();
5572
+ const parts = s.split(",").map((p) => p.trim()).filter(Boolean);
5573
+ const bad = parts.filter((p) => !allowed.includes(p));
5574
+ if (bad.length) throw new QoreError("QORE_CONTEXT_INVALID", `--with accepts ${allowed.join(", ")}; not ${bad.join(", ")}`);
5575
+ return new Set(parts);
5576
+ }
5577
+ function one(flag, value, allowed) {
5578
+ if (!allowed.includes(value)) {
5579
+ throw new QoreError("QORE_CONTEXT_INVALID", `${flag} must be ${allowed.join("|")}, not "${value}"`);
5580
+ }
5581
+ return value;
5582
+ }
5583
+ async function fanOut(oc2, fn) {
5584
+ const out = { results: [], skipped: [] };
5585
+ let last;
5586
+ for (const target of oc2.targets) {
5587
+ try {
5588
+ out.results.push({ target, result: await fn(target) });
5589
+ } catch (e) {
5590
+ if (oc2.targets.length < 2 || !(e instanceof QoreError) || e.serverCode !== NOT_PROVISIONED) throw e;
5591
+ out.skipped.push({ id: target.module.id, slug: target.module.slug });
5592
+ last = e;
5593
+ }
5594
+ }
5595
+ if (out.results.length === 0 && last) {
5596
+ throw new QoreError("QORE_RESOURCE_NOT_FOUND", "No module of this app reports to Sentry yet", {
5597
+ serverCode: NOT_PROVISIONED,
5598
+ details: { modules: out.skipped.map((m) => m.slug) }
5599
+ });
5600
+ }
5601
+ return out;
5602
+ }
5603
+ var skipWarnings = (f) => f.skipped.length ? ["module_not_provisioned"] : [];
5604
+ var skipJson = (f) => f.skipped.length ? { unprovisionedModules: f.skipped } : {};
5605
+ function printWarnings(io2, w, json2, skipped = []) {
5606
+ if (!json2) for (const l of warningLines(w, skipped.map((m) => m.slug))) io2.err(l);
5607
+ }
5608
+
5609
+ // src/cli/commands/errors.ts
5610
+ var WITHS = ["trace", "logs"];
5611
+ var STATUSES = ["unresolved", "resolved", "all"];
5612
+ var SORTS = ["date", "freq", "new"];
5613
+ var MERGE = {
5614
+ date: (a, b) => b.lastSeen.localeCompare(a.lastSeen),
5615
+ freq: (a, b) => b.count - a.count,
5616
+ new: (a, b) => b.firstSeen.localeCompare(a.firstSeen)
5617
+ };
5618
+ function printDetail(ctx, oc2, d, showIssue) {
5619
+ const { io: io2 } = ctx;
5620
+ printWarnings(io2, d.warnings, false);
5621
+ if (showIssue) for (const l of fence(d.issue.markdown.split("\n"))) io2.out(l);
5622
+ if (d.trace) {
5623
+ io2.out("");
5624
+ io2.out("Trace");
5625
+ for (const l of fence(spanTree(d.trace))) io2.out(l);
5626
+ }
5627
+ if (d.logs) {
5628
+ io2.out("");
5629
+ io2.out(`Logs (${d.logs.length})`);
5630
+ for (const l of fence(logLines(d.logs, oc2.fanOut))) io2.out(l);
5631
+ }
5632
+ }
5633
+ async function interactiveDetail(ctx, oc2, c, id) {
5634
+ const first = await errorDetail(c, id, { env: oc2.env, ...oc2.window }, /* @__PURE__ */ new Set());
5635
+ printDetail(ctx, oc2, first, true);
5636
+ const traceId = first.issue.event.traceId;
5637
+ if (!traceId) return;
5638
+ const scope2 = { env: oc2.env, ...oc2.window };
5639
+ for (; ; ) {
5640
+ const next = await ctx.io.select("Next?", [
5641
+ { name: `Show trace ${oneLine(traceId).slice(0, 8)}`, value: "trace" },
5642
+ { name: "Logs of this trace", value: "logs" },
5643
+ { name: "Done", value: "done" }
5644
+ ]);
5645
+ if (next === "done") return;
5646
+ const extra = { issue: first.issue, warnings: [] };
5647
+ if (next === "trace") extra.trace = await c.trace(traceId, scope2);
5648
+ else {
5649
+ const p = await c.logs({ ...scope2, trace: traceId, limit: 100 });
5650
+ extra.logs = p.items;
5651
+ extra.warnings.push(...pageWarnings(p));
5652
+ }
5653
+ printDetail(ctx, oc2, extra, false);
5654
+ }
5655
+ }
5656
+ function registerErrors(program, ctx) {
5657
+ addObsFlags(program.command("errors [id]").description("Errors of a deployed environment, or one error with its stack trace (requires --env)")).option("--with <list>", `with an id: also show ${WITHS.join(",")}`).option("--status <status>", `${STATUSES.join("|")} (default unresolved)`).option("--sort <sort>", `${SORTS.join("|")} (default date)`).action(async (id, flags) => {
5658
+ const { io: io2, deps: deps2 } = ctx;
5659
+ const g = ctx.opts();
5660
+ const withs = withList(flags.with, WITHS);
5661
+ const status2 = one("--status", flags.status ?? "unresolved", STATUSES);
5662
+ const sort = one("--sort", flags.sort ?? "date", SORTS);
5663
+ const limit = limitOf(flags.limit);
5664
+ const oc2 = await obsContext(ctx, flags, "errors", { single: id !== void 0 });
5665
+ const q = { env: oc2.env, ...oc2.window, q: flags.query, limit };
5666
+ if (id) {
5667
+ const c2 = oc2.targets[0].client;
5668
+ if (g.json) {
5669
+ renderJson(io2, { context: oc2.json, ...await errorDetail(c2, id, q, withs) });
5670
+ return;
5671
+ }
5672
+ io2.err(oc2.header);
5673
+ if (io2.interactive && withs.size === 0) {
5674
+ await interactiveDetail(ctx, oc2, c2, id);
5675
+ return;
5676
+ }
5677
+ printDetail(ctx, oc2, await errorDetail(c2, id, q, withs), true);
5678
+ return;
5679
+ }
5680
+ const f = await fanOut(oc2, (t) => t.client.issues({ ...q, status: status2, sort }));
5681
+ const pages = f.results.map((r) => r.result);
5682
+ const items = pages.flatMap((p) => p.items);
5683
+ if (pages.length > 1) items.sort(MERGE[sort]);
5684
+ const warnings = [...pageWarnings(...pages), ...skipWarnings(f)];
5685
+ if (g.json) {
5686
+ renderJson(io2, { context: oc2.json, items, warnings, ...skipJson(f) });
5687
+ return;
5688
+ }
5689
+ io2.err(oc2.header);
5690
+ printWarnings(io2, warnings, false, f.skipped);
5691
+ if (items.length === 0) {
5692
+ io2.out(`No ${status2 === "all" ? "" : `${status2} `}errors in ${oc2.env} (${windowText(oc2.window)}).`);
5693
+ return;
5694
+ }
5695
+ for (const l of fence(issuesTable(items, deps2.now(), oc2.fanOut))) io2.out(l);
5696
+ if (!io2.interactive) return;
5697
+ const picked = await io2.select(
5698
+ "Which error to open?",
5699
+ [...items.map((i) => ({ name: `${oneLine(i.shortId)} ${oneLine(i.title)}`.slice(0, 100), value: i })), { name: "(none)", value: null }]
5700
+ );
5701
+ if (!picked) return;
5702
+ const c = oc2.targets.find((t) => t.module.id === picked.module.id)?.client;
5703
+ if (!c) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `module ${picked.module.slug} is not among the queried modules`);
5704
+ await interactiveDetail(ctx, oc2, c, picked.shortId);
5705
+ });
5706
+ }
5707
+
5209
5708
  // src/cli/commands/init.ts
5210
5709
  import { realpath as realpath11 } from "fs/promises";
5211
5710
 
5212
5711
  // src/core/auth/oidc.ts
5213
5712
  import * as oc from "openid-client";
5214
- var LOGIN_SCOPE = "openid profile email offline_access qore:read qore:local-env";
5713
+ var LOGIN_SCOPE = "openid profile email offline_access qore:read qore:local-env qore:observability";
5215
5714
  function decodeSub(accessToken) {
5216
5715
  const part = accessToken.split(".")[1] ?? "";
5217
5716
  const claims = JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
@@ -5262,9 +5761,9 @@ async function createOidc(opts) {
5262
5761
  throw mapOidcError(e, host);
5263
5762
  }
5264
5763
  return {
5265
- async startDevice(scope) {
5764
+ async startDevice(scope2) {
5266
5765
  try {
5267
- const r = await oc.initiateDeviceAuthorization(config, { scope });
5766
+ const r = await oc.initiateDeviceAuthorization(config, { scope: scope2 });
5268
5767
  return {
5269
5768
  userCode: r.user_code,
5270
5769
  verificationUri: r.verification_uri,
@@ -5284,9 +5783,9 @@ async function createOidc(opts) {
5284
5783
  throw mapOidcError(e, host);
5285
5784
  }
5286
5785
  },
5287
- async refresh(rt, scope) {
5786
+ async refresh(rt, scope2) {
5288
5787
  try {
5289
- const t = await oc.refreshTokenGrant(config, rt, scope ? { scope } : void 0);
5788
+ const t = await oc.refreshTokenGrant(config, rt, scope2 ? { scope: scope2 } : void 0);
5290
5789
  return toTokenSet(t, rt);
5291
5790
  } catch (e) {
5292
5791
  throw mapOidcError(e, host);
@@ -5538,6 +6037,34 @@ function registerInit(program, ctx) {
5538
6037
  });
5539
6038
  }
5540
6039
 
6040
+ // src/cli/commands/logs.ts
6041
+ var LEVELS = ["error", "warn", "info", "debug"];
6042
+ function registerLogs(program, ctx) {
6043
+ addObsFlags(program.command("logs").description("Logs of a deployed environment (requires --env)")).option("--level <level>", `exactly this severity: ${LEVELS.join("|")}`).option("--trace <id>", "only the logs of one trace").action(async (flags) => {
6044
+ const { io: io2 } = ctx;
6045
+ const g = ctx.opts();
6046
+ const limit = limitOf(flags.limit);
6047
+ const level = flags.level === void 0 ? void 0 : one("--level", flags.level, LEVELS);
6048
+ const oc2 = await obsContext(ctx, flags, "logs", { single: false });
6049
+ const q = { env: oc2.env, ...oc2.window, q: flags.query, limit, level, trace: flags.trace };
6050
+ const f = await fanOut(oc2, (t) => t.client.logs(q));
6051
+ const pages = f.results.map((r) => r.result);
6052
+ const items = pages.flatMap((p) => p.items).sort((a, b) => b.timestamp.localeCompare(a.timestamp));
6053
+ const warnings = [...pageWarnings(...pages), ...skipWarnings(f)];
6054
+ if (g.json) {
6055
+ renderJson(io2, { context: oc2.json, items, warnings, ...skipJson(f) });
6056
+ return;
6057
+ }
6058
+ io2.err(oc2.header);
6059
+ printWarnings(io2, warnings, false, f.skipped);
6060
+ if (items.length === 0) {
6061
+ io2.out(`No logs in ${oc2.env} (${windowText(oc2.window)}).`);
6062
+ return;
6063
+ }
6064
+ for (const l of fence(logLines(items, oc2.fanOut))) io2.out(l);
6065
+ });
6066
+ }
6067
+
5541
6068
  // src/cli/commands/logout.ts
5542
6069
  function registerLogout(program, ctx) {
5543
6070
  program.command("logout").description("End the CLI session").option("--all", "log out of every portal").action(async (cmd) => {
@@ -5570,26 +6097,6 @@ function registerLogout(program, ctx) {
5570
6097
  });
5571
6098
  }
5572
6099
 
5573
- // src/cli/format.ts
5574
- function ago(iso, now) {
5575
- if (!iso) return "\u2013";
5576
- const s = Math.max(0, Math.round((now.getTime() - new Date(iso).getTime()) / 1e3));
5577
- if (s < 60) return "just now";
5578
- if (s < 3600) return `${Math.floor(s / 60)}m ago`;
5579
- if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
5580
- return `${Math.floor(s / 86400)}d ago`;
5581
- }
5582
- var pct = (f) => String(Number((f * 100).toFixed(2)));
5583
- function uptime(h) {
5584
- if (!h) return "\u2013";
5585
- const kind = h.cause && h.kind !== "up" ? `${h.kind}: ${h.cause}` : h.kind;
5586
- return h.uptime24h === null ? kind : `${kind} (${pct(h.uptime24h)}% 24h)`;
5587
- }
5588
- function table(rows) {
5589
- const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length)));
5590
- return rows.map((r) => " " + r.map((c, i) => i === r.length - 1 ? c : c.padEnd(widths[i])).join(" "));
5591
- }
5592
-
5593
6100
  // src/cli/commands/status.ts
5594
6101
  var HEAD = ["ENV", "DEPLOY", "COMMIT", "DEPLOYED", "APP HEALTH"];
5595
6102
  var envRow = (e, now) => [e.name, e.paused ? "paused" : e.status, e.commit?.sha ?? "\u2013", ago(e.commit?.deployedAt ?? null, now), uptime(e.health)];
@@ -5631,7 +6138,7 @@ async function appStatus(ctx, ws, appFlag) {
5631
6138
  }
5632
6139
  function registerStatus(program, ctx) {
5633
6140
  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 });
6141
+ const c = await projectCtx(ctx, { ref: flags.project, module: flags.module, app: flags.app, needModule: false, command: "status" });
5635
6142
  const { deps: deps2, io: io2 } = c;
5636
6143
  const g = c.opts();
5637
6144
  if (!flags.module) {
@@ -5717,6 +6224,109 @@ function registerSync(program, ctx) {
5717
6224
  });
5718
6225
  }
5719
6226
 
6227
+ // src/cli/commands/traces.ts
6228
+ var WITHS2 = ["logs", "errors"];
6229
+ async function spansOf(c, q, sort, warnings) {
6230
+ try {
6231
+ return await c.spans({ ...q, sort });
6232
+ } catch (e) {
6233
+ if (sort === "-duration" && e instanceof QoreError && e.serverCode === "observability.sort_unsupported") {
6234
+ warnings.push("sort_unavailable");
6235
+ return c.spans(q);
6236
+ }
6237
+ throw e;
6238
+ }
6239
+ }
6240
+ function registerTraces(program, ctx) {
6241
+ addObsFlags(program.command("traces [id]").description("Slow or recent requests, or one trace as a span tree (requires --env)")).option("--slower-than <duration>", "only requests slower than this, slowest first (e.g. 500ms, 1s)").option("--with <list>", `with a trace id: also show ${WITHS2.join(",")}`).action(async (id, flags) => {
6242
+ const { io: io2, deps: deps2 } = ctx;
6243
+ const g = ctx.opts();
6244
+ const withs = withList(flags.with, WITHS2);
6245
+ const slower = flags.slowerThan === void 0 ? void 0 : parseDuration(flags.slowerThan);
6246
+ const limit = limitOf(flags.limit);
6247
+ const oc2 = await obsContext(ctx, flags, "traces", { single: id !== void 0 });
6248
+ const q = { env: oc2.env, ...oc2.window, limit };
6249
+ const scope2 = { env: oc2.env, ...oc2.window };
6250
+ const printLogs = (items2) => {
6251
+ io2.out("");
6252
+ io2.out(`Logs (${items2.length})`);
6253
+ for (const l of fence(logLines(items2, oc2.fanOut))) io2.out(l);
6254
+ };
6255
+ const printErrors = (items2) => {
6256
+ io2.out("");
6257
+ io2.out(`Errors (${items2.length})`);
6258
+ for (const l of fence(issuesTable(items2, deps2.now(), oc2.fanOut))) io2.out(l);
6259
+ };
6260
+ const printTree = (trace2) => {
6261
+ for (const l of fence(spanTree(trace2))) io2.out(l);
6262
+ };
6263
+ const more = async (c2, traceId, w) => {
6264
+ const f2 = { ...scope2, trace: traceId };
6265
+ if (w === "logs") {
6266
+ const p = await c2.logs({ ...f2, limit: 100 });
6267
+ printWarnings(io2, pageWarnings(p), false);
6268
+ printLogs(p.items);
6269
+ } else {
6270
+ const p = await c2.issues({ ...f2, status: "all" });
6271
+ printWarnings(io2, pageWarnings(p), false);
6272
+ printErrors(p.items);
6273
+ }
6274
+ };
6275
+ if (id) {
6276
+ const c2 = oc2.targets[0].client;
6277
+ const b = await traceDetail(c2, id, q, withs);
6278
+ if (g.json) {
6279
+ renderJson(io2, { context: oc2.json, ...b });
6280
+ return;
6281
+ }
6282
+ io2.err(oc2.header);
6283
+ printWarnings(io2, b.warnings, false);
6284
+ printTree(b.trace);
6285
+ if (b.logs) printLogs(b.logs);
6286
+ if (b.errors) printErrors(b.errors);
6287
+ return;
6288
+ }
6289
+ const warnings = [];
6290
+ const listQ = { ...q, q: joinQuery(ROOT_SPAN_FILTER, slower && `span.duration:>${slower}`, flags.query) };
6291
+ const sort = slower ? "-duration" : "-timestamp";
6292
+ const f = await fanOut(oc2, (t) => spansOf(t.client, listQ, sort, warnings));
6293
+ const pages = f.results.map((r) => r.result);
6294
+ warnings.push(...pageWarnings(...pages), ...skipWarnings(f));
6295
+ const items = pages.flatMap((p) => p.items).sort((a, b) => slower ? b.durationMs - a.durationMs : b.timestamp.localeCompare(a.timestamp));
6296
+ const uniq = [...new Set(warnings)];
6297
+ if (g.json) {
6298
+ renderJson(io2, { context: oc2.json, items, warnings: uniq, ...skipJson(f) });
6299
+ return;
6300
+ }
6301
+ io2.err(oc2.header);
6302
+ printWarnings(io2, uniq, false, f.skipped);
6303
+ if (items.length === 0) {
6304
+ io2.out(`No ${slower ? `requests slower than ${slower}` : "traces"} in ${oc2.env} (${windowText(oc2.window)}).`);
6305
+ return;
6306
+ }
6307
+ for (const l of fence(spansTable(items, deps2.now(), oc2.fanOut))) io2.out(l);
6308
+ if (!io2.interactive) return;
6309
+ const picked = await io2.select(
6310
+ "Which trace to open?",
6311
+ [...items.map((s) => ({ name: `${oneLine(s.traceId).slice(0, 8)} ${oneLine(s.transaction ?? s.op)}`, value: s })), { name: "(none)", value: null }]
6312
+ );
6313
+ if (!picked) return;
6314
+ const c = oc2.targets.find((t) => t.module.id === picked.module.id)?.client;
6315
+ if (!c) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `module ${picked.module.slug} is not among the queried modules`);
6316
+ const { trace } = await traceDetail(c, picked.traceId, q, /* @__PURE__ */ new Set());
6317
+ printTree(trace);
6318
+ for (; ; ) {
6319
+ const next = await io2.select("Next?", [
6320
+ { name: "Logs of this trace", value: "logs" },
6321
+ { name: "Errors in this trace", value: "errors" },
6322
+ { name: "Done", value: "done" }
6323
+ ]);
6324
+ if (next === "done") return;
6325
+ await more(c, picked.traceId, next);
6326
+ }
6327
+ });
6328
+ }
6329
+
5720
6330
  // src/cli/commands/version.ts
5721
6331
  function registerVersion(program, ctx) {
5722
6332
  program.command("version").description("Show the CLI and portal version").action(async () => {
@@ -5785,6 +6395,9 @@ var COMMANDS = [
5785
6395
  registerWhoami,
5786
6396
  registerTenant,
5787
6397
  registerStatus,
6398
+ registerLogs,
6399
+ registerTraces,
6400
+ registerErrors,
5788
6401
  registerEnv,
5789
6402
  registerRun,
5790
6403
  registerDev,
@@ -5844,13 +6457,13 @@ function createTelemetry(deps2, o) {
5844
6457
  },
5845
6458
  async end(signal) {
5846
6459
  if (!sdk) return { drained: true };
5847
- const ms = crashed ? flushMs.crash : flushMs.clean;
6460
+ const ms2 = crashed ? flushMs.crash : flushMs.clean;
5848
6461
  const work = sdk.then(async (s) => {
5849
6462
  if (!s) return true;
5850
6463
  if (crashed) s.crash(crashed.e);
5851
- return s.end(ms);
6464
+ return s.end(ms2);
5852
6465
  }).catch(() => false);
5853
- return { drained: await withDeadline(work, ms, signal) };
6466
+ return { drained: await withDeadline(work, ms2, signal) };
5854
6467
  }
5855
6468
  };
5856
6469
  }
@@ -5871,7 +6484,7 @@ async function start(deps2, argv2, command, load, onScrub) {
5871
6484
  sdk.start();
5872
6485
  return sdk;
5873
6486
  }
5874
- function withDeadline(work, ms, signal) {
6487
+ function withDeadline(work, ms2, signal) {
5875
6488
  if (signal?.aborted) return Promise.resolve(false);
5876
6489
  return new Promise((resolve6) => {
5877
6490
  const done = (v) => {
@@ -5880,7 +6493,7 @@ function withDeadline(work, ms, signal) {
5880
6493
  resolve6(v);
5881
6494
  };
5882
6495
  const onAbort = () => done(false);
5883
- const timer = setTimeout(() => done(false), ms);
6496
+ const timer = setTimeout(() => done(false), ms2);
5884
6497
  signal?.addEventListener("abort", onAbort, { once: true });
5885
6498
  work.then(done, () => done(false));
5886
6499
  });
@@ -6508,7 +7121,7 @@ function systemProxy(platform = process.platform) {
6508
7121
  }
6509
7122
 
6510
7123
  // src/version.ts
6511
- var VERSION = true ? "0.1.6" : "0.0.0-dev";
7124
+ var VERSION = true ? "0.1.7" : "0.0.0-dev";
6512
7125
 
6513
7126
  // src/cli.ts
6514
7127
  if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
@@ -6586,4 +7199,4 @@ if (!drained) {
6586
7199
  process.exit();
6587
7200
  }
6588
7201
  //# sourceMappingURL=cli.js.map
6589
- //# debugId=28c91dc8-dbb9-5c0a-91e3-f368931a4ed3
7202
+ //# debugId=bf813c0e-4054-532d-9e74-62fbe8673876
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quatronic/qore",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Qore developer CLI",
5
5
  "type": "module",
6
6
  "bin": {