@akira-tl/forgerelay 0.5.6 → 0.6.0
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 +43 -0
- package/README.md +7 -3
- package/capabilities/host-integration/GUIDE.md +15 -9
- package/dist/activity/host-turn-store.js +9 -7
- package/dist/activity/lifecycle.js +1 -1
- package/dist/activity/mcp-query-tools.js +110 -18
- package/dist/activity/query-service.js +4 -4
- package/dist/activity/ui/contract.js +2 -0
- package/dist/cli.js +47 -32
- package/dist/config.js +31 -3
- package/dist/db/migrations.js +30 -0
- package/dist/db/schema.js +2 -0
- package/dist/logger.js +6 -5
- package/dist/lsp/test-support/server-fixture.js +1 -1
- package/dist/mcp/server-instructions.js +7 -6
- package/dist/mcp-app-template.js +108 -12
- package/dist/oauth/public-url.js +14 -0
- package/dist/oauth/router.js +49 -0
- package/dist/process-sessions.js +2 -1
- package/dist/request-meta.js +4 -0
- package/dist/server.js +250 -84
- package/dist/skills.js +58 -1
- package/dist/ui/.vite/manifest.json +360 -312
- package/dist/ui/activity-panel-app.html +17 -0
- package/dist/ui/assets/activity-panel-app-CjZVvVNc.js +2 -0
- package/dist/ui/assets/angular-html-BkgDQl-G.js +1 -0
- package/dist/ui/assets/{angular-ts-BwPE8YDt.js → angular-ts-Bxh4-O1-.js} +1 -1
- package/dist/ui/assets/{apl-CGbONVvU.js → apl-BmYHKQ6m.js} +1 -1
- package/dist/ui/assets/{astro-DIAPy7cj.js → astro-C51vqDKq.js} +1 -1
- package/dist/ui/assets/{blade-Cgtd19Ky.js → blade-CdhVsmwq.js} +1 -1
- package/dist/ui/assets/c-IBzBpxAO.js +1 -0
- package/dist/ui/assets/chunk-EyZ2wyi3.js +1 -0
- package/dist/ui/assets/{cobol-G-7HLVKW.js → cobol-Cf6azoN_.js} +1 -1
- package/dist/ui/assets/{coffee-DQFyKxEB.js → coffee-vVdGzlmt.js} +1 -1
- package/dist/ui/assets/cpp-CVHeO-Jk.js +1 -0
- package/dist/ui/assets/{crystal-DuiWhEO3.js → crystal-5UZy6KIX.js} +1 -1
- package/dist/ui/assets/css-DEfus9J5.js +1 -0
- package/dist/ui/assets/{edge-COkalhFi.js → edge-CESgo4ia.js} +1 -1
- package/dist/ui/assets/{elixir-cdlzbYR1.js → elixir-9FKfw4cJ.js} +1 -1
- package/dist/ui/assets/{elm-ZwDYuYpb.js → elm-Dv7A0aLa.js} +1 -1
- package/dist/ui/assets/{erb-C8WG9TtC.js → erb-DHFmDvM0.js} +1 -1
- package/dist/ui/assets/{git-rebase-CMlDVJBT.js → git-rebase-utbU6CzV.js} +1 -1
- package/dist/ui/assets/{glimmer-js-DeLOQlol.js → glimmer-js-t2kiCkyB.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-DTEM04TK.js → glimmer-ts-F0Jv5okV.js} +1 -1
- package/dist/ui/assets/glsl-_ooCjaeB.js +1 -0
- package/dist/ui/assets/graphql-B9LqP-cY.js +1 -0
- package/dist/ui/assets/{hack-C14sh6Lh.js → hack-8yjdbaSA.js} +1 -1
- package/dist/ui/assets/haml-BU5EoZBn.js +1 -0
- package/dist/ui/assets/{handlebars-eS-J0O-M.js → handlebars-BBFEg4Lo.js} +1 -1
- package/dist/ui/assets/{heavy-payload-BKB0QDOI.js → heavy-payload-vGgBRvNX.js} +1 -1
- package/dist/ui/assets/html-D-C6YZk4.js +1 -0
- package/dist/ui/assets/{html-derivative-BlVZU2id.js → html-derivative-DJAQcZyc.js} +1 -1
- package/dist/ui/assets/{http-BmbZzA5x.js → http-Dg5LSYRK.js} +1 -1
- package/dist/ui/assets/{hurl-Dnd_YpOD.js → hurl-Y5uYzMZR.js} +1 -1
- package/dist/ui/assets/java-ppHvycj5.js +1 -0
- package/dist/ui/assets/javascript-DDTjKLlQ.js +1 -0
- package/dist/ui/assets/{jinja-DTvkZdHA.js → jinja-Bt30h8Ik.js} +1 -1
- package/dist/ui/assets/{jison-DUJG-tmh.js → jison-D5uZ1Nyo.js} +1 -1
- package/dist/ui/assets/json-Cw91r8h1.js +1 -0
- package/dist/ui/assets/jsx-04X2FT_1.js +1 -0
- package/dist/ui/assets/{julia-P5B4pHFa.js → julia-76hRaaGz.js} +1 -1
- package/dist/ui/assets/{just-DYaoh7C6.js → just-CDE7W0vW.js} +1 -1
- package/dist/ui/assets/{latex-CI5lnkWV.js → latex-CVO2Byab.js} +1 -1
- package/dist/ui/assets/{liquid-D3LnAs_z.js → liquid-CA-AS3EX.js} +1 -1
- package/dist/ui/assets/lua-CiyBp52Y.js +1 -0
- package/dist/ui/assets/{marko-CRDrjqfS.js → marko-_BWrPfny.js} +1 -1
- package/dist/ui/assets/{mdc-ZzwgmJVn.js → mdc-PyJv85eS.js} +1 -1
- package/dist/ui/assets/{nginx-BAvFXGSV.js → nginx-DAWVw8yu.js} +1 -1
- package/dist/ui/assets/{nim-BHIFawe9.js → nim-CKE1dzpU.js} +1 -1
- package/dist/ui/assets/{perl-C_f5AnLa.js → perl-BBhWva19.js} +1 -1
- package/dist/ui/assets/{php-DDqpegL5.js → php-ChIeaAoF.js} +1 -1
- package/dist/ui/assets/{pug-CSaAZXyd.js → pug-CMgI5afi.js} +1 -1
- package/dist/ui/assets/{qml-CNHvRs_z.js → qml-CjxTkcFH.js} +1 -1
- package/dist/ui/assets/r-DfFga4si.js +1 -0
- package/dist/ui/assets/{razor-BDfAPy7I.js → razor-i4DbHr8p.js} +1 -1
- package/dist/ui/assets/regexp-iC9_nbSI.js +1 -0
- package/dist/ui/assets/{review-payload-D7GIdYDj.js → review-payload-4erWKckt.js} +1 -1
- package/dist/ui/assets/{rst-BzPP2gle.js → rst-oy_97Kl4.js} +1 -1
- package/dist/ui/assets/{ruby-CQjzokzX.js → ruby-C5zYZIyl.js} +1 -1
- package/dist/ui/assets/{sas-Bp37KGr7.js → sas-BnMEh2S3.js} +1 -1
- package/dist/ui/assets/{scrollbar-HUMBdLLK.js → scrollbar-CaOPzUJd.js} +6 -6
- package/dist/ui/assets/scss-BfZXsAWQ.js +1 -0
- package/dist/ui/assets/shellscript-BC2srNaB.js +1 -0
- package/dist/ui/assets/{shellsession-CNc62UhY.js → shellsession-BpE5o7_3.js} +1 -1
- package/dist/ui/assets/{soy-CF44SpBo.js → soy-COesmhTY.js} +1 -1
- package/dist/ui/assets/sql-Ccv_yeUm.js +1 -0
- package/dist/ui/assets/{stata-DVeMA68W.js → stata-CGQEuSZq.js} +1 -1
- package/dist/ui/assets/{surrealql-52sF96Yv.js → surrealql-Bz4JbU-9.js} +1 -1
- package/dist/ui/assets/{svelte-ydwT7UbF.js → svelte-1jM2ejXj.js} +1 -1
- package/dist/ui/assets/{templ-BXcoVoGh.js → templ-C-VwELcp.js} +1 -1
- package/dist/ui/assets/{tex-COsYFp7O.js → tex-t1rUiQCm.js} +1 -1
- package/dist/ui/assets/{ts-tags-CF--LE1s.js → ts-tags-Bewoa6pS.js} +1 -1
- package/dist/ui/assets/tsx-CTXzrBkN.js +1 -0
- package/dist/ui/assets/{twig-YoM-DiI5.js → twig-DVg-vELl.js} +1 -1
- package/dist/ui/assets/typescript--ElHoFkw.js +1 -0
- package/dist/ui/assets/{vue-html-CPdGGk4T.js → vue-html-BHtJ8geX.js} +1 -1
- package/dist/ui/assets/{vue-vine-B5bjgk0a.js → vue-vine-CNSGAWwc.js} +1 -1
- package/dist/ui/assets/{vue-B5LDEy4l.js → vue-wPQ7UlMy.js} +1 -1
- package/dist/ui/assets/workspace-app-CcrHAUIn.css +1 -0
- package/dist/ui/assets/workspace-app-DJmkPYJC.js +1 -0
- package/dist/ui/assets/workspace-app-DkAiSl_0.js +111 -0
- package/dist/ui/assets/workspace-app-QyauBrJX.js +5 -0
- package/dist/ui/assets/workspace-lifecycle-app-BIXEo53I.js +1 -0
- package/dist/ui/assets/xml-JnmX6vyS.js +1 -0
- package/dist/ui/assets/{xsl-wls_CEK_.js → xsl-3_vJ27aE.js} +1 -1
- package/dist/ui/assets/yaml-OAcZkP-w.js +1 -0
- package/dist/ui/workspace-app.html +5 -2
- package/dist/ui/workspace-lifecycle-app.html +18 -0
- package/dist/workspaces.js +11 -0
- package/docs/configuration.md +36 -4
- package/docs/debugging.md +75 -17
- package/docs/gotchas.md +15 -9
- package/docs/setup.md +24 -10
- package/package.json +4 -3
- package/scripts/debug/accept.mjs +79 -27
- package/scripts/debug/host/run.sh +180 -0
- package/scripts/debug/host/serve.mjs +64 -0
- package/scripts/debug/runtime.mjs +57 -2
- package/scripts/debug/runtime.test.mjs +74 -0
- package/scripts/debug/serve.mjs +4 -5
- package/scripts/dev-server.mjs +32 -3
- package/scripts/release/release-gate.test.mjs +43 -0
- package/scripts/release/release-version.test.mjs +107 -0
- package/scripts/release-parity.mjs +70 -46
- package/scripts/release-proof.mjs +2 -2
- package/scripts/release-proof.test.mjs +18 -0
- package/scripts/release-version.mjs +17 -8
- package/dist/ui/assets/angular-html-BT2Z6jee.js +0 -1
- package/dist/ui/assets/c-acnvZ8tm.js +0 -1
- package/dist/ui/assets/cpp-UyrmHXan.js +0 -1
- package/dist/ui/assets/css-D_WCbY6m.js +0 -1
- package/dist/ui/assets/glsl-IJV4SB-M.js +0 -1
- package/dist/ui/assets/graphql-PM-WDnoB.js +0 -1
- package/dist/ui/assets/haml-B_5nS6Kx.js +0 -1
- package/dist/ui/assets/html-C3TPjrC5.js +0 -1
- package/dist/ui/assets/java-ne9HdIVn.js +0 -1
- package/dist/ui/assets/javascript-DpHT58rr.js +0 -1
- package/dist/ui/assets/json-DJmnBdxW.js +0 -1
- package/dist/ui/assets/jsx-BclJnZnN.js +0 -1
- package/dist/ui/assets/lua-dZC7ZQIY.js +0 -1
- package/dist/ui/assets/r-CAfoQND2.js +0 -1
- package/dist/ui/assets/regexp-Mug5jltQ.js +0 -1
- package/dist/ui/assets/scss-C_Z1DYnw.js +0 -1
- package/dist/ui/assets/shellscript-CLfJpn3I.js +0 -1
- package/dist/ui/assets/sql-CYnzfiQ1.js +0 -1
- package/dist/ui/assets/tsx-DLBt2-2f.js +0 -1
- package/dist/ui/assets/typescript-Ce9JGkNI.js +0 -1
- package/dist/ui/assets/workspace-app-Cm9EX5x4.js +0 -117
- package/dist/ui/assets/workspace-app-DHvE2ojW.css +0 -1
- package/dist/ui/assets/xml-Cun8xb0T.js +0 -1
- package/dist/ui/assets/yaml-BxuXn9OL.js +0 -1
- package/scripts/release/tag-local-ci.mjs +0 -144
package/scripts/debug/accept.mjs
CHANGED
|
@@ -120,7 +120,7 @@ try {
|
|
|
120
120
|
assert.match(serverInstructions, /Shell commands may modify ordinary project files/);
|
|
121
121
|
assert.match(serverInstructions, /\/etc\/sudoers/);
|
|
122
122
|
assert.match(serverInstructions, /configuration files through shell only when the user's request explicitly calls for that configuration change/);
|
|
123
|
-
assert.match(serverInstructions, /
|
|
123
|
+
assert.match(serverInstructions, /Project-work order: open_workspace if needed → activity_panel\(workspaceId\) once → work tools/);
|
|
124
124
|
assert.ok(serverInstructions.length < 3_000, `server instructions should stay compact, got ${serverInstructions.length} characters`);
|
|
125
125
|
assert.doesNotMatch(serverInstructions, /fast-forwards the original target branch/);
|
|
126
126
|
assert.doesNotMatch(serverInstructions, /Do not create or modify files with bash/);
|
|
@@ -146,6 +146,10 @@ try {
|
|
|
146
146
|
const toolNames = tools.map((tool) => tool.name);
|
|
147
147
|
assert.deepEqual(toolNames, [
|
|
148
148
|
"open_workspace",
|
|
149
|
+
"activity_panel",
|
|
150
|
+
"activity_snapshot",
|
|
151
|
+
"activity_detail",
|
|
152
|
+
"activity_output",
|
|
149
153
|
"capability",
|
|
150
154
|
"close_workspace",
|
|
151
155
|
"read",
|
|
@@ -176,14 +180,13 @@ try {
|
|
|
176
180
|
assert.ok(openWorkspaceTool?.outputSchema?.properties?.capabilityGuides);
|
|
177
181
|
const capabilityTool = tools.find((tool) => tool.name === "capability");
|
|
178
182
|
assert.deepEqual(capabilityTool?._meta?.["openai/fileParams"], ["file"]);
|
|
179
|
-
const
|
|
183
|
+
const activityPanelTool = tools.find((tool) => tool.name === "activity_panel");
|
|
184
|
+
const activityTemplateUri = activityPanelTool?._meta?.ui?.resourceUri;
|
|
180
185
|
assert.match(
|
|
181
|
-
|
|
182
|
-
/^ui:\/\/forgerelay\/
|
|
183
|
-
JSON.stringify(bashTool ?? {}),
|
|
186
|
+
activityTemplateUri ?? "",
|
|
187
|
+
/^ui:\/\/forgerelay\/activity-panel-app-[0-9a-f]{12}\.html$/,
|
|
184
188
|
);
|
|
185
|
-
assert.deepEqual(
|
|
186
|
-
assert.equal(capabilityTool?._meta?.["openai/outputTemplate"], templateUri);
|
|
189
|
+
assert.deepEqual(activityPanelTool?._meta?.ui?.visibility, ["model", "app"]);
|
|
187
190
|
pass("MCP tools/list", `${toolNames.length} tools: ${toolNames.join(", ")}`);
|
|
188
191
|
|
|
189
192
|
const resources = mcpRequest(oauth.accessToken, sessionId, {
|
|
@@ -192,9 +195,13 @@ try {
|
|
|
192
195
|
method: "resources/list",
|
|
193
196
|
params: {},
|
|
194
197
|
}).message.result.resources;
|
|
195
|
-
const
|
|
196
|
-
assert.ok(
|
|
197
|
-
assert.equal(
|
|
198
|
+
const currentActivityResource = resources.find((resource) => resource.uri === activityTemplateUri);
|
|
199
|
+
assert.ok(currentActivityResource);
|
|
200
|
+
assert.equal(currentActivityResource._meta?.ui?.domain, debugBaseUrl);
|
|
201
|
+
assert.equal(
|
|
202
|
+
resources.some((resource) => /^ui:\/\/forgerelay\/workspace-lifecycle-app-/.test(resource.uri)),
|
|
203
|
+
false,
|
|
204
|
+
);
|
|
198
205
|
assert.ok(resources.some((resource) => resource.uri === "ui://forgerelay/workspace-app.html"));
|
|
199
206
|
|
|
200
207
|
const resourceTemplates = mcpRequest(oauth.accessToken, sessionId, {
|
|
@@ -204,7 +211,10 @@ try {
|
|
|
204
211
|
params: {},
|
|
205
212
|
}).message.result.resourceTemplates;
|
|
206
213
|
assert.ok(resourceTemplates.some(
|
|
207
|
-
(resourceTemplate) => resourceTemplate.uriTemplate === "ui://forgerelay/workspace-app-{revision}.html",
|
|
214
|
+
(resourceTemplate) => resourceTemplate.uriTemplate === "ui://forgerelay/workspace-lifecycle-app-{revision}.html",
|
|
215
|
+
));
|
|
216
|
+
assert.ok(resourceTemplates.some(
|
|
217
|
+
(resourceTemplate) => resourceTemplate.uriTemplate === "ui://forgerelay/activity-panel-app-{revision}.html",
|
|
208
218
|
));
|
|
209
219
|
|
|
210
220
|
const readTemplate = (id, uri) => mcpRequest(oauth.accessToken, sessionId, {
|
|
@@ -214,28 +224,21 @@ try {
|
|
|
214
224
|
params: { uri },
|
|
215
225
|
}).message.result.contents[0];
|
|
216
226
|
|
|
217
|
-
const
|
|
218
|
-
assert.equal(
|
|
219
|
-
assert.equal(
|
|
220
|
-
assert.match(
|
|
221
|
-
assert.equal(
|
|
222
|
-
assert.ok(
|
|
223
|
-
const scriptUrl =
|
|
227
|
+
const activityTemplate = readTemplate(24, activityTemplateUri);
|
|
228
|
+
assert.equal(activityTemplate.uri, activityTemplateUri);
|
|
229
|
+
assert.equal(activityTemplate.mimeType, "text/html;profile=mcp-app");
|
|
230
|
+
assert.match(activityTemplate.text ?? "", /activity-panel-app-[^\"]+\.js/);
|
|
231
|
+
assert.equal(activityTemplate._meta?.ui?.domain, debugBaseUrl);
|
|
232
|
+
assert.ok(activityTemplate._meta?.ui?.csp?.resourceDomains?.includes(debugBaseUrl));
|
|
233
|
+
const scriptUrl = activityTemplate.text?.match(/<script type="module" crossorigin src="([^"]+)"/)?.[1];
|
|
224
234
|
assert.ok(scriptUrl);
|
|
225
235
|
const scriptAsset = curlRequest({ method: "GET", url: scriptUrl });
|
|
226
236
|
assert.equal(scriptAsset.status, 200, scriptAsset.body);
|
|
227
237
|
|
|
228
|
-
const legacyTemplate = readTemplate(
|
|
238
|
+
const legacyTemplate = readTemplate(25, "ui://forgerelay/workspace-app.html");
|
|
229
239
|
assert.equal(legacyTemplate.uri, "ui://forgerelay/workspace-app.html");
|
|
230
240
|
assert.equal(legacyTemplate.mimeType, "text/html;profile=mcp-app");
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const historicalUri = "ui://forgerelay/workspace-app-0.2.4.html";
|
|
234
|
-
const historicalTemplate = readTemplate(25, historicalUri);
|
|
235
|
-
assert.equal(historicalTemplate.uri, historicalUri);
|
|
236
|
-
assert.equal(historicalTemplate.mimeType, "text/html;profile=mcp-app");
|
|
237
|
-
assert.equal(historicalTemplate.text, template.text);
|
|
238
|
-
pass("MCP app template", `${templateUri} + legacy/history compatibility -> ${scriptUrl}`);
|
|
241
|
+
pass("MCP app templates", `${activityTemplateUri} + historical compatibility templates`);
|
|
239
242
|
|
|
240
243
|
const workspaceConversationMeta = { "openai/session": "acceptance-workspace" };
|
|
241
244
|
const opened = callTool(oauth.accessToken, sessionId, 3, "open_workspace", {
|
|
@@ -259,6 +262,7 @@ try {
|
|
|
259
262
|
"hooks.lifecycle",
|
|
260
263
|
"capability-guides.read",
|
|
261
264
|
"code.intelligence",
|
|
265
|
+
"batch.execute",
|
|
262
266
|
...(process.platform === "linux" ? ["artifact.native-download"] : []),
|
|
263
267
|
"ui.mcp-app",
|
|
264
268
|
"review.changes",
|
|
@@ -269,6 +273,7 @@ try {
|
|
|
269
273
|
"hooks.check",
|
|
270
274
|
"review.changes",
|
|
271
275
|
"code.intelligence",
|
|
276
|
+
"batch.execute",
|
|
272
277
|
...(process.platform === "linux" ? ["artifact.download"] : []),
|
|
273
278
|
]);
|
|
274
279
|
assert.equal(capabilityCatalog[0].available, true);
|
|
@@ -398,6 +403,7 @@ try {
|
|
|
398
403
|
"host-integration",
|
|
399
404
|
"shell-processes",
|
|
400
405
|
"code-intelligence",
|
|
406
|
+
"batch-execution",
|
|
401
407
|
]);
|
|
402
408
|
const hooksGuide = callTool(oauth.accessToken, sessionId, 78, "read", {
|
|
403
409
|
workspaceId,
|
|
@@ -407,6 +413,52 @@ try {
|
|
|
407
413
|
assert.match(hooksGuide.structuredContent.result, /BeforeWorktreeClose/);
|
|
408
414
|
pass("open_workspace", `${workspaceId} -> ${capabilityCatalog.length} capabilities + ${capabilityGuides.length} capability guides`);
|
|
409
415
|
|
|
416
|
+
const unifiedPanel = callTool(oauth.accessToken, sessionId, 89, "activity_panel", {
|
|
417
|
+
workspaceId,
|
|
418
|
+
}, workspaceConversationMeta);
|
|
419
|
+
assert.equal(unifiedPanel.isError, undefined);
|
|
420
|
+
assert.equal(
|
|
421
|
+
unifiedPanel._meta?.["forgerelay/activityPanelWorkspace"]?.workspaceId,
|
|
422
|
+
workspaceId,
|
|
423
|
+
);
|
|
424
|
+
assert.equal(
|
|
425
|
+
unifiedPanel._meta?.["forgerelay/activityPanelWorkspace"]?.root,
|
|
426
|
+
checkoutWorkspace,
|
|
427
|
+
);
|
|
428
|
+
pass("unified ForgeRelay Panel", `${workspaceId} -> Workspace + Activity`);
|
|
429
|
+
|
|
430
|
+
const inspectorActivityPath = join(checkoutWorkspace, "inspector-activity.txt");
|
|
431
|
+
writeFileSync(inspectorActivityPath, "inspector transport-scoped activity\n");
|
|
432
|
+
try {
|
|
433
|
+
const inspectorPanel = callTool(oauth.accessToken, sessionId, 90, "activity_panel", {
|
|
434
|
+
workspaceId,
|
|
435
|
+
});
|
|
436
|
+
const inspectorTurnId = inspectorPanel.structuredContent.turnId;
|
|
437
|
+
const inspectorRead = callTool(oauth.accessToken, sessionId, 91, "read", {
|
|
438
|
+
workspaceId,
|
|
439
|
+
path: "inspector-activity.txt",
|
|
440
|
+
offset: 1,
|
|
441
|
+
limit: 2,
|
|
442
|
+
});
|
|
443
|
+
assert.equal(inspectorRead.isError, undefined);
|
|
444
|
+
const inspectorSnapshot = callTool(oauth.accessToken, sessionId, 92, "activity_snapshot", {
|
|
445
|
+
turnId: inspectorTurnId,
|
|
446
|
+
});
|
|
447
|
+
assert.equal(inspectorSnapshot.isError, undefined);
|
|
448
|
+
assert.ok(inspectorSnapshot.structuredContent.revision > 0);
|
|
449
|
+
assert.deepEqual(
|
|
450
|
+
inspectorSnapshot.structuredContent.activities.map(({ tool, workspaceId: activityWorkspaceId, target }) => ({
|
|
451
|
+
tool,
|
|
452
|
+
workspaceId: activityWorkspaceId,
|
|
453
|
+
target,
|
|
454
|
+
})),
|
|
455
|
+
[{ tool: "read", workspaceId, target: "inspector-activity.txt" }],
|
|
456
|
+
);
|
|
457
|
+
pass("Inspector-style Activity scope", `${sessionId} -> ${inspectorTurnId} -> read captured`);
|
|
458
|
+
} finally {
|
|
459
|
+
rmSync(inspectorActivityPath, { force: true });
|
|
460
|
+
}
|
|
461
|
+
|
|
410
462
|
const written = callTool(oauth.accessToken, sessionId, 4, "write", {
|
|
411
463
|
workspaceId,
|
|
412
464
|
path: "acceptance.txt",
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
|
5
|
+
cd "$repo_root"
|
|
6
|
+
|
|
7
|
+
debug_host_port="${FORGERELAY_DEBUG_HOST_PORT:-7677}"
|
|
8
|
+
inspector_client_port="${MCP_INSPECTOR_CLIENT_PORT:-6274}"
|
|
9
|
+
inspector_sandbox_port="${MCP_INSPECTOR_SANDBOX_PORT:-6275}"
|
|
10
|
+
for port_value in "$debug_host_port" "$inspector_client_port" "$inspector_sandbox_port"; do
|
|
11
|
+
if [[ ! "$port_value" =~ ^[0-9]+$ ]] || (( port_value < 1 || port_value > 65535 )); then
|
|
12
|
+
echo "[forgerelay:debug-host] debug/Inspector ports must be integers from 1 to 65535." >&2
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
done
|
|
16
|
+
|
|
17
|
+
health_url="http://127.0.0.1:${debug_host_port}/healthz"
|
|
18
|
+
mcp_url="http://127.0.0.1:${debug_host_port}/mcp"
|
|
19
|
+
inspector_url="http://127.0.0.1:${inspector_client_port}"
|
|
20
|
+
inspector_package="${MCP_INSPECTOR_PACKAGE:-@modelcontextprotocol/inspector@2.3.0}"
|
|
21
|
+
inspector_node_dir="${MCP_INSPECTOR_NODE_DIR:-}"
|
|
22
|
+
|
|
23
|
+
if [[ -z "$inspector_node_dir" ]]; then
|
|
24
|
+
current_node_major="$(node -p 'Number(process.versions.node.split(".")[0])')"
|
|
25
|
+
if (( current_node_major >= 25 )) && [[ -d "$HOME/.nvm/versions/node" ]]; then
|
|
26
|
+
inspector_node_dir="$(find "$HOME/.nvm/versions/node" -mindepth 1 -maxdepth 1 -type d -name 'v24.*' -printf '%p\n' 2>/dev/null | sort -V | tail -n 1)/bin"
|
|
27
|
+
if [[ "$inspector_node_dir" == "/bin" || ! -x "$inspector_node_dir/node" || ! -x "$inspector_node_dir/npx" ]]; then
|
|
28
|
+
inspector_node_dir=""
|
|
29
|
+
fi
|
|
30
|
+
fi
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
inspector_path="$PATH"
|
|
34
|
+
if [[ -n "$inspector_node_dir" ]]; then
|
|
35
|
+
if [[ ! -x "$inspector_node_dir/node" || ! -x "$inspector_node_dir/npx" ]]; then
|
|
36
|
+
echo "[forgerelay:debug-host] MCP_INSPECTOR_NODE_DIR must contain executable node and npx binaries." >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
inspector_path="$inspector_node_dir:$PATH"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
if ! PATH="$inspector_path" command -v npx >/dev/null 2>&1; then
|
|
43
|
+
echo "[forgerelay:debug-host] npx is required to launch MCP Inspector." >&2
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
port_in_use() {
|
|
48
|
+
ss -H -ltn "sport = :$1" 2>/dev/null | grep -q .
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if port_in_use "$debug_host_port"; then
|
|
52
|
+
cat >&2 <<EOF
|
|
53
|
+
[forgerelay:debug-host] port ${debug_host_port} is already in use.
|
|
54
|
+
[forgerelay:debug-host] refusing to replace or reuse that process because its publicBaseUrl/widgets may not match local Host mode.
|
|
55
|
+
[forgerelay:debug-host] stop it, or temporarily set FORGERELAY_DEBUG_HOST_PORT to another loopback port.
|
|
56
|
+
EOF
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
for inspector_port in "$inspector_client_port" "$inspector_sandbox_port"; do
|
|
61
|
+
if port_in_use "$inspector_port"; then
|
|
62
|
+
echo "[forgerelay:debug-host] Inspector port ${inspector_port} is already in use; refusing to replace that process." >&2
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
log_dir="$repo_root/logs"
|
|
68
|
+
server_log="$log_dir/debug-host-server.log"
|
|
69
|
+
inspector_log="$log_dir/debug-host-inspector.log"
|
|
70
|
+
mkdir -p "$log_dir"
|
|
71
|
+
: >"$server_log"
|
|
72
|
+
: >"$inspector_log"
|
|
73
|
+
|
|
74
|
+
server_pid=""
|
|
75
|
+
inspector_pid=""
|
|
76
|
+
cleanup() {
|
|
77
|
+
local status=$?
|
|
78
|
+
trap - EXIT INT TERM
|
|
79
|
+
if [[ -n "$inspector_pid" ]] && kill -0 "$inspector_pid" >/dev/null 2>&1; then
|
|
80
|
+
kill "$inspector_pid" >/dev/null 2>&1 || true
|
|
81
|
+
wait "$inspector_pid" >/dev/null 2>&1 || true
|
|
82
|
+
fi
|
|
83
|
+
if [[ -n "$server_pid" ]] && kill -0 "$server_pid" >/dev/null 2>&1; then
|
|
84
|
+
kill "$server_pid" >/dev/null 2>&1 || true
|
|
85
|
+
wait "$server_pid" >/dev/null 2>&1 || true
|
|
86
|
+
fi
|
|
87
|
+
exit "$status"
|
|
88
|
+
}
|
|
89
|
+
trap cleanup EXIT INT TERM
|
|
90
|
+
|
|
91
|
+
FORGERELAY_DEBUG_HOST_PORT="$debug_host_port" \
|
|
92
|
+
node scripts/debug/host/serve.mjs > >(tee -a "$server_log") 2>&1 &
|
|
93
|
+
server_pid=$!
|
|
94
|
+
|
|
95
|
+
ready=0
|
|
96
|
+
for _ in $(seq 1 120); do
|
|
97
|
+
if curl --silent --fail --max-time 1 "$health_url" >/dev/null 2>&1; then
|
|
98
|
+
ready=1
|
|
99
|
+
break
|
|
100
|
+
fi
|
|
101
|
+
if ! kill -0 "$server_pid" >/dev/null 2>&1; then
|
|
102
|
+
echo "[forgerelay:debug-host] ForgeRelay debug server exited before becoming ready." >&2
|
|
103
|
+
echo "[forgerelay:debug-host] see $server_log" >&2
|
|
104
|
+
exit 1
|
|
105
|
+
fi
|
|
106
|
+
sleep 0.25
|
|
107
|
+
done
|
|
108
|
+
|
|
109
|
+
if [[ "$ready" != "1" ]]; then
|
|
110
|
+
echo "[forgerelay:debug-host] ForgeRelay debug server did not become ready on ${debug_host_port}." >&2
|
|
111
|
+
echo "[forgerelay:debug-host] see $server_log" >&2
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
inspector_token="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("hex"))')"
|
|
116
|
+
|
|
117
|
+
echo "[forgerelay:debug-host] ForgeRelay ready: $mcp_url"
|
|
118
|
+
echo "[forgerelay:debug-host] launching MCP Inspector Web Host via $inspector_package"
|
|
119
|
+
if [[ -n "$inspector_node_dir" ]]; then
|
|
120
|
+
inspector_node_version="$(PATH="$inspector_path" node --version)"
|
|
121
|
+
echo "[forgerelay:debug-host] Inspector compatibility runtime: $inspector_node_version ($inspector_node_dir)"
|
|
122
|
+
fi
|
|
123
|
+
echo "[forgerelay:debug-host] complete OAuth with the persisted debug Owner password when prompted."
|
|
124
|
+
echo "[forgerelay:debug-host] logs: $server_log and $inspector_log (Inspector tokens are redacted from the log)"
|
|
125
|
+
|
|
126
|
+
HOST=127.0.0.1 \
|
|
127
|
+
CLIENT_PORT="$inspector_client_port" \
|
|
128
|
+
MCP_SANDBOX_PORT="$inspector_sandbox_port" \
|
|
129
|
+
MCP_AUTO_OPEN_ENABLED=false \
|
|
130
|
+
MCP_INSPECTOR_API_TOKEN="$inspector_token" \
|
|
131
|
+
PATH="$inspector_path" \
|
|
132
|
+
npx -y "$inspector_package" --web --server-url "$mcp_url" --transport http \
|
|
133
|
+
> >(tee >(sed -E \
|
|
134
|
+
-e 's/(MCP_INSPECTOR_API_TOKEN=)[[:alnum:]]+/\1<redacted>/g' \
|
|
135
|
+
-e 's/(Auth token: )[[:alnum:]]+/\1<redacted>/g' \
|
|
136
|
+
>"$inspector_log")) 2>&1 &
|
|
137
|
+
inspector_pid=$!
|
|
138
|
+
|
|
139
|
+
inspector_ready=0
|
|
140
|
+
for _ in $(seq 1 160); do
|
|
141
|
+
if curl --silent --fail --max-time 1 "$inspector_url/" >/dev/null 2>&1; then
|
|
142
|
+
inspector_ready=1
|
|
143
|
+
break
|
|
144
|
+
fi
|
|
145
|
+
if ! kill -0 "$inspector_pid" >/dev/null 2>&1; then
|
|
146
|
+
echo "[forgerelay:debug-host] MCP Inspector exited before becoming ready." >&2
|
|
147
|
+
echo "[forgerelay:debug-host] see $inspector_log" >&2
|
|
148
|
+
wait "$inspector_pid" || true
|
|
149
|
+
exit 1
|
|
150
|
+
fi
|
|
151
|
+
sleep 0.25
|
|
152
|
+
done
|
|
153
|
+
|
|
154
|
+
if [[ "$inspector_ready" != "1" ]]; then
|
|
155
|
+
echo "[forgerelay:debug-host] MCP Inspector did not become ready on ${inspector_client_port}." >&2
|
|
156
|
+
echo "[forgerelay:debug-host] see $inspector_log" >&2
|
|
157
|
+
exit 1
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
encoded_mcp_url="$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$mcp_url")"
|
|
161
|
+
deep_link="${inspector_url}/?serverUrl=${encoded_mcp_url}&transport=http&autoConnect=${inspector_token}"
|
|
162
|
+
|
|
163
|
+
echo "[forgerelay:debug-host] Inspector ready: $inspector_url"
|
|
164
|
+
echo "[forgerelay:debug-host] MCP Apps sandbox: http://127.0.0.1:${inspector_sandbox_port}/sandbox"
|
|
165
|
+
echo "[forgerelay:debug-host] open this one-shot local Host link:"
|
|
166
|
+
echo "$deep_link"
|
|
167
|
+
|
|
168
|
+
if [[ "${MCP_INSPECTOR_AUTO_OPEN:-1}" == "1" ]]; then
|
|
169
|
+
if command -v wslview >/dev/null 2>&1; then
|
|
170
|
+
wslview "$deep_link" >/dev/null 2>&1 || true
|
|
171
|
+
elif command -v xdg-open >/dev/null 2>&1; then
|
|
172
|
+
xdg-open "$deep_link" >/dev/null 2>&1 || true
|
|
173
|
+
fi
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
set +e
|
|
177
|
+
wait "$inspector_pid"
|
|
178
|
+
inspector_status=$?
|
|
179
|
+
set -e
|
|
180
|
+
exit "$inspector_status"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
createInteractiveDebugEnvironment,
|
|
4
|
+
repoRoot,
|
|
5
|
+
} from "../runtime.mjs";
|
|
6
|
+
|
|
7
|
+
const { configDir, env } = createInteractiveDebugEnvironment();
|
|
8
|
+
const hostPort = parseHostPort(process.env.FORGERELAY_DEBUG_HOST_PORT);
|
|
9
|
+
const localBaseUrl = `http://127.0.0.1:${hostPort}`;
|
|
10
|
+
const localMcpUrl = `${localBaseUrl}/mcp`;
|
|
11
|
+
|
|
12
|
+
// The normal interactive debug profile may mirror the product's public tunnel.
|
|
13
|
+
// A local MCP Apps Host must instead receive localhost resource and asset URLs,
|
|
14
|
+
// otherwise the Inspector ends up exercising the remote deployment again.
|
|
15
|
+
env.HOST = "127.0.0.1";
|
|
16
|
+
env.PORT = String(hostPort);
|
|
17
|
+
env.FORGERELAY_PUBLIC_BASE_URL = localBaseUrl;
|
|
18
|
+
env.FORGERELAY_WIDGETS = process.env.FORGERELAY_DEBUG_WIDGETS ?? "full";
|
|
19
|
+
|
|
20
|
+
console.error("[forgerelay:debug-host] local MCP Apps Host server");
|
|
21
|
+
console.error(`[forgerelay:debug-host] debug config: ${configDir}/config.json`);
|
|
22
|
+
console.error(`[forgerelay:debug-host] health: ${localBaseUrl}/healthz`);
|
|
23
|
+
console.error(`[forgerelay:debug-host] MCP: ${localMcpUrl}`);
|
|
24
|
+
console.error(`[forgerelay:debug-host] public base override: ${localBaseUrl}`);
|
|
25
|
+
console.error(`[forgerelay:debug-host] widgets: ${env.FORGERELAY_WIDGETS}`);
|
|
26
|
+
console.error(`[forgerelay:debug-host] Owner password loaded from ${configDir}/auth.json`);
|
|
27
|
+
console.error("[forgerelay:debug-host] all runtime state is under .forgerelay-debug/");
|
|
28
|
+
|
|
29
|
+
const child = spawn(process.execPath, ["scripts/dev-server.mjs"], {
|
|
30
|
+
cwd: repoRoot,
|
|
31
|
+
env,
|
|
32
|
+
stdio: "inherit",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let stopping = false;
|
|
36
|
+
function stop(signal = "SIGTERM") {
|
|
37
|
+
if (stopping) return;
|
|
38
|
+
stopping = true;
|
|
39
|
+
child.kill(signal);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
43
|
+
process.on(signal, () => stop(signal));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
child.on("exit", (code, signal) => {
|
|
47
|
+
if (signal) {
|
|
48
|
+
process.kill(process.pid, signal);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
process.exitCode = code ?? 1;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function parseHostPort(value) {
|
|
55
|
+
if (value === undefined || value === "") return 7677;
|
|
56
|
+
if (!/^\d+$/.test(value)) {
|
|
57
|
+
throw new Error("FORGERELAY_DEBUG_HOST_PORT must be an integer from 1 to 65535.");
|
|
58
|
+
}
|
|
59
|
+
const port = Number(value);
|
|
60
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {
|
|
61
|
+
throw new Error("FORGERELAY_DEBUG_HOST_PORT must be an integer from 1 to 65535.");
|
|
62
|
+
}
|
|
63
|
+
return port;
|
|
64
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
3
|
-
import {
|
|
2
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
|
|
6
7
|
export const repoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
@@ -15,6 +16,60 @@ export function createDebugOwnerToken() {
|
|
|
15
16
|
return randomBytes(32).toString("base64url");
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
export function interactiveDebugConfigDir({ env = process.env, home = homedir() } = {}) {
|
|
20
|
+
const explicit = env.FORGERELAY_DEBUG_CONFIG_DIR;
|
|
21
|
+
if (explicit) return resolve(explicit.startsWith("~/") ? join(home, explicit.slice(2)) : explicit);
|
|
22
|
+
return resolve(join(home, ".forgerelay", "debug"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function interactiveDebugOwnerToken(configDir) {
|
|
26
|
+
const authPath = join(configDir, "auth.json");
|
|
27
|
+
let auth;
|
|
28
|
+
try {
|
|
29
|
+
auth = JSON.parse(readFileSync(authPath, "utf8"));
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
32
|
+
throw new Error(`Unable to load interactive debug Owner password from ${authPath}: ${reason}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const token = typeof auth.ownerToken === "string" ? auth.ownerToken.trim() : "";
|
|
36
|
+
if (!token) {
|
|
37
|
+
throw new Error(`Interactive debug Owner password is missing from ${authPath}.`);
|
|
38
|
+
}
|
|
39
|
+
return token;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createInteractiveDebugEnvironment({
|
|
43
|
+
env = process.env,
|
|
44
|
+
home = homedir(),
|
|
45
|
+
} = {}) {
|
|
46
|
+
const configDir = interactiveDebugConfigDir({ env, home });
|
|
47
|
+
const ownerToken = interactiveDebugOwnerToken(configDir);
|
|
48
|
+
mkdirSync(debugRoot, { recursive: true });
|
|
49
|
+
|
|
50
|
+
const debugEnv = { ...env };
|
|
51
|
+
for (const key of Object.keys(debugEnv)) {
|
|
52
|
+
if (key === "HOST" || key === "PORT") {
|
|
53
|
+
delete debugEnv[key];
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (key.startsWith("DEVSPACE_")) {
|
|
57
|
+
delete debugEnv[key];
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (
|
|
61
|
+
key.startsWith("FORGERELAY_")
|
|
62
|
+
&& !key.startsWith("FORGERELAY_LOG_")
|
|
63
|
+
&& !key.startsWith("FORGERELAY_DEBUG_")
|
|
64
|
+
) {
|
|
65
|
+
delete debugEnv[key];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
debugEnv.FORGERELAY_CONFIG_DIR = configDir;
|
|
69
|
+
|
|
70
|
+
return { ownerToken, configDir, env: debugEnv };
|
|
71
|
+
}
|
|
72
|
+
|
|
18
73
|
export function createDebugEnvironment({
|
|
19
74
|
ownerToken,
|
|
20
75
|
stateDir = resolve(debugRoot, "state"),
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { createInteractiveDebugEnvironment, interactiveDebugConfigDir } from "./runtime.mjs";
|
|
8
|
+
|
|
9
|
+
async function withDebugConfig(t) {
|
|
10
|
+
const home = await mkdtemp(join(tmpdir(), "forgerelay-interactive-debug-"));
|
|
11
|
+
t.after(() => rm(home, { recursive: true, force: true }));
|
|
12
|
+
const configDir = join(home, ".forgerelay", "debug");
|
|
13
|
+
await mkdir(configDir, { recursive: true });
|
|
14
|
+
await writeFile(join(configDir, "config.json"), JSON.stringify({
|
|
15
|
+
host: "127.0.0.1",
|
|
16
|
+
port: 7677,
|
|
17
|
+
allowedRoots: [home],
|
|
18
|
+
allowedHosts: ["localhost", "127.0.0.1", "::1", "debug.example.test"],
|
|
19
|
+
publicBaseUrl: "https://debug.example.test/forgerelay/debug",
|
|
20
|
+
}) + "\n");
|
|
21
|
+
await writeFile(join(configDir, "auth.json"), JSON.stringify({
|
|
22
|
+
ownerToken: "debug-owner-password-123456",
|
|
23
|
+
}) + "\n");
|
|
24
|
+
return { home, configDir };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("interactive debug uses one dedicated persisted config under ~/.forgerelay/debug", async (t) => {
|
|
28
|
+
const { home, configDir } = await withDebugConfig(t);
|
|
29
|
+
const result = createInteractiveDebugEnvironment({
|
|
30
|
+
env: {
|
|
31
|
+
HOST: "0.0.0.0",
|
|
32
|
+
PORT: "9999",
|
|
33
|
+
FORGERELAY_CONFIG_DIR: "/wrong/product/config",
|
|
34
|
+
FORGERELAY_PUBLIC_BASE_URL: "https://wrong.example.test",
|
|
35
|
+
FORGERELAY_ALLOWED_HOSTS: "wrong.example.test",
|
|
36
|
+
FORGERELAY_OAUTH_OWNER_TOKEN: "wrong-owner-password-123456",
|
|
37
|
+
FORGERELAY_WIDGETS: "off",
|
|
38
|
+
DEVSPACE_ALLOWED_ROOTS: "/wrong/root",
|
|
39
|
+
FORGERELAY_LOG_LEVEL: "debug",
|
|
40
|
+
},
|
|
41
|
+
home,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
assert.equal(interactiveDebugConfigDir({ env: {}, home }), configDir);
|
|
45
|
+
assert.equal(result.ownerToken, "debug-owner-password-123456");
|
|
46
|
+
assert.equal(result.configDir, configDir);
|
|
47
|
+
assert.equal(result.env.FORGERELAY_CONFIG_DIR, configDir);
|
|
48
|
+
assert.equal(result.env.HOST, undefined);
|
|
49
|
+
assert.equal(result.env.PORT, undefined);
|
|
50
|
+
assert.equal(result.env.FORGERELAY_PUBLIC_BASE_URL, undefined);
|
|
51
|
+
assert.equal(result.env.FORGERELAY_ALLOWED_ROOTS, undefined);
|
|
52
|
+
assert.equal(result.env.FORGERELAY_ALLOWED_HOSTS, undefined);
|
|
53
|
+
assert.equal(result.env.FORGERELAY_OAUTH_OWNER_TOKEN, undefined);
|
|
54
|
+
assert.equal(result.env.FORGERELAY_WIDGETS, undefined);
|
|
55
|
+
assert.equal(result.env.DEVSPACE_ALLOWED_ROOTS, undefined);
|
|
56
|
+
assert.equal(result.env.FORGERELAY_LOG_LEVEL, "debug");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("interactive debug config directory may be explicitly relocated without falling back to product config", async (t) => {
|
|
60
|
+
const { home } = await withDebugConfig(t);
|
|
61
|
+
const customDir = join(home, "custom-debug-config");
|
|
62
|
+
await mkdir(customDir, { recursive: true });
|
|
63
|
+
await writeFile(join(customDir, "config.json"), "{}\n");
|
|
64
|
+
await writeFile(join(customDir, "auth.json"), JSON.stringify({ ownerToken: "custom-debug-password-123456" }) + "\n");
|
|
65
|
+
|
|
66
|
+
const result = createInteractiveDebugEnvironment({
|
|
67
|
+
env: { FORGERELAY_DEBUG_CONFIG_DIR: customDir },
|
|
68
|
+
home,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
assert.equal(result.configDir, customDir);
|
|
72
|
+
assert.equal(result.ownerToken, "custom-debug-password-123456");
|
|
73
|
+
assert.equal(result.env.FORGERELAY_CONFIG_DIR, customDir);
|
|
74
|
+
});
|
package/scripts/debug/serve.mjs
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { createInteractiveDebugEnvironment, debugBaseUrl, debugMcpUrl, repoRoot } from "./runtime.mjs";
|
|
3
3
|
|
|
4
|
-
const {
|
|
4
|
+
const { configDir, env } = createInteractiveDebugEnvironment();
|
|
5
5
|
|
|
6
6
|
console.error("[forgerelay:debug] local debug server");
|
|
7
|
-
console.error(`[forgerelay:debug] config: ${
|
|
7
|
+
console.error(`[forgerelay:debug] debug config: ${configDir}/config.json`);
|
|
8
8
|
console.error(`[forgerelay:debug] health: ${debugBaseUrl}/healthz`);
|
|
9
9
|
console.error(`[forgerelay:debug] MCP: ${debugMcpUrl}`);
|
|
10
|
-
console.error(`[forgerelay:debug]
|
|
11
|
-
console.error(`[forgerelay:debug] Owner password: ${ownerToken}`);
|
|
10
|
+
console.error(`[forgerelay:debug] Owner password loaded from ${configDir}/auth.json`);
|
|
12
11
|
console.error("[forgerelay:debug] all runtime state is under .forgerelay-debug/");
|
|
13
12
|
|
|
14
13
|
const child = spawn(process.execPath, ["scripts/dev-server.mjs"], {
|
package/scripts/dev-server.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { readdirSync, statSync, watch } from "node:fs";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
3
|
+
import { join, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
7
7
|
const watchRoots = ["src"].map((entry) => join(repoRoot, entry));
|
|
8
|
+
const uiRoot = join(repoRoot, "src", "ui");
|
|
8
9
|
const restartDelayMs = 750;
|
|
9
10
|
const crashDelayMs = 1500;
|
|
10
11
|
|
|
@@ -12,11 +13,32 @@ let child;
|
|
|
12
13
|
let restartTimer;
|
|
13
14
|
let stoppingForRestart = false;
|
|
14
15
|
let shuttingDown = false;
|
|
16
|
+
let uiBuildDirty = false;
|
|
15
17
|
|
|
16
18
|
function log(message) {
|
|
17
19
|
console.error(`[forgerelay:dev] ${message}`);
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
function buildUi() {
|
|
23
|
+
log("building MCP App UI");
|
|
24
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
25
|
+
const result = spawnSync(npm, ["run", "build:app"], {
|
|
26
|
+
cwd: repoRoot,
|
|
27
|
+
env: process.env,
|
|
28
|
+
stdio: "inherit",
|
|
29
|
+
});
|
|
30
|
+
if (result.status === 0) {
|
|
31
|
+
uiBuildDirty = false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
log(`MCP App UI build failed (${result.signal ?? result.status ?? "unknown"})`);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isUiSourcePath(path) {
|
|
39
|
+
return path === uiRoot || path.startsWith(`${uiRoot}${sep}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
20
42
|
function start() {
|
|
21
43
|
stoppingForRestart = false;
|
|
22
44
|
child = spawn(process.execPath, ["--import", "tsx", "src/cli.ts", "serve"], {
|
|
@@ -44,6 +66,11 @@ function restart() {
|
|
|
44
66
|
if (shuttingDown) return;
|
|
45
67
|
clearTimeout(restartTimer);
|
|
46
68
|
|
|
69
|
+
if (uiBuildDirty && !buildUi()) {
|
|
70
|
+
scheduleRestart(crashDelayMs);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
if (!child) {
|
|
48
75
|
start();
|
|
49
76
|
return;
|
|
@@ -76,6 +103,7 @@ function watchDirectory(root) {
|
|
|
76
103
|
|
|
77
104
|
const path = join(dir, filename.toString());
|
|
78
105
|
if (event === "rename") maybeAddDirectory(path);
|
|
106
|
+
if (isUiSourcePath(path)) uiBuildDirty = true;
|
|
79
107
|
scheduleRestart();
|
|
80
108
|
});
|
|
81
109
|
watchers.push(watcher);
|
|
@@ -116,5 +144,6 @@ for (const root of watchRoots) {
|
|
|
116
144
|
watchDirectory(root);
|
|
117
145
|
}
|
|
118
146
|
|
|
119
|
-
log("watching src;
|
|
147
|
+
log("watching src; UI changes rebuild MCP App assets before server restart");
|
|
148
|
+
if (!buildUi()) process.exit(1);
|
|
120
149
|
start();
|