@devframes/hub 0.8.2 → 0.9.0-beta.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.
@@ -1,16 +1,7 @@
1
- import { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS } from "../constants.mjs";
2
- import { r as defineHubRpcFunction } from "../define-Pbx8dsKz.mjs";
3
- import { t as buildRemoteConnectionUrl } from "../remote-url-KVXtKP47.mjs";
4
- import { createEventEmitter } from "devframe/utils/events";
5
- import { coerceAgentPositionalArgs, createHostContext, createStorage } from "devframe/node";
6
- import { debounce } from "perfect-debounce";
7
- import { defineDiagnostics } from "nostics";
8
- import { colors } from "devframe/utils/colors";
9
- import { ansiFormatter } from "nostics/formatters/ansi";
10
- import { getInternalContext, resolveBasePath } from "devframe/node/hub-internals";
11
- import { join, resolve } from "pathe";
12
- import { nanoid } from "devframe/utils/nanoid";
13
- import process$1 from "node:process";
1
+ import { _ as DevframeCommandsHost, a as hubMessagesAdd, c as hubMessagesUpdate, d as hubTerminalsRestart, f as hubTerminalsTerminate, g as DevframeDocksHost, h as DevframeMessagesHost, i as hubDocksActivate, l as hubTerminalsRemove, m as DevframeTerminalsHost, n as builtinHubRpcDeclarations, o as hubMessagesClear, p as hubTerminalsWrite, r as hubCommandsExecute, s as hubMessagesRemove, t as createHubContext, u as hubTerminalsResize } from "../context-DzqMQ2mI.mjs";
2
+ import "ufo";
3
+ import "pathe";
4
+ import "node:process";
14
5
  import { builtinModules } from "node:module";
15
6
  import "node:fs";
16
7
  import "node:url";
@@ -18,1168 +9,6 @@ import assert from "node:assert";
18
9
  import "node:path";
19
10
  import v8 from "node:v8";
20
11
  import { format, inspect } from "node:util";
