@monster-orange/den 0.4.15 → 0.4.17

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 (37) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +7 -7
  3. package/docs/distributions.md +137 -11
  4. package/package.json +1 -1
  5. package/src/shared/agents/conversations.ts +122 -4
  6. package/src/shared/agents/den-claude.ts +13 -1
  7. package/src/shared/agents/records.ts +19 -0
  8. package/src/shared/agents/runtime.ts +5 -0
  9. package/src/shared/github-app.ts +7 -0
  10. package/src/ui/cli/agent-ops.ts +304 -1
  11. package/src/ui/cli/agents-command.ts +71 -7
  12. package/src/ui/server/agent-routes.ts +92 -2
  13. package/src/ui/server/check-pool.ts +72 -0
  14. package/src/ui/server/github-routes.ts +1 -1
  15. package/src/ui/server/index.ts +127 -15
  16. package/src/ui/server/public/agents-page.js +358 -38
  17. package/src/ui/server/public/agents.html +10 -0
  18. package/src/ui/server/public/environments.html +223 -140
  19. package/src/ui/server/public/error-page.js +70 -15
  20. package/src/ui/server/public/error.html +18 -0
  21. package/src/ui/server/public/graph.css +18 -0
  22. package/src/ui/server/public/graph.js +23 -6
  23. package/src/ui/server/public/i18n/es.js +47 -5
  24. package/src/ui/server/public/i18n/ja.js +47 -5
  25. package/src/ui/server/public/index.html +6 -0
  26. package/src/ui/server/public/local-dev.html +188 -25
  27. package/src/ui/server/public/panel.js +1 -1
  28. package/src/ui/server/public/persona-ui.css +11 -0
  29. package/src/ui/server/public/release.html +19 -1
  30. package/src/ui/server/public/shell.js +187 -12
  31. package/src/ui/server/public/topbar.js +34 -1
  32. package/src/ui/server/public/view-scope.js +61 -0
  33. package/src/ui/server/public/workspace-picker.js +2 -0
  34. package/src/ui/server/route-classes.ts +8 -0
  35. package/src/ui/server/run-history.ts +181 -0
  36. package/src/ui/server/session-routes.ts +97 -17
  37. package/src/ui/server/ui-assets.ts +2 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * How health checks run (0.4.17): at most CHECK_CONCURRENCY at once, each
