@akira-tl/forgerelay 0.8.7 → 0.8.9
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 +18 -0
- package/README.md +10 -18
- package/capabilities/host-integration/GUIDE.md +2 -2
- package/dist/activity/{audit-store.js → history/audit-store.js} +129 -21
- package/dist/activity/history/bash-output-store.js +250 -0
- package/dist/activity/{host-turn-store.js → history/host-turn-store.js} +4 -1
- package/dist/activity/{query-service.js → history/query-service.js} +13 -7
- package/dist/activity/{lifecycle.js → runtime/lifecycle.js} +1 -1
- package/dist/activity/{mcp-query-tools.js → runtime/mcp-query-tools.js} +3 -3
- package/dist/activity/storage/history.js +12 -0
- package/dist/activity/storage/paths.js +56 -0
- package/dist/activity/storage/segmented-log.js +149 -0
- package/dist/cli/setup-support.js +116 -0
- package/dist/cli.js +18 -126
- package/dist/lsp/code-intelligence.js +1 -1
- package/dist/lsp/test-support/server-fixture.js +9 -9
- package/dist/mcp/filesystem/filesystem-tools.js +245 -0
- package/dist/mcp/hooks/command-runner.js +79 -0
- package/dist/{hook-cli.js → mcp/hooks/hook-cli.js} +1 -1
- package/dist/{hooks.js → mcp/hooks/hooks.js} +3 -80
- package/dist/{oauth-provider.js → mcp/oauth/oauth-provider.js} +1 -1
- package/dist/{oauth-store.js → mcp/oauth/oauth-store.js} +5 -4
- package/dist/{operations → mcp/operations}/native-bulk-mutations.js +2 -2
- package/dist/mcp/panel/app.js +143 -0
- package/dist/{mcp-app-template.js → mcp/panel/mcp-app-template.js} +2 -38
- package/dist/mcp/process/runtime.js +257 -0
- package/dist/mcp/process/tools.js +346 -0
- package/dist/mcp/server/core/activity-support.js +70 -0
- package/dist/{capabilities.js → mcp/server/core/capabilities.js} +5 -3
- package/dist/{capability-registry.js → mcp/server/core/capability-registry.js} +2 -2
- package/dist/mcp/server/core/capability-support.js +90 -0
- package/dist/mcp/server/core/schemas.js +216 -0
- package/dist/mcp/server/core/tool-support.js +207 -0
- package/dist/mcp/server/core/version.js +9 -0
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +400 -0
- package/dist/mcp/server/operations/runtime/operation-runtime.js +523 -0
- package/dist/mcp/server/transport/http-server.js +301 -0
- package/dist/mcp/server/transport/http-support.js +43 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +222 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
- package/dist/mcp/server/workspace/runtime/workspace-open.js +552 -0
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
- package/dist/{config.js → runtime/config/config.js} +3 -3
- package/dist/{user-config.js → runtime/config/user-config.js} +14 -54
- package/dist/{db → runtime/state/db}/client.js +1 -1
- package/dist/{db → runtime/state/db}/migrations.js +34 -3
- package/dist/runtime/state/lock/file-lock.js +130 -0
- package/dist/runtime/testing/server-fixture.js +190 -0
- package/dist/server.js +139 -4192
- package/dist/subagents/providers/adapters/claude.js +86 -52
- package/dist/subagents/providers/adapters/codex.js +103 -39
- package/dist/subagents/providers/adapters/opencode.js +114 -84
- package/dist/subagents/providers/adapters/pi.js +59 -8
- package/dist/subagents/providers/availability.js +15 -30
- package/dist/subagents/providers/runtime/external-command.js +56 -0
- package/dist/subagents/sessions/capability.js +1 -1
- package/dist/subagents/sessions/execution.js +1 -1
- package/dist/subagents/sessions/mcp/activity-audit-test-support.js +30 -0
- package/dist/subagents/sessions/store.js +1 -1
- package/dist/ui/.vite/manifest.json +342 -409
- package/dist/ui/activity-panel-app.html +2 -4
- package/dist/ui/assets/activity-panel-app-7Xe0alhm.css +1 -0
- package/dist/ui/assets/activity-panel-app-Sl1Jro8v.js +115 -0
- package/dist/ui/assets/angular-html-B_yn91wW.js +1 -0
- package/dist/ui/assets/{angular-ts-Bxh4-O1-.js → angular-ts-DSgtloKz.js} +1 -1
- package/dist/ui/assets/{apl-BmYHKQ6m.js → apl-NatWtF8X.js} +1 -1
- package/dist/ui/assets/{astro-C51vqDKq.js → astro-BYFEVBuN.js} +1 -1
- package/dist/ui/assets/{blade-CdhVsmwq.js → blade-DoO7yvMR.js} +1 -1
- package/dist/ui/assets/c-Rl2hMCcg.js +1 -0
- package/dist/ui/assets/{cobol-Cf6azoN_.js → cobol-ByC5mWiB.js} +1 -1
- package/dist/ui/assets/{coffee-vVdGzlmt.js → coffee-Dez77Qov.js} +1 -1
- package/dist/ui/assets/cpp-DtaAQT9h.js +1 -0
- package/dist/ui/assets/{crystal-5UZy6KIX.js → crystal-Bgmbf8ec.js} +1 -1
- package/dist/ui/assets/css-C1tjBtVN.js +1 -0
- package/dist/ui/assets/{edge-CESgo4ia.js → edge-DeH6u8Wg.js} +1 -1
- package/dist/ui/assets/{elixir-9FKfw4cJ.js → elixir-_H3KnmWT.js} +1 -1
- package/dist/ui/assets/{elm-Dv7A0aLa.js → elm-CsCe3U28.js} +1 -1
- package/dist/ui/assets/{erb-DHFmDvM0.js → erb-CbM-L2g8.js} +1 -1
- package/dist/ui/assets/{git-rebase-utbU6CzV.js → git-rebase-D0s5pDh9.js} +1 -1
- package/dist/ui/assets/{glimmer-js-t2kiCkyB.js → glimmer-js-BfOToq_A.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-F0Jv5okV.js → glimmer-ts-CCUOkL2K.js} +1 -1
- package/dist/ui/assets/glsl-DH4ec9dE.js +1 -0
- package/dist/ui/assets/graphql-DilAMBoI.js +1 -0
- package/dist/ui/assets/{hack-8yjdbaSA.js → hack-BDkNDZlF.js} +1 -1
- package/dist/ui/assets/haml-D_QBp6K7.js +1 -0
- package/dist/ui/assets/{handlebars-BBFEg4Lo.js → handlebars-DePUrTpC.js} +1 -1
- package/dist/ui/assets/heavy-payload-DOwZpO6n.js +290 -0
- package/dist/ui/assets/html-By4dNnM4.js +1 -0
- package/dist/ui/assets/{html-derivative-DJAQcZyc.js → html-derivative-DDw9Ef4Q.js} +1 -1
- package/dist/ui/assets/{http-Dg5LSYRK.js → http-B0QxWc9M.js} +1 -1
- package/dist/ui/assets/{hurl-Y5uYzMZR.js → hurl-vMQ4lda1.js} +1 -1
- package/dist/ui/assets/java-DYRe2lWG.js +1 -0
- package/dist/ui/assets/javascript-CxyGqwcL.js +1 -0
- package/dist/ui/assets/{jinja-Bt30h8Ik.js → jinja-JUwV-fZt.js} +1 -1
- package/dist/ui/assets/{jison-D5uZ1Nyo.js → jison-7ZqCEvS8.js} +1 -1
- package/dist/ui/assets/json-dbltgiiA.js +1 -0
- package/dist/ui/assets/jsx-oDopT7YQ.js +1 -0
- package/dist/ui/assets/{julia-76hRaaGz.js → julia-CkKiNSav.js} +1 -1
- package/dist/ui/assets/{just-CDE7W0vW.js → just-DuqCAKJ3.js} +1 -1
- package/dist/ui/assets/{latex-CVO2Byab.js → latex-sudcRDSn.js} +1 -1
- package/dist/ui/assets/{liquid-CA-AS3EX.js → liquid-Di-08Js7.js} +1 -1
- package/dist/ui/assets/lua-YlqdMoV0.js +1 -0
- package/dist/ui/assets/{marko-_BWrPfny.js → marko-CJRNyJDb.js} +1 -1
- package/dist/ui/assets/{mdc-PyJv85eS.js → mdc-t12CzZHs.js} +1 -1
- package/dist/ui/assets/{nginx-DAWVw8yu.js → nginx-57-b4XwT.js} +1 -1
- package/dist/ui/assets/{nim-CKE1dzpU.js → nim-BzrswYQa.js} +1 -1
- package/dist/ui/assets/{perl-BBhWva19.js → perl-D9JR_iIh.js} +1 -1
- package/dist/ui/assets/{php-ChIeaAoF.js → php-Py2gFHil.js} +1 -1
- package/dist/ui/assets/{pug-CMgI5afi.js → pug-PzLLy0h1.js} +1 -1
- package/dist/ui/assets/{qml-CjxTkcFH.js → qml-By8fwgV4.js} +1 -1
- package/dist/ui/assets/r-CcezTbLq.js +1 -0
- package/dist/ui/assets/{razor-i4DbHr8p.js → razor-dG-vdhL_.js} +1 -1
- package/dist/ui/assets/regexp-BlsQ-N_8.js +1 -0
- package/dist/ui/assets/{rst-oy_97Kl4.js → rst-C2awDCat.js} +1 -1
- package/dist/ui/assets/{ruby-C5zYZIyl.js → ruby-zA2W_49d.js} +1 -1
- package/dist/ui/assets/{sas-BnMEh2S3.js → sas-D49bhyaN.js} +1 -1
- package/dist/ui/assets/scss-BQb-cp01.js +1 -0
- package/dist/ui/assets/shellscript-Dga4H_Ni.js +1 -0
- package/dist/ui/assets/{shellsession-BpE5o7_3.js → shellsession-n3-5CqcF.js} +1 -1
- package/dist/ui/assets/{soy-COesmhTY.js → soy-XYBOf_BA.js} +1 -1
- package/dist/ui/assets/sql-ET4u5Cpl.js +1 -0
- package/dist/ui/assets/{stata-CGQEuSZq.js → stata-CLHcJHO_.js} +1 -1
- package/dist/ui/assets/{surrealql-Bz4JbU-9.js → surrealql-BdjQ60T4.js} +1 -1
- package/dist/ui/assets/{svelte-1jM2ejXj.js → svelte-B2RPtKJe.js} +1 -1
- package/dist/ui/assets/{templ-C-VwELcp.js → templ-CrByPW_W.js} +1 -1
- package/dist/ui/assets/{tex-t1rUiQCm.js → tex-DZBkg363.js} +1 -1
- package/dist/ui/assets/{ts-tags-Bewoa6pS.js → ts-tags-DJR3Gphb.js} +1 -1
- package/dist/ui/assets/tsx-CajzROyy.js +1 -0
- package/dist/ui/assets/{twig-DVg-vELl.js → twig-BZjk5Zda.js} +1 -1
- package/dist/ui/assets/typescript-B-EWToLj.js +1 -0
- package/dist/ui/assets/{vue-wPQ7UlMy.js → vue-D_rHCCr8.js} +1 -1
- package/dist/ui/assets/{vue-html-BHtJ8geX.js → vue-html-CC4WlvVt.js} +1 -1
- package/dist/ui/assets/{vue-vine-CNSGAWwc.js → vue-vine-BgNMVgpk.js} +1 -1
- package/dist/ui/assets/xml-BI86-WbH.js +1 -0
- package/dist/ui/assets/{xsl-3_vJ27aE.js → xsl-DXeLZV4J.js} +1 -1
- package/dist/ui/assets/yaml-60_PFlWa.js +1 -0
- package/dist/workspaces/bootstrap.js +72 -0
- package/dist/workspaces/context.js +302 -0
- package/dist/workspaces/conversation-test-support.js +86 -0
- package/dist/{git-worktrees.js → workspaces/git/git-worktrees.js} +1 -1
- package/dist/workspaces/inventory.js +131 -0
- package/dist/workspaces/paths.js +55 -0
- package/dist/{remote-auth.js → workspaces/relay/auth/remote-auth.js} +2 -2
- package/dist/workspaces/relay/result-support.js +111 -0
- package/dist/workspaces/relay/tests/test-support.js +203 -0
- package/dist/{remote-mcp-connection-pool.js → workspaces/relay/transport/remote-mcp-connection-pool.js} +1 -1
- package/dist/workspaces/relay/types.js +1 -0
- package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
- package/dist/workspaces/resources/skills.js +285 -0
- package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
- package/dist/workspaces/sessions.js +457 -0
- package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
- package/dist/workspaces.js +69 -991
- package/docs/chatgpt-coding-workflow.md +3 -11
- package/docs/configuration.md +12 -25
- package/docs/gotchas.md +0 -30
- package/docs/roadmap.md +6 -10
- package/docs/security.md +1 -5
- package/docs/setup.md +0 -11
- package/package.json +6 -8
- package/scripts/ci/architecture.mjs +109 -0
- package/scripts/ci/verify.mjs +5 -0
- package/scripts/debug/accept/bootstrap.mjs +326 -0
- package/scripts/debug/accept/harness.mjs +76 -0
- package/scripts/debug/accept/support.mjs +432 -0
- package/scripts/debug/accept.mjs +43 -806
- package/scripts/debug/relay-accept/support.mjs +135 -0
- package/scripts/debug/relay-accept.mjs +26 -115
- package/scripts/debug/traffic/support.mjs +167 -0
- package/scripts/debug/traffic/traffic-audit.mjs +4 -161
- package/scripts/{lsp-interop-support.test.mjs → lsp/interop-support.test.mjs} +1 -1
- package/scripts/{lsp-interop.mjs → lsp/interop.mjs} +3 -3
- package/scripts/wiki/sync.mjs +21 -10
- package/dist/activity/bash-output-store.js +0 -127
- package/dist/pi-tools.js +0 -110
- package/dist/skills.js +0 -127
- package/dist/ui/assets/activity-panel-app-CUAN6zyW.js +0 -2
- package/dist/ui/assets/angular-html-BkgDQl-G.js +0 -1
- package/dist/ui/assets/c-IBzBpxAO.js +0 -1
- package/dist/ui/assets/chunk-EyZ2wyi3.js +0 -1
- package/dist/ui/assets/cpp-CVHeO-Jk.js +0 -1
- package/dist/ui/assets/css-DEfus9J5.js +0 -1
- package/dist/ui/assets/glsl-_ooCjaeB.js +0 -1
- package/dist/ui/assets/graphql-B9LqP-cY.js +0 -1
- package/dist/ui/assets/haml-BU5EoZBn.js +0 -1
- package/dist/ui/assets/heavy-payload-CgzrutLm.js +0 -4
- package/dist/ui/assets/html-D-C6YZk4.js +0 -1
- package/dist/ui/assets/java-ppHvycj5.js +0 -1
- package/dist/ui/assets/javascript-DDTjKLlQ.js +0 -1
- package/dist/ui/assets/json-Cw91r8h1.js +0 -1
- package/dist/ui/assets/jsx-04X2FT_1.js +0 -1
- package/dist/ui/assets/lua-CiyBp52Y.js +0 -1
- package/dist/ui/assets/r-DfFga4si.js +0 -1
- package/dist/ui/assets/regexp-iC9_nbSI.js +0 -1
- package/dist/ui/assets/review-payload-BrLbezbq.js +0 -1
- package/dist/ui/assets/scrollbar-CbhpdW05.js +0 -287
- package/dist/ui/assets/scss-BfZXsAWQ.js +0 -1
- package/dist/ui/assets/shellscript-BC2srNaB.js +0 -1
- package/dist/ui/assets/sql-Ccv_yeUm.js +0 -1
- package/dist/ui/assets/tsx-CTXzrBkN.js +0 -1
- package/dist/ui/assets/typescript--ElHoFkw.js +0 -1
- package/dist/ui/assets/workspace-app-Bhj96tsR.js +0 -1
- package/dist/ui/assets/workspace-app-CxwJuZyS.js +0 -111
- package/dist/ui/assets/workspace-app-D6UR0AFl.js +0 -5
- package/dist/ui/assets/workspace-app-ldjBmCJR.css +0 -1
- package/dist/ui/assets/workspace-lifecycle-app-Cqfhx9pV.js +0 -1
- package/dist/ui/assets/xml-JnmX6vyS.js +0 -1
- package/dist/ui/assets/yaml-OAcZkP-w.js +0 -1
- package/dist/ui/workspace-app.html +0 -18
- package/dist/ui/workspace-lifecycle-app.html +0 -18
- package/docs/agent-profile-schema.md +0 -114
- package/docs/artifact-exchange.md +0 -65
- package/docs/versioning.md +0 -205
- /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
- /package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +0 -0
- /package/dist/activity/{process-output-audit.js → runtime/process-output-audit.js} +0 -0
- /package/dist/{artifact-error.js → mcp/artifacts/artifact-error.js} +0 -0
- /package/dist/{artifact-tools.js → mcp/artifacts/artifact-tools.js} +0 -0
- /package/dist/{incoming-artifacts.js → mcp/artifacts/incoming-artifacts.js} +0 -0
- /package/dist/{advertised-files.js → mcp/filesystem/advertised-files.js} +0 -0
- /package/dist/{apply-patch.js → mcp/filesystem/apply-patch.js} +0 -0
- /package/dist/{file-mutations.js → mcp/filesystem/file-mutations.js} +0 -0
- /package/dist/{roots.js → mcp/filesystem/roots.js} +0 -0
- /package/dist/{oauth → mcp/oauth}/public-url.js +0 -0
- /package/dist/{oauth → mcp/oauth}/router.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/executor.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/scheduler.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/types.js +0 -0
- /package/dist/{operations → mcp/operations}/bulk-mutation.js +0 -0
- /package/dist/{operations → mcp/operations}/bulk-read.js +0 -0
- /package/dist/{operations → mcp/operations}/core-operation-executor.js +0 -0
- /package/dist/{process-platform.js → mcp/process/process-platform.js} +0 -0
- /package/dist/{process-sessions.js → mcp/process/process-sessions.js} +0 -0
- /package/dist/{request-meta.js → mcp/request-meta.js} +0 -0
- /package/dist/{mcp-sessions.js → mcp/server/transport/mcp-sessions.js} +0 -0
- /package/dist/{server-shutdown.js → mcp/server/transport/server-shutdown.js} +0 -0
- /package/dist/{logger.js → runtime/logging/logger.js} +0 -0
- /package/dist/{db → runtime/state/db}/schema.js +0 -0
- /package/dist/{composite-activity.js → workspaces/composite/composite-activity.js} +0 -0
- /package/dist/{composite-workspaces.js → workspaces/composite/composite-workspaces.js} +0 -0
- /package/dist/{git.js → workspaces/git/git.js} +0 -0
- /package/dist/{workspace-presentation.js → workspaces/presentation/workspace-presentation.js} +0 -0
- /package/dist/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +0 -0
- /package/dist/{workspace-task-reminders.js → workspaces/tasks/workspace-task-reminders.js} +0 -0
- /package/dist/{workspace-tasks.js → workspaces/tasks/workspace-tasks.js} +0 -0
- /package/scripts/{lsp-interop-support.mjs → lsp/interop-support.mjs} +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
|
+
import { once } from "node:events";
|
|
5
|
+
import { connect } from "node:net";
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
15
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
16
|
+
import { debugRoot, repoRoot } from "../runtime.mjs";
|
|
17
|
+
|
|
18
|
+
export function relayAcceptanceTopology() {
|
|
19
|
+
const gatewayPort = 7677;
|
|
20
|
+
const executionPort = 7678;
|
|
21
|
+
const gatewayBaseUrl = `http://127.0.0.1:${gatewayPort}`;
|
|
22
|
+
const gatewayMcpUrl = `${gatewayBaseUrl}/mcp`;
|
|
23
|
+
const executionBaseUrl = `http://127.0.0.1:${executionPort}`;
|
|
24
|
+
const acceptanceRoot = resolve(debugRoot, "relay-acceptance");
|
|
25
|
+
const gatewayConfigDir = join(acceptanceRoot, "gateway", "config");
|
|
26
|
+
const gatewayStateDir = join(acceptanceRoot, "gateway", "state");
|
|
27
|
+
const gatewayWorktreeRoot = join(acceptanceRoot, "gateway", "worktrees");
|
|
28
|
+
const gatewayProjects = join(acceptanceRoot, "gateway-projects");
|
|
29
|
+
const gatewayLocalProject = join(gatewayProjects, "local");
|
|
30
|
+
const executionConfigDir = join(acceptanceRoot, "execution", "config");
|
|
31
|
+
const executionStateDir = join(acceptanceRoot, "execution", "state");
|
|
32
|
+
const executionWorktreeRoot = join(acceptanceRoot, "execution", "worktrees");
|
|
33
|
+
const executionProjects = join(acceptanceRoot, "execution-projects");
|
|
34
|
+
const executionCheckout = join(executionProjects, "checkout");
|
|
35
|
+
const executionWorktreeSource = join(executionProjects, "worktree-source");
|
|
36
|
+
const bootstrapSecret = "RELAY_ACCEPTANCE_BOOTSTRAP_SECRET";
|
|
37
|
+
const checkoutTaskBody = "RELAY_ACCEPTANCE_EXECUTION_TASK_BODY";
|
|
38
|
+
const worktreeTaskBody = "RELAY_ACCEPTANCE_WORKTREE_TASK_BODY";
|
|
39
|
+
const compositeMemberTaskBody = "RELAY_ACCEPTANCE_COMPOSITE_MEMBER_TASK_BODY";
|
|
40
|
+
const compositeTaskBody = "RELAY_ACCEPTANCE_COMPOSITE_OWN_TASK_BODY";
|
|
41
|
+
return {
|
|
42
|
+
gatewayPort, executionPort, gatewayBaseUrl, gatewayMcpUrl, executionBaseUrl,
|
|
43
|
+
acceptanceRoot, gatewayConfigDir, gatewayStateDir, gatewayWorktreeRoot,
|
|
44
|
+
gatewayProjects, gatewayLocalProject, executionConfigDir, executionStateDir,
|
|
45
|
+
executionWorktreeRoot, executionProjects, executionCheckout, executionWorktreeSource,
|
|
46
|
+
bootstrapSecret, checkoutTaskBody, worktreeTaskBody, compositeMemberTaskBody, compositeTaskBody,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function assertPortsFree(ports) {
|
|
51
|
+
for (const port of ports) {
|
|
52
|
+
const occupied = await new Promise((resolvePromise) => {
|
|
53
|
+
const socket = connect({ host: "127.0.0.1", port });
|
|
54
|
+
socket.once("connect", () => {
|
|
55
|
+
socket.destroy();
|
|
56
|
+
resolvePromise(true);
|
|
57
|
+
});
|
|
58
|
+
socket.once("error", () => resolvePromise(false));
|
|
59
|
+
});
|
|
60
|
+
assert.equal(occupied, false, `reserved debug port ${port} is already in use; refusing to touch the existing process`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function setupGitProject(root) {
|
|
65
|
+
mkdirSync(root, { recursive: true });
|
|
66
|
+
writeFileSync(join(root, "README.md"), "Relay managed worktree acceptance\n");
|
|
67
|
+
runGit(root, ["init"]);
|
|
68
|
+
runGit(root, ["config", "user.email", "forgerelay-debug@example.com"]);
|
|
69
|
+
runGit(root, ["config", "user.name", "ForgeRelay Debug"]);
|
|
70
|
+
runGit(root, ["add", "."]);
|
|
71
|
+
runGit(root, ["commit", "-m", "Initial Relay acceptance commit"]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function runGit(cwd, args) {
|
|
75
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
76
|
+
if (result.status !== 0) {
|
|
77
|
+
throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function taskStateFiles(stateDir) {
|
|
82
|
+
const workspacesDir = join(stateDir, "workspaces");
|
|
83
|
+
if (!existsSync(workspacesDir)) return [];
|
|
84
|
+
const files = [];
|
|
85
|
+
for (const entry of readdirSync(workspacesDir, { withFileTypes: true })) {
|
|
86
|
+
if (!entry.isDirectory()) continue;
|
|
87
|
+
const candidate = join(workspacesDir, entry.name, "tasks.json");
|
|
88
|
+
if (existsSync(candidate)) files.push(candidate);
|
|
89
|
+
}
|
|
90
|
+
return files;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function findTaskStateFile(stateDir, needle) {
|
|
94
|
+
return taskStateFiles(stateDir).find((path) => readFileSync(path, "utf8").includes(needle));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function assertTaskBodyAbsentFromGateway(gatewayStateDir, body) {
|
|
98
|
+
const leaked = taskStateFiles(gatewayStateDir).filter((path) => readFileSync(path, "utf8").includes(body));
|
|
99
|
+
assert.deepEqual(leaked, [], `Gateway stored an Execution-owned Task body in: ${leaked.join(", ")}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function assertSafeRelayInspection(result, { executionWorkspaceId, executionBaseUrl, executionPort, executionOwnerToken, bootstrapSecret, remoteRecord, taskBodies }) {
|
|
103
|
+
const json = JSON.stringify(result);
|
|
104
|
+
for (const forbidden of [
|
|
105
|
+
executionWorkspaceId,
|
|
106
|
+
executionBaseUrl,
|
|
107
|
+
`127.0.0.1:${executionPort}`,
|
|
108
|
+
executionOwnerToken,
|
|
109
|
+
remoteRecord.accessToken,
|
|
110
|
+
remoteRecord.refreshToken,
|
|
111
|
+
bootstrapSecret,
|
|
112
|
+
"remoteInstanceId",
|
|
113
|
+
"remoteWorkspaceId",
|
|
114
|
+
"sshRoute",
|
|
115
|
+
...taskBodies,
|
|
116
|
+
]) {
|
|
117
|
+
assert.equal(json.includes(forbidden), false, `Relay inspection leaked ${forbidden}`);
|
|
118
|
+
}
|
|
119
|
+
assert.doesNotMatch(json, /"ws_[0-9a-f]{10}"/);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function assertToolOk(result, label) {
|
|
123
|
+
assert.equal(result.isError, undefined, `${label}: ${toolText(result)}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function toolText(result) {
|
|
127
|
+
return (result.content ?? [])
|
|
128
|
+
.filter((entry) => entry.type === "text")
|
|
129
|
+
.map((entry) => entry.text)
|
|
130
|
+
.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function pass(label, detail) {
|
|
134
|
+
console.log(`PASS ${label}: ${detail}`);
|
|
135
|
+
}
|
|
@@ -14,31 +14,17 @@ import {
|
|
|
14
14
|
import { join, resolve } from "node:path";
|
|
15
15
|
import { setTimeout as delay } from "node:timers/promises";
|
|
16
16
|
import { debugRoot, repoRoot } from "./runtime.mjs";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const gatewayWorktreeRoot = join(acceptanceRoot, "gateway", "worktrees");
|
|
27
|
-
const gatewayProjects = join(acceptanceRoot, "gateway-projects");
|
|
28
|
-
const gatewayLocalProject = join(gatewayProjects, "local");
|
|
29
|
-
const executionConfigDir = join(acceptanceRoot, "execution", "config");
|
|
30
|
-
const executionStateDir = join(acceptanceRoot, "execution", "state");
|
|
31
|
-
const executionWorktreeRoot = join(acceptanceRoot, "execution", "worktrees");
|
|
32
|
-
const executionProjects = join(acceptanceRoot, "execution-projects");
|
|
33
|
-
const executionCheckout = join(executionProjects, "checkout");
|
|
34
|
-
const executionWorktreeSource = join(executionProjects, "worktree-source");
|
|
17
|
+
import { relayAcceptanceTopology, assertPortsFree, setupGitProject, runGit, taskStateFiles, findTaskStateFile, assertTaskBodyAbsentFromGateway, assertSafeRelayInspection, assertToolOk, toolText, pass } from "./relay-accept/support.mjs";
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
gatewayPort, executionPort, gatewayBaseUrl, gatewayMcpUrl, executionBaseUrl,
|
|
21
|
+
acceptanceRoot, gatewayConfigDir, gatewayStateDir, gatewayWorktreeRoot,
|
|
22
|
+
gatewayProjects, gatewayLocalProject, executionConfigDir, executionStateDir,
|
|
23
|
+
executionWorktreeRoot, executionProjects, executionCheckout, executionWorktreeSource,
|
|
24
|
+
bootstrapSecret, checkoutTaskBody, worktreeTaskBody, compositeMemberTaskBody, compositeTaskBody,
|
|
25
|
+
} = relayAcceptanceTopology();
|
|
35
26
|
const gatewayOwnerToken = randomBytes(32).toString("base64url");
|
|
36
27
|
const executionOwnerToken = randomBytes(32).toString("base64url");
|
|
37
|
-
const bootstrapSecret = "RELAY_ACCEPTANCE_BOOTSTRAP_SECRET";
|
|
38
|
-
const checkoutTaskBody = "RELAY_ACCEPTANCE_EXECUTION_TASK_BODY";
|
|
39
|
-
const worktreeTaskBody = "RELAY_ACCEPTANCE_WORKTREE_TASK_BODY";
|
|
40
|
-
const compositeMemberTaskBody = "RELAY_ACCEPTANCE_COMPOSITE_MEMBER_TASK_BODY";
|
|
41
|
-
const compositeTaskBody = "RELAY_ACCEPTANCE_COMPOSITE_OWN_TASK_BODY";
|
|
42
28
|
|
|
43
29
|
await assertPortsFree([gatewayPort, executionPort]);
|
|
44
30
|
rmSync(acceptanceRoot, { recursive: true, force: true });
|
|
@@ -152,7 +138,7 @@ try {
|
|
|
152
138
|
assert.ok(checkoutTaskFile, "Execution Task state file was not found");
|
|
153
139
|
const checkoutExecutionWorkspaceId = checkoutTaskFile.split("/").at(-2);
|
|
154
140
|
assert.match(checkoutExecutionWorkspaceId, /^ws_[0-9a-f]{10}$/);
|
|
155
|
-
assertTaskBodyAbsentFromGateway(checkoutTaskBody);
|
|
141
|
+
assertTaskBodyAbsentFromGateway(gatewayStateDir, checkoutTaskBody);
|
|
156
142
|
pass("Execution-owned Task truth", `${checkoutExecutionWorkspaceId} owns the Task state; Gateway has no shadow copy`);
|
|
157
143
|
|
|
158
144
|
for (let index = 0; index < 2; index += 1) {
|
|
@@ -188,6 +174,10 @@ try {
|
|
|
188
174
|
assert.equal(inspectedCheckout.structuredContent.inspection.taskSummary.lists[0].unfinishedTaskCount, 1);
|
|
189
175
|
assertSafeRelayInspection(inspectedCheckout, {
|
|
190
176
|
executionWorkspaceId: checkoutExecutionWorkspaceId,
|
|
177
|
+
executionBaseUrl,
|
|
178
|
+
executionPort,
|
|
179
|
+
executionOwnerToken,
|
|
180
|
+
bootstrapSecret,
|
|
191
181
|
remoteRecord,
|
|
192
182
|
taskBodies: [checkoutTaskBody],
|
|
193
183
|
});
|
|
@@ -269,7 +259,7 @@ try {
|
|
|
269
259
|
const worktreeTaskFile = findTaskStateFile(executionStateDir, worktreeTaskBody);
|
|
270
260
|
assert.ok(worktreeTaskFile, "Execution managed-worktree Task state file was not found");
|
|
271
261
|
const worktreeExecutionWorkspaceId = worktreeTaskFile.split("/").at(-2);
|
|
272
|
-
assertTaskBodyAbsentFromGateway(worktreeTaskBody);
|
|
262
|
+
assertTaskBodyAbsentFromGateway(gatewayStateDir, worktreeTaskBody);
|
|
273
263
|
|
|
274
264
|
const worktreeWrite = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "write", {
|
|
275
265
|
workspaceId: worktreeRelayId,
|
|
@@ -298,6 +288,10 @@ try {
|
|
|
298
288
|
assert.equal(inspectedWorktree.structuredContent.inspection.rootValid, false);
|
|
299
289
|
assertSafeRelayInspection(inspectedWorktree, {
|
|
300
290
|
executionWorkspaceId: worktreeExecutionWorkspaceId,
|
|
291
|
+
executionBaseUrl,
|
|
292
|
+
executionPort,
|
|
293
|
+
executionOwnerToken,
|
|
294
|
+
bootstrapSecret,
|
|
301
295
|
remoteRecord,
|
|
302
296
|
taskBodies: [worktreeTaskBody],
|
|
303
297
|
});
|
|
@@ -364,7 +358,7 @@ try {
|
|
|
364
358
|
const memberTaskFile = findTaskStateFile(executionStateDir, compositeMemberTaskBody);
|
|
365
359
|
assert.ok(memberTaskFile, "Execution Composite-member Task state file was not found");
|
|
366
360
|
const memberExecutionWorkspaceId = memberTaskFile.split("/").at(-2);
|
|
367
|
-
assertTaskBodyAbsentFromGateway(compositeMemberTaskBody);
|
|
361
|
+
assertTaskBodyAbsentFromGateway(gatewayStateDir, compositeMemberTaskBody);
|
|
368
362
|
|
|
369
363
|
const compositeOpened = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "open_workspace", {
|
|
370
364
|
kind: "composite",
|
|
@@ -445,6 +439,10 @@ try {
|
|
|
445
439
|
assertToolOk(memberInspection, "inspect relayed Composite member");
|
|
446
440
|
assertSafeRelayInspection(memberInspection, {
|
|
447
441
|
executionWorkspaceId: memberExecutionWorkspaceId,
|
|
442
|
+
executionBaseUrl,
|
|
443
|
+
executionPort,
|
|
444
|
+
executionOwnerToken,
|
|
445
|
+
bootstrapSecret,
|
|
448
446
|
remoteRecord,
|
|
449
447
|
taskBodies: [compositeMemberTaskBody],
|
|
450
448
|
});
|
|
@@ -489,7 +487,7 @@ try {
|
|
|
489
487
|
}, conversationB);
|
|
490
488
|
assertToolOk(restoredMemberTasks, "restore relayed Composite member Tasks");
|
|
491
489
|
assert.equal(restoredMemberTasks.structuredContent.result.lists[0].tasks[0].subject, "Remote Composite member Task");
|
|
492
|
-
assertTaskBodyAbsentFromGateway(compositeMemberTaskBody);
|
|
490
|
+
assertTaskBodyAbsentFromGateway(gatewayStateDir, compositeMemberTaskBody);
|
|
493
491
|
pass("Composite local + Relay member", "member execution/lifecycle/Task truth stayed with owning Workspace");
|
|
494
492
|
|
|
495
493
|
const deleteComposite = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "close_workspace", {
|
|
@@ -616,7 +614,7 @@ function authorizeHost(baseUrl, mcpUrl, ownerToken) {
|
|
|
616
614
|
redirect_uri: redirectUri,
|
|
617
615
|
code_challenge: challenge,
|
|
618
616
|
code_challenge_method: "S256",
|
|
619
|
-
scope: "
|
|
617
|
+
scope: "forgerelay",
|
|
620
618
|
resource: mcpUrl,
|
|
621
619
|
state: "forgerelay-relay-acceptance",
|
|
622
620
|
owner_token: ownerToken,
|
|
@@ -800,90 +798,3 @@ async function stopServer(child) {
|
|
|
800
798
|
}),
|
|
801
799
|
]);
|
|
802
800
|
}
|
|
803
|
-
|
|
804
|
-
async function assertPortsFree(ports) {
|
|
805
|
-
for (const port of ports) {
|
|
806
|
-
const occupied = await new Promise((resolvePromise) => {
|
|
807
|
-
const socket = connect({ host: "127.0.0.1", port });
|
|
808
|
-
socket.once("connect", () => {
|
|
809
|
-
socket.destroy();
|
|
810
|
-
resolvePromise(true);
|
|
811
|
-
});
|
|
812
|
-
socket.once("error", () => resolvePromise(false));
|
|
813
|
-
});
|
|
814
|
-
assert.equal(occupied, false, `reserved debug port ${port} is already in use; refusing to touch the existing process`);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function setupGitProject(root) {
|
|
819
|
-
mkdirSync(root, { recursive: true });
|
|
820
|
-
writeFileSync(join(root, "README.md"), "Relay managed worktree acceptance\n");
|
|
821
|
-
runGit(root, ["init"]);
|
|
822
|
-
runGit(root, ["config", "user.email", "forgerelay-debug@example.com"]);
|
|
823
|
-
runGit(root, ["config", "user.name", "ForgeRelay Debug"]);
|
|
824
|
-
runGit(root, ["add", "."]);
|
|
825
|
-
runGit(root, ["commit", "-m", "Initial Relay acceptance commit"]);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function runGit(cwd, args) {
|
|
829
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
830
|
-
if (result.status !== 0) {
|
|
831
|
-
throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function taskStateFiles(stateDir) {
|
|
836
|
-
const workspacesDir = join(stateDir, "workspaces");
|
|
837
|
-
if (!existsSync(workspacesDir)) return [];
|
|
838
|
-
const files = [];
|
|
839
|
-
for (const entry of readdirSync(workspacesDir, { withFileTypes: true })) {
|
|
840
|
-
if (!entry.isDirectory()) continue;
|
|
841
|
-
const candidate = join(workspacesDir, entry.name, "tasks.json");
|
|
842
|
-
if (existsSync(candidate)) files.push(candidate);
|
|
843
|
-
}
|
|
844
|
-
return files;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function findTaskStateFile(stateDir, needle) {
|
|
848
|
-
return taskStateFiles(stateDir).find((path) => readFileSync(path, "utf8").includes(needle));
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function assertTaskBodyAbsentFromGateway(body) {
|
|
852
|
-
const leaked = taskStateFiles(gatewayStateDir).filter((path) => readFileSync(path, "utf8").includes(body));
|
|
853
|
-
assert.deepEqual(leaked, [], `Gateway stored an Execution-owned Task body in: ${leaked.join(", ")}`);
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
function assertSafeRelayInspection(result, { executionWorkspaceId, remoteRecord, taskBodies }) {
|
|
857
|
-
const json = JSON.stringify(result);
|
|
858
|
-
for (const forbidden of [
|
|
859
|
-
executionWorkspaceId,
|
|
860
|
-
executionBaseUrl,
|
|
861
|
-
`127.0.0.1:${executionPort}`,
|
|
862
|
-
executionOwnerToken,
|
|
863
|
-
remoteRecord.accessToken,
|
|
864
|
-
remoteRecord.refreshToken,
|
|
865
|
-
bootstrapSecret,
|
|
866
|
-
"remoteInstanceId",
|
|
867
|
-
"remoteWorkspaceId",
|
|
868
|
-
"sshRoute",
|
|
869
|
-
...taskBodies,
|
|
870
|
-
]) {
|
|
871
|
-
assert.equal(json.includes(forbidden), false, `Relay inspection leaked ${forbidden}`);
|
|
872
|
-
}
|
|
873
|
-
assert.doesNotMatch(json, /"ws_[0-9a-f]{10}"/);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
function assertToolOk(result, label) {
|
|
877
|
-
assert.equal(result.isError, undefined, `${label}: ${toolText(result)}`);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
function toolText(result) {
|
|
881
|
-
return (result.content ?? [])
|
|
882
|
-
.filter((entry) => entry.type === "text")
|
|
883
|
-
.map((entry) => entry.text)
|
|
884
|
-
.join("\n");
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
function pass(label, detail) {
|
|
888
|
-
console.log(`PASS ${label}: ${detail}`);
|
|
889
|
-
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { once } from "node:events";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
7
|
+
import { repoRoot } from "../runtime.mjs";
|
|
8
|
+
|
|
9
|
+
export function curlRequest({ method = "GET", url, headers = {}, body }) {
|
|
10
|
+
const marker = `__FORGERELAY_TRAFFIC_${randomUUID()}__`;
|
|
11
|
+
const args = [
|
|
12
|
+
"--silent",
|
|
13
|
+
"--show-error",
|
|
14
|
+
"--max-time",
|
|
15
|
+
"20",
|
|
16
|
+
"--request",
|
|
17
|
+
method,
|
|
18
|
+
"--dump-header",
|
|
19
|
+
"-",
|
|
20
|
+
"--output",
|
|
21
|
+
"-",
|
|
22
|
+
"--write-out",
|
|
23
|
+
`\n${marker}%{http_code}|%{size_upload}|%{size_download}|%{size_request}|%{size_header}`,
|
|
24
|
+
];
|
|
25
|
+
for (const [name, value] of Object.entries(headers)) args.push("--header", `${name}: ${value}`);
|
|
26
|
+
if (body !== undefined) args.push("--data-binary", "@-");
|
|
27
|
+
args.push(url);
|
|
28
|
+
|
|
29
|
+
const result = spawnSync("curl", args, {
|
|
30
|
+
cwd: repoRoot,
|
|
31
|
+
input: body,
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
maxBuffer: 40 * 1024 * 1024,
|
|
34
|
+
});
|
|
35
|
+
if (result.status !== 0) {
|
|
36
|
+
throw new Error(`curl ${method} ${url} failed: ${result.stderr.trim() || `exit ${result.status}`}`);
|
|
37
|
+
}
|
|
38
|
+
const statusMarker = `\n${marker}`;
|
|
39
|
+
const markerIndex = result.stdout.lastIndexOf(statusMarker);
|
|
40
|
+
assert.notEqual(markerIndex, -1, `curl response did not contain status marker for ${url}`);
|
|
41
|
+
const rawResponse = result.stdout.slice(0, markerIndex);
|
|
42
|
+
const stats = result.stdout.slice(markerIndex + statusMarker.length).trim().split("|");
|
|
43
|
+
const [status, sizeUpload, sizeDownload, sizeRequest, sizeHeader] = stats.map(Number);
|
|
44
|
+
const separator = rawResponse.indexOf("\r\n\r\n") >= 0 ? "\r\n\r\n" : "\n\n";
|
|
45
|
+
const headerEnd = rawResponse.indexOf(separator);
|
|
46
|
+
assert.notEqual(headerEnd, -1, `curl response did not contain headers for ${url}`);
|
|
47
|
+
const headerBlock = rawResponse.slice(0, headerEnd);
|
|
48
|
+
const responseBody = rawResponse.slice(headerEnd + separator.length);
|
|
49
|
+
const responseHeaders = new Map();
|
|
50
|
+
for (const line of headerBlock.split(/\r?\n/).slice(1)) {
|
|
51
|
+
const colon = line.indexOf(":");
|
|
52
|
+
if (colon < 0) continue;
|
|
53
|
+
responseHeaders.set(line.slice(0, colon).trim().toLowerCase(), line.slice(colon + 1).trim());
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
status,
|
|
57
|
+
headers: responseHeaders,
|
|
58
|
+
body: responseBody,
|
|
59
|
+
sizeUpload,
|
|
60
|
+
sizeDownload,
|
|
61
|
+
sizeRequest,
|
|
62
|
+
sizeHeader,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function jsonLogEntries(path) {
|
|
67
|
+
if (!existsSync(path)) return [];
|
|
68
|
+
return readFileSync(path, "utf8")
|
|
69
|
+
.split(/\r?\n/)
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.flatMap((line) => {
|
|
72
|
+
try {
|
|
73
|
+
return [JSON.parse(line)];
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function requestLogEntries(path) {
|
|
81
|
+
return jsonLogEntries(path).filter((entry) => entry.event === "http_request");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function requestBodiesForRpc(entries, rpcTarget) {
|
|
85
|
+
const requestIds = new Set(entries
|
|
86
|
+
.filter((entry) => entry.event === "mcp_request" && entry.rpcTarget === rpcTarget)
|
|
87
|
+
.map((entry) => entry.requestId)
|
|
88
|
+
.filter(Boolean));
|
|
89
|
+
return entries
|
|
90
|
+
.filter((entry) => entry.event === "http_request" && requestIds.has(entry.requestId))
|
|
91
|
+
.map((entry) => Number(entry.contentLength ?? 0))
|
|
92
|
+
.filter((value) => Number.isFinite(value));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function countBy(values, keyFor) {
|
|
96
|
+
const result = {};
|
|
97
|
+
for (const value of values) {
|
|
98
|
+
const key = keyFor(value);
|
|
99
|
+
result[key] = (result[key] ?? 0) + 1;
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function waitForHealth(child, baseUrl) {
|
|
105
|
+
for (let attempt = 0; attempt < 80; attempt += 1) {
|
|
106
|
+
if (child.exitCode !== null) throw new Error(`debug server exited before health check: ${child.exitCode}`);
|
|
107
|
+
try {
|
|
108
|
+
const response = jsonRequest(`${baseUrl}/healthz`);
|
|
109
|
+
if (response.status === 200) return;
|
|
110
|
+
} catch {
|
|
111
|
+
// still starting
|
|
112
|
+
}
|
|
113
|
+
await delay(100);
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`debug server did not become healthy on ${baseUrl}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function stopServer(child) {
|
|
119
|
+
if (child.exitCode !== null) return;
|
|
120
|
+
child.kill("SIGTERM");
|
|
121
|
+
const exited = once(child, "exit");
|
|
122
|
+
await Promise.race([
|
|
123
|
+
exited,
|
|
124
|
+
delay(3000).then(() => {
|
|
125
|
+
if (child.exitCode === null) child.kill("SIGKILL");
|
|
126
|
+
}),
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function assertPortsFree(ports) {
|
|
131
|
+
const script = [
|
|
132
|
+
"const net=require('node:net');",
|
|
133
|
+
`const ports=${JSON.stringify(ports)};`,
|
|
134
|
+
"let pending=ports.length; let bad=[];",
|
|
135
|
+
"for(const port of ports){const s=net.connect({host:'127.0.0.1',port}); s.setTimeout(250);",
|
|
136
|
+
"s.once('connect',()=>{bad.push(port);s.destroy();done();});",
|
|
137
|
+
"s.once('error',()=>{s.destroy();done();}); s.once('timeout',()=>{s.destroy();done();});}",
|
|
138
|
+
"function done(){if(--pending===0){if(bad.length){console.error('busy:'+bad.join(','));process.exit(2)}}}",
|
|
139
|
+
].join("");
|
|
140
|
+
const result = spawnSync(process.execPath, ["-e", script], { encoding: "utf8" });
|
|
141
|
+
if (result.status !== 0) throw new Error(`reserved debug port already in use: ${result.stderr.trim()}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function jsonBytes(value) {
|
|
145
|
+
if (value === undefined) return 0;
|
|
146
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatBytes(value) {
|
|
150
|
+
const bytes = Number(value);
|
|
151
|
+
if (!Number.isFinite(bytes)) return String(value);
|
|
152
|
+
if (bytes < 1024) return `${bytes.toFixed(0)} B`;
|
|
153
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
154
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`;
|
|
155
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
export function jsonRequest(url, options = {}) {
|
|
160
|
+
const response = curlRequest({
|
|
161
|
+
method: options.method ?? "GET",
|
|
162
|
+
url,
|
|
163
|
+
headers: options.headers,
|
|
164
|
+
body: options.body,
|
|
165
|
+
});
|
|
166
|
+
return { ...response, json: JSON.parse(response.body) };
|
|
167
|
+
}
|