21
- //#region src/utils/diagnostics-reporter.ts
22
- const formatAnsi = ansiFormatter(colors);
23
- function hubReporter(d, { method = "warn" } = {}) {
24
- console[method](formatAnsi(d));
25
- }
26
- //#endregion
27
- //#region src/node/diagnostics.ts
28
- const diagnostics = defineDiagnostics({
29
- docsBase: "https://devfra.me/errors",
30
- reporters: [hubReporter],
31
- codes: {
32
- DF8100: {
33
- why: (p) => `Dock with id "${p.id}" is already registered`,
34
- fix: "Use the `force` parameter to overwrite an existing registration."
35
- },
36
- DF8101: {
37
- why: (p) => `Cannot change the id of dock "${p.id}" to "${p.attempted}". Dock ids are immutable once registered`,
38
- fix: (p) => `Remove \`id\` from the patch to keep updating "${p.id}", or call register() with the full entry to add "${p.attempted}" as a new dock.`
39
- },
40
- DF8102: {
41
- why: (p) => `Dock with id "${p.id}" is not registered and cannot be updated`,
42
- fix: (p) => `Call register() to add "${p.id}" as a new dock, or check the id for typos.`
43
- },
44
- DF8103: {
45
- why: (p) => `Dock entry "${p.id}" cannot set groupId to its own id`,
46
- fix: "Point groupId at a different group entry, or omit it."
47
- },
48
- DF8104: {
49
- why: (p) => `Dock group "${p.id}" cannot itself belong to a group (nested groups are unsupported)`,
50
- fix: "Remove groupId from the group entry; nest members one level only."
51
- },
52
- DF8105: {
53
- why: (p) => `Devframe "${p.name}" (id "${p.id}") is already mounted on this hub`,
54
- 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."
55
- },
56
- DF8106: {
57
- 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\`.`,
58
- 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."
59
- },
60
- DF8107: {
61
- why: (p) => `Dock activation requested for unknown dock id "${p.id}"`,
62
- fix: "Pass a `dockId` that matches a registered dock entry. The activation is still broadcast, but no viewer will switch to it. Ids are case-sensitive — check for typos, and ensure the target dock is registered before activating it."
63
- },
64
- DF8200: { why: (p) => `Terminal session with id "${p.id}" already registered` },
65
- DF8201: { why: (p) => `Terminal session with id "${p.id}" not registered` },
66
- DF8202: {
67
- why: (p) => `Terminal session "${p.id}" does not accept input`,
68
- fix: "Spawn it via ctx.terminals.startPtySession() to get an interactive, writable session."
69
- },
70
- DF8203: { why: (p) => `Failed to spawn PTY session for "${p.command}": ${p.reason}` },
71
- DF8204: {
72
- why: (p) => `Terminal session "${p.id}" cannot be controlled (no lifecycle handle)`,
73
- fix: "Spawn it via ctx.terminals.startChildProcess() or startPtySession() — sessions added with a bare register() expose no terminate/restart handle."
74
- },
75
- DF8205: {
76
- why: (p) => `Terminal session "${p.id}" is not restartable`,
77
- 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."
78
- },
79
- DF8206: {
80
- why: (p) => `Terminal session "${p.id}" cannot be restarted — its output stream is already closed`,
81
- 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."
82
- },
83
- DF8400: { why: (p) => `Command "${p.id}" is already registered` },
84
- DF8401: { why: "Cannot change the id of a command. Use register() to add new commands" },
85
- DF8402: { why: (p) => `Command "${p.id}" is not registered` },
86
- DF8403: {
87
- why: (p) => `Command id "${p.id}" is already used by another command or child command`,
88
- fix: "Use globally unique command ids for top-level commands and all child commands."
89
- },
90
- DF8404: {
91
- why: (p) => `Command "${p.id}" declares agent exposure but has no handler`,
92
- fix: "Agent-exposed commands must be executable server-side. Add a `handler` to the command, or move the `agent` field to an executable child command."
93
- }
94
- }
95
- });
96
- //#endregion
97
- //#region src/node/host-commands.ts
98
- function findChildCommand(command, id) {
99
- for (const child of command.children ?? []) {
100
- if (child.id === id) return child;
101
- const nested = findChildCommand(child, id);
102
- if (nested) return nested;
103
- }
104
- }
105
- function collectCommandIds(command, ids = []) {
106
- ids.push(command.id);
107
- for (const child of command.children ?? []) collectCommandIds(child, ids);
108
- return ids;
109
- }
110
- function validateCommandIds(commands, command, ignoreTopLevelId) {
111
- const ids = collectCommandIds(command);
112
- const seen = /* @__PURE__ */ new Set();
113
- for (const id of ids) {
114
- if (seen.has(id)) throw diagnostics.DF8403({ id });
115
- seen.add(id);
116
- }
117
- for (const [registeredId, registered] of commands) {
118
- if (registeredId === ignoreTopLevelId) continue;
119
- const registeredIds = new Set(collectCommandIds(registered));
120
- for (const id of ids) if (registeredIds.has(id)) throw diagnostics.DF8403({ id });
121
- }
122
- }
123
- var DevframeCommandsHost = class {
124
- context;
125
- commands = /* @__PURE__ */ new Map();
126
- events = createEventEmitter();
127
- /**
128
- * Lazy agent projection: `ctx.agent` queries this provider at list/invoke
129
- * time, deriving tools from {@link commands} on demand — the commands map
130
- * stays the single source of truth, nothing is mirrored or kept in sync.
131
- */
132
- agentProvider;
133
- constructor(context) {
134
- this.context = context;
135
- this.agentProvider = context.agent?.registerToolProvider(() => this.collectAgentTools());
136
- }
137
- register(command) {
138
- if (this.commands.has(command.id)) throw diagnostics.DF8400({ id: command.id });
139
- validateCommandIds(this.commands, command);
140
- this.validateAgentExposure(command);
141
- this.commands.set(command.id, command);
142
- this.events.emit("command:registered", this.toSerializable(command));
143
- this.agentProvider?.notifyChanged();
144
- return {
145
- id: command.id,
146
- update: (patch) => {
147
- if ("id" in patch) throw diagnostics.DF8401();
148
- const existing = this.commands.get(command.id);
149
- if (!existing) throw diagnostics.DF8402({ id: command.id });
150
- const next = {
151
- ...existing,
152
- ...patch,
153
- id: existing.id
154
- };
155
- validateCommandIds(this.commands, next, existing.id);
156
- this.validateAgentExposure(next);
157
- Object.assign(existing, patch);
158
- this.events.emit("command:registered", this.toSerializable(existing));
159
- this.agentProvider?.notifyChanged();
160
- },
161
- unregister: () => this.unregister(command.id)
162
- };
163
- }
164
- unregister(id) {
165
- const deleted = this.commands.delete(id);
166
- if (deleted) {
167
- this.events.emit("command:unregistered", id);
168
- this.agentProvider?.notifyChanged();
169
- }
170
- return deleted;
171
- }
172
- async execute(id, ...args) {
173
- const found = this.findCommand(id);
174
- if (!found) throw diagnostics.DF8402({ id });
175
- if (!found.handler) throw new Error(`Command "${id}" has no handler (group-only command)`);
176
- return found.handler(...args);
177
- }
178
- list() {
179
- return Array.from(this.commands.values()).map((cmd) => this.toSerializable(cmd));
180
- }
181
- findCommand(id) {
182
- const topLevel = this.commands.get(id);
183
- if (topLevel) return topLevel;
184
- for (const cmd of this.commands.values()) {
185
- const child = findChildCommand(cmd, id);
186
- if (child) return child;
187
- }
188
- }
189
- toSerializable(cmd) {
190
- const { handler: _, agent: __, children, ...rest } = cmd;
191
- return {
192
- ...rest,
193
- source: "server",
194
- ...children ? { children: children.map((c) => this.toSerializable(c)) } : {}
195
- };
196
- }
197
- /** Reject `agent` on handler-less commands anywhere in the tree, up front. */
198
- validateAgentExposure(command) {
199
- if (command.agent && !command.handler) throw diagnostics.DF8404({ id: command.id });
200
- for (const child of command.children ?? []) this.validateAgentExposure(child);
201
- }
202
- /**
203
- * Derive the agent-tool projection of the current command trees: every
204
- * agent-flagged, handler-bearing command (children included) becomes a
205
- * callable tool. Queried lazily by the provider registered in the
206
- * constructor. `when` clauses evaluate client-side only and are not
207
- * enforced here — opting in a `when`-gated command is a deliberate author
208
- * decision (documented on `DevframeCommandAgentOptions`).
209
- */
210
- collectAgentTools() {
211
- const tools = [];
212
- const walk = (command) => {
213
- const agent = command.agent;
214
- if (agent && command.handler) tools.push({
215
- id: command.id,
216
- title: agent.title ?? command.title,
217
- description: agent.description,
218
- safety: agent.safety ?? "action",
219
- tags: agent.tags,
220
- args: agent.args,
221
- handler: async (args) => this.execute(command.id, ...coerceAgentPositionalArgs(args, agent.args, "drop"))
222
- });
223
- for (const child of command.children ?? []) walk(child);
224
- };
225
- for (const command of this.commands.values()) walk(command);
226
- return tools;
227
- }
228
- };
229
- //#endregion
230
- //#region src/node/host-docks.ts
231
- function normaliseRemoteOptions(remote) {
232
- const opts = remote === true ? {} : remote;
233
- return {
234
- transport: opts.transport ?? "fragment",
235
- originLock: opts.originLock ?? true
236
- };
237
- }
238
- var DevframeDocksHost = class {
239
- context;
240
- views = /* @__PURE__ */ new Map();
241
- events = createEventEmitter();
242
- userSettings = void 0;
243
- /** Dock-id → allocated remote token + resolved options. */
244
- remoteDocks = /* @__PURE__ */ new Map();
245
- constructor(context) {
246
- this.context = context;
247
- }
248
- async init() {
249
- this.userSettings = await this.context.rpc.sharedState.get("devframe:user-settings", { sharedState: createStorage({
250
- filepath: join(this.context.host.getStorageDir("project"), "settings.json"),
251
- initialValue: DEFAULT_STATE_USER_SETTINGS()
252
- }) });
253
- }
254
- values() {
255
- return Array.from(this.views.values(), (view) => this.projectView(view));
256
- }
257
- projectView(view) {
258
- if (view.type !== "iframe" || !view.remote) return view;
259
- const record = this.remoteDocks.get(view.id);
260
- const endpoint = getInternalContext(this.context).wsEndpoint;
261
- if (!record || !endpoint) return view;
262
- const payload = {
263
- v: 1,
264
- backend: "websocket",
265
- websocket: endpoint.url,
266
- authToken: record.token,
267
- origin: this.resolveDevServerOrigin()
268
- };
269
- return {
270
- ...view,
271
- url: buildRemoteConnectionUrl(view.url, payload, record.options.transport)
272
- };
273
- }
274
- resolveDevServerOrigin() {
275
- return this.context.host.resolveOrigin();
276
- }
277
- register(view, force) {
278
- if (this.views.has(view.id) && !force) throw diagnostics.DF8100({ id: view.id });
279
- this.validateGroupMembership(view);
280
- this.prepareRemoteRegistration(view);
281
- this.views.set(view.id, view);
282
- this.events.emit("dock:entry:updated", view);
283
- return { update: (patch) => {
284
- if (patch.id && patch.id !== view.id) throw diagnostics.DF8101({
285
- id: view.id,
286
- attempted: patch.id
287
- });
288
- this.update({
289
- ...this.views.get(view.id),
290
- ...patch
291
- });
292
- } };
293
- }
294
- update(view) {
295
- if (!this.views.has(view.id)) throw diagnostics.DF8102({ id: view.id });
296
- this.validateGroupMembership(view);
297
- this.prepareRemoteRegistration(view);
298
- this.views.set(view.id, view);
299
- this.events.emit("dock:entry:updated", view);
300
- }
301
- activate(dockId, params) {
302
- if (!this.views.has(dockId)) diagnostics.DF8107({ id: dockId });
303
- this.events.emit("dock:activate", {
304
- dockId,
305
- params
306
- });
307
- }
308
- validateGroupMembership(view) {
309
- if (view.groupId === void 0) return;
310
- if (view.groupId === view.id) throw diagnostics.DF8103({ id: view.id });
311
- if (view.type === "group") throw diagnostics.DF8104({ id: view.id });
312
- }
313
- prepareRemoteRegistration(view) {
314
- const internal = getInternalContext(this.context);
315
- internal.revokeRemoteTokensForDock(view.id);
316
- this.remoteDocks.delete(view.id);
317
- if (view.type !== "iframe" || !view.remote) return;
318
- const options = normaliseRemoteOptions(view.remote);
319
- let dockOrigin;
320
- try {
321
- dockOrigin = new URL(view.url).origin;
322
- } catch {
323
- dockOrigin = this.resolveDevServerOrigin();
324
- }
325
- const token = internal.allocateRemoteToken(view.id, dockOrigin, options.originLock);
326
- this.remoteDocks.set(view.id, {
327
- token,
328
- options
329
- });
330
- }
331
- };
332
- //#endregion
333
- //#region src/node/host-messages.ts
334
- const MAX_ENTRIES = 1e3;
335
- const MAX_REMOVALS = 1e3;
336
- var DevframeMessagesHost = class {
337
- context;
338
- entries = /* @__PURE__ */ new Map();
339
- events = createEventEmitter();
340
- /** Tracks when each entry was last added or updated (monotonic) */
341
- lastModified = /* @__PURE__ */ new Map();
342
- /** Tracks recently removed entry IDs with their removal time */
343
- removals = [];
344
- _autoDeleteTimers = /* @__PURE__ */ new Map();
345
- _clock = 0;
346
- /**
347
- * The tick of the newest removal record dropped from the capped
348
- * `removals` log — cursors older than this can't get a reliable delta
349
- * and fall back to a full snapshot in {@link listSince}.
350
- */
351
- _removalsTrimmedAt = 0;
352
- _tick() {
353
- return ++this._clock;
354
- }
355
- _recordRemoval(id, time) {
356
- this.removals.push({
357
- id,
358
- time
359
- });
360
- if (this.removals.length > MAX_REMOVALS) {
361
- const dropped = this.removals.splice(0, this.removals.length - MAX_REMOVALS);
362
- this._removalsTrimmedAt = dropped[dropped.length - 1].time;
363
- }
364
- }
365
- constructor(context) {
366
- this.context = context;
367
- }
368
- async add(input) {
369
- if (input.id && this.entries.has(input.id)) {
370
- await this.update(input.id, input);
371
- return this._createHandle(input.id);
372
- }
373
- const entry = {
374
- ...input,
375
- id: input.id ?? nanoid(),
376
- timestamp: input.timestamp ?? Date.now(),
377
- from: input.from ?? "server"
378
- };
379
- if (this.entries.size >= MAX_ENTRIES) {
380
- const oldest = this.entries.keys().next().value;
381
- await this.remove(oldest);
382
- }
383
- this.entries.set(entry.id, entry);
384
- this.lastModified.set(entry.id, this._tick());
385
- this.events.emit("message:added", entry);
386
- if (entry.autoDelete) this._autoDeleteTimers.set(entry.id, setTimeout(() => {
387
- this.remove(entry.id);
388
- }, entry.autoDelete));
389
- return this._createHandle(entry.id);
390
- }
391
- async update(id, patch) {
392
- const existing = this.entries.get(id);
393
- if (!existing) return void 0;
394
- const updated = {
395
- ...existing,
396
- ...patch,
397
- id: existing.id,
398
- from: existing.from,
399
- timestamp: existing.timestamp
400
- };
401
- this.entries.set(id, updated);
402
- this.lastModified.set(id, this._tick());
403
- this.events.emit("message:updated", updated);
404
- if (patch.autoDelete !== void 0) {
405
- const timer = this._autoDeleteTimers.get(id);
406
- if (timer) {
407
- clearTimeout(timer);
408
- this._autoDeleteTimers.delete(id);
409
- }
410
- if (patch.autoDelete) this._autoDeleteTimers.set(id, setTimeout(() => {
411
- this.remove(id);
412
- }, patch.autoDelete));
413
- }
414
- return updated;
415
- }
416
- async remove(id) {
417
- const timer = this._autoDeleteTimers.get(id);
418
- if (timer) {
419
- clearTimeout(timer);
420
- this._autoDeleteTimers.delete(id);
421
- }
422
- this.entries.delete(id);
423
- this.lastModified.delete(id);
424
- this._recordRemoval(id, this._tick());
425
- this.events.emit("message:removed", id);
426
- }
427
- info(message, extra) {
428
- return this.add({
429
- ...extra,
430
- message,
431
- level: "info"
432
- });
433
- }
434
- warn(message, extra) {
435
- return this.add({
436
- ...extra,
437
- message,
438
- level: "warn"
439
- });
440
- }
441
- error(message, extra) {
442
- return this.add({
443
- ...extra,
444
- message,
445
- level: "error"
446
- });
447
- }
448
- success(message, extra) {
449
- return this.add({
450
- ...extra,
451
- message,
452
- level: "success"
453
- });
454
- }
455
- debug(message, extra) {
456
- return this.add({
457
- ...extra,
458
- message,
459
- level: "debug"
460
- });
461
- }
462
- async clear() {
463
- for (const timer of this._autoDeleteTimers.values()) clearTimeout(timer);
464
- this._autoDeleteTimers.clear();
465
- const tick = this._tick();
466
- for (const id of this.entries.keys()) this._recordRemoval(id, tick);
467
- this.entries.clear();
468
- this.lastModified.clear();
469
- this.events.emit("message:cleared");
470
- }
471
- listSince(since) {
472
- const version = this._clock;
473
- if (since == null || since < this._removalsTrimmedAt || since > version) return {
474
- entries: Array.from(this.entries.values()),
475
- removedIds: [],
476
- version,
477
- full: true
478
- };
479
- const entries = [];
480
- for (const [id, entry] of this.entries) {
481
- const mod = this.lastModified.get(id);
482
- if (mod != null && mod > since) entries.push(entry);
483
- }
484
- const removedIds = [];
485
- for (const removal of this.removals) if (removal.time > since) removedIds.push(removal.id);
486
- return {
487
- entries,
488
- removedIds,
489
- version,
490
- full: false
491
- };
492
- }
493
- _createHandle(id) {
494
- const host = this;
495
- return {
496
- get entry() {
497
- return host.entries.get(id);
498
- },
499
- get id() {
500
- return id;
501
- },
502
- update: (patch) => host.update(id, patch),
503
- dismiss: () => host.remove(id)
504
- };
505
- }
506
- };
507
- //#endregion
508
- //#region src/node/host-terminals.ts
509
- /**
510
- * Channel name used for terminal stream output. Stable, well-known so
511
- * hub-aware clients can subscribe by name.
512
- */
513
- const TERMINAL_STREAM_CHANNEL = "devframe:terminals";
514
- const TERMINAL_REPLAY_WINDOW = 1e3;
515
- /** Max chunks retained in the per-session scrollback buffer (bounded like the replay window). */
516
- const TERMINAL_BUFFER_LIMIT = 1e3;
517
- /** TERM handed to spawned PTYs; also used to reject fallback process labels. */
518
- const PTY_TERM_NAME = "xterm-256color";
519
- var DevframeTerminalsHost = class {
520
- context;
521
- sessions = /* @__PURE__ */ new Map();
522
- events = createEventEmitter();
523
- _boundStreams = /* @__PURE__ */ new Map();
524
- _channel;
525
- constructor(context) {
526
- this.context = context;
527
- }
528
- /**
529
- * Lazily acquire the streaming channel — `context.rpc` isn't assigned
530
- * until after every host is constructed, so we can't grab it in the
531
- * constructor.
532
- */
533
- getStreamingChannel() {
534
- if (this._channel) return this._channel;
535
- if (!this.context.rpc?.streaming) return void 0;
536
- this._channel = this.context.rpc.streaming.create(TERMINAL_STREAM_CHANNEL, { replayWindow: TERMINAL_REPLAY_WINDOW });
537
- return this._channel;
538
- }
539
- register(session) {
540
- if (this.sessions.has(session.id)) throw diagnostics.DF8200({ id: session.id });
541
- this.sessions.set(session.id, session);
542
- this.bindStream(session);
543
- this.events.emit("terminal:session:updated", session);
544
- return session;
545
- }
546
- update(patch) {
547
- if (!this.sessions.has(patch.id)) throw diagnostics.DF8201({ id: patch.id });
548
- const session = this.sessions.get(patch.id);
549
- Object.assign(session, patch);
550
- this.sessions.set(patch.id, session);
551
- this.bindStream(session);
552
- this.events.emit("terminal:session:updated", session);
553
- }
554
- remove(session) {
555
- this._boundStreams.get(session.id)?.dispose();
556
- this.sessions.delete(session.id);
557
- this.events.emit("terminal:session:updated", session);
558
- this._boundStreams.delete(session.id);
559
- }
560
- bindStream(session) {
561
- if (this._boundStreams.has(session.id) && this._boundStreams.get(session.id)?.stream === session.stream) return;
562
- this._boundStreams.get(session.id)?.dispose();
563
- this._boundStreams.delete(session.id);
564
- if (!session.stream) return;
565
- session.buffer ||= [];
566
- const sessionBuffer = session.buffer;
567
- const sink = this.getStreamingChannel()?.start({ id: session.id });
568
- const reader = session.stream.getReader();
569
- let disposed = false;
570
- (async () => {
571
- try {
572
- while (true) {
573
- if (disposed) break;
574
- const result = await reader.read();
575
- if (disposed) break;
576
- if (result.done) break;
577
- sessionBuffer.push(result.value);
578
- if (sessionBuffer.length > TERMINAL_BUFFER_LIMIT) sessionBuffer.splice(0, sessionBuffer.length - TERMINAL_BUFFER_LIMIT);
579
- sink?.write(result.value);
580
- }
581
- if (!disposed && sink && !sink.closed) sink.close();
582
- } catch (error) {
583
- if (!disposed && sink && !sink.closed) sink.error(error);
584
- } finally {
585
- try {
586
- reader.releaseLock();
587
- } catch {}
588
- }
589
- })();
590
- this._boundStreams.set(session.id, {
591
- dispose: () => {
592
- disposed = true;
593
- reader.cancel("terminal stream disposed").catch(() => {});
594
- if (sink && !sink.closed) sink.close();
595
- },
596
- stream: session.stream
597
- });
598
- }
599
- async startChildProcess(executeOptions, terminal) {
600
- if (this.sessions.has(terminal.id)) throw diagnostics.DF8200({ id: terminal.id });
601
- const { exec } = await import("tinyexec");
602
- let controller;
603
- let cp;
604
- let currentResult;
605
- let runId = 0;
606
- let streamClosed = false;
607
- let session;
608
- const markStatus = (next) => {
609
- if (session.status === next) return;
610
- session.status = next;
611
- this.events.emit("terminal:session:updated", session);
612
- };
613
- const closeStream = () => {
614
- if (streamClosed) return;
615
- streamClosed = true;
616
- try {
617
- controller?.close();
618
- } catch {}
619
- };
620
- const errorStream = (error) => {
621
- if (streamClosed) return;
622
- streamClosed = true;
623
- try {
624
- controller?.error(error);
625
- } catch {}
626
- };
627
- const stream = new ReadableStream({
628
- start(_controller) {
629
- controller = _controller;
630
- },
631
- cancel() {
632
- cp?.kill();
633
- cp = void 0;
634
- closeStream();
635
- }
636
- });
637
- function createChildProcess() {
638
- const currentRun = ++runId;
639
- let runErrored = false;
640
- const cp = exec(executeOptions.command, executeOptions.args || [], { nodeOptions: {
641
- env: {
642
- COLORS: "true",
643
- FORCE_COLOR: "true",
644
- ...executeOptions.env || {}
645
- },
646
- cwd: executeOptions.cwd ?? process$1.cwd(),
647
- stdio: "pipe"
648
- } });
649
- const stdoutChunks = [];
650
- const stderrChunks = [];
651
- let settled = false;
652
- let resolveOutput;
653
- const outputPromise = new Promise((resolve) => {
654
- resolveOutput = resolve;
655
- });
656
- const settle = (exitCode) => {
657
- if (settled || currentRun !== runId) return;
658
- settled = true;
659
- resolveOutput({
660
- stdout: stdoutChunks.join(""),
661
- stderr: stderrChunks.join(""),
662
- exitCode
663
- });
664
- };
665
- cp.process?.stdout?.on("data", (chunk) => {
666
- if (currentRun !== runId) return;
667
- const text = chunk.toString();
668
- stdoutChunks.push(text);
669
- if (!streamClosed) controller?.enqueue(text);
670
- });
671
- cp.process?.stderr?.on("data", (chunk) => {
672
- if (currentRun !== runId) return;
673
- const text = chunk.toString();
674
- stderrChunks.push(text);
675
- if (!streamClosed) controller?.enqueue(text);
676
- });
677
- cp.process?.once("error", (error) => {
678
- if (currentRun !== runId) return;
679
- runErrored = true;
680
- settle(cp.process?.exitCode ?? void 0);
681
- errorStream(error);
682
- markStatus("error");
683
- });
684
- cp.process?.once("close", (code) => {
685
- settle(code ?? void 0);
686
- if (currentRun !== runId) return;
687
- closeStream();
688
- if (!runErrored) markStatus(typeof code === "number" && code !== 0 ? "error" : "stopped");
689
- });
690
- currentResult = {
691
- get pid() {
692
- return cp.process?.pid;
693
- },
694
- get exitCode() {
695
- return cp.process?.exitCode ?? void 0;
696
- },
697
- get killed() {
698
- return cp.process?.killed === true;
699
- },
700
- kill: (signal) => cp.kill(signal),
701
- then: (onfulfilled, onrejected) => outputPromise.then(onfulfilled, onrejected)
702
- };
703
- return cp;
704
- }
705
- cp = createChildProcess();
706
- const restart = async () => {
707
- if (streamClosed) throw diagnostics.DF8206({ id: terminal.id });
708
- cp?.kill();
709
- cp = createChildProcess();
710
- markStatus("running");
711
- };
712
- const terminate = async () => {
713
- cp?.kill();
714
- cp = void 0;
715
- closeStream();
716
- markStatus("stopped");
717
- };
718
- session = {
719
- ...terminal,
720
- status: "running",
721
- stream,
722
- type: "child-process",
723
- executeOptions,
724
- getChildProcess: () => cp?.process,
725
- getResult: () => currentResult,
726
- terminate,
727
- restart
728
- };
729
- this.register(session);
730
- return Promise.resolve(session);
731
- }
732
- async startPtySession(executeOptions, terminal) {
733
- if (this.sessions.has(terminal.id)) throw diagnostics.DF8200({ id: terminal.id });
734
- const { spawn } = await import("zigpty");
735
- const cols = executeOptions.cols ?? 80;
736
- const rows = executeOptions.rows ?? 24;
737
- let controller;
738
- let pty;
739
- let runId = 0;
740
- let streamClosed = false;
741
- let session;
742
- const markStatus = (next) => {
743
- if (session.status === next) return;
744
- session.status = next;
745
- this.events.emit("terminal:session:updated", session);
746
- };
747
- const closeStream = () => {
748
- if (streamClosed) return;
749
- streamClosed = true;
750
- try {
751
- controller?.close();
752
- } catch {}
753
- };
754
- const errorStream = (error) => {
755
- if (streamClosed) return;
756
- streamClosed = true;
757
- try {
758
- controller?.error(error);
759
- } catch {}
760
- };
761
- const stream = new ReadableStream({
762
- start(_controller) {
763
- controller = _controller;
764
- },
765
- cancel() {
766
- pty?.kill();
767
- pty = void 0;
768
- closeStream();
769
- }
770
- });
771
- const spawnPty = () => {
772
- const currentRun = ++runId;
773
- const proc = spawn(executeOptions.command, executeOptions.args ?? [], {
774
- name: PTY_TERM_NAME,
775
- cols,
776
- rows,
777
- cwd: executeOptions.cwd ?? process$1.cwd(),
778
- env: {
779
- ...process$1.env,
780
- TERM: PTY_TERM_NAME,
781
- COLORTERM: "truecolor",
782
- FORCE_COLOR: "1",
783
- ...executeOptions.env ?? {}
784
- }
785
- });
786
- proc.onData((data) => {
787
- if (streamClosed || currentRun !== runId) return;
788
- controller?.enqueue(typeof data === "string" ? data : data.toString("utf8"));
789
- });
790
- proc.onExit(({ exitCode, signal }) => {
791
- if (currentRun !== runId) return;
792
- closeStream();
793
- markStatus(signal === 0 && exitCode !== 0 ? "error" : "stopped");
794
- });
795
- return proc;
796
- };
797
- try {
798
- pty = spawnPty();
799
- } catch (error) {
800
- errorStream(error);
801
- throw diagnostics.DF8203({
802
- command: executeOptions.command,
803
- reason: error instanceof Error ? error.message : String(error)
804
- });
805
- }
806
- session = {
807
- ...terminal,
808
- status: "running",
809
- interactive: true,
810
- stream,
811
- type: "pty",
812
- executeOptions,
813
- write: (data) => {
814
- try {
815
- pty?.write(data);
816
- } catch {}
817
- },
818
- resize: (nextCols, nextRows) => {
819
- try {
820
- pty?.resize(Math.max(1, nextCols), Math.max(1, nextRows));
821
- } catch {}
822
- },
823
- getProcessName: () => {
824
- try {
825
- const name = pty?.process;
826
- return name && name !== PTY_TERM_NAME ? name : void 0;
827
- } catch {
828
- return;
829
- }
830
- },
831
- terminate: async () => {
832
- pty?.kill();
833
- pty = void 0;
834
- closeStream();
835
- markStatus("stopped");
836
- },
837
- restart: async () => {
838
- if (streamClosed) throw diagnostics.DF8206({ id: terminal.id });
839
- pty?.kill();
840
- pty = spawnPty();
841
- markStatus("running");
842
- }
843
- };
844
- this.register(session);
845
- return session;
846
- }
847
- };
848
- //#endregion
849
- //#region src/node/rpc-builtins.ts
850
- /**
851
- * Resolve an interactive (PTY) terminal session by id, or throw. Sessions
852
- * spawned via `startChildProcess` are output-only and are rejected here.
853
- */
854
- function resolveInteractiveSession(sessions, id) {
855
- const session = sessions.get(id);
856
- if (!session) throw diagnostics.DF8201({ id });
857
- if (typeof session.write !== "function") throw diagnostics.DF8202({ id });
858
- return session;
859
- }
860
- /**
861
- * Resolve a session that can be terminated/restarted (spawned via
862
- * `startChildProcess` or `startPtySession`), or throw. Sessions added with a
863
- * bare `register()` carry no lifecycle handle and are rejected.
864
- */
865
- function resolveControllableSession(sessions, id) {
866
- const session = sessions.get(id);
867
- if (!session) throw diagnostics.DF8201({ id });
868
- if (typeof session.terminate !== "function") throw diagnostics.DF8204({ id });
869
- return session;
870
- }
871
- /**
872
- * `hub:commands:execute` — Invoke a registered server command by id. The
873
- * arguments after `id` are forwarded to the command's `handler(...)`.
874
- * Returns whatever the handler returns.
875
- *
876
- * Pairs with the `devframe:commands` shared state: clients read the list
877
- * from the shared state and dispatch by id via this RPC.
878
- */
879
- const hubCommandsExecute = defineHubRpcFunction({
880
- name: "hub:commands:execute",
881
- type: "action",
882
- setup: (context) => ({ async handler(id, ...args) {
883
- return context.commands.execute(id, ...args);
884
- } })
885
- });
886
- /**
887
- * `hub:messages:add` — Add a message from a browser client into the hub's
888
- * messages subsystem. Marked `from: 'browser'`. Returns the serializable
889
- * entry (the mutation handle stays server-side).
890
- *
891
- * Pairs with the client-side {@link import('../client').createDevframeClientHost}
892
- * context, whose `messages` client dispatches through these built-ins so a
893
- * dock client script can report into the same feed the server writes to.
894
- */
895
- const hubMessagesAdd = defineHubRpcFunction({
896
- name: "hub:messages:add",
897
- type: "action",
898
- jsonSerializable: true,
899
- setup: (context) => ({ async handler(input) {
900
- return (await context.messages.add({
901
- ...input,
902
- from: "browser"
903
- })).entry;
904
- } })
905
- });
906
- /** `hub:messages:update` — Patch a message by id; returns the updated entry (or `undefined`). */
907
- const hubMessagesUpdate = defineHubRpcFunction({
908
- name: "hub:messages:update",
909
- type: "action",
910
- jsonSerializable: true,
911
- setup: (context) => ({ async handler(id, patch) {
912
- return context.messages.update(id, patch);
913
- } })
914
- });
915
- /** `hub:messages:remove` — Remove a message by id. */
916
- const hubMessagesRemove = defineHubRpcFunction({
917
- name: "hub:messages:remove",
918
- type: "action",
919
- setup: (context) => ({ async handler(id) {
920
- await context.messages.remove(id);
921
- } })
922
- });
923
- /** `hub:messages:clear` — Remove every message. */
924
- const hubMessagesClear = defineHubRpcFunction({
925
- name: "hub:messages:clear",
926
- type: "action",
927
- setup: (context) => ({ async handler() {
928
- await context.messages.clear();
929
- } })
930
- });
931
- /**
932
- * `hub:terminals:write` — Send input to an interactive PTY session spawned
933
- * via `ctx.terminals.startPtySession`. Lets a hub-aware terminal UI (e.g. the
934
- * terminals plugin) drive a session owned by another plugin.
935
- */
936
- const hubTerminalsWrite = defineHubRpcFunction({
937
- name: "hub:terminals:write",
938
- type: "action",
939
- setup: (context) => ({ async handler(id, data) {
940
- resolveInteractiveSession(context.terminals.sessions, id).write(data);
941
- } })
942
- });
943
- /** `hub:terminals:resize` — Resize an interactive PTY session by id. */
944
- const hubTerminalsResize = defineHubRpcFunction({
945
- name: "hub:terminals:resize",
946
- type: "action",
947
- setup: (context) => ({ async handler(id, cols, rows) {
948
- resolveInteractiveSession(context.terminals.sessions, id).resize(cols, rows);
949
- } })
950
- });
951
- /**
952
- * `hub:terminals:terminate` — Kill a session's process while keeping the
953
- * session registered (its output/scrollback stays). Works for both read-only
954
- * child-process and interactive PTY sessions, letting a hub-aware terminal UI
955
- * force-kill a session owned by another plugin.
956
- */
957
- const hubTerminalsTerminate = defineHubRpcFunction({
958
- name: "hub:terminals:terminate",
959
- type: "action",
960
- setup: (context) => ({ async handler(id) {
961
- await resolveControllableSession(context.terminals.sessions, id).terminate();
962
- } })
963
- });
964
- /**
965
- * `hub:terminals:restart` — Re-run a session's command in place. Rejected for
966
- * sessions registered with `restartable: false`, whose lifecycle is owned
967
- * elsewhere.
968
- */
969
- const hubTerminalsRestart = defineHubRpcFunction({
970
- name: "hub:terminals:restart",
971
- type: "action",
972
- setup: (context) => ({ async handler(id) {
973
- const session = resolveControllableSession(context.terminals.sessions, id);
974
- if (session.restartable === false) throw diagnostics.DF8205({ id });
975
- await session.restart();
976
- } })
977
- });
978
- /**
979
- * `hub:terminals:remove` — Kill a session's process (when it still owns one)
980
- * and drop it from the registry, disposing its output stream. Lets a hub-aware
981
- * terminal UI discard a stopped aggregated session.
982
- */
983
- const hubTerminalsRemove = defineHubRpcFunction({
984
- name: "hub:terminals:remove",
985
- type: "action",
986
- setup: (context) => ({ async handler(id) {
987
- const session = context.terminals.sessions.get(id);
988
- if (!session) throw diagnostics.DF8201({ id });
989
- const controllable = session;
990
- if (typeof controllable.terminate === "function") await controllable.terminate();
991
- context.terminals.remove(session);
992
- } })
993
- });
994
- /**
995
- * `hub:docks:activate` — Ask the active viewer to switch its focused dock to
996
- * `dockId`, optionally carrying `params` for the target dock to interpret
997
- * (e.g. `{ sessionId }` for the terminals dock to focus a session).
998
- *
999
- * Any connected client may call it, which is the point: a mounted devframe
1000
- * running in its own iframe (on its own RPC client) can steer the host shell's
1001
- * dock selection — client-local state it otherwise can't reach. The hub
1002
- * broadcasts the request live to connected clients (the host shell switches)
1003
- * and mirrors it into the `devframe:docks:active` shared state (a dock that
1004
- * mounts in response still converges on it).
1005
- */
1006
- const hubDocksActivate = defineHubRpcFunction({
1007
- name: "hub:docks:activate",
1008
- type: "action",
1009
- setup: (context) => ({ async handler(input) {
1010
- context.docks.activate(input.dockId, input.params);
1011
- } })
1012
- });
1013
- /**
1014
- * Framework-neutral RPC declarations auto-registered by
1015
- * {@link createHubContext}. Provide additional RPCs by passing your own
1016
- * array via `CreateHubContextOptions.builtinRpcDeclarations`; the hub's
1017
- * list is prepended automatically.
1018
- */
1019
- const builtinHubRpcDeclarations = [
1020
- hubCommandsExecute,
1021
- hubDocksActivate,
1022
- hubMessagesAdd,
1023
- hubMessagesUpdate,
1024
- hubMessagesRemove,
1025
- hubMessagesClear,
1026
- hubTerminalsWrite,
1027
- hubTerminalsResize,
1028
- hubTerminalsTerminate,
1029
- hubTerminalsRestart,
1030
- hubTerminalsRemove
1031
- ];
1032
- //#endregion
1033
- //#region src/node/context.ts
1034
- /**
1035
- * Create a hub-level node context: wraps devframe's `createHostContext`,
1036
- * attaches the hub hosts (`docks`, `terminals`, `messages`, `commands`),
1037
- * registers the hub's built-in RPC commands, and wires the shared-state
1038
- * synchronization that powers a hub-aware client UI.
1039
- */
1040
- async function createHubContext(options) {
1041
- const context = await createHostContext({
1042
- ...options,
1043
- builtinRpcDeclarations: [...builtinHubRpcDeclarations, ...options.builtinRpcDeclarations ?? []]
1044
- });
1045
- const docks = new DevframeDocksHost(context);
1046
- const terminals = new DevframeTerminalsHost(context);
1047
- const messages = new DevframeMessagesHost(context);
1048
- const commands = new DevframeCommandsHost(context);
1049
- context.docks = docks;
1050
- context.terminals = terminals;
1051
- context.messages = messages;
1052
- context.commands = commands;
1053
- await docks.init();
1054
- let jsonRenderCounter = 0;
1055
- context.createJsonRenderer = (initialSpec) => {
1056
- const stateKey = `devframe:json-render:${jsonRenderCounter++}`;
1057
- const statePromise = context.rpc.sharedState.get(stateKey, { initialValue: initialSpec });
1058
- return {
1059
- _stateKey: stateKey,
1060
- async updateSpec(spec) {
1061
- (await statePromise).mutate(() => spec);
1062
- },
1063
- async updateState(newState) {
1064
- (await statePromise).mutate((draft) => {
1065
- draft.state = {
1066
- ...draft.state,
1067
- ...newState
1068
- };
1069
- });
1070
- }
1071
- };
1072
- };
1073
- const debounceMs = options.mode === "build" ? 0 : 10;
1074
- const docksSharedState = await context.rpc.sharedState.get("devframe:docks", { initialValue: [] });
1075
- const refreshDocks = debounce(() => {
1076
- docksSharedState.mutate(() => docks.values());
1077
- }, debounceMs);
1078
- docks.events.on("dock:entry:updated", refreshDocks);
1079
- docksSharedState.mutate(() => docks.values());
1080
- const activeDockSharedState = await context.rpc.sharedState.get("devframe:docks:active", { initialValue: { activation: null } });
1081
- docks.events.on("dock:activate", (activation) => {
1082
- activeDockSharedState.mutate((state) => {
1083
- state.activation = activation;
1084
- });
1085
- context.rpc.broadcast({
1086
- method: "devframe:docks:activate",
1087
- args: [activation]
1088
- });
1089
- });
1090
- const broadcastTerminals = debounce(() => {
1091
- context.rpc.broadcast({
1092
- method: "devframe:terminals:updated",
1093
- args: []
1094
- });
1095
- docksSharedState.mutate(() => docks.values());
1096
- }, debounceMs);
1097
- terminals.events.on("terminal:session:updated", broadcastTerminals);
1098
- const broadcastMessages = debounce(() => {
1099
- context.rpc.broadcast({
1100
- method: "devframe:messages:updated",
1101
- args: []
1102
- });
1103
- docksSharedState.mutate(() => docks.values());
1104
- }, debounceMs);
1105
- messages.events.on("message:added", broadcastMessages);
1106
- messages.events.on("message:updated", broadcastMessages);
1107
- messages.events.on("message:removed", broadcastMessages);
1108
- messages.events.on("message:cleared", broadcastMessages);
1109
- const commandsSharedState = await context.rpc.sharedState.get("devframe:commands", { initialValue: [] });
1110
- const syncCommands = debounce(() => {
1111
- commandsSharedState.mutate(() => commands.list());
1112
- }, debounceMs);
1113
- commands.events.on("command:registered", syncCommands);
1114
- commands.events.on("command:unregistered", syncCommands);
1115
- commandsSharedState.mutate(() => commands.list());
1116
- return context;
1117
- }
1118
- //#endregion
1119
- //#region src/node/mount-devframe.ts
1120
- /**
1121
- * Find the next free dock id derived from `baseId`. Returns `baseId`
1122
- * when it is unused, otherwise appends `-2`, `-3`, … until a free slot
1123
- * is found. Used by the `'duplicate'` strategy so co-existing instances
1124
- * never collide in the dock registry.
1125
- */
1126
- function nextAvailableDockId(views, baseId) {
1127
- if (!views.has(baseId)) return baseId;
1128
- let n = 2;
1129
- while (views.has(`${baseId}-${n}`)) n++;
1130
- return `${baseId}-${n}`;
1131
- }
1132
- /**
1133
- * Framework-neutral primitive — mounts a {@link DevframeDefinition} as a
1134
- * dock inside a hub-aware context: serves the devframe's SPA at the
1135
- * resolved base path, synthesizes an iframe dock entry from the
1136
- * definition's metadata, and runs the definition's `setup(ctx)`.
1137
- *
1138
- * Framework kits wrap this with their own plugin/middleware machinery —
1139
- * e.g. `@vitejs/devtools-kit`'s `createPluginFromDevframe` returns a
1140
- * Vite `Plugin` whose `devtools.setup` ultimately delegates here.
1141
- */
1142
- async function mountDevframe(ctx, d, options = {}) {
1143
- const strategy = d.duplicationStrategy ?? "warn";
1144
- const isDuplicate = ctx.docks.views.has(d.id);
1145
- if (isDuplicate && strategy !== "duplicate") {
1146
- if (strategy === "throw") throw diagnostics.DF8105({
1147
- id: d.id,
1148
- name: d.name
1149
- });
1150
- if (strategy === "warn") diagnostics.DF8105({
1151
- id: d.id,
1152
- name: d.name
1153
- });
1154
- return;
1155
- }
1156
- const id = isDuplicate ? nextAvailableDockId(ctx.docks.views, d.id) : d.id;
1157
- const base = options.base ?? (id === d.id ? resolveBasePath(d, "hosted") : resolveBasePath({
1158
- ...d,
1159
- id,
1160
- basePath: void 0
1161
- }, "hosted"));
1162
- if (d.cli?.distDir) {
1163
- ctx.views.hostStatic(base, resolve(d.cli.distDir));
1164
- if (ctx.host.mountConnectionMeta) await ctx.host.mountConnectionMeta(base);
1165
- else diagnostics.DF8106({
1166
- id,
1167
- name: d.name,
1168
- base
1169
- });
1170
- }
1171
- ctx.docks.register({
1172
- id,
1173
- title: d.name,
1174
- icon: d.icon ?? "ph:plug-duotone",
1175
- ...d.dock,
1176
- ...options.dock,
1177
- type: "iframe",
1178
- url: base
1179
- });
1180
- await d.setup(ctx);
1181
- }
1182
- //#endregion
1183
12
  //#region ../../node_modules/.pnpm/acorn@8.16.0/node_modules/acorn/dist/acorn.mjs
1184
13
  var astralIdentifierCodes = [
1185
14
  509,
@@ -6639,4 +5468,4 @@ function createSimpleClientScript(fn) {
6639
5468
  };
6640
5469
  }
6641
5470
  //#endregion
6642
- export { DEFAULT_CATEGORIES_ORDER, DevframeCommandsHost, DevframeDocksHost, DevframeMessagesHost, DevframeTerminalsHost, builtinHubRpcDeclarations, createHubContext, createSimpleClientScript, hubCommandsExecute, hubDocksActivate, hubMessagesAdd, hubMessagesClear, hubMessagesRemove, hubMessagesUpdate, hubTerminalsRemove, hubTerminalsResize, hubTerminalsRestart, hubTerminalsTerminate, hubTerminalsWrite, mountDevframe };
5471
+ export { DevframeCommandsHost, DevframeDocksHost, DevframeMessagesHost, DevframeTerminalsHost, builtinHubRpcDeclarations, createHubContext, createSimpleClientScript, hubCommandsExecute, hubDocksActivate, hubMessagesAdd, hubMessagesClear, hubMessagesRemove, hubMessagesUpdate, hubTerminalsRemove, hubTerminalsResize, hubTerminalsRestart, hubTerminalsTerminate, hubTerminalsWrite };