3
+ * given at most CHECK_TIMEOUT_MS. A check that runs over its time gets the
4
+ * caller's stand-in result ("timed out after 10 s") rather than holding up
5
+ * the run, and the run never waits on it again.
6
+ */
7
+
8
+ /** The most checks in flight at once. */
9
+ export const CHECK_CONCURRENCY = 12;
10
+ /** The longest one check may take, whatever its kind. */
11
+ export const CHECK_TIMEOUT_MS = 10_000;
12
+
13
+ /** What a check that ran out of time says. */
14
+ export function timedOutText(ms: number): string {
15
+ return `timed out after ${Math.round(ms / 1000)} s`;
16
+ }
17
+
18
+ /**
19
+ * `p`, or `late()` once `ms` passes first. A late `p` that later settles
20
+ * is ignored, and so is its rejection.
21
+ */
22
+ export function withTimeout<T>(
23
+ p: Promise<T>,
24
+ ms: number,
25
+ late: () => T,
26
+ ): Promise<T> {
27
+ return new Promise<T>((resolve, reject) => {
28
+ let done = false;
29
+ const timer = setTimeout(() => {
30
+ if (done) return;
31
+ done = true;
32
+ resolve(late());
33
+ }, ms);
34
+ timer.unref?.();
35
+ p.then(
36
+ (v) => {
37
+ if (done) return;
38
+ done = true;
39
+ clearTimeout(timer);
40
+ resolve(v);
41
+ },
42
+ (e: unknown) => {
43
+ if (done) return;
44
+ done = true;
45
+ clearTimeout(timer);
46
+ reject(e);
47
+ },
48
+ );
49
+ });
50
+ }
51
+
52
+ /**
53
+ * `fn` over `items`, at most `limit` at a time, the results in the items'
54
+ * order. Each starts as soon as a slot frees up.
55
+ */
56
+ export async function runCapped<T, R>(
57
+ items: readonly T[],
58
+ limit: number,
59
+ fn: (item: T, index: number) => Promise<R>,
60
+ ): Promise<R[]> {
61
+ const out = new Array<R>(items.length);
62
+ let next = 0;
63
+ const worker = async (): Promise<void> => {
64
+ while (next < items.length) {
65
+ const i = next++;
66
+ out[i] = await fn(items[i], i);
67
+ }
68
+ };
69
+ const n = Math.max(1, Math.min(limit, items.length));
70
+ await Promise.all(Array.from({ length: n }, worker));
71
+ return out;
72
+ }
@@ -98,7 +98,7 @@ function finishedMessage(v: FlowView, cli: string): string | undefined {
98
98
  switch (v.state) {
99
99
  case "refused":
100
100
  return v.purpose === "signin"
101
- ? `@${v.login} isn't allowed on this Den. Ask the owner to add you.`
101
+ ? `@${v.login} isn't the owner of this Den. Only the owner's account can use Sign in with GitHub. Use Approve this computer instead.`
102
102
  : `An owner is linked already; \`${cli} web owner unlink\` first.`;
103
103
  case "expired":
104
104
  return "The code expired before it was approved. Start again.";
@@ -31,6 +31,8 @@ import { moduleScanner } from "../../shared/test-discovery/module-scanner.js";
31
31
  import { resolveFieldEnv } from "../../shared/perf/optional-fields.js";
32
32
  import { testRunner } from "../cli/test-runner.js";
33
33
  import { discoverRepos } from "../../pillars/smoke/discover.js";
34
+ import { planSuites } from "../../pillars/suites/run.js";
35
+ import { listServicesWithManifests } from "../../shared/manifest/workspace-manifests.js";
34
36
  import {
35
37
  awsProfileEnv,
36
38
  loadAwsProfiles,
@@ -244,6 +246,7 @@ import {
244
246
  githubClientId,
245
247
  githubEndpoints,
246
248
  noClientIdMessage,
249
+ GITHUB_SIGNIN_DOCS,
247
250
  } from "../../shared/github-app.js";
248
251
  import { githubDeviceClient } from "../../shared/github-device.js";
249
252
  import { readWebOwner } from "../../shared/web-owner.js";
@@ -855,6 +858,19 @@ import { heldActions } from "./held-actions.js";
855
858
  import type { ApprovalQueue } from "./approvals.js";
856
859
  import { processPathRules } from "../cli/onboard-reads.js";
857
860
  import { handleAgentRoutes, startAgentUpkeep } from "./agent-routes.js";
861
+ import {
862
+ lastRunsByRepo,
863
+ recordRunEnd,
864
+ recordRunStart,
865
+ reposOfRun,
866
+ } from "./run-history.js";
867
+ import {
868
+ CHECK_CONCURRENCY,
869
+ CHECK_TIMEOUT_MS,
870
+ runCapped,
871
+ timedOutText,
872
+ withTimeout,
873
+ } from "./check-pool.js";
858
874
  import {
859
875
  handleSessionList,
860
876
  handleSessionUpgrade,
@@ -1272,6 +1288,9 @@ function sshUserNow(): string {
1272
1288
  return sshUser(set, osUser);
1273
1289
  }
1274
1290
 
1291
+ /** Test runs this server is running now, by id (run-history.ts). */
1292
+ const LIVE_RUNS = new Set<string>();
1293
+
1275
1294
  /** The ssh target for this request: the Host it used, minus the port. */
1276
1295
  function sshTargetOf(req: http.IncomingMessage): string {
1277
1296
  return sshTarget(req.headers.host, os.hostname());
@@ -1331,10 +1350,19 @@ function denEdgeRoutes(): { host: string; url: string }[] {
1331
1350
  }
1332
1351
  }
1333
1352
 
1334
- /** What the signed-out page says about Sign in with GitHub. */
1335
- function githubSigninState(): "ready" | "no_client_id" | "no_owner" {
1336
- if (!GITHUB_CLIENT_ID) return "no_client_id";
1337
- return readWebOwner(getStateDir()) ? "ready" : "no_owner";
1353
+ /**
1354
+ * What the signed-out page says about Sign in with GitHub: its state, and
1355
+ * (SEC-2a-UX) the owner's login once one is linked, for step 1's "Owner:
1356
+ * @login".
1357
+ */
1358
+ function githubSignin():
1359
+ | { state: "no_client_id" | "no_owner" }
1360
+ | { state: "ready"; ownerLogin: string } {
1361
+ if (!GITHUB_CLIENT_ID) return { state: "no_client_id" };
1362
+ const owner = readWebOwner(getStateDir());
1363
+ return owner
1364
+ ? { state: "ready", ownerLogin: owner.login }
1365
+ : { state: "no_owner" };
1338
1366
  }
1339
1367
 
1340
1368
  /**
@@ -1426,6 +1454,7 @@ function sendSignedOut(
1426
1454
  const cmd = cliCommand(bin, "web", "login", ...loginArgs);
1427
1455
  const ssh = sshTargetOf(req);
1428
1456
  const user = sshUserNow();
1457
+ const gh = githubSignin();
1429
1458
  res.setHeader("Cache-Control", "no-store");
1430
1459
  sendErrorPage(req, res, {
1431
1460
  status: 401,
@@ -1449,8 +1478,10 @@ function sendSignedOut(
1449
1478
  },
1450
1479
  // SEC-2a: Sign in with GitHub, and why it can't be offered yet.
1451
1480
  github: {
1452
- state: githubSigninState(),
1481
+ state: gh.state,
1453
1482
  owner: cliCommand(bin, "web", "owner", "link"),
1483
+ ...(gh.state === "ready" ? { ownerLogin: gh.ownerLogin } : {}),
1484
+ learnMore: GITHUB_SIGNIN_DOCS,
1454
1485
  ...(GITHUB_CLIENT_ID
1455
1486
  ? {}
1456
1487
  : { message: noClientIdMessage(product().cli) }),
@@ -1956,8 +1987,10 @@ async function handleRequest(
1956
1987
  // AG-4a-term's session routes (`/api/sessions/<id>/io`): shell-grade, and
1957
1988
  // an agent's token reaches its own session's only (request-guard). The io
1958
1989
  // is a WebSocket (the server's upgrade handler): a plain GET of it is 426.
1959
- if (pathname.startsWith("/api/sessions/"))
1960
- return sendSessionHttp(req, res, pathname);
1990
+ if (pathname.startsWith("/api/sessions/")) {
1991
+ await sendSessionHttp(req, res, pathname);
1992
+ return;
1993
+ }
1961
1994
  // ON-3: an agent's held writes; Allow from the person's browser only.
1962
1995
  if (
1963
1996
  guard.ok &&
@@ -2769,15 +2802,55 @@ async function handleRequest(
2769
2802
  // Streaming emits in completion order, which varies per poll (shell probes
2770
2803
  // finish at wildly different speeds), so without a stable key the client
2771
2804
  // repaints the dots in a different order every 5s.
2772
- const results = await Promise.all(
2773
- sortedEntries.map((e, idx) =>
2774
- checkEntry(e).then((r) => {
2775
- const ranked = { ...r, openUrl: e.openUrl, links: e.links, idx };
2776
- onResult?.(ranked);
2777
- return ranked;
2778
- }),
2805
+ // 0.4.17: at most CHECK_CONCURRENCY at once, each with at most
2806
+ // CHECK_TIMEOUT_MS: a dead host's check says "timed out after 10 s" and
2807
+ // never holds up the run (check-pool.ts).
2808
+ const late = (e: (typeof sortedEntries)[number]): HealthResult => ({
2809
+ repo: e.repo,
2810
+ service: e.service,
2811
+ label: e.label,
2812
+ ...(e.checkType !== "none"
2813
+ ? healthCheckFields(e.checkType, e.target)
2814
+ : { url: null }),
2815
+ repoPath: e.repoPath,
2816
+ mode: e.mode,
2817
+ isEntrypoint: e.isEntrypoint,
2818
+ ...e.meta,
2819
+ ms: CHECK_TIMEOUT_MS,
2820
+ ok: false,
2821
+ error: timedOutText(CHECK_TIMEOUT_MS),
2822
+ });
2823
+ const run = (e: (typeof sortedEntries)[number], idx: number) =>
2824
+ withTimeout(checkEntry(e), CHECK_TIMEOUT_MS, () => late(e)).then((r) => {
2825
+ const ranked = { ...r, openUrl: e.openUrl, links: e.links, idx };
2826
+ onResult?.(ranked);
2827
+ return ranked;
2828
+ });
2829
+ // Compose and container checks are answered in-process from one shared
2830
+ // docker listing (the call in flight): they start no process, so they
2831
+ // run outside the cap, all at once, and a cycle still lists once.
2832
+ const inProcess = (e: (typeof sortedEntries)[number]) =>
2833
+ (e.checkType === "container" && !!e.container) ||
2834
+ (e.checkType === "shell" && !!e.compose && e.target === e.compose.cmd);
2835
+ const indexed = sortedEntries.map((e, idx) => ({ e, idx }));
2836
+ const results = new Array<HealthResult>(sortedEntries.length);
2837
+ await Promise.all([
2838
+ ...indexed
2839
+ .filter(({ e }) => inProcess(e))
2840
+ .map(({ e, idx }) =>
2841
+ run(e, idx).then((r) => {
2842
+ results[idx] = r;
2843
+ }),
2844
+ ),
2845
+ runCapped(
2846
+ indexed.filter(({ e }) => !inProcess(e)),
2847
+ CHECK_CONCURRENCY,
2848
+ ({ e, idx }) =>
2849
+ run(e, idx).then((r) => {
2850
+ results[idx] = r;
2851
+ }),
2779
2852
  ),
2780
- );
2853
+ ]);
2781
2854
  return results;
2782
2855
  }
2783
2856
 
@@ -3400,6 +3473,13 @@ async function handleRequest(
3400
3473
  return;
3401
3474
  }
3402
3475
 
3476
+ // ── API: each repo's last test run (0.4.17, run-history.ts) ─────────────────
3477
+ if (pathname === "/api/runs/last") {
3478
+ return sendJson(res, {
3479
+ repos: lastRunsByRepo(getStateDir(), LIVE_RUNS),
3480
+ });
3481
+ }
3482
+
3403
3483
  // ── API: running daemons list ──────────────────────────────────────────────
3404
3484
  if (pathname === "/api/service/daemons") {
3405
3485
  await startupPrune;
@@ -3865,8 +3945,36 @@ async function handleRequest(
3865
3945
 
3866
3946
  const startTime = Date.now();
3867
3947
  let overallExit = 0;
3948
+ // 0.4.17: the server's run history (every browser's runs, kept), which
3949
+ // Workspaces' Last test run reads per repo.
3950
+ // Every run kind that runs a repo's own tests names it, the suites
3951
+ // pillar (`tests:`) and a repo's performance modules included.
3952
+ const tested = await reposOfRun(String(pillar), String(env), modules, {
3953
+ pillarSources: (p) =>
3954
+ collectTestSources(p as Pillar).map((x) => x.repoName),
3955
+ suiteRepos: (e, only) =>
3956
+ planSuites(listServicesWithManifests(), e as Environment, only).run.map(
3957
+ (x) => x.repo,
3958
+ ),
3959
+ perfModules: async () =>
3960
+ (await moduleScanner.scanModules("performance")).map((m) => m.name),
3961
+ repoNames: () => discoverRepos().map((r) => r.repoName),
3962
+ });
3963
+ LIVE_RUNS.add(runId);
3964
+ recordRunStart(getStateDir(), {
3965
+ id: runId,
3966
+ pillar: String(pillar),
3967
+ env: String(env),
3968
+ repos: tested,
3969
+ });
3970
+ let recorded = false;
3868
3971
 
3869
3972
  const finishLifecycle = (exitCode: number, note?: string): void => {
3973
+ if (!recorded) {
3974
+ recorded = true;
3975
+ LIVE_RUNS.delete(runId);
3976
+ recordRunEnd(getStateDir(), runId, exitCode);
3977
+ }
3870
3978
  const dur = formatDuration(Date.now() - startTime);
3871
3979
  const extra = note ? ` · ${note}` : "";
3872
3980
  serverLog(
@@ -4073,6 +4181,10 @@ async function handleRequest(
4073
4181
  actor: null,
4074
4182
  signedIn: false,
4075
4183
  ...(g && !g.beyond ? { identity: { kind: "local" } } : {}),
4184
+ // SEC-2a-UX: the signed-out page watches this, so Sign in with
4185
+ // GitHub appears once an owner is linked, with no reload. The
4186
+ // owner's login is what that page shows anyway.
4187
+ ...(g?.beyond || !g ? { github: githubSignin() } : {}),
4076
4188
  });
4077
4189
  }
4078
4190
  // Sign out: this browser's session ends and its cookie is cleared.