@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.
- package/CHANGELOG.md +125 -0
- package/README.md +7 -7
- package/docs/distributions.md +137 -11
- package/package.json +1 -1
- package/src/shared/agents/conversations.ts +122 -4
- package/src/shared/agents/den-claude.ts +13 -1
- package/src/shared/agents/records.ts +19 -0
- package/src/shared/agents/runtime.ts +5 -0
- package/src/shared/github-app.ts +7 -0
- package/src/ui/cli/agent-ops.ts +304 -1
- package/src/ui/cli/agents-command.ts +71 -7
- package/src/ui/server/agent-routes.ts +92 -2
- package/src/ui/server/check-pool.ts +72 -0
- package/src/ui/server/github-routes.ts +1 -1
- package/src/ui/server/index.ts +127 -15
- package/src/ui/server/public/agents-page.js +358 -38
- package/src/ui/server/public/agents.html +10 -0
- package/src/ui/server/public/environments.html +223 -140
- package/src/ui/server/public/error-page.js +70 -15
- package/src/ui/server/public/error.html +18 -0
- package/src/ui/server/public/graph.css +18 -0
- package/src/ui/server/public/graph.js +23 -6
- package/src/ui/server/public/i18n/es.js +47 -5
- package/src/ui/server/public/i18n/ja.js +47 -5
- package/src/ui/server/public/index.html +6 -0
- package/src/ui/server/public/local-dev.html +188 -25
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/public/persona-ui.css +11 -0
- package/src/ui/server/public/release.html +19 -1
- package/src/ui/server/public/shell.js +187 -12
- package/src/ui/server/public/topbar.js +34 -1
- package/src/ui/server/public/view-scope.js +61 -0
- package/src/ui/server/public/workspace-picker.js +2 -0
- package/src/ui/server/route-classes.ts +8 -0
- package/src/ui/server/run-history.ts +181 -0
- package/src/ui/server/session-routes.ts +97 -17
- 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
|
|
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.";
|
package/src/ui/server/index.ts
CHANGED
|
@@ -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
|
-
/**
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
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.
|