@devframes/hub 0.9.7 → 0.9.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/dist/client/index.d.mts +72 -51
- package/dist/client/index.mjs +119 -44
- package/dist/{constants-DrF61GFx.mjs → constants-BUd6oMci.mjs} +9 -8
- package/dist/{constants-C5t0DeZ0.d.mts → constants-Coo1p3ja.d.mts} +12 -9
- package/dist/constants.d.mts +1 -1
- package/dist/constants.mjs +1 -1
- package/dist/{context-C3hUUCC1.mjs → context-D4L966cN.mjs} +233 -135
- package/dist/{context-D5JSW_X5.d.mts → context-Di-fhMKP.d.mts} +40 -18
- package/dist/{index-DhxIxS-Q.d.mts → index-faD2d5iX.d.mts} +2 -2
- package/dist/index.d.mts +4 -4
- package/dist/node/index.d.mts +28 -19
- package/dist/node/index.mjs +2 -2
- package/dist/node/initiate.d.mts +35 -30
- package/dist/node/initiate.mjs +80 -53
- package/dist/{remote-url-Bgc7gtsP.mjs → remote-url-UClKLQ5_.mjs} +34 -39
- package/dist/{settings-D7whfcx2.d.mts → settings-CWHPzCWS.d.mts} +65 -39
- package/dist/types/index.d.mts +4 -4
- package/package.json +6 -6
|
@@ -1,35 +1,51 @@
|
|
|
1
1
|
import { r as defineHubRpcFunction } from "./define-Ceekw2EO.mjs";
|
|
2
|
-
import { n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS } from "./constants-
|
|
3
|
-
import { i as isBareModuleSpecifier, t as buildRemoteConnectionUrl } from "./remote-url-
|
|
2
|
+
import { n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS } from "./constants-BUd6oMci.mjs";
|
|
3
|
+
import { i as isBareModuleSpecifier, t as buildRemoteConnectionUrl } from "./remote-url-UClKLQ5_.mjs";
|
|
4
|
+
import { joinURL, withTrailingSlash } from "ufo";
|
|
4
5
|
import { createEventEmitter } from "devframe/utils/events";
|
|
5
6
|
import { createHostContext, createStorage } from "devframe/node";
|
|
6
7
|
import { getInternalContext, resolveBasePath } from "devframe/node/hub-internals";
|
|
7
8
|
import { debounce } from "perfect-debounce";
|
|
8
|
-
import { coerceAgentPositionalArgs } from "devframe/internal";
|
|
9
|
+
import { coerceAgentPositionalArgs, resolveClientAssets } from "devframe/internal";
|
|
9
10
|
import { defineDiagnostics } from "devframe/utils/nostics";
|
|
10
|
-
import { join, resolve } from "pathe";
|
|
11
|
+
import { basename, dirname, isAbsolute, join, resolve } from "pathe";
|
|
11
12
|
import { nanoid } from "devframe/utils/nanoid";
|
|
12
13
|
import process from "node:process";
|
|
13
|
-
import {
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
14
15
|
//#region src/node/diagnostics.ts
|
|
16
|
+
/**
|
|
17
|
+
* Hub-side diagnostics for docks, terminals, messages, and commands.
|
|
18
|
+
* Shares the `DF` prefix with devframe core; the hub reserves the
|
|
19
|
+
* `DF8xxx` range so the unified surface stays collision-free.
|
|
20
|
+
* Sub-ranges:
|
|
21
|
+
* DF8000-DF8099: hub context / lifecycle
|
|
22
|
+
* DF8100-DF8199: docks
|
|
23
|
+
* DF8200-DF8299: terminals
|
|
24
|
+
* DF8300-DF8399: messages
|
|
25
|
+
* DF8400-DF8499: commands
|
|
26
|
+
*/
|
|
15
27
|
const diagnostics = defineDiagnostics({
|
|
16
28
|
docsBase: "https://devfra.me/errors",
|
|
17
29
|
codes: {
|
|
18
30
|
DF8000: {
|
|
19
|
-
why: (p) => `Devframe id "${p.id}" collides with a reserved hub path
|
|
31
|
+
why: (p) => `Devframe id "${p.id}" collides with a reserved hub path, so it cannot be mounted directly under the hub base.`,
|
|
20
32
|
fix: "The filenames directly under the hub base (`__connection.json`, `__ws`, `__index.json`, `__client-imports.js`, `__mcp`, `embedded.js`) are reserved for the hub protocol. Rename the devframe id, or override its mount with a non-colliding `basePath`."
|
|
21
33
|
},
|
|
22
34
|
DF8002: {
|
|
23
|
-
why: "initHub received both `devframes` and `context
|
|
24
|
-
fix: "Pass `devframes` to let the instance create the hub context and mount each frame itself, or pass a pre-built `context` (your host already mounted the frames)
|
|
35
|
+
why: "initHub received both `devframes` and `context`; the two assembly modes are mutually exclusive.",
|
|
36
|
+
fix: "Pass `devframes` to let the instance create the hub context and mount each frame itself, or pass a pre-built `context` (your host already mounted the frames), but never both."
|
|
25
37
|
},
|
|
26
38
|
DF8003: {
|
|
27
39
|
why: "connectionMeta() was called before initHub finished initializing.",
|
|
28
|
-
fix: "Await `instance.ready` (or any request through `instance.handler`) before reading `connectionMeta()
|
|
40
|
+
fix: "Await `instance.ready` (or any request through `instance.handler`) before reading `connectionMeta()`, since the WebSocket binding it describes is only known once initialization completes."
|
|
29
41
|
},
|
|
30
42
|
DF8004: {
|
|
31
|
-
why: (p) => `Devframe id "${p.id}" is not a mountable URL segment
|
|
32
|
-
fix: "Ids become route segments, so they may only contain letters, digits, `_`, `-`, and
|
|
43
|
+
why: (p) => `Devframe id "${p.id}" is not a mountable URL segment, and the hub mounts each frame at \`<base><id>/\`.`,
|
|
44
|
+
fix: "Ids become route segments, so they may only contain letters, digits, `_`, `-`, and `.`; `:` and `*` are route-pattern markers to the underlying router, and `/` would escape the segment. Set a route-safe `id` on the definition (e.g. `my_plugin` instead of `my:plugin`)."
|
|
45
|
+
},
|
|
46
|
+
DF8005: {
|
|
47
|
+
why: (p) => `Devframe "${p.id}" requests an MCP route, but the hub's aggregate MCP is off, so its tools are not exposed over MCP.`,
|
|
48
|
+
fix: "A hub exposes one aggregate MCP endpoint over every mounted devframe, so per-devframe `mcp` settings are ignored. Enable the hub's own MCP (pass `mcp` to `initHub`) to surface this devframe's tools, or drop `mcp` from the devframe to silence this warning."
|
|
33
49
|
},
|
|
34
50
|
DF8100: {
|
|
35
51
|
why: (p) => `Dock with id "${p.id}" is already registered`,
|
|
@@ -56,12 +72,12 @@ const diagnostics = defineDiagnostics({
|
|
|
56
72
|
fix: "Each devframe is deduplicated by id. Set `duplicationStrategy: \"duplicate\"` on the definition to let instances coexist, `\"silent\"` to drop duplicates quietly, or `\"throw\"` to surface them as errors."
|
|
57
73
|
},
|
|
58
74
|
DF8106: {
|
|
59
|
-
why: (p) => `The host cannot serve the RPC connection meta for devframe "${p.name}" (id "${p.id}") at "${p.base}"
|
|
75
|
+
why: (p) => `The host cannot serve the RPC connection meta for devframe "${p.name}" (id "${p.id}") at "${p.base}"; its \`DevframeHost\` does not implement \`mountConnectionMeta\`.`,
|
|
60
76
|
fix: "Implement `mountConnectionMeta(base)` on your DevframeHost so it serves `__connection.json` at each mounted base. Without it, the devframe SPA connects only when it shares an origin with the hub UI (same-origin window inheritance); cross-origin, sandboxed, or directly-opened iframes stay disconnected. Static-snapshot hosts that bake the meta into the served files can implement it as a no-op to acknowledge this intentionally."
|
|
61
77
|
},
|
|
62
78
|
DF8107: {
|
|
63
79
|
why: (p) => `Dock activation requested for unknown dock id "${p.id}"`,
|
|
64
|
-
fix: "Pass a `dockId` that matches a registered dock entry. The activation is still broadcast, but no hub UI provider will switch to it. Ids are case-sensitive
|
|
80
|
+
fix: "Pass a `dockId` that matches a registered dock entry. The activation is still broadcast, but no hub UI provider will switch to it. Ids are case-sensitive, so check for typos, and ensure the target dock is registered before activating it."
|
|
65
81
|
},
|
|
66
82
|
DF8108: {
|
|
67
83
|
why: (p) => `A renderer module is already registered for dock type "${p.type}"`,
|
|
@@ -72,11 +88,11 @@ const diagnostics = defineDiagnostics({
|
|
|
72
88
|
fix: "Point the registration's `file` at the prebuilt browser ES module (build the renderer package first, or check the path). Registration helpers like `jsonRenderUiRenderer()` resolve the path for you."
|
|
73
89
|
},
|
|
74
90
|
DF8110: {
|
|
75
|
-
why: (p) => `Dock type "${p.type}" is not a servable renderer-module name
|
|
91
|
+
why: (p) => `Dock type "${p.type}" is not a servable renderer-module name; the hub serves each module at \`<base>__renderers/<type>.mjs\``,
|
|
76
92
|
fix: "Renderer types become URL segments, so they may only contain letters, digits, `_`, `-`, and `.`. Use a route-safe dock type (e.g. `json-render`)."
|
|
77
93
|
},
|
|
78
94
|
DF8111: {
|
|
79
|
-
why: (p) => `Dock "${p.id}" declares the bare-specifier client script "${p.specifier}", but this host advertises no client-module resolution
|
|
95
|
+
why: (p) => `Dock "${p.id}" declares the bare-specifier client script "${p.specifier}", but this host advertises no client-module resolution, and the browser cannot resolve a bare npm specifier natively, so the script will fail to load.`,
|
|
80
96
|
fix: "Run under a host framework that declares `initHub({ clientModuleResolution })` (e.g. Vite's `'/@id/{specifier}'`), ship the script as a self-contained bundle served by URL, or resolve it in the hub UI provider via `createDevframeClientRuntime({ resolveClientModule })` (then disregard this warning)."
|
|
81
97
|
},
|
|
82
98
|
DF8200: { why: (p) => `Terminal session with id "${p.id}" already registered` },
|
|
@@ -88,14 +104,14 @@ const diagnostics = defineDiagnostics({
|
|
|
88
104
|
DF8203: { why: (p) => `Failed to spawn PTY session for "${p.command}": ${p.reason}` },
|
|
89
105
|
DF8204: {
|
|
90
106
|
why: (p) => `Terminal session "${p.id}" cannot be controlled (no lifecycle handle)`,
|
|
91
|
-
fix: "Spawn it via ctx.terminals.startChildProcess() or startPtySession()
|
|
107
|
+
fix: "Spawn it via ctx.terminals.startChildProcess() or startPtySession(); sessions added with a bare register() expose no terminate/restart handle."
|
|
92
108
|
},
|
|
93
109
|
DF8205: {
|
|
94
110
|
why: (p) => `Terminal session "${p.id}" is not restartable`,
|
|
95
111
|
fix: "It was registered with `restartable: false`; restart it through its owner's controls, or spawn it with `restartable: true` (the default) to allow in-place restarts."
|
|
96
112
|
},
|
|
97
113
|
DF8206: {
|
|
98
|
-
why: (p) => `Terminal session "${p.id}" cannot be restarted
|
|
114
|
+
why: (p) => `Terminal session "${p.id}" cannot be restarted because its output stream is already closed`,
|
|
99
115
|
fix: "The session already exited (or was terminated) and its stream is spent. Drop it with `ctx.terminals.remove(session)`, then spawn a replacement via `ctx.terminals.startChildProcess()` or `ctx.terminals.startPtySession()` with a fresh id."
|
|
100
116
|
},
|
|
101
117
|
DF8400: { why: (p) => `Command "${p.id}" is already registered` },
|
|
@@ -144,7 +160,7 @@ var DevframeCommandsHost = class {
|
|
|
144
160
|
events = createEventEmitter();
|
|
145
161
|
/**
|
|
146
162
|
* Lazy agent projection: `ctx.agent` queries this provider at list/invoke
|
|
147
|
-
* time, deriving tools from {@link commands} on demand
|
|
163
|
+
* time, deriving tools from {@link commands} on demand, so the commands map
|
|
148
164
|
* stays the single source of truth, nothing is mirrored or kept in sync.
|
|
149
165
|
*/
|
|
150
166
|
agentProvider;
|
|
@@ -222,7 +238,7 @@ var DevframeCommandsHost = class {
|
|
|
222
238
|
* agent-flagged, handler-bearing command (children included) becomes a
|
|
223
239
|
* callable tool. Queried lazily by the provider registered in the
|
|
224
240
|
* constructor. `when` clauses evaluate client-side only and are not
|
|
225
|
-
* enforced here
|
|
241
|
+
* enforced here; opting in a `when`-gated command is a deliberate author
|
|
226
242
|
* decision (documented on `DevframeCommandAgentOptions`).
|
|
227
243
|
*/
|
|
228
244
|
collectAgentTools() {
|
|
@@ -235,7 +251,12 @@ var DevframeCommandsHost = class {
|
|
|
235
251
|
description: agent.description,
|
|
236
252
|
safety: agent.safety ?? "action",
|
|
237
253
|
tags: agent.tags,
|
|
254
|
+
/** The agent host derives the tool's JSON-Schema input from these. */
|
|
238
255
|
args: agent.args,
|
|
256
|
+
/**
|
|
257
|
+
* A command handler's positional parameters come solely from its
|
|
258
|
+
* declared `agent.args` schemas; undeclared payload is dropped.
|
|
259
|
+
*/
|
|
239
260
|
handler: async (args) => this.execute(command.id, ...coerceAgentPositionalArgs(args, agent.args, "drop"))
|
|
240
261
|
});
|
|
241
262
|
for (const child of command.children ?? []) walk(child);
|
|
@@ -265,6 +286,7 @@ var DevframeDocksHost = class {
|
|
|
265
286
|
}
|
|
266
287
|
async init() {
|
|
267
288
|
this.userSettings = await this.context.rpc.sharedState.get(HUB_EVENTS.sharedState.userSettings, { sharedState: createStorage({
|
|
289
|
+
/** Personal dock layout/preferences: per-checkout private state. */
|
|
268
290
|
filepath: join(this.context.host.getStorageDir("project"), "settings.json"),
|
|
269
291
|
initialValue: DEFAULT_STATE_USER_SETTINGS()
|
|
270
292
|
}) });
|
|
@@ -325,7 +347,7 @@ var DevframeDocksHost = class {
|
|
|
325
347
|
});
|
|
326
348
|
}
|
|
327
349
|
/**
|
|
328
|
-
* Warn (don't throw
|
|
350
|
+
* Warn (don't throw, since a client-runtime `resolveClientModule` override may still cover
|
|
329
351
|
* it) when a dock declares a **bare-specifier** client script on a host
|
|
330
352
|
* that advertises no `staticConfig.dock.clientModuleResolution`: the
|
|
331
353
|
* browser cannot resolve a bare npm specifier natively, so the script is
|
|
@@ -379,7 +401,7 @@ var DevframeMessagesHost = class {
|
|
|
379
401
|
_clock = 0;
|
|
380
402
|
/**
|
|
381
403
|
* The tick of the newest removal record dropped from the capped
|
|
382
|
-
* `removals` log
|
|
404
|
+
* `removals` log; cursors older than this can't get a reliable delta
|
|
383
405
|
* and fall back to a full snapshot in {@link listSince}.
|
|
384
406
|
*/
|
|
385
407
|
_removalsTrimmedAt = 0;
|
|
@@ -560,7 +582,7 @@ var DevframeTerminalsHost = class {
|
|
|
560
582
|
this.context = context;
|
|
561
583
|
}
|
|
562
584
|
/**
|
|
563
|
-
* Lazily acquire the streaming channel
|
|
585
|
+
* Lazily acquire the streaming channel, since `context.rpc` isn't assigned
|
|
564
586
|
* until after every host is constructed, so we can't grab it in the
|
|
565
587
|
* constructor.
|
|
566
588
|
*/
|
|
@@ -570,6 +592,41 @@ var DevframeTerminalsHost = class {
|
|
|
570
592
|
this._channel = this.context.rpc.streaming.create(TERMINAL_STREAM_CHANNEL, { replayWindow: TERMINAL_REPLAY_WINDOW });
|
|
571
593
|
return this._channel;
|
|
572
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* Per-run stream lifecycle shared by the child-process and PTY paths: guards
|
|
597
|
+
* the single-close/error transition and keeps the session's `status` in step
|
|
598
|
+
* with the process lifecycle (a hub-aware client sees `running` →
|
|
599
|
+
* `stopped`/`error` instead of a value frozen at spawn time).
|
|
600
|
+
*/
|
|
601
|
+
createStreamLifecycle() {
|
|
602
|
+
const state = { streamClosed: false };
|
|
603
|
+
const markStatus = (next) => {
|
|
604
|
+
const session = state.session;
|
|
605
|
+
if (session.status === next) return;
|
|
606
|
+
session.status = next;
|
|
607
|
+
this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session);
|
|
608
|
+
};
|
|
609
|
+
const closeStream = () => {
|
|
610
|
+
if (state.streamClosed) return;
|
|
611
|
+
state.streamClosed = true;
|
|
612
|
+
try {
|
|
613
|
+
state.controller?.close();
|
|
614
|
+
} catch {}
|
|
615
|
+
};
|
|
616
|
+
const errorStream = (error) => {
|
|
617
|
+
if (state.streamClosed) return;
|
|
618
|
+
state.streamClosed = true;
|
|
619
|
+
try {
|
|
620
|
+
state.controller?.error(error);
|
|
621
|
+
} catch {}
|
|
622
|
+
};
|
|
623
|
+
return {
|
|
624
|
+
state,
|
|
625
|
+
markStatus,
|
|
626
|
+
closeStream,
|
|
627
|
+
errorStream
|
|
628
|
+
};
|
|
629
|
+
}
|
|
573
630
|
register(session) {
|
|
574
631
|
if (this.sessions.has(session.id)) throw diagnostics.DF8200({ id: session.id });
|
|
575
632
|
this.sessions.set(session.id, session);
|
|
@@ -591,6 +648,25 @@ var DevframeTerminalsHost = class {
|
|
|
591
648
|
this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session);
|
|
592
649
|
this._boundStreams.delete(session.id);
|
|
593
650
|
}
|
|
651
|
+
/** Drain a session's output stream into its scrollback buffer and streaming sink. */
|
|
652
|
+
async pumpStream(reader, sessionBuffer, sink, isDisposed) {
|
|
653
|
+
try {
|
|
654
|
+
while (!isDisposed()) {
|
|
655
|
+
const result = await reader.read();
|
|
656
|
+
if (isDisposed() || result.done) break;
|
|
657
|
+
sessionBuffer.push(result.value);
|
|
658
|
+
if (sessionBuffer.length > TERMINAL_BUFFER_LIMIT) sessionBuffer.splice(0, sessionBuffer.length - TERMINAL_BUFFER_LIMIT);
|
|
659
|
+
sink?.write(result.value);
|
|
660
|
+
}
|
|
661
|
+
if (!isDisposed() && sink && !sink.closed) sink.close();
|
|
662
|
+
} catch (error) {
|
|
663
|
+
if (!isDisposed() && sink && !sink.closed) sink.error(error);
|
|
664
|
+
} finally {
|
|
665
|
+
try {
|
|
666
|
+
reader.releaseLock();
|
|
667
|
+
} catch {}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
594
670
|
bindStream(session) {
|
|
595
671
|
if (this._boundStreams.has(session.id) && this._boundStreams.get(session.id)?.stream === session.stream) return;
|
|
596
672
|
this._boundStreams.get(session.id)?.dispose();
|
|
@@ -601,26 +677,7 @@ var DevframeTerminalsHost = class {
|
|
|
601
677
|
const sink = this.getStreamingChannel()?.start({ id: session.id });
|
|
602
678
|
const reader = session.stream.getReader();
|
|
603
679
|
let disposed = false;
|
|
604
|
-
(
|
|
605
|
-
try {
|
|
606
|
-
while (true) {
|
|
607
|
-
if (disposed) break;
|
|
608
|
-
const result = await reader.read();
|
|
609
|
-
if (disposed) break;
|
|
610
|
-
if (result.done) break;
|
|
611
|
-
sessionBuffer.push(result.value);
|
|
612
|
-
if (sessionBuffer.length > TERMINAL_BUFFER_LIMIT) sessionBuffer.splice(0, sessionBuffer.length - TERMINAL_BUFFER_LIMIT);
|
|
613
|
-
sink?.write(result.value);
|
|
614
|
-
}
|
|
615
|
-
if (!disposed && sink && !sink.closed) sink.close();
|
|
616
|
-
} catch (error) {
|
|
617
|
-
if (!disposed && sink && !sink.closed) sink.error(error);
|
|
618
|
-
} finally {
|
|
619
|
-
try {
|
|
620
|
-
reader.releaseLock();
|
|
621
|
-
} catch {}
|
|
622
|
-
}
|
|
623
|
-
})();
|
|
680
|
+
this.pumpStream(reader, sessionBuffer, sink, () => disposed);
|
|
624
681
|
this._boundStreams.set(session.id, {
|
|
625
682
|
dispose: () => {
|
|
626
683
|
disposed = true;
|
|
@@ -633,34 +690,13 @@ var DevframeTerminalsHost = class {
|
|
|
633
690
|
async startChildProcess(executeOptions, terminal) {
|
|
634
691
|
if (this.sessions.has(terminal.id)) throw diagnostics.DF8200({ id: terminal.id });
|
|
635
692
|
const { exec } = await import("tinyexec");
|
|
636
|
-
|
|
693
|
+
const { state, markStatus, closeStream, errorStream } = this.createStreamLifecycle();
|
|
637
694
|
let cp;
|
|
638
695
|
let currentResult;
|
|
639
696
|
let runId = 0;
|
|
640
|
-
let streamClosed = false;
|
|
641
|
-
let session;
|
|
642
|
-
const markStatus = (next) => {
|
|
643
|
-
if (session.status === next) return;
|
|
644
|
-
session.status = next;
|
|
645
|
-
this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session);
|
|
646
|
-
};
|
|
647
|
-
const closeStream = () => {
|
|
648
|
-
if (streamClosed) return;
|
|
649
|
-
streamClosed = true;
|
|
650
|
-
try {
|
|
651
|
-
controller?.close();
|
|
652
|
-
} catch {}
|
|
653
|
-
};
|
|
654
|
-
const errorStream = (error) => {
|
|
655
|
-
if (streamClosed) return;
|
|
656
|
-
streamClosed = true;
|
|
657
|
-
try {
|
|
658
|
-
controller?.error(error);
|
|
659
|
-
} catch {}
|
|
660
|
-
};
|
|
661
697
|
const stream = new ReadableStream({
|
|
662
698
|
start(_controller) {
|
|
663
|
-
controller = _controller;
|
|
699
|
+
state.controller = _controller;
|
|
664
700
|
},
|
|
665
701
|
cancel() {
|
|
666
702
|
cp?.kill();
|
|
@@ -700,13 +736,13 @@ var DevframeTerminalsHost = class {
|
|
|
700
736
|
if (currentRun !== runId) return;
|
|
701
737
|
const text = chunk.toString();
|
|
702
738
|
stdoutChunks.push(text);
|
|
703
|
-
if (!streamClosed) controller?.enqueue(text);
|
|
739
|
+
if (!state.streamClosed) state.controller?.enqueue(text);
|
|
704
740
|
});
|
|
705
741
|
cp.process?.stderr?.on("data", (chunk) => {
|
|
706
742
|
if (currentRun !== runId) return;
|
|
707
743
|
const text = chunk.toString();
|
|
708
744
|
stderrChunks.push(text);
|
|
709
|
-
if (!streamClosed) controller?.enqueue(text);
|
|
745
|
+
if (!state.streamClosed) state.controller?.enqueue(text);
|
|
710
746
|
});
|
|
711
747
|
cp.process?.once("error", (error) => {
|
|
712
748
|
if (currentRun !== runId) return;
|
|
@@ -738,7 +774,7 @@ var DevframeTerminalsHost = class {
|
|
|
738
774
|
}
|
|
739
775
|
cp = createChildProcess();
|
|
740
776
|
const restart = async () => {
|
|
741
|
-
if (streamClosed) throw diagnostics.DF8206({ id: terminal.id });
|
|
777
|
+
if (state.streamClosed) throw diagnostics.DF8206({ id: terminal.id });
|
|
742
778
|
cp?.kill();
|
|
743
779
|
cp = createChildProcess();
|
|
744
780
|
markStatus("running");
|
|
@@ -749,7 +785,7 @@ var DevframeTerminalsHost = class {
|
|
|
749
785
|
closeStream();
|
|
750
786
|
markStatus("stopped");
|
|
751
787
|
};
|
|
752
|
-
session = {
|
|
788
|
+
const session = {
|
|
753
789
|
...terminal,
|
|
754
790
|
status: "running",
|
|
755
791
|
stream,
|
|
@@ -760,6 +796,7 @@ var DevframeTerminalsHost = class {
|
|
|
760
796
|
terminate,
|
|
761
797
|
restart
|
|
762
798
|
};
|
|
799
|
+
state.session = session;
|
|
763
800
|
this.register(session);
|
|
764
801
|
return Promise.resolve(session);
|
|
765
802
|
}
|
|
@@ -768,43 +805,24 @@ var DevframeTerminalsHost = class {
|
|
|
768
805
|
const { spawn } = await import("zigpty");
|
|
769
806
|
const cols = executeOptions.cols ?? 80;
|
|
770
807
|
const rows = executeOptions.rows ?? 24;
|
|
771
|
-
|
|
808
|
+
const { state, markStatus, closeStream, errorStream } = this.createStreamLifecycle();
|
|
772
809
|
let pty;
|
|
810
|
+
let currentResult;
|
|
811
|
+
let killCurrentRun;
|
|
773
812
|
let runId = 0;
|
|
774
|
-
let streamClosed = false;
|
|
775
|
-
let session;
|
|
776
|
-
const markStatus = (next) => {
|
|
777
|
-
if (session.status === next) return;
|
|
778
|
-
session.status = next;
|
|
779
|
-
this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session);
|
|
780
|
-
};
|
|
781
|
-
const closeStream = () => {
|
|
782
|
-
if (streamClosed) return;
|
|
783
|
-
streamClosed = true;
|
|
784
|
-
try {
|
|
785
|
-
controller?.close();
|
|
786
|
-
} catch {}
|
|
787
|
-
};
|
|
788
|
-
const errorStream = (error) => {
|
|
789
|
-
if (streamClosed) return;
|
|
790
|
-
streamClosed = true;
|
|
791
|
-
try {
|
|
792
|
-
controller?.error(error);
|
|
793
|
-
} catch {}
|
|
794
|
-
};
|
|
795
813
|
const stream = new ReadableStream({
|
|
796
814
|
start(_controller) {
|
|
797
|
-
controller = _controller;
|
|
815
|
+
state.controller = _controller;
|
|
798
816
|
},
|
|
799
817
|
cancel() {
|
|
800
|
-
|
|
818
|
+
killCurrentRun?.();
|
|
801
819
|
pty = void 0;
|
|
802
820
|
closeStream();
|
|
803
821
|
}
|
|
804
822
|
});
|
|
805
823
|
const spawnPty = () => {
|
|
806
824
|
const currentRun = ++runId;
|
|
807
|
-
const
|
|
825
|
+
const ptyProcess = spawn(executeOptions.command, executeOptions.args ?? [], {
|
|
808
826
|
name: PTY_TERM_NAME,
|
|
809
827
|
cols,
|
|
810
828
|
rows,
|
|
@@ -817,16 +835,58 @@ var DevframeTerminalsHost = class {
|
|
|
817
835
|
...executeOptions.env ?? {}
|
|
818
836
|
}
|
|
819
837
|
});
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
838
|
+
const outputChunks = [];
|
|
839
|
+
let killed = false;
|
|
840
|
+
let settled = false;
|
|
841
|
+
let settledExitCode;
|
|
842
|
+
let resolveOutput;
|
|
843
|
+
const outputPromise = new Promise((resolve) => {
|
|
844
|
+
resolveOutput = resolve;
|
|
823
845
|
});
|
|
824
|
-
|
|
846
|
+
const settle = (exitCode, signal) => {
|
|
847
|
+
if (settled) return;
|
|
848
|
+
settled = true;
|
|
849
|
+
killed ||= signal !== 0;
|
|
850
|
+
settledExitCode = killed ? void 0 : exitCode;
|
|
851
|
+
resolveOutput({
|
|
852
|
+
output: outputChunks.join(""),
|
|
853
|
+
exitCode: settledExitCode,
|
|
854
|
+
signal: signal === 0 ? void 0 : signal
|
|
855
|
+
});
|
|
856
|
+
};
|
|
857
|
+
ptyProcess.onData((data) => {
|
|
858
|
+
const text = typeof data === "string" ? data : data.toString("utf8");
|
|
859
|
+
outputChunks.push(text);
|
|
860
|
+
if (!state.streamClosed && currentRun === runId) state.controller?.enqueue(text);
|
|
861
|
+
});
|
|
862
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
863
|
+
settle(exitCode, signal);
|
|
825
864
|
if (currentRun !== runId) return;
|
|
826
865
|
closeStream();
|
|
827
|
-
|
|
866
|
+
/**
|
|
867
|
+
* Killed runs and clean exits are stopped. Only a non-killed non-zero exit
|
|
868
|
+
* code is a crash, matching the child-process path.
|
|
869
|
+
*/
|
|
870
|
+
markStatus(!killed && exitCode !== 0 ? "error" : "stopped");
|
|
828
871
|
});
|
|
829
|
-
|
|
872
|
+
currentResult = {
|
|
873
|
+
get pid() {
|
|
874
|
+
return ptyProcess.pid;
|
|
875
|
+
},
|
|
876
|
+
get exitCode() {
|
|
877
|
+
return killed ? void 0 : ptyProcess.exitCode ?? settledExitCode;
|
|
878
|
+
},
|
|
879
|
+
get killed() {
|
|
880
|
+
return killed;
|
|
881
|
+
},
|
|
882
|
+
then: (onfulfilled, onrejected) => outputPromise.then(onfulfilled, onrejected)
|
|
883
|
+
};
|
|
884
|
+
killCurrentRun = () => {
|
|
885
|
+
if (ptyProcess.exitCode !== null) return;
|
|
886
|
+
killed = true;
|
|
887
|
+
ptyProcess.kill();
|
|
888
|
+
};
|
|
889
|
+
return ptyProcess;
|
|
830
890
|
};
|
|
831
891
|
try {
|
|
832
892
|
pty = spawnPty();
|
|
@@ -837,7 +897,7 @@ var DevframeTerminalsHost = class {
|
|
|
837
897
|
reason: error instanceof Error ? error.message : String(error)
|
|
838
898
|
});
|
|
839
899
|
}
|
|
840
|
-
session = {
|
|
900
|
+
const session = {
|
|
841
901
|
...terminal,
|
|
842
902
|
status: "running",
|
|
843
903
|
interactive: true,
|
|
@@ -862,19 +922,31 @@ var DevframeTerminalsHost = class {
|
|
|
862
922
|
return;
|
|
863
923
|
}
|
|
864
924
|
},
|
|
925
|
+
getResult: () => currentResult,
|
|
865
926
|
terminate: async () => {
|
|
866
|
-
|
|
927
|
+
killCurrentRun?.();
|
|
867
928
|
pty = void 0;
|
|
868
929
|
closeStream();
|
|
869
930
|
markStatus("stopped");
|
|
870
931
|
},
|
|
871
932
|
restart: async () => {
|
|
872
|
-
if (streamClosed) throw diagnostics.DF8206({ id: terminal.id });
|
|
873
|
-
|
|
874
|
-
|
|
933
|
+
if (state.streamClosed) throw diagnostics.DF8206({ id: terminal.id });
|
|
934
|
+
killCurrentRun?.();
|
|
935
|
+
killCurrentRun = void 0;
|
|
936
|
+
pty = void 0;
|
|
937
|
+
try {
|
|
938
|
+
pty = spawnPty();
|
|
939
|
+
} catch (error) {
|
|
940
|
+
markStatus("error");
|
|
941
|
+
throw diagnostics.DF8203({
|
|
942
|
+
command: executeOptions.command,
|
|
943
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
944
|
+
});
|
|
945
|
+
}
|
|
875
946
|
markStatus("running");
|
|
876
947
|
}
|
|
877
948
|
};
|
|
949
|
+
state.session = session;
|
|
878
950
|
this.register(session);
|
|
879
951
|
return session;
|
|
880
952
|
}
|
|
@@ -894,7 +966,23 @@ function nextAvailableDockId(views, baseId) {
|
|
|
894
966
|
return `${baseId}-${n}`;
|
|
895
967
|
}
|
|
896
968
|
/**
|
|
897
|
-
*
|
|
969
|
+
* When a dock's `clientScript.importFrom` is an absolute filesystem path, serve
|
|
970
|
+
* its directory under `<base>__page-script/` and rewrite `importFrom` to that
|
|
971
|
+
* URL. A URL or bare specifier (not existing on disk) passes through untouched.
|
|
972
|
+
*/
|
|
973
|
+
async function resolvePageScriptClientScript(ctx, clientScript, base) {
|
|
974
|
+
if (!clientScript?.importFrom) return clientScript;
|
|
975
|
+
const { importFrom } = clientScript;
|
|
976
|
+
if (!isAbsolute(importFrom) || !existsSync(importFrom)) return clientScript;
|
|
977
|
+
const scriptBase = withTrailingSlash(joinURL(base, "__page-script"));
|
|
978
|
+
await ctx.host.mountStatic(scriptBase, dirname(importFrom));
|
|
979
|
+
return {
|
|
980
|
+
...clientScript,
|
|
981
|
+
importFrom: joinURL(scriptBase, basename(importFrom))
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Framework-neutral primitive backing {@link DevframeHubContext.install} -
|
|
898
986
|
* installs a {@link DevframeDefinition} as a dock inside a hub-aware context:
|
|
899
987
|
* serves the devframe's SPA at the resolved base path, synthesizes an iframe
|
|
900
988
|
* dock entry from the definition's metadata, and runs the definition's
|
|
@@ -902,20 +990,35 @@ function nextAvailableDockId(views, baseId) {
|
|
|
902
990
|
* calling it directly.
|
|
903
991
|
*
|
|
904
992
|
* Framework kits wrap `ctx.install` with their own plugin/middleware
|
|
905
|
-
* machinery
|
|
993
|
+
* machinery, e.g. `@vitejs/devtools-kit`'s `createPluginFromDevframe`
|
|
906
994
|
* returns a Vite `Plugin` whose `devtools.setup` ultimately delegates here.
|
|
907
995
|
*/
|
|
908
996
|
/**
|
|
909
997
|
* Phase one of an install: run the duplication guard, serve the SPA + meta,
|
|
910
998
|
* register the iframe dock, and queue the definition's declarative wire
|
|
911
|
-
* services
|
|
999
|
+
* services, everything up to (but not including) `setup(ctx)`. Returns a
|
|
912
1000
|
* deferred setup thunk, or `null` when the devframe was deduplicated.
|
|
913
1001
|
*
|
|
914
1002
|
* The hub's initial batch uses this to collect every devframe's services
|
|
915
|
-
* across the whole hub, `ready()` them once, and only then run the setups
|
|
1003
|
+
* across the whole hub, `ready()` them once, and only then run the setups,
|
|
916
1004
|
* so services are ready before any setup, and a plugin can consume a service
|
|
917
1005
|
* another plugin declared regardless of mount order.
|
|
918
1006
|
*/
|
|
1007
|
+
/**
|
|
1008
|
+
* Serve a devframe's SPA (and the hub's connection meta) under `base`, if the
|
|
1009
|
+
* definition ships client assets.
|
|
1010
|
+
*/
|
|
1011
|
+
async function serveDevframeAssets(ctx, d, id, base) {
|
|
1012
|
+
const clientAssets = resolveClientAssets(d);
|
|
1013
|
+
if (!clientAssets) return;
|
|
1014
|
+
if (ctx.host.mountConnectionMeta) await ctx.host.mountConnectionMeta(base);
|
|
1015
|
+
else diagnostics.DF8106({
|
|
1016
|
+
id,
|
|
1017
|
+
name: d.name,
|
|
1018
|
+
base
|
|
1019
|
+
});
|
|
1020
|
+
ctx.views.hostStatic(base, typeof clientAssets === "string" ? resolve(clientAssets) : clientAssets, d.importMetaUrl);
|
|
1021
|
+
}
|
|
919
1022
|
async function prepareDevframe(ctx, d, options = {}) {
|
|
920
1023
|
const strategy = d.duplicationStrategy ?? "warn";
|
|
921
1024
|
const isDuplicate = ctx.docks.views.has(d.id);
|
|
@@ -936,23 +1039,18 @@ async function prepareDevframe(ctx, d, options = {}) {
|
|
|
936
1039
|
id,
|
|
937
1040
|
basePath: void 0
|
|
938
1041
|
}, "hosted"));
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
});
|
|
947
|
-
const distSource = clientAssets;
|
|
948
|
-
ctx.views.hostStatic(base, typeof distSource === "string" ? resolve(distSource) : distSource, d.importMetaUrl);
|
|
949
|
-
}
|
|
1042
|
+
const dockDefaults = {
|
|
1043
|
+
...d.dock,
|
|
1044
|
+
...options.dock
|
|
1045
|
+
};
|
|
1046
|
+
const clientScript = await resolvePageScriptClientScript(ctx, dockDefaults.clientScript, base);
|
|
1047
|
+
if (clientScript) dockDefaults.clientScript = clientScript;
|
|
1048
|
+
await serveDevframeAssets(ctx, d, id, base);
|
|
950
1049
|
ctx.docks.register({
|
|
951
1050
|
id,
|
|
952
1051
|
title: d.name,
|
|
953
1052
|
icon: d.icon,
|
|
954
|
-
...
|
|
955
|
-
...options.dock,
|
|
1053
|
+
...dockDefaults,
|
|
956
1054
|
type: "iframe",
|
|
957
1055
|
url: base
|
|
958
1056
|
});
|
|
@@ -960,7 +1058,7 @@ async function prepareDevframe(ctx, d, options = {}) {
|
|
|
960
1058
|
return () => Promise.resolve(d.setup(ctx));
|
|
961
1059
|
}
|
|
962
1060
|
/**
|
|
963
|
-
* Install a {@link DevframeDefinition} into a hub in one call
|
|
1061
|
+
* Install a {@link DevframeDefinition} into a hub in one call: serve its SPA,
|
|
964
1062
|
* register its dock, ready its services, and run `setup(ctx)`. The imperative
|
|
965
1063
|
* counterpart to the hub's declarative `devframes` list (which batches the
|
|
966
1064
|
* phases via {@link prepareDevframe}); use it from `configure(ctx)` or
|
|
@@ -996,7 +1094,7 @@ function resolveControllableSession(sessions, id) {
|
|
|
996
1094
|
return session;
|
|
997
1095
|
}
|
|
998
1096
|
/**
|
|
999
|
-
* `hub:commands:execute
|
|
1097
|
+
* `hub:commands:execute`: Invoke a registered server command by id. The
|
|
1000
1098
|
* arguments after `id` are forwarded to the command's `handler(...)`.
|
|
1001
1099
|
* Returns whatever the handler returns.
|
|
1002
1100
|
*
|
|
@@ -1011,7 +1109,7 @@ const hubCommandsExecute = defineHubRpcFunction({
|
|
|
1011
1109
|
} })
|
|
1012
1110
|
});
|
|
1013
1111
|
/**
|
|
1014
|
-
* `hub:messages:add
|
|
1112
|
+
* `hub:messages:add`: Add a message from a browser client into the hub's
|
|
1015
1113
|
* messages subsystem. Marked `from: 'browser'`. Returns the serializable
|
|
1016
1114
|
* entry (the mutation handle stays server-side).
|
|
1017
1115
|
*
|
|
@@ -1030,7 +1128,7 @@ const hubMessagesAdd = defineHubRpcFunction({
|
|
|
1030
1128
|
})).entry;
|
|
1031
1129
|
} })
|
|
1032
1130
|
});
|
|
1033
|
-
/** `hub:messages:update
|
|
1131
|
+
/** `hub:messages:update`: Patch a message by id; returns the updated entry (or `undefined`). */
|
|
1034
1132
|
const hubMessagesUpdate = defineHubRpcFunction({
|
|
1035
1133
|
name: HUB_EVENTS.rpc.messagesUpdate,
|
|
1036
1134
|
type: "action",
|
|
@@ -1039,7 +1137,7 @@ const hubMessagesUpdate = defineHubRpcFunction({
|
|
|
1039
1137
|
return context.messages.update(id, patch);
|
|
1040
1138
|
} })
|
|
1041
1139
|
});
|
|
1042
|
-
/** `hub:messages:remove
|
|
1140
|
+
/** `hub:messages:remove`: Remove a message by id. */
|
|
1043
1141
|
const hubMessagesRemove = defineHubRpcFunction({
|
|
1044
1142
|
name: HUB_EVENTS.rpc.messagesRemove,
|
|
1045
1143
|
type: "action",
|
|
@@ -1047,7 +1145,7 @@ const hubMessagesRemove = defineHubRpcFunction({
|
|
|
1047
1145
|
await context.messages.remove(id);
|
|
1048
1146
|
} })
|
|
1049
1147
|
});
|
|
1050
|
-
/** `hub:messages:clear
|
|
1148
|
+
/** `hub:messages:clear`: Remove every message. */
|
|
1051
1149
|
const hubMessagesClear = defineHubRpcFunction({
|
|
1052
1150
|
name: HUB_EVENTS.rpc.messagesClear,
|
|
1053
1151
|
type: "action",
|
|
@@ -1056,7 +1154,7 @@ const hubMessagesClear = defineHubRpcFunction({
|
|
|
1056
1154
|
} })
|
|
1057
1155
|
});
|
|
1058
1156
|
/**
|
|
1059
|
-
* `hub:terminals:write
|
|
1157
|
+
* `hub:terminals:write`: Send input to an interactive PTY session spawned
|
|
1060
1158
|
* via `ctx.terminals.startPtySession`. Lets a hub-aware terminal UI (e.g. the
|
|
1061
1159
|
* terminals plugin) drive a session owned by another plugin.
|
|
1062
1160
|
*/
|
|
@@ -1067,7 +1165,7 @@ const hubTerminalsWrite = defineHubRpcFunction({
|
|
|
1067
1165
|
resolveInteractiveSession(context.terminals.sessions, id).write(data);
|
|
1068
1166
|
} })
|
|
1069
1167
|
});
|
|
1070
|
-
/** `hub:terminals:resize
|
|
1168
|
+
/** `hub:terminals:resize`: Resize an interactive PTY session by id. */
|
|
1071
1169
|
const hubTerminalsResize = defineHubRpcFunction({
|
|
1072
1170
|
name: HUB_EVENTS.rpc.terminalsResize,
|
|
1073
1171
|
type: "action",
|
|
@@ -1076,7 +1174,7 @@ const hubTerminalsResize = defineHubRpcFunction({
|
|
|
1076
1174
|
} })
|
|
1077
1175
|
});
|
|
1078
1176
|
/**
|
|
1079
|
-
* `hub:terminals:terminate
|
|
1177
|
+
* `hub:terminals:terminate`: Kill a session's process while keeping the
|
|
1080
1178
|
* session registered (its output/scrollback stays). Works for both read-only
|
|
1081
1179
|
* child-process and interactive PTY sessions, letting a hub-aware terminal UI
|
|
1082
1180
|
* force-kill a session owned by another plugin.
|
|
@@ -1089,7 +1187,7 @@ const hubTerminalsTerminate = defineHubRpcFunction({
|
|
|
1089
1187
|
} })
|
|
1090
1188
|
});
|
|
1091
1189
|
/**
|
|
1092
|
-
* `hub:terminals:restart
|
|
1190
|
+
* `hub:terminals:restart`: Re-run a session's command in place. Rejected for
|
|
1093
1191
|
* sessions registered with `restartable: false`, whose lifecycle is owned
|
|
1094
1192
|
* elsewhere.
|
|
1095
1193
|
*/
|
|
@@ -1103,7 +1201,7 @@ const hubTerminalsRestart = defineHubRpcFunction({
|
|
|
1103
1201
|
} })
|
|
1104
1202
|
});
|
|
1105
1203
|
/**
|
|
1106
|
-
* `hub:terminals:remove
|
|
1204
|
+
* `hub:terminals:remove`: Kill a session's process (when it still owns one)
|
|
1107
1205
|
* and drop it from the registry, disposing its output stream. Lets a hub-aware
|
|
1108
1206
|
* terminal UI discard a stopped aggregated session.
|
|
1109
1207
|
*/
|
|
@@ -1119,13 +1217,13 @@ const hubTerminalsRemove = defineHubRpcFunction({
|
|
|
1119
1217
|
} })
|
|
1120
1218
|
});
|
|
1121
1219
|
/**
|
|
1122
|
-
* `hub:docks:activate
|
|
1220
|
+
* `hub:docks:activate`: Ask the active viewer to switch its focused dock to
|
|
1123
1221
|
* `dockId`, optionally carrying `params` for the target dock to interpret
|
|
1124
1222
|
* (e.g. `{ sessionId }` for the terminals dock to focus a session).
|
|
1125
1223
|
*
|
|
1126
1224
|
* Any connected client may call it, which is the point: a mounted devframe
|
|
1127
1225
|
* running in its own iframe (on its own RPC client) can steer the host shell's
|
|
1128
|
-
* dock selection
|
|
1226
|
+
* dock selection, client-local state it otherwise can't reach. The hub
|
|
1129
1227
|
* broadcasts the request live to connected clients (the host shell switches)
|
|
1130
1228
|
* and mirrors it into the `devframe:docks:active` shared state (a dock that
|
|
1131
1229
|
* mounts in response still converges on it).
|