@blackbelt-technology/pi-agent-dashboard 0.2.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/AGENTS.md +342 -0
- package/README.md +619 -0
- package/docs/architecture.md +646 -0
- package/package.json +92 -0
- package/packages/extension/package.json +33 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
- package/packages/extension/src/__tests__/connection.test.ts +344 -0
- package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
- package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
- package/packages/extension/src/__tests__/git-info.test.ts +112 -0
- package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
- package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
- package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
- package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
- package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
- package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
- package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
- package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
- package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
- package/packages/extension/src/ask-user-tool.ts +63 -0
- package/packages/extension/src/bridge-context.ts +64 -0
- package/packages/extension/src/bridge.ts +926 -0
- package/packages/extension/src/command-handler.ts +538 -0
- package/packages/extension/src/connection.ts +204 -0
- package/packages/extension/src/dev-build.ts +39 -0
- package/packages/extension/src/event-forwarder.ts +40 -0
- package/packages/extension/src/flow-event-wiring.ts +102 -0
- package/packages/extension/src/git-info.ts +65 -0
- package/packages/extension/src/git-link-builder.ts +112 -0
- package/packages/extension/src/model-tracker.ts +56 -0
- package/packages/extension/src/pi-env.d.ts +23 -0
- package/packages/extension/src/process-metrics.ts +70 -0
- package/packages/extension/src/process-scanner.ts +396 -0
- package/packages/extension/src/prompt-expander.ts +87 -0
- package/packages/extension/src/provider-register.ts +276 -0
- package/packages/extension/src/server-auto-start.ts +87 -0
- package/packages/extension/src/server-launcher.ts +82 -0
- package/packages/extension/src/server-probe.ts +33 -0
- package/packages/extension/src/session-sync.ts +154 -0
- package/packages/extension/src/source-detector.ts +26 -0
- package/packages/extension/src/ui-proxy.ts +269 -0
- package/packages/extension/tsconfig.json +11 -0
- package/packages/server/package.json +37 -0
- package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
- package/packages/server/src/__tests__/auth.test.ts +224 -0
- package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
- package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
- package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
- package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
- package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
- package/packages/server/src/__tests__/config-api.test.ts +104 -0
- package/packages/server/src/__tests__/cors.test.ts +48 -0
- package/packages/server/src/__tests__/directory-service.test.ts +240 -0
- package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
- package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
- package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
- package/packages/server/src/__tests__/extension-register.test.ts +61 -0
- package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
- package/packages/server/src/__tests__/git-operations.test.ts +251 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
- package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
- package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
- package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
- package/packages/server/src/__tests__/json-store.test.ts +70 -0
- package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
- package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
- package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
- package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
- package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
- package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
- package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
- package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
- package/packages/server/src/__tests__/package-routes.test.ts +172 -0
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
- package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
- package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
- package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
- package/packages/server/src/__tests__/process-manager.test.ts +184 -0
- package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
- package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
- package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
- package/packages/server/src/__tests__/server-pid.test.ts +89 -0
- package/packages/server/src/__tests__/session-api.test.ts +244 -0
- package/packages/server/src/__tests__/session-diff.test.ts +138 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
- package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
- package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
- package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
- package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
- package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
- package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
- package/packages/server/src/__tests__/tunnel.test.ts +206 -0
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
- package/packages/server/src/auth-plugin.ts +302 -0
- package/packages/server/src/auth.ts +323 -0
- package/packages/server/src/browse.ts +55 -0
- package/packages/server/src/browser-gateway.ts +495 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
- package/packages/server/src/browser-handlers/handler-context.ts +45 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
- package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
- package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
- package/packages/server/src/cli.ts +347 -0
- package/packages/server/src/config-api.ts +130 -0
- package/packages/server/src/directory-service.ts +162 -0
- package/packages/server/src/editor-detection.ts +60 -0
- package/packages/server/src/editor-manager.ts +352 -0
- package/packages/server/src/editor-proxy.ts +134 -0
- package/packages/server/src/editor-registry.ts +108 -0
- package/packages/server/src/event-status-extraction.ts +131 -0
- package/packages/server/src/event-wiring.ts +589 -0
- package/packages/server/src/extension-register.ts +92 -0
- package/packages/server/src/git-operations.ts +200 -0
- package/packages/server/src/headless-pid-registry.ts +207 -0
- package/packages/server/src/idle-timer.ts +61 -0
- package/packages/server/src/json-store.ts +32 -0
- package/packages/server/src/localhost-guard.ts +117 -0
- package/packages/server/src/memory-event-store.ts +193 -0
- package/packages/server/src/memory-session-manager.ts +123 -0
- package/packages/server/src/meta-persistence.ts +64 -0
- package/packages/server/src/migrate-persistence.ts +195 -0
- package/packages/server/src/npm-search-proxy.ts +143 -0
- package/packages/server/src/oauth-callback-server.ts +177 -0
- package/packages/server/src/openspec-archive.ts +60 -0
- package/packages/server/src/package-manager-wrapper.ts +200 -0
- package/packages/server/src/pending-fork-registry.ts +53 -0
- package/packages/server/src/pending-load-manager.ts +110 -0
- package/packages/server/src/pending-resume-registry.ts +69 -0
- package/packages/server/src/pi-gateway.ts +419 -0
- package/packages/server/src/pi-resource-scanner.ts +369 -0
- package/packages/server/src/preferences-store.ts +116 -0
- package/packages/server/src/process-manager.ts +311 -0
- package/packages/server/src/provider-auth-handlers.ts +438 -0
- package/packages/server/src/provider-auth-storage.ts +200 -0
- package/packages/server/src/resolve-path.ts +12 -0
- package/packages/server/src/routes/editor-routes.ts +86 -0
- package/packages/server/src/routes/file-routes.ts +116 -0
- package/packages/server/src/routes/git-routes.ts +89 -0
- package/packages/server/src/routes/openspec-routes.ts +99 -0
- package/packages/server/src/routes/package-routes.ts +172 -0
- package/packages/server/src/routes/provider-auth-routes.ts +244 -0
- package/packages/server/src/routes/provider-routes.ts +101 -0
- package/packages/server/src/routes/route-deps.ts +23 -0
- package/packages/server/src/routes/session-routes.ts +91 -0
- package/packages/server/src/routes/system-routes.ts +271 -0
- package/packages/server/src/server-pid.ts +84 -0
- package/packages/server/src/server.ts +554 -0
- package/packages/server/src/session-api.ts +330 -0
- package/packages/server/src/session-bootstrap.ts +80 -0
- package/packages/server/src/session-diff.ts +178 -0
- package/packages/server/src/session-discovery.ts +134 -0
- package/packages/server/src/session-file-reader.ts +135 -0
- package/packages/server/src/session-order-manager.ts +73 -0
- package/packages/server/src/session-scanner.ts +233 -0
- package/packages/server/src/session-stats-reader.ts +99 -0
- package/packages/server/src/terminal-gateway.ts +51 -0
- package/packages/server/src/terminal-manager.ts +241 -0
- package/packages/server/src/tunnel.ts +329 -0
- package/packages/server/tsconfig.json +11 -0
- package/packages/shared/package.json +15 -0
- package/packages/shared/src/__tests__/config.test.ts +358 -0
- package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
- package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
- package/packages/shared/src/__tests__/protocol.test.ts +243 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
- package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
- package/packages/shared/src/archive-types.ts +11 -0
- package/packages/shared/src/browser-protocol.ts +534 -0
- package/packages/shared/src/config.ts +245 -0
- package/packages/shared/src/diff-types.ts +41 -0
- package/packages/shared/src/editor-types.ts +18 -0
- package/packages/shared/src/mdns-discovery.ts +248 -0
- package/packages/shared/src/openspec-activity-detector.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +96 -0
- package/packages/shared/src/protocol.ts +369 -0
- package/packages/shared/src/resolve-jiti.ts +43 -0
- package/packages/shared/src/rest-api.ts +255 -0
- package/packages/shared/src/server-identity.ts +51 -0
- package/packages/shared/src/session-meta.ts +86 -0
- package/packages/shared/src/state-replay.ts +174 -0
- package/packages/shared/src/stats-extractor.ts +54 -0
- package/packages/shared/src/terminal-types.ts +18 -0
- package/packages/shared/src/types.ts +351 -0
- package/packages/shared/tsconfig.json +8 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard HTTP + WebSocket server.
|
|
3
|
+
*/
|
|
4
|
+
import Fastify from "fastify";
|
|
5
|
+
import fastifyStatic from "@fastify/static";
|
|
6
|
+
import cors from "@fastify/cors";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { createMemoryEventStore, type EventStore } from "./memory-event-store.js";
|
|
13
|
+
import { createMemorySessionManager, type SessionManager } from "./memory-session-manager.js";
|
|
14
|
+
import { createPiGateway, type PiGateway } from "./pi-gateway.js";
|
|
15
|
+
import { createBrowserGateway, type BrowserGateway } from "./browser-gateway.js";
|
|
16
|
+
import { createPreferencesStore, type PreferencesStore } from "./preferences-store.js";
|
|
17
|
+
import { createMetaPersistence, type MetaPersistence } from "./meta-persistence.js";
|
|
18
|
+
import { createSessionOrderManager, type SessionOrderManager } from "./session-order-manager.js";
|
|
19
|
+
import { createPendingForkRegistry, type PendingForkRegistry } from "./pending-fork-registry.js";
|
|
20
|
+
|
|
21
|
+
// pending-load-manager removed — server loads sessions directly via DirectoryService
|
|
22
|
+
import { createDirectoryService, type DirectoryService } from "./directory-service.js";
|
|
23
|
+
import { createTerminalManager, type TerminalManager } from "./terminal-manager.js";
|
|
24
|
+
import { createTerminalGateway, type TerminalGateway } from "./terminal-gateway.js";
|
|
25
|
+
import { writePid, removePid } from "./server-pid.js";
|
|
26
|
+
import { advertiseDashboard, stopAdvertising, createBrowser, type DashboardBrowser, type DiscoveredServer } from "@blackbelt-technology/pi-dashboard-shared/mdns-discovery.js";
|
|
27
|
+
import { wireEvents } from "./event-wiring.js";
|
|
28
|
+
import { createIdleTimer } from "./idle-timer.js";
|
|
29
|
+
import { discoverAndBroadcastSessions } from "./session-bootstrap.js";
|
|
30
|
+
import { scanAllSessions } from "./session-scanner.js";
|
|
31
|
+
import { needsMigration, runMigration } from "./migrate-persistence.js";
|
|
32
|
+
import { detectZrokBinary, cleanupStaleZrok, createTunnel, deleteTunnel } from "./tunnel.js";
|
|
33
|
+
import { registerAuthPlugin, validateWsUpgrade } from "./auth-plugin.js";
|
|
34
|
+
import { ensureBridgeExtensionRegistered } from "./extension-register.js";
|
|
35
|
+
import { createNetworkGuard, isLoopback, isBypassedHost } from "./localhost-guard.js";
|
|
36
|
+
import type { AuthConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
37
|
+
import { registerSessionApi } from "./session-api.js";
|
|
38
|
+
import { registerSessionRoutes } from "./routes/session-routes.js";
|
|
39
|
+
import { registerGitRoutes } from "./routes/git-routes.js";
|
|
40
|
+
import { registerFileRoutes } from "./routes/file-routes.js";
|
|
41
|
+
import { registerOpenSpecRoutes } from "./routes/openspec-routes.js";
|
|
42
|
+
import { registerSystemRoutes } from "./routes/system-routes.js";
|
|
43
|
+
import { registerProviderAuthRoutes } from "./routes/provider-auth-routes.js";
|
|
44
|
+
import { registerPackageRoutes } from "./routes/package-routes.js";
|
|
45
|
+
import { registerProviderRoutes } from "./routes/provider-routes.js";
|
|
46
|
+
import { PackageManagerWrapper } from "./package-manager-wrapper.js";
|
|
47
|
+
import { createEditorManager, type EditorManager } from "./editor-manager.js";
|
|
48
|
+
import { registerEditorRoutes } from "./routes/editor-routes.js";
|
|
49
|
+
import { registerEditorProxy, handleEditorUpgrade } from "./editor-proxy.js";
|
|
50
|
+
import { detectCodeServerBinary } from "./editor-detection.js";
|
|
51
|
+
|
|
52
|
+
export interface ServerConfig {
|
|
53
|
+
port: number;
|
|
54
|
+
piPort: number;
|
|
55
|
+
dev: boolean;
|
|
56
|
+
autoShutdown: boolean;
|
|
57
|
+
shutdownIdleSeconds: number;
|
|
58
|
+
tunnel: boolean;
|
|
59
|
+
tunnelReservedToken?: string;
|
|
60
|
+
authConfig?: AuthConfig;
|
|
61
|
+
/** Override WS ping interval for pi-gateway (ms). Default 60000. Set 0 to disable. */
|
|
62
|
+
pingInterval?: number;
|
|
63
|
+
/** Memory limit overrides from config */
|
|
64
|
+
maxEventsPerSession?: number;
|
|
65
|
+
maxStringFieldSize?: number;
|
|
66
|
+
maxWsBufferBytes?: number;
|
|
67
|
+
/** Editor (code-server) config */
|
|
68
|
+
editor: import("@blackbelt-technology/pi-dashboard-shared/config.js").EditorConfig;
|
|
69
|
+
/** Merged trusted networks from config */
|
|
70
|
+
resolvedTrustedNetworks?: string[];
|
|
71
|
+
/** CORS allowed origins from config */
|
|
72
|
+
corsAllowedOrigins?: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface DashboardServer {
|
|
76
|
+
start(): Promise<void>;
|
|
77
|
+
stop(): Promise<void>;
|
|
78
|
+
sessionManager: SessionManager;
|
|
79
|
+
eventStore: EventStore;
|
|
80
|
+
browserGateway: BrowserGateway;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function createServer(config: ServerConfig): Promise<DashboardServer> {
|
|
84
|
+
// Ensure bridge extension is registered in pi's global settings
|
|
85
|
+
// (needed for bundled installs where pi can't discover it from package.json)
|
|
86
|
+
ensureBridgeExtensionRegistered();
|
|
87
|
+
|
|
88
|
+
// Run migration from sessions.json + state.json if needed
|
|
89
|
+
if (needsMigration()) {
|
|
90
|
+
const migResult = runMigration();
|
|
91
|
+
console.log(`[dashboard] Migration complete: ${migResult.sessionsWritten} sessions, ${migResult.hiddenApplied} hidden applied, ${migResult.hiddenOrphaned} orphaned, renamed: ${migResult.oldFilesRenamed.join(", ")}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const preferencesStore = createPreferencesStore();
|
|
95
|
+
const sessionManager = createMemorySessionManager();
|
|
96
|
+
const metaPersistence = createMetaPersistence();
|
|
97
|
+
const sessionOrderManager = createSessionOrderManager(preferencesStore);
|
|
98
|
+
const pendingForkRegistry = createPendingForkRegistry();
|
|
99
|
+
|
|
100
|
+
// Restore sessions from per-session .meta.json files (scans ~/.pi/agent/sessions/)
|
|
101
|
+
const scanResult = scanAllSessions();
|
|
102
|
+
for (const session of scanResult.sessions) {
|
|
103
|
+
const restored = { ...session, dataUnavailable: true };
|
|
104
|
+
if (restored.status !== "ended") {
|
|
105
|
+
restored.status = "ended";
|
|
106
|
+
restored.endedAt = restored.endedAt ?? Date.now();
|
|
107
|
+
}
|
|
108
|
+
sessionManager.restore(restored);
|
|
109
|
+
}
|
|
110
|
+
if (scanResult.cacheUpdates > 0) {
|
|
111
|
+
console.log(`[dashboard] Session scan: ${scanResult.sessions.length} sessions, ${scanResult.cacheUpdates} cache updates`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Save per-session .meta.json on any change
|
|
115
|
+
sessionManager.onChange = (sessionId: string) => {
|
|
116
|
+
const session = sessionManager.get(sessionId);
|
|
117
|
+
if (!session?.sessionFile) return;
|
|
118
|
+
metaPersistence.save(session.sessionFile, {
|
|
119
|
+
source: session.source,
|
|
120
|
+
name: session.name,
|
|
121
|
+
attachedProposal: session.attachedProposal,
|
|
122
|
+
hidden: session.hidden,
|
|
123
|
+
cwd: session.cwd,
|
|
124
|
+
status: session.status,
|
|
125
|
+
startedAt: session.startedAt,
|
|
126
|
+
endedAt: session.endedAt,
|
|
127
|
+
model: session.model,
|
|
128
|
+
thinkingLevel: session.thinkingLevel,
|
|
129
|
+
tokensIn: session.tokensIn,
|
|
130
|
+
tokensOut: session.tokensOut,
|
|
131
|
+
cacheRead: session.cacheRead,
|
|
132
|
+
cacheWrite: session.cacheWrite,
|
|
133
|
+
cost: session.cost,
|
|
134
|
+
contextTokens: session.contextTokens,
|
|
135
|
+
contextWindow: session.contextWindow,
|
|
136
|
+
firstMessage: session.firstMessage,
|
|
137
|
+
cachedAt: Date.now(),
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Track cwds with pending dashboard-spawned sessions (for writing .meta.json).
|
|
142
|
+
// Uses a counter per cwd to handle multiple spawns and avoid reconnects consuming entries.
|
|
143
|
+
const pendingDashboardSpawns = new Map<string, number>();
|
|
144
|
+
// Track known session IDs so we can distinguish new sessions from reconnections.
|
|
145
|
+
const knownSessionIds = new Set<string>();
|
|
146
|
+
// Populate from persisted sessions
|
|
147
|
+
for (const s of sessionManager.listAll()) {
|
|
148
|
+
knownSessionIds.add(s.id);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const directoryService = createDirectoryService(preferencesStore, sessionManager);
|
|
152
|
+
|
|
153
|
+
// mDNS peer discovery state
|
|
154
|
+
let mdnsBrowser: DashboardBrowser | null = null;
|
|
155
|
+
const peerServers = new Map<string, DiscoveredServer>();
|
|
156
|
+
|
|
157
|
+
const piGateway = createPiGateway(sessionManager, {
|
|
158
|
+
...(config.pingInterval !== undefined ? { pingInterval: config.pingInterval } : {}),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Create event store with pinning callback and configurable limits
|
|
162
|
+
const eventStore = createMemoryEventStore(
|
|
163
|
+
(sessionId) =>
|
|
164
|
+
piGateway.isSessionConnected(sessionId) ||
|
|
165
|
+
browserGateway.getSubscriberCount(sessionId) > 0,
|
|
166
|
+
undefined, // maxCachedSessions (use default)
|
|
167
|
+
config.maxEventsPerSession,
|
|
168
|
+
config.maxStringFieldSize,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Create terminal manager with exit callback
|
|
172
|
+
const terminalManager = createTerminalManager({
|
|
173
|
+
onExit: (terminalId) => {
|
|
174
|
+
// Find and remove from session order
|
|
175
|
+
const allOrders = sessionOrderManager.getAllOrders();
|
|
176
|
+
for (const [cwd, ids] of Object.entries(allOrders)) {
|
|
177
|
+
if (ids.includes(terminalId)) {
|
|
178
|
+
sessionOrderManager.remove(cwd, terminalId);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
browserGateway.broadcastToAll({ type: "terminal_removed", terminalId });
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const terminalGateway = createTerminalGateway(terminalManager);
|
|
187
|
+
|
|
188
|
+
// Create editor manager for code-server instances
|
|
189
|
+
const editorDetection = detectCodeServerBinary(config.editor);
|
|
190
|
+
const editorManager = createEditorManager({
|
|
191
|
+
config: config.editor,
|
|
192
|
+
detection: editorDetection,
|
|
193
|
+
onStatusChange: (cwd, id, status) => {
|
|
194
|
+
browserGateway.broadcastToAll({ type: "editor_status", cwd, id, status });
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const browserGateway = createBrowserGateway(sessionManager, eventStore, piGateway, undefined, pendingForkRegistry, sessionOrderManager, preferencesStore, directoryService, terminalManager, pendingDashboardSpawns, config.maxWsBufferBytes);
|
|
199
|
+
|
|
200
|
+
// Resolve package version once at startup
|
|
201
|
+
const __require = createRequire(import.meta.url);
|
|
202
|
+
let pkgVersion = "unknown";
|
|
203
|
+
try { pkgVersion = __require("../../package.json").version ?? "unknown"; } catch {}
|
|
204
|
+
const selfHostname = os.hostname();
|
|
205
|
+
|
|
206
|
+
// Send this server + discovered peers to new browser connections
|
|
207
|
+
browserGateway.onConnect = (ws) => {
|
|
208
|
+
const selfServer: DiscoveredServer = {
|
|
209
|
+
host: selfHostname,
|
|
210
|
+
port: config.port,
|
|
211
|
+
piPort: config.piPort,
|
|
212
|
+
version: pkgVersion,
|
|
213
|
+
pid: process.pid,
|
|
214
|
+
isLocal: true,
|
|
215
|
+
source: "mdns",
|
|
216
|
+
};
|
|
217
|
+
const all = [selfServer, ...Array.from(peerServers.values())];
|
|
218
|
+
browserGateway.sendToClient(ws, { type: "servers_discovered", servers: all });
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Wire up event forwarding from pi gateway to browser gateway
|
|
222
|
+
wireEvents({
|
|
223
|
+
sessionManager,
|
|
224
|
+
eventStore,
|
|
225
|
+
piGateway,
|
|
226
|
+
browserGateway,
|
|
227
|
+
sessionOrderManager,
|
|
228
|
+
pendingForkRegistry,
|
|
229
|
+
directoryService,
|
|
230
|
+
knownSessionIds,
|
|
231
|
+
pendingDashboardSpawns,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Auto-shutdown idle timer
|
|
235
|
+
const idleTimer = createIdleTimer(config, piGateway);
|
|
236
|
+
|
|
237
|
+
const fastify = Fastify({
|
|
238
|
+
logger: false,
|
|
239
|
+
keepAliveTimeout: 30_000,
|
|
240
|
+
connectionTimeout: 10_000,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// CORS: allow localhost by default + configured origins
|
|
244
|
+
const corsAllowedOrigins = config.corsAllowedOrigins ?? [];
|
|
245
|
+
await fastify.register(cors, {
|
|
246
|
+
origin: (origin, cb) => {
|
|
247
|
+
// Same-origin (no Origin header) — always allow
|
|
248
|
+
if (!origin) return cb(null, true);
|
|
249
|
+
// Localhost / 127.0.0.1 / [::1] — any port
|
|
250
|
+
try {
|
|
251
|
+
const u = new URL(origin);
|
|
252
|
+
const host = u.hostname;
|
|
253
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1") {
|
|
254
|
+
return cb(null, true);
|
|
255
|
+
}
|
|
256
|
+
} catch { /* ignore parse errors */ }
|
|
257
|
+
// Configured origins
|
|
258
|
+
if (corsAllowedOrigins.includes(origin)) return cb(null, true);
|
|
259
|
+
cb(new Error("CORS origin not allowed"), false);
|
|
260
|
+
},
|
|
261
|
+
credentials: true,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Register auth plugin if configured (must be before routes)
|
|
265
|
+
if (config.authConfig) {
|
|
266
|
+
await registerAuthPlugin(fastify, {
|
|
267
|
+
authConfig: config.authConfig,
|
|
268
|
+
port: config.port,
|
|
269
|
+
resolvedTrustedNetworks: config.resolvedTrustedNetworks,
|
|
270
|
+
});
|
|
271
|
+
} else {
|
|
272
|
+
// Auth disabled — register isAuthenticated decorator so guard can always read it
|
|
273
|
+
fastify.decorateRequest("isAuthenticated", false);
|
|
274
|
+
// Still expose /auth/status so clients can detect this
|
|
275
|
+
fastify.get("/auth/status", async () => ({ authenticated: true, authEnabled: false }));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Session control REST API (wraps WebSocket-only operations)
|
|
279
|
+
registerSessionApi(fastify, {
|
|
280
|
+
sessionManager,
|
|
281
|
+
piGateway,
|
|
282
|
+
browserGateway,
|
|
283
|
+
pendingForkRegistry,
|
|
284
|
+
pendingDashboardSpawns,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Register route modules
|
|
288
|
+
// Create network guard from merged trusted networks
|
|
289
|
+
const networkGuard = createNetworkGuard(config.resolvedTrustedNetworks ?? []);
|
|
290
|
+
|
|
291
|
+
registerSessionRoutes(fastify, { sessionManager, eventStore, networkGuard });
|
|
292
|
+
registerGitRoutes(fastify, { networkGuard });
|
|
293
|
+
registerFileRoutes(fastify, { sessionManager, preferencesStore, networkGuard });
|
|
294
|
+
registerOpenSpecRoutes(fastify, { sessionManager, preferencesStore, directoryService, networkGuard });
|
|
295
|
+
registerSystemRoutes(fastify, { sessionManager, preferencesStore, metaPersistence, config, networkGuard });
|
|
296
|
+
// Package management
|
|
297
|
+
const packageManagerWrapper = new PackageManagerWrapper();
|
|
298
|
+
|
|
299
|
+
// Forward progress events to all browser clients
|
|
300
|
+
packageManagerWrapper.setProgressListener((operationId, event) => {
|
|
301
|
+
browserGateway.broadcastToAll({ type: "package_progress", operationId, event } as any);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// On completion: broadcast to browsers
|
|
305
|
+
packageManagerWrapper.setCompleteListener((result) => {
|
|
306
|
+
browserGateway.broadcastToAll({
|
|
307
|
+
type: "package_operation_complete",
|
|
308
|
+
operationId: result.operationId,
|
|
309
|
+
action: result.action,
|
|
310
|
+
source: result.source,
|
|
311
|
+
scope: result.scope,
|
|
312
|
+
success: result.success,
|
|
313
|
+
error: result.error,
|
|
314
|
+
sessionsReloaded: (result as any).sessionsReloaded,
|
|
315
|
+
} as any);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Reload all active sessions after a successful package operation
|
|
319
|
+
packageManagerWrapper.setReloadSessions(async () => {
|
|
320
|
+
const connectedIds = piGateway.getConnectedSessionIds();
|
|
321
|
+
let count = 0;
|
|
322
|
+
for (const sid of connectedIds) {
|
|
323
|
+
const session = sessionManager.get(sid);
|
|
324
|
+
if (session && session.status !== "ended") {
|
|
325
|
+
piGateway.sendToSession(sid, {
|
|
326
|
+
type: "send_prompt",
|
|
327
|
+
sessionId: sid,
|
|
328
|
+
text: "/reload",
|
|
329
|
+
});
|
|
330
|
+
count++;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return count;
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
registerPackageRoutes(fastify, { packageManagerWrapper });
|
|
337
|
+
|
|
338
|
+
// Editor (code-server) routes and proxy
|
|
339
|
+
registerEditorRoutes(fastify, editorManager, { networkGuard });
|
|
340
|
+
registerEditorProxy(fastify, editorManager);
|
|
341
|
+
|
|
342
|
+
registerProviderAuthRoutes(fastify, { piGateway });
|
|
343
|
+
registerProviderRoutes(fastify, { networkGuard });
|
|
344
|
+
|
|
345
|
+
// Serve static files / SPA fallback
|
|
346
|
+
// Search order: npm package → workspace sibling → legacy dist/client
|
|
347
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
348
|
+
const clientSearchPaths = [
|
|
349
|
+
// Installed as npm dependency
|
|
350
|
+
path.join(__dirname, "../../node_modules/@blackbelt-technology/pi-dashboard-web/dist"),
|
|
351
|
+
// Monorepo workspace sibling
|
|
352
|
+
path.join(__dirname, "../../client/dist"),
|
|
353
|
+
// Legacy path
|
|
354
|
+
path.join(__dirname, "../../dist/client"),
|
|
355
|
+
];
|
|
356
|
+
const clientDir = clientSearchPaths.find(p => existsSync(path.join(p, "index.html"))) ?? "";
|
|
357
|
+
const hasProductionBuild = !!clientDir;
|
|
358
|
+
if (!hasProductionBuild) {
|
|
359
|
+
console.log("[dashboard] No client build found — running in API-only mode");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Register static file serving for production build.
|
|
363
|
+
// Always enabled — in dev mode, Vite handles most requests via the
|
|
364
|
+
// not-found proxy, but asset files (JS/CSS with hashed names) must be
|
|
365
|
+
// served directly when Vite is not running (production fallback).
|
|
366
|
+
if (hasProductionBuild) {
|
|
367
|
+
await fastify.register(fastifyStatic, {
|
|
368
|
+
root: clientDir,
|
|
369
|
+
prefix: "/",
|
|
370
|
+
setHeaders: (res, filePath) => {
|
|
371
|
+
if (filePath.endsWith(".html")) {
|
|
372
|
+
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (config.dev) {
|
|
379
|
+
// Dev mode: proxy to Vite dev server, fall back to production build
|
|
380
|
+
const VITE_PORTS = [3000, 5173, 5174];
|
|
381
|
+
let vitePort = 0;
|
|
382
|
+
|
|
383
|
+
async function detectVitePort(): Promise<number> {
|
|
384
|
+
for (const port of VITE_PORTS) {
|
|
385
|
+
try {
|
|
386
|
+
const res = await fetch(`http://localhost:${port}/`, { signal: AbortSignal.timeout(500) });
|
|
387
|
+
if (res.ok) return port;
|
|
388
|
+
} catch { /* not listening */ }
|
|
389
|
+
}
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
vitePort = await detectVitePort();
|
|
394
|
+
|
|
395
|
+
fastify.setNotFoundHandler(async (request, reply) => {
|
|
396
|
+
// Try Vite proxy first
|
|
397
|
+
if (!vitePort) vitePort = await detectVitePort();
|
|
398
|
+
if (vitePort) {
|
|
399
|
+
try {
|
|
400
|
+
const viteUrl = `http://localhost:${vitePort}${request.url}`;
|
|
401
|
+
const res = await fetch(viteUrl);
|
|
402
|
+
const contentType = res.headers.get("content-type");
|
|
403
|
+
if (contentType) reply.header("Content-Type", contentType);
|
|
404
|
+
reply.code(res.status);
|
|
405
|
+
return reply.send(Buffer.from(await res.arrayBuffer()));
|
|
406
|
+
} catch {
|
|
407
|
+
vitePort = 0; // Vite stopped — re-probe next time
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Fallback: serve production build if available
|
|
411
|
+
if (hasProductionBuild) {
|
|
412
|
+
reply.header("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
413
|
+
return reply.sendFile("index.html");
|
|
414
|
+
}
|
|
415
|
+
return reply.code(404).send({ error: "API-only mode: no client build available. Install @blackbelt-technology/pi-dashboard-web or run npm run build." });
|
|
416
|
+
});
|
|
417
|
+
} else if (hasProductionBuild) {
|
|
418
|
+
// Production mode: SPA fallback
|
|
419
|
+
fastify.setNotFoundHandler(async (_request, reply) => {
|
|
420
|
+
reply.header("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
421
|
+
return reply.sendFile("index.html");
|
|
422
|
+
});
|
|
423
|
+
} else {
|
|
424
|
+
fastify.setNotFoundHandler(async (_request, reply) => {
|
|
425
|
+
return reply.code(500).send({ error: "No client build found. Run `npm run build` first." });
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const server: DashboardServer = {
|
|
430
|
+
sessionManager,
|
|
431
|
+
eventStore,
|
|
432
|
+
browserGateway,
|
|
433
|
+
|
|
434
|
+
async start() {
|
|
435
|
+
// Clean up orphan headless processes from a previous server instance
|
|
436
|
+
browserGateway.headlessPidRegistry.cleanupOrphans();
|
|
437
|
+
|
|
438
|
+
piGateway.start(config.piPort);
|
|
439
|
+
|
|
440
|
+
fastify.server.on("upgrade", (request, socket, head) => {
|
|
441
|
+
// Access check for WebSocket upgrades
|
|
442
|
+
const remoteAddress = request.socket.remoteAddress || "";
|
|
443
|
+
const trusted = config.resolvedTrustedNetworks ?? [];
|
|
444
|
+
if (config.authConfig?.secret) {
|
|
445
|
+
if (!validateWsUpgrade(request.headers.cookie, remoteAddress, config.authConfig.secret, trusted)) {
|
|
446
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
447
|
+
socket.destroy();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
} else if (!isLoopback(remoteAddress) && (trusted.length === 0 || !isBypassedHost(remoteAddress, trusted))) {
|
|
451
|
+
// No auth configured — only allow loopback or trusted networks
|
|
452
|
+
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
453
|
+
socket.destroy();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (request.url === "/ws") {
|
|
458
|
+
browserGateway.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
459
|
+
browserGateway.wss.emit("connection", ws, request);
|
|
460
|
+
});
|
|
461
|
+
} else if (request.url?.startsWith("/ws/terminal/")) {
|
|
462
|
+
terminalGateway.handleUpgrade(request, socket, head);
|
|
463
|
+
} else if (request.url?.startsWith("/editor/")) {
|
|
464
|
+
handleEditorUpgrade(editorManager, request, socket, head);
|
|
465
|
+
} else {
|
|
466
|
+
socket.destroy();
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
await fastify.listen({ port: config.port, host: "0.0.0.0" });
|
|
471
|
+
writePid(process.pid);
|
|
472
|
+
console.log(`Dashboard server running at http://localhost:${config.port}`);
|
|
473
|
+
console.log(`Pi gateway listening on port ${config.piPort}`);
|
|
474
|
+
|
|
475
|
+
// Advertise via mDNS
|
|
476
|
+
try {
|
|
477
|
+
advertiseDashboard(config.port, config.piPort);
|
|
478
|
+
console.log(`mDNS: advertising _pi-dashboard._tcp on port ${config.port}`);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.warn(`mDNS advertisement failed (will continue without):`, err);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Start continuous mDNS browser for peer discovery
|
|
484
|
+
try {
|
|
485
|
+
mdnsBrowser = createBrowser();
|
|
486
|
+
mdnsBrowser.on("server-up", (server: DiscoveredServer) => {
|
|
487
|
+
// Don't include ourselves
|
|
488
|
+
if (server.isLocal && server.port === config.port) return;
|
|
489
|
+
peerServers.set(`${server.host}:${server.port}`, server);
|
|
490
|
+
browserGateway.broadcast({ type: "servers_updated", servers: Array.from(peerServers.values()) });
|
|
491
|
+
});
|
|
492
|
+
mdnsBrowser.on("server-down", (server: DiscoveredServer) => {
|
|
493
|
+
peerServers.delete(`${server.host}:${server.port}`);
|
|
494
|
+
browserGateway.broadcast({ type: "servers_updated", servers: Array.from(peerServers.values()) });
|
|
495
|
+
});
|
|
496
|
+
} catch (err) {
|
|
497
|
+
console.warn(`mDNS browser failed (peer discovery disabled):`, err);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (config.tunnel) {
|
|
501
|
+
const hasZrok = detectZrokBinary();
|
|
502
|
+
if (hasZrok) {
|
|
503
|
+
cleanupStaleZrok();
|
|
504
|
+
const tunnelUrl = await createTunnel(config.port, config.tunnelReservedToken);
|
|
505
|
+
if (tunnelUrl) {
|
|
506
|
+
console.log(`🌐 Tunnel: ${tunnelUrl}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Discover sessions and start OpenSpec polling (async, non-blocking)
|
|
512
|
+
discoverAndBroadcastSessions({ sessionManager, browserGateway, directoryService });
|
|
513
|
+
|
|
514
|
+
idleTimer.start();
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
async stop() {
|
|
518
|
+
// Stop mDNS before closing
|
|
519
|
+
try {
|
|
520
|
+
if (mdnsBrowser) { mdnsBrowser.stop(); mdnsBrowser = null; }
|
|
521
|
+
stopAdvertising();
|
|
522
|
+
} catch { /* ignore mDNS cleanup errors */ }
|
|
523
|
+
removePid();
|
|
524
|
+
idleTimer.cancel();
|
|
525
|
+
directoryService.stopPolling();
|
|
526
|
+
browserGateway.shutdownHeadlessProcesses();
|
|
527
|
+
metaPersistence.flushAll();
|
|
528
|
+
metaPersistence.dispose();
|
|
529
|
+
pendingForkRegistry.dispose();
|
|
530
|
+
preferencesStore.flush();
|
|
531
|
+
preferencesStore.dispose();
|
|
532
|
+
|
|
533
|
+
await deleteTunnel();
|
|
534
|
+
piGateway.stop();
|
|
535
|
+
for (const client of browserGateway.wss.clients) {
|
|
536
|
+
client.terminate();
|
|
537
|
+
}
|
|
538
|
+
browserGateway.wss.close();
|
|
539
|
+
terminalGateway.close();
|
|
540
|
+
// Kill all active terminal PTY processes
|
|
541
|
+
for (const t of terminalManager.list()) {
|
|
542
|
+
try { terminalManager.kill(t.id); } catch {}
|
|
543
|
+
}
|
|
544
|
+
// Stop all code-server instances
|
|
545
|
+
editorManager.stopAll();
|
|
546
|
+
// Close any pending OAuth callback servers
|
|
547
|
+
try { const { closeAllCallbackServers } = await import("./oauth-callback-server.js"); await closeAllCallbackServers(); } catch {}
|
|
548
|
+
await fastify.close();
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
idleTimer.setStopFn(server.stop.bind(server));
|
|
553
|
+
return server;
|
|
554
|
+
}
|