@akira-tl/forgerelay 0.8.8 → 0.8.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +10 -18
- package/capabilities/code-intelligence/GUIDE.md +7 -3
- package/capabilities/host-integration/GUIDE.md +2 -2
- package/capabilities/subagents/GUIDE.md +9 -0
- package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +6 -0
- 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/init.js +180 -0
- package/dist/cli/setup-support.js +181 -0
- package/dist/cli.js +23 -216
- package/dist/lsp/code-intelligence.js +18 -4
- package/dist/lsp/runtime/diagnostic-snapshots.js +52 -1
- package/dist/lsp/runtime/managed-language-servers.js +172 -0
- package/dist/lsp/runtime/manager.js +37 -2
- package/dist/lsp/runtime/process-launch.js +3 -0
- package/dist/lsp/test-support/server-fixture.js +14 -11
- 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/{process-platform.js → mcp/process/process-platform.js} +21 -9
- package/dist/{process-sessions.js → mcp/process/process-sessions.js} +3 -3
- 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} +9 -4
- 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 +210 -0
- package/dist/mcp/server/core/version.js +9 -0
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +455 -0
- package/dist/mcp/server/operations/runtime/mutation-diagnostics.js +54 -0
- package/dist/mcp/server/operations/runtime/operation-runtime.js +524 -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 +242 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
- package/dist/mcp/server/workspace/runtime/workspace-open.js +555 -0
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/{config.js → runtime/config/config.js} +4 -3
- package/dist/{user-config.js → runtime/config/user-config.js} +14 -63
- 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 +133 -4189
- 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 -410
- 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 +337 -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/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +30 -3
- 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/resource-monitor.js +377 -0
- package/dist/workspaces/resources/skills.js +283 -0
- package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
- package/dist/workspaces/sessions.js +460 -0
- package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
- package/dist/workspaces.js +77 -993
- package/docs/chatgpt-coding-workflow.md +7 -14
- package/docs/configuration.md +26 -34
- 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 -9
- 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/release/publish.mjs +12 -9
- package/scripts/release/release-gate.test.mjs +3 -1
- package/scripts/release/release-version.test.mjs +13 -1
- package/scripts/release-parity.mjs +1 -1
- package/scripts/release-proof.mjs +15 -0
- package/scripts/release-proof.test.mjs +27 -1
- package/scripts/release-version.mjs +16 -1
- 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-7AckaxIR.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-Bol1_mcs.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-BqJp0c5e.js +0 -1
- package/dist/ui/assets/scrollbar-CvE-I-jG.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-BLW7p6IZ.js +0 -114
- package/dist/ui/assets/workspace-app-BjNqR0en.js +0 -1
- package/dist/ui/assets/workspace-app-CmaYU4DW.js +0 -3
- package/dist/ui/assets/workspace-app-D2bJ5fjt.css +0 -1
- package/dist/ui/assets/workspace-lifecycle-app-BVbI2Ilf.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/skills/subagent-delegation/SKILL.md +0 -132
- /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/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/{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/{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
|
@@ -14,10 +14,12 @@ import { join, resolve } from "node:path";
|
|
|
14
14
|
import { setTimeout as delay } from "node:timers/promises";
|
|
15
15
|
import { activityRefreshDelayMs } from "../../../src/ui/activity/model.ts";
|
|
16
16
|
import { debugRoot, repoRoot } from "../runtime.mjs";
|
|
17
|
+
import { jsonRequest, curlRequest, jsonLogEntries, requestLogEntries, requestBodiesForRpc, countBy, waitForHealth, stopServer, assertPortsFree, jsonBytes, formatBytes } from "./support.mjs";
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
const mode = process.argv[2] ?? "all";
|
|
19
21
|
if (!["all", "local", "relay"].includes(mode)) {
|
|
20
|
-
throw new Error("Usage:
|
|
22
|
+
throw new Error("Usage: npm run traffic:audit -- [all|local|relay]");
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
const trafficRoot = resolve(debugRoot, "traffic-audit");
|
|
@@ -637,7 +639,7 @@ function authorizeHost(baseUrl, mcpUrl, ownerToken) {
|
|
|
637
639
|
redirect_uri: redirectUri,
|
|
638
640
|
code_challenge: challenge,
|
|
639
641
|
code_challenge_method: "S256",
|
|
640
|
-
scope: "
|
|
642
|
+
scope: "forgerelay",
|
|
641
643
|
resource: mcpUrl,
|
|
642
644
|
state: "traffic-audit",
|
|
643
645
|
owner_token: ownerToken,
|
|
@@ -746,162 +748,3 @@ function parseMcpMessage(body, expectedId) {
|
|
|
746
748
|
return messages.find((message) => message.id === expectedId) ?? messages[0];
|
|
747
749
|
}
|
|
748
750
|
|
|
749
|
-
function jsonRequest(url, options = {}) {
|
|
750
|
-
const response = curlRequest({
|
|
751
|
-
method: options.method ?? "GET",
|
|
752
|
-
url,
|
|
753
|
-
headers: options.headers,
|
|
754
|
-
body: options.body,
|
|
755
|
-
});
|
|
756
|
-
return { ...response, json: JSON.parse(response.body) };
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function curlRequest({ method = "GET", url, headers = {}, body }) {
|
|
760
|
-
const marker = `__FORGERELAY_TRAFFIC_${randomUUID()}__`;
|
|
761
|
-
const args = [
|
|
762
|
-
"--silent",
|
|
763
|
-
"--show-error",
|
|
764
|
-
"--max-time",
|
|
765
|
-
"20",
|
|
766
|
-
"--request",
|
|
767
|
-
method,
|
|
768
|
-
"--dump-header",
|
|
769
|
-
"-",
|
|
770
|
-
"--output",
|
|
771
|
-
"-",
|
|
772
|
-
"--write-out",
|
|
773
|
-
`\n${marker}%{http_code}|%{size_upload}|%{size_download}|%{size_request}|%{size_header}`,
|
|
774
|
-
];
|
|
775
|
-
for (const [name, value] of Object.entries(headers)) args.push("--header", `${name}: ${value}`);
|
|
776
|
-
if (body !== undefined) args.push("--data-binary", "@-");
|
|
777
|
-
args.push(url);
|
|
778
|
-
|
|
779
|
-
const result = spawnSync("curl", args, {
|
|
780
|
-
cwd: repoRoot,
|
|
781
|
-
input: body,
|
|
782
|
-
encoding: "utf8",
|
|
783
|
-
maxBuffer: 40 * 1024 * 1024,
|
|
784
|
-
});
|
|
785
|
-
if (result.status !== 0) {
|
|
786
|
-
throw new Error(`curl ${method} ${url} failed: ${result.stderr.trim() || `exit ${result.status}`}`);
|
|
787
|
-
}
|
|
788
|
-
const statusMarker = `\n${marker}`;
|
|
789
|
-
const markerIndex = result.stdout.lastIndexOf(statusMarker);
|
|
790
|
-
assert.notEqual(markerIndex, -1, `curl response did not contain status marker for ${url}`);
|
|
791
|
-
const rawResponse = result.stdout.slice(0, markerIndex);
|
|
792
|
-
const stats = result.stdout.slice(markerIndex + statusMarker.length).trim().split("|");
|
|
793
|
-
const [status, sizeUpload, sizeDownload, sizeRequest, sizeHeader] = stats.map(Number);
|
|
794
|
-
const separator = rawResponse.indexOf("\r\n\r\n") >= 0 ? "\r\n\r\n" : "\n\n";
|
|
795
|
-
const headerEnd = rawResponse.indexOf(separator);
|
|
796
|
-
assert.notEqual(headerEnd, -1, `curl response did not contain headers for ${url}`);
|
|
797
|
-
const headerBlock = rawResponse.slice(0, headerEnd);
|
|
798
|
-
const responseBody = rawResponse.slice(headerEnd + separator.length);
|
|
799
|
-
const responseHeaders = new Map();
|
|
800
|
-
for (const line of headerBlock.split(/\r?\n/).slice(1)) {
|
|
801
|
-
const colon = line.indexOf(":");
|
|
802
|
-
if (colon < 0) continue;
|
|
803
|
-
responseHeaders.set(line.slice(0, colon).trim().toLowerCase(), line.slice(colon + 1).trim());
|
|
804
|
-
}
|
|
805
|
-
return {
|
|
806
|
-
status,
|
|
807
|
-
headers: responseHeaders,
|
|
808
|
-
body: responseBody,
|
|
809
|
-
sizeUpload,
|
|
810
|
-
sizeDownload,
|
|
811
|
-
sizeRequest,
|
|
812
|
-
sizeHeader,
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
function jsonLogEntries(path) {
|
|
817
|
-
if (!existsSync(path)) return [];
|
|
818
|
-
return readFileSync(path, "utf8")
|
|
819
|
-
.split(/\r?\n/)
|
|
820
|
-
.filter(Boolean)
|
|
821
|
-
.flatMap((line) => {
|
|
822
|
-
try {
|
|
823
|
-
return [JSON.parse(line)];
|
|
824
|
-
} catch {
|
|
825
|
-
return [];
|
|
826
|
-
}
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function requestLogEntries(path) {
|
|
831
|
-
return jsonLogEntries(path).filter((entry) => entry.event === "http_request");
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
function requestBodiesForRpc(entries, rpcTarget) {
|
|
835
|
-
const requestIds = new Set(entries
|
|
836
|
-
.filter((entry) => entry.event === "mcp_request" && entry.rpcTarget === rpcTarget)
|
|
837
|
-
.map((entry) => entry.requestId)
|
|
838
|
-
.filter(Boolean));
|
|
839
|
-
return entries
|
|
840
|
-
.filter((entry) => entry.event === "http_request" && requestIds.has(entry.requestId))
|
|
841
|
-
.map((entry) => Number(entry.contentLength ?? 0))
|
|
842
|
-
.filter((value) => Number.isFinite(value));
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
function countBy(values, keyFor) {
|
|
846
|
-
const result = {};
|
|
847
|
-
for (const value of values) {
|
|
848
|
-
const key = keyFor(value);
|
|
849
|
-
result[key] = (result[key] ?? 0) + 1;
|
|
850
|
-
}
|
|
851
|
-
return result;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
async function waitForHealth(child, baseUrl) {
|
|
855
|
-
for (let attempt = 0; attempt < 80; attempt += 1) {
|
|
856
|
-
if (child.exitCode !== null) throw new Error(`debug server exited before health check: ${child.exitCode}`);
|
|
857
|
-
try {
|
|
858
|
-
const response = jsonRequest(`${baseUrl}/healthz`);
|
|
859
|
-
if (response.status === 200) return;
|
|
860
|
-
} catch {
|
|
861
|
-
// still starting
|
|
862
|
-
}
|
|
863
|
-
await delay(100);
|
|
864
|
-
}
|
|
865
|
-
throw new Error(`debug server did not become healthy on ${baseUrl}`);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
async function stopServer(child) {
|
|
869
|
-
if (child.exitCode !== null) return;
|
|
870
|
-
child.kill("SIGTERM");
|
|
871
|
-
const exited = once(child, "exit");
|
|
872
|
-
await Promise.race([
|
|
873
|
-
exited,
|
|
874
|
-
delay(3000).then(() => {
|
|
875
|
-
if (child.exitCode === null) child.kill("SIGKILL");
|
|
876
|
-
}),
|
|
877
|
-
]);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
async function assertPortsFree(ports) {
|
|
881
|
-
const script = [
|
|
882
|
-
"const net=require('node:net');",
|
|
883
|
-
`const ports=${JSON.stringify(ports)};`,
|
|
884
|
-
"let pending=ports.length; let bad=[];",
|
|
885
|
-
"for(const port of ports){const s=net.connect({host:'127.0.0.1',port}); s.setTimeout(250);",
|
|
886
|
-
"s.once('connect',()=>{bad.push(port);s.destroy();done();});",
|
|
887
|
-
"s.once('error',()=>{s.destroy();done();}); s.once('timeout',()=>{s.destroy();done();});}",
|
|
888
|
-
"function done(){if(--pending===0){if(bad.length){console.error('busy:'+bad.join(','));process.exit(2)}}}",
|
|
889
|
-
].join("");
|
|
890
|
-
const result = spawnSync(process.execPath, ["-e", script], { encoding: "utf8" });
|
|
891
|
-
if (result.status !== 0) throw new Error(`reserved debug port already in use: ${result.stderr.trim()}`);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function jsonBytes(value) {
|
|
895
|
-
if (value === undefined) return 0;
|
|
896
|
-
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function formatBytes(value) {
|
|
900
|
-
const bytes = Number(value);
|
|
901
|
-
if (!Number.isFinite(bytes)) return String(value);
|
|
902
|
-
if (bytes < 1024) return `${bytes.toFixed(0)} B`;
|
|
903
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
904
|
-
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`;
|
|
905
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
|
|
906
|
-
}
|
|
907
|
-
|
|
@@ -3,7 +3,7 @@ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { delimiter, join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
|
-
import { findExecutable, probeExecutable } from "./
|
|
6
|
+
import { findExecutable, probeExecutable } from "./interop-support.mjs";
|
|
7
7
|
|
|
8
8
|
const root = mkdtempSync(join(tmpdir(), "forgerelay-lsp-preflight-test-"));
|
|
9
9
|
|
|
@@ -2,10 +2,10 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { CodeIntelligenceManager } from "
|
|
6
|
-
import { findExecutable, probeExecutable } from "./
|
|
5
|
+
import { CodeIntelligenceManager } from "../../dist/lsp/runtime/manager.js";
|
|
6
|
+
import { findExecutable, probeExecutable } from "./interop-support.mjs";
|
|
7
7
|
|
|
8
|
-
const repoRoot = resolve(fileURLToPath(new URL("
|
|
8
|
+
const repoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
9
9
|
const interopRoot = resolve(repoRoot, ".forgerelay-debug", "lsp-interop");
|
|
10
10
|
const fixtures = [
|
|
11
11
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
4
|
-
import {
|
|
4
|
+
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
@@ -28,6 +28,7 @@ if (head !== tagHead) {
|
|
|
28
28
|
if (!gitSucceeds(["merge-base", "--is-ancestor", tagHead, "origin/main"])) {
|
|
29
29
|
throw new Error(`${releaseTag} does not point to a commit contained in origin/main`);
|
|
30
30
|
}
|
|
31
|
+
const notesPath = prepareReleaseNotes(releaseTag);
|
|
31
32
|
|
|
32
33
|
const packages = readdirSync(packageDir).filter((name) => name.endsWith(".tgz"));
|
|
33
34
|
if (packages.length !== 1) {
|
|
@@ -53,7 +54,6 @@ if (npmPackageExists(packageSpec)) {
|
|
|
53
54
|
if (ghReleaseExists(releaseTag)) {
|
|
54
55
|
console.log(`GitHub Release ${releaseTag} already exists; leaving it unchanged.`);
|
|
55
56
|
} else {
|
|
56
|
-
const notesPath = prepareReleaseNotes(releaseTag);
|
|
57
57
|
const args = [
|
|
58
58
|
"release",
|
|
59
59
|
"create",
|
|
@@ -128,13 +128,16 @@ function ghReleaseExists(tag) {
|
|
|
128
128
|
|
|
129
129
|
function prepareReleaseNotes(tag) {
|
|
130
130
|
const manualNotes = join(repoRoot, "docs", "releases", `${tag}.md`);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
let notes;
|
|
132
|
+
try {
|
|
133
|
+
notes = readFileSync(manualNotes, "utf8");
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
136
|
+
throw new Error(`Missing dedicated release notes: docs/releases/${tag}.md`);
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
if (!notes.trim()) throw new Error(`Dedicated release notes are empty: docs/releases/${tag}.md`);
|
|
138
141
|
const tempRoot = resolve(process.env.RUNNER_TEMP ?? join(repoRoot, ".forgerelay-debug"));
|
|
139
142
|
mkdirSync(tempRoot, { recursive: true });
|
|
140
143
|
const notesPath = join(tempRoot, `release-notes-${tag}.md`);
|
|
@@ -69,7 +69,9 @@ test("release runtime and local parity share the checked-in Node contract", asyn
|
|
|
69
69
|
|
|
70
70
|
const source = await readFile(resolve(repoRoot, "scripts/release-parity.mjs"), "utf8");
|
|
71
71
|
assert.match(source, /readFileSync\(join\(repoRoot, "\.nvmrc"\), "utf8"\)/);
|
|
72
|
-
assert.match(source, /const NPM_VERSION = "
|
|
72
|
+
assert.match(source, /const NPM_VERSION = "11\.19\.1"/);
|
|
73
|
+
const ciWorkflow = await readFile(resolve(repoRoot, ".github/workflows/ci.yml"), "utf8");
|
|
74
|
+
assert.match(ciWorkflow, /npm install --global npm@11\.19\.1/);
|
|
73
75
|
assert.ok(source.includes('["npm", "ci", "--no-audit", "--no-fund"]'));
|
|
74
76
|
assert.ok(source.includes('["npm", "run", "ci:verify"]'));
|
|
75
77
|
assert.ok(source.includes('["npm", "run", "release:pack"]'));
|
|
@@ -14,6 +14,7 @@ async function createFixture(t) {
|
|
|
14
14
|
const root = await mkdtemp(join(tmpdir(), "forgerelay-release-version-"));
|
|
15
15
|
t.after(() => rm(root, { recursive: true, force: true }));
|
|
16
16
|
await mkdir(join(root, "scripts"), { recursive: true });
|
|
17
|
+
await mkdir(join(root, "docs", "releases"), { recursive: true });
|
|
17
18
|
await cp(sourceScript, join(root, "scripts", "release-version.mjs"));
|
|
18
19
|
|
|
19
20
|
const pkg = {
|
|
@@ -88,8 +89,19 @@ test("release version tooling prepares and validates an rc release", async (t) =
|
|
|
88
89
|
const checked = await runRelease(root, "check");
|
|
89
90
|
assert.match(checked.stdout, /release metadata is consistent at 0\.6\.0-rc\.1/);
|
|
90
91
|
|
|
92
|
+
await assert.rejects(
|
|
93
|
+
() => runRelease(root, "tag", "v0.6.0-rc.1"),
|
|
94
|
+
(error) => {
|
|
95
|
+
assert.match(String(error.stderr ?? error), /missing dedicated release notes: docs\/releases\/v0\.6\.0-rc\.1\.md/);
|
|
96
|
+
return true;
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
await writeFile(
|
|
100
|
+
join(root, "docs", "releases", "v0.6.0-rc.1.md"),
|
|
101
|
+
"# ForgeRelay v0.6.0-rc.1\n\nRelease candidate notes.\n",
|
|
102
|
+
);
|
|
91
103
|
const tagged = await runRelease(root, "tag", "v0.6.0-rc.1");
|
|
92
|
-
assert.match(tagged.stdout, /release tag v0\.6\.0-rc\.1 matches package version and
|
|
104
|
+
assert.match(tagged.stdout, /release tag v0\.6\.0-rc\.1 matches package version, changelog, and dedicated release notes/);
|
|
93
105
|
|
|
94
106
|
const notes = await runRelease(root, "notes", "v0.6.0-rc.1");
|
|
95
107
|
assert.match(notes.stdout, /Release candidate behavior/);
|
|
@@ -5,7 +5,7 @@ import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
|
-
const NPM_VERSION = "
|
|
8
|
+
const NPM_VERSION = "11.19.1";
|
|
9
9
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
10
|
const NODE_VERSION = readFileSync(join(repoRoot, ".nvmrc"), "utf8").trim();
|
|
11
11
|
const debugRoot = join(repoRoot, ".forgerelay-debug");
|
|
@@ -29,6 +29,20 @@ function packageVersion() {
|
|
|
29
29
|
return pkg.version;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function requireDedicatedReleaseNotes(version) {
|
|
33
|
+
const path = join(process.cwd(), "docs", "releases", `v${version}.md`);
|
|
34
|
+
let notes;
|
|
35
|
+
try {
|
|
36
|
+
notes = readFileSync(path, "utf8");
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
39
|
+
throw new Error(`missing dedicated release notes: docs/releases/v${version}.md`);
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
if (!notes.trim()) throw new Error(`dedicated release notes are empty: docs/releases/v${version}.md`);
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
function gitDir() {
|
|
33
47
|
const path = git(["rev-parse", "--git-dir"]);
|
|
34
48
|
return path.startsWith("/") ? path : join(process.cwd(), path);
|
|
@@ -138,6 +152,7 @@ function checkHookTag() {
|
|
|
138
152
|
if (tag !== expectedTag) {
|
|
139
153
|
throw new Error(`tag ${tag} does not match package version ${version}; expected ${expectedTag}`);
|
|
140
154
|
}
|
|
155
|
+
requireDedicatedReleaseNotes(version);
|
|
141
156
|
|
|
142
157
|
let tagHead;
|
|
143
158
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
@@ -25,6 +25,8 @@ test("release tag hook gate uses repository facts instead of requiring a local p
|
|
|
25
25
|
const root = await mkdtemp(join(tmpdir(), "forgerelay-release-proof-"));
|
|
26
26
|
t.after(() => rm(root, { recursive: true, force: true }));
|
|
27
27
|
await writeFile(join(root, "package.json"), JSON.stringify({ version: "1.2.3" }) + "\n");
|
|
28
|
+
await mkdir(join(root, "docs", "releases"), { recursive: true });
|
|
29
|
+
await writeFile(join(root, "docs", "releases", "v1.2.3.md"), "# v1.2.3\n\nDedicated notes.\n");
|
|
28
30
|
await writeFile(join(root, "tracked.txt"), "verified\n");
|
|
29
31
|
await git(root, ["init"]);
|
|
30
32
|
await git(root, ["config", "user.email", "proof@example.com"]);
|
|
@@ -102,10 +104,34 @@ test("release tag hook gate uses repository facts instead of requiring a local p
|
|
|
102
104
|
);
|
|
103
105
|
});
|
|
104
106
|
|
|
107
|
+
test("release tag hook gate rejects a matching tag when dedicated notes are missing", async (t) => {
|
|
108
|
+
const root = await mkdtemp(join(tmpdir(), "forgerelay-release-proof-"));
|
|
109
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
110
|
+
await writeFile(join(root, "package.json"), JSON.stringify({ version: "1.4.0" }) + "\n");
|
|
111
|
+
await git(root, ["init"]);
|
|
112
|
+
await git(root, ["config", "user.email", "proof@example.com"]);
|
|
113
|
+
await git(root, ["config", "user.name", "Release Proof Test"]);
|
|
114
|
+
await git(root, ["add", "."]);
|
|
115
|
+
await git(root, ["commit", "-m", "release 1.4.0"]);
|
|
116
|
+
await git(root, ["tag", "v1.4.0"]);
|
|
117
|
+
|
|
118
|
+
await assert.rejects(
|
|
119
|
+
() => runProof(root, "check-hook", {
|
|
120
|
+
FORGERELAY_HOOK_PAYLOAD: JSON.stringify({ command: "git push origin v1.4.0" }),
|
|
121
|
+
}),
|
|
122
|
+
(error) => {
|
|
123
|
+
assert.match(String(error.stderr ?? error), /missing dedicated release notes: docs\/releases\/v1\.4\.0\.md/);
|
|
124
|
+
return true;
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
105
129
|
test("release tag hook gate accepts an rc tag for the package version", async (t) => {
|
|
106
130
|
const root = await mkdtemp(join(tmpdir(), "forgerelay-release-proof-"));
|
|
107
131
|
t.after(() => rm(root, { recursive: true, force: true }));
|
|
108
132
|
await writeFile(join(root, "package.json"), JSON.stringify({ version: "0.6.0-rc.1" }) + "\n");
|
|
133
|
+
await mkdir(join(root, "docs", "releases"), { recursive: true });
|
|
134
|
+
await writeFile(join(root, "docs", "releases", "v0.6.0-rc.1.md"), "# v0.6.0-rc.1\n\nDedicated RC notes.\n");
|
|
109
135
|
await git(root, ["init"]);
|
|
110
136
|
await git(root, ["config", "user.email", "proof@example.com"]);
|
|
111
137
|
await git(root, ["config", "user.name", "Release Proof Test"]);
|
|
@@ -35,7 +35,8 @@ switch (command) {
|
|
|
35
35
|
if (value !== expectedTag) {
|
|
36
36
|
fail(`tag ${JSON.stringify(value)} does not match package version; expected ${expectedTag}`);
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
await requireReleaseNotes(state.pkg.version);
|
|
39
|
+
console.log(`release tag ${value} matches package version, changelog, and dedicated release notes`);
|
|
39
40
|
break;
|
|
40
41
|
}
|
|
41
42
|
case "prepare": {
|
|
@@ -203,6 +204,20 @@ function getUnreleasedBody(changelog) {
|
|
|
203
204
|
return changelog.slice(bodyStart, bodyEnd).trim();
|
|
204
205
|
}
|
|
205
206
|
|
|
207
|
+
async function requireReleaseNotes(version) {
|
|
208
|
+
const path = resolve(repoRoot, "docs", "releases", `v${version}.md`);
|
|
209
|
+
let content;
|
|
210
|
+
try {
|
|
211
|
+
content = await readFile(path, "utf8");
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
214
|
+
fail(`missing dedicated release notes: docs/releases/v${version}.md`);
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
if (!content.trim()) fail(`dedicated release notes are empty: docs/releases/v${version}.md`);
|
|
219
|
+
}
|
|
220
|
+
|
|
206
221
|
function getReleaseBody(changelog, version) {
|
|
207
222
|
const heading = `## [${version}]`;
|
|
208
223
|
const headingIndex = changelog.indexOf(heading);
|
package/scripts/wiki/sync.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
copyFileSync,
|
|
5
5
|
existsSync,
|
|
6
6
|
mkdtempSync,
|
|
7
7
|
readdirSync,
|
|
@@ -49,7 +49,13 @@ function validateWikiSource(directory) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const files = listFiles(directory);
|
|
52
|
-
const
|
|
52
|
+
const destinationNames = files.map((file) => wikiDestinationName(file.relativePath));
|
|
53
|
+
const names = new Set(destinationNames);
|
|
54
|
+
if (names.size !== destinationNames.length) {
|
|
55
|
+
const duplicates = destinationNames.filter((name, index) => destinationNames.indexOf(name) !== index);
|
|
56
|
+
fail(`Wiki source contains duplicate flattened page names:
|
|
57
|
+
${[...new Set(duplicates)].map((name) => `- ${name}`).join("\n")}`);
|
|
58
|
+
}
|
|
53
59
|
|
|
54
60
|
for (const required of ["Home.md", "_Sidebar.md"]) {
|
|
55
61
|
if (!names.has(required)) {
|
|
@@ -59,7 +65,7 @@ function validateWikiSource(directory) {
|
|
|
59
65
|
|
|
60
66
|
const invalidNames = files
|
|
61
67
|
.map((file) => file.relativePath)
|
|
62
|
-
.filter((name) => !
|
|
68
|
+
.filter((name) => !isSafeWikiSourcePath(name));
|
|
63
69
|
if (invalidNames.length > 0) {
|
|
64
70
|
fail(`Wiki source contains unsupported paths:\n${invalidNames.map((name) => `- ${name}`).join("\n")}`);
|
|
65
71
|
}
|
|
@@ -67,7 +73,7 @@ function validateWikiSource(directory) {
|
|
|
67
73
|
const markdownNames = new Set(
|
|
68
74
|
files
|
|
69
75
|
.filter((file) => file.relativePath.endsWith(".md"))
|
|
70
|
-
.map((file) => file.relativePath),
|
|
76
|
+
.map((file) => wikiDestinationName(file.relativePath)),
|
|
71
77
|
);
|
|
72
78
|
const brokenLinks = [];
|
|
73
79
|
|
|
@@ -86,7 +92,7 @@ function validateWikiSource(directory) {
|
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
const candidate = target.endsWith(".md") ? target : `${target}.md`;
|
|
89
|
-
if (!markdownNames.has(candidate)) {
|
|
95
|
+
if (!markdownNames.has(wikiDestinationName(candidate))) {
|
|
90
96
|
brokenLinks.push(`${file.relativePath}: ${rawTarget}`);
|
|
91
97
|
}
|
|
92
98
|
}
|
|
@@ -169,8 +175,8 @@ function mirrorSource(source, destination) {
|
|
|
169
175
|
rmSync(join(destination, entry), { recursive: true, force: true });
|
|
170
176
|
}
|
|
171
177
|
|
|
172
|
-
for (const
|
|
173
|
-
|
|
178
|
+
for (const file of listFiles(source)) {
|
|
179
|
+
copyFileSync(file.absolutePath, join(destination, wikiDestinationName(file.relativePath)));
|
|
174
180
|
}
|
|
175
181
|
}
|
|
176
182
|
|
|
@@ -191,10 +197,15 @@ function listFiles(directory, prefix = "") {
|
|
|
191
197
|
return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
192
198
|
}
|
|
193
199
|
|
|
194
|
-
function
|
|
200
|
+
function wikiDestinationName(relativePath) {
|
|
201
|
+
return basename(relativePath);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isSafeWikiSourcePath(relativePath) {
|
|
195
205
|
if (!relativePath.endsWith(".md")) return false;
|
|
196
|
-
|
|
197
|
-
|
|
206
|
+
return relativePath
|
|
207
|
+
.split("/")
|
|
208
|
+
.every((segment) => segment.length > 0 && !/[\\:*?"<>|]/.test(segment));
|
|
198
209
|
}
|
|
199
210
|
|
|
200
211
|
function isExternalOrAnchorLink(target) {
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { StringDecoder } from "node:string_decoder";
|
|
3
|
-
import { openDatabase } from "../db/client.js";
|
|
4
|
-
export class BashOutputStore {
|
|
5
|
-
database;
|
|
6
|
-
now;
|
|
7
|
-
nextOutputId;
|
|
8
|
-
nextSequences = new Map();
|
|
9
|
-
constructor(stateDir, options = {}) {
|
|
10
|
-
this.database = openDatabase(stateDir);
|
|
11
|
-
this.now = options.now ?? (() => new Date());
|
|
12
|
-
this.nextOutputId = options.outputId ?? (() => `out_${randomUUID().replaceAll("-", "")}`);
|
|
13
|
-
}
|
|
14
|
-
begin(input) {
|
|
15
|
-
const outputId = this.nextOutputId();
|
|
16
|
-
const startedAt = this.now().toISOString();
|
|
17
|
-
this.database.sqlite.prepare(`insert into bash_output_streams (
|
|
18
|
-
id, activity_id, turn_id, conversation_scope_id, process_id,
|
|
19
|
-
workspace_id, workspace_root, command, tty, status, timed_out, started_at
|
|
20
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', 0, ?)`).run(outputId, input.activityId, input.turnId, input.conversationScopeId ?? null, input.processId, input.workspaceId, input.workspaceRoot, input.command, input.tty ? 1 : 0, startedAt);
|
|
21
|
-
this.nextSequences.set(outputId, 1);
|
|
22
|
-
return outputId;
|
|
23
|
-
}
|
|
24
|
-
append(outputId, channel, data) {
|
|
25
|
-
const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
|
|
26
|
-
if (bytes.length === 0)
|
|
27
|
-
return;
|
|
28
|
-
const sequence = this.nextSequence(outputId);
|
|
29
|
-
this.database.sqlite.prepare(`insert into bash_output_chunks (output_id, sequence, channel, data, created_at)
|
|
30
|
-
values (?, ?, ?, ?, ?)`).run(outputId, sequence, channel, bytes, this.now().toISOString());
|
|
31
|
-
}
|
|
32
|
-
markReturned(outputId) {
|
|
33
|
-
this.database.sqlite.prepare("update bash_output_streams set returned = 1 where id = ?").run(outputId);
|
|
34
|
-
}
|
|
35
|
-
claimCompletion(outputId) {
|
|
36
|
-
const claimedAt = this.now().toISOString();
|
|
37
|
-
const claimed = this.database.sqlite.prepare(`update bash_output_streams
|
|
38
|
-
set completion_claimed_at = ?
|
|
39
|
-
where id = ? and returned = 1 and status != 'running' and completion_claimed_at is null`).run(claimedAt, outputId);
|
|
40
|
-
return claimed.changes === 1 ? this.read(outputId) : undefined;
|
|
41
|
-
}
|
|
42
|
-
finish(outputId, input) {
|
|
43
|
-
const status = input.error || input.timedOut || input.signal || (input.exitCode !== undefined && input.exitCode !== 0)
|
|
44
|
-
? "failed"
|
|
45
|
-
: "done";
|
|
46
|
-
this.database.sqlite.prepare(`update bash_output_streams
|
|
47
|
-
set status = ?, exit_code = ?, signal = ?, timed_out = ?, error = ?, finished_at = ?
|
|
48
|
-
where id = ?`).run(status, input.exitCode ?? null, input.signal ?? null, input.timedOut ? 1 : 0, input.error ?? null, this.now().toISOString(), outputId);
|
|
49
|
-
}
|
|
50
|
-
read(outputId) {
|
|
51
|
-
const stream = this.database.sqlite.prepare("select * from bash_output_streams where id = ?").get(outputId);
|
|
52
|
-
if (!stream)
|
|
53
|
-
return undefined;
|
|
54
|
-
const rows = this.database.sqlite.prepare(`select * from bash_output_chunks
|
|
55
|
-
where output_id = ?
|
|
56
|
-
order by sequence asc`).all(outputId);
|
|
57
|
-
const decoded = decodeChunks(rows);
|
|
58
|
-
return {
|
|
59
|
-
outputId: stream.id,
|
|
60
|
-
activityId: stream.activity_id,
|
|
61
|
-
turnId: stream.turn_id,
|
|
62
|
-
...(stream.conversation_scope_id ? { conversationScopeId: stream.conversation_scope_id } : {}),
|
|
63
|
-
processId: stream.process_id,
|
|
64
|
-
workspaceId: stream.workspace_id,
|
|
65
|
-
workspaceRoot: stream.workspace_root,
|
|
66
|
-
command: stream.command,
|
|
67
|
-
tty: stream.tty === 1,
|
|
68
|
-
output: decoded.map((chunk) => chunk.data).join(""),
|
|
69
|
-
chunks: decoded,
|
|
70
|
-
status: isBashOutputStatus(stream.status) ? stream.status : "failed",
|
|
71
|
-
...(stream.exit_code !== null ? { exitCode: stream.exit_code } : {}),
|
|
72
|
-
...(stream.signal ? { signal: stream.signal } : {}),
|
|
73
|
-
timedOut: stream.timed_out === 1,
|
|
74
|
-
...(stream.error ? { error: stream.error } : {}),
|
|
75
|
-
returned: stream.returned === 1,
|
|
76
|
-
startedAt: stream.started_at,
|
|
77
|
-
...(stream.finished_at ? { finishedAt: stream.finished_at } : {}),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
close() {
|
|
81
|
-
this.database.close();
|
|
82
|
-
}
|
|
83
|
-
nextSequence(outputId) {
|
|
84
|
-
const known = this.nextSequences.get(outputId);
|
|
85
|
-
if (known !== undefined) {
|
|
86
|
-
this.nextSequences.set(outputId, known + 1);
|
|
87
|
-
return known;
|
|
88
|
-
}
|
|
89
|
-
const row = this.database.sqlite.prepare("select coalesce(max(sequence), 0) as sequence from bash_output_chunks where output_id = ?").get(outputId);
|
|
90
|
-
const sequence = row.sequence + 1;
|
|
91
|
-
this.nextSequences.set(outputId, sequence + 1);
|
|
92
|
-
return sequence;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
function decodeChunks(rows) {
|
|
96
|
-
const decoders = new Map();
|
|
97
|
-
const chunks = [];
|
|
98
|
-
const lastChunkIndex = new Map();
|
|
99
|
-
for (const row of rows) {
|
|
100
|
-
if (!isBashOutputChannel(row.channel)) {
|
|
101
|
-
throw new Error(`Unknown Bash output channel: ${row.channel}`);
|
|
102
|
-
}
|
|
103
|
-
const decoder = decoders.get(row.channel) ?? new StringDecoder("utf8");
|
|
104
|
-
decoders.set(row.channel, decoder);
|
|
105
|
-
const data = decoder.write(row.data);
|
|
106
|
-
chunks.push({ sequence: row.sequence, channel: row.channel, data });
|
|
107
|
-
lastChunkIndex.set(row.channel, chunks.length - 1);
|
|
108
|
-
}
|
|
109
|
-
for (const [channel, decoder] of decoders) {
|
|
110
|
-
const tail = decoder.end();
|
|
111
|
-
if (!tail)
|
|
112
|
-
continue;
|
|
113
|
-
const index = lastChunkIndex.get(channel);
|
|
114
|
-
if (index === undefined)
|
|
115
|
-
continue;
|
|
116
|
-
const chunk = chunks[index];
|
|
117
|
-
if (chunk)
|
|
118
|
-
chunk.data += tail;
|
|
119
|
-
}
|
|
120
|
-
return chunks;
|
|
121
|
-
}
|
|
122
|
-
function isBashOutputChannel(value) {
|
|
123
|
-
return value === "stdout" || value === "stderr" || value === "pty" || value === "process";
|
|
124
|
-
}
|
|
125
|
-
function isBashOutputStatus(value) {
|
|
126
|
-
return value === "running" || value === "done" || value === "failed";
|
|
127
|
-
}
|