@agimon-ai/doompi-web 0.0.1-alpha.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/LICENSE +29 -0
- package/README.md +196 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/bin/serve.cjs +3 -0
- package/dist/bin/serve.cjs.map +1 -0
- package/dist/bin/serve.d.cts +1 -0
- package/dist/bin/serve.d.mts +1 -0
- package/dist/bin/serve.mjs +3 -0
- package/dist/bin/serve.mjs.map +1 -0
- package/dist/bundler.cjs +1 -0
- package/dist/bundler.d.cts +2 -0
- package/dist/bundler.d.mts +2 -0
- package/dist/bundler.mjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +1 -0
- package/dist/src/adapters/authRoutes.cjs +2 -0
- package/dist/src/adapters/authRoutes.cjs.map +1 -0
- package/dist/src/adapters/authRoutes.mjs +2 -0
- package/dist/src/adapters/authRoutes.mjs.map +1 -0
- package/dist/src/adapters/bundledServer.cjs +2 -0
- package/dist/src/adapters/bundledServer.cjs.map +1 -0
- package/dist/src/adapters/bundledServer.mjs +2 -0
- package/dist/src/adapters/bundledServer.mjs.map +1 -0
- package/dist/src/adapters/directoryListing.cjs +2 -0
- package/dist/src/adapters/directoryListing.cjs.map +1 -0
- package/dist/src/adapters/directoryListing.mjs +2 -0
- package/dist/src/adapters/directoryListing.mjs.map +1 -0
- package/dist/src/adapters/directorySearch.cjs +3 -0
- package/dist/src/adapters/directorySearch.cjs.map +1 -0
- package/dist/src/adapters/directorySearch.mjs +3 -0
- package/dist/src/adapters/directorySearch.mjs.map +1 -0
- package/dist/src/adapters/gitStatus.cjs +2 -0
- package/dist/src/adapters/gitStatus.cjs.map +1 -0
- package/dist/src/adapters/gitStatus.mjs +2 -0
- package/dist/src/adapters/gitStatus.mjs.map +1 -0
- package/dist/src/adapters/httpServer.cjs +2 -0
- package/dist/src/adapters/httpServer.cjs.map +1 -0
- package/dist/src/adapters/httpServer.d.cts +14 -0
- package/dist/src/adapters/httpServer.d.cts.map +1 -0
- package/dist/src/adapters/httpServer.d.mts +14 -0
- package/dist/src/adapters/httpServer.d.mts.map +1 -0
- package/dist/src/adapters/httpServer.mjs +2 -0
- package/dist/src/adapters/httpServer.mjs.map +1 -0
- package/dist/src/adapters/hubProbe.cjs +2 -0
- package/dist/src/adapters/hubProbe.cjs.map +1 -0
- package/dist/src/adapters/hubProbe.mjs +2 -0
- package/dist/src/adapters/hubProbe.mjs.map +1 -0
- package/dist/src/adapters/packageApiProxy.cjs +2 -0
- package/dist/src/adapters/packageApiProxy.cjs.map +1 -0
- package/dist/src/adapters/packageApiProxy.mjs +2 -0
- package/dist/src/adapters/packageApiProxy.mjs.map +1 -0
- package/dist/src/adapters/piHubService.cjs +2 -0
- package/dist/src/adapters/piHubService.cjs.map +1 -0
- package/dist/src/adapters/piHubService.mjs +2 -0
- package/dist/src/adapters/piHubService.mjs.map +1 -0
- package/dist/src/adapters/piWebSocketListener.cjs +2 -0
- package/dist/src/adapters/piWebSocketListener.cjs.map +1 -0
- package/dist/src/adapters/piWebSocketListener.mjs +2 -0
- package/dist/src/adapters/piWebSocketListener.mjs.map +1 -0
- package/dist/src/adapters/providerAuth.cjs +2 -0
- package/dist/src/adapters/providerAuth.cjs.map +1 -0
- package/dist/src/adapters/providerAuth.mjs +2 -0
- package/dist/src/adapters/providerAuth.mjs.map +1 -0
- package/dist/src/adapters/registryWatcher.cjs +2 -0
- package/dist/src/adapters/registryWatcher.cjs.map +1 -0
- package/dist/src/adapters/registryWatcher.mjs +2 -0
- package/dist/src/adapters/registryWatcher.mjs.map +1 -0
- package/dist/src/adapters/serverSpawner.cjs +2 -0
- package/dist/src/adapters/serverSpawner.cjs.map +1 -0
- package/dist/src/adapters/serverSpawner.mjs +2 -0
- package/dist/src/adapters/serverSpawner.mjs.map +1 -0
- package/dist/src/adapters/sessionFiles.cjs +3 -0
- package/dist/src/adapters/sessionFiles.cjs.map +1 -0
- package/dist/src/adapters/sessionFiles.mjs +3 -0
- package/dist/src/adapters/sessionFiles.mjs.map +1 -0
- package/dist/src/adapters/sessionHub.cjs +2 -0
- package/dist/src/adapters/sessionHub.cjs.map +1 -0
- package/dist/src/adapters/sessionHub.mjs +2 -0
- package/dist/src/adapters/sessionHub.mjs.map +1 -0
- package/dist/src/adapters/sessionSocketClient.cjs +2 -0
- package/dist/src/adapters/sessionSocketClient.cjs.map +1 -0
- package/dist/src/adapters/sessionSocketClient.mjs +2 -0
- package/dist/src/adapters/sessionSocketClient.mjs.map +1 -0
- package/dist/src/adapters/syncGuard.cjs +2 -0
- package/dist/src/adapters/syncGuard.cjs.map +1 -0
- package/dist/src/adapters/syncGuard.mjs +2 -0
- package/dist/src/adapters/syncGuard.mjs.map +1 -0
- package/dist/src/adapters/threadJournals.cjs +2 -0
- package/dist/src/adapters/threadJournals.cjs.map +1 -0
- package/dist/src/adapters/threadJournals.mjs +2 -0
- package/dist/src/adapters/threadJournals.mjs.map +1 -0
- package/dist/src/adapters/webBundler.cjs +2 -0
- package/dist/src/adapters/webBundler.cjs.map +1 -0
- package/dist/src/adapters/webBundler.d.cts +28 -0
- package/dist/src/adapters/webBundler.d.cts.map +1 -0
- package/dist/src/adapters/webBundler.d.mts +28 -0
- package/dist/src/adapters/webBundler.d.mts.map +1 -0
- package/dist/src/adapters/webBundler.mjs +2 -0
- package/dist/src/adapters/webBundler.mjs.map +1 -0
- package/dist/src/adapters/webHubPluginLoader.cjs +2 -0
- package/dist/src/adapters/webHubPluginLoader.cjs.map +1 -0
- package/dist/src/adapters/webHubPluginLoader.mjs +2 -0
- package/dist/src/adapters/webHubPluginLoader.mjs.map +1 -0
- package/dist/src/adapters/webHubPlugins.generated.cjs +2 -0
- package/dist/src/adapters/webHubPlugins.generated.cjs.map +1 -0
- package/dist/src/adapters/webHubPlugins.generated.mjs +2 -0
- package/dist/src/adapters/webHubPlugins.generated.mjs.map +1 -0
- package/dist/src/adapters/webPluginGenerate.cjs +4 -0
- package/dist/src/adapters/webPluginGenerate.cjs.map +1 -0
- package/dist/src/adapters/webPluginGenerate.mjs +4 -0
- package/dist/src/adapters/webPluginGenerate.mjs.map +1 -0
- package/dist/src/adapters/webPluginScan.cjs +2 -0
- package/dist/src/adapters/webPluginScan.cjs.map +1 -0
- package/dist/src/adapters/webPluginScan.mjs +2 -0
- package/dist/src/adapters/webPluginScan.mjs.map +1 -0
- package/dist/src/adapters/webPluginVite.cjs +2 -0
- package/dist/src/adapters/webPluginVite.cjs.map +1 -0
- package/dist/src/adapters/webPluginVite.mjs +2 -0
- package/dist/src/adapters/webPluginVite.mjs.map +1 -0
- package/dist/src/services/directoryMatch.cjs +2 -0
- package/dist/src/services/directoryMatch.cjs.map +1 -0
- package/dist/src/services/directoryMatch.mjs +2 -0
- package/dist/src/services/directoryMatch.mjs.map +1 -0
- package/dist/src/services/fileMatch.cjs +2 -0
- package/dist/src/services/fileMatch.cjs.map +1 -0
- package/dist/src/services/fileMatch.mjs +2 -0
- package/dist/src/services/fileMatch.mjs.map +1 -0
- package/dist/src/services/fileMedia.cjs +2 -0
- package/dist/src/services/fileMedia.cjs.map +1 -0
- package/dist/src/services/fileMedia.mjs +2 -0
- package/dist/src/services/fileMedia.mjs.map +1 -0
- package/dist/src/services/frameRing.cjs +2 -0
- package/dist/src/services/frameRing.cjs.map +1 -0
- package/dist/src/services/frameRing.mjs +2 -0
- package/dist/src/services/frameRing.mjs.map +1 -0
- package/dist/src/services/journalTail.cjs +3 -0
- package/dist/src/services/journalTail.cjs.map +1 -0
- package/dist/src/services/journalTail.mjs +3 -0
- package/dist/src/services/journalTail.mjs.map +1 -0
- package/dist/src/services/loginFlow.cjs +2 -0
- package/dist/src/services/loginFlow.cjs.map +1 -0
- package/dist/src/services/loginFlow.mjs +2 -0
- package/dist/src/services/loginFlow.mjs.map +1 -0
- package/dist/src/services/registryStore.cjs +2 -0
- package/dist/src/services/registryStore.cjs.map +1 -0
- package/dist/src/services/registryStore.mjs +2 -0
- package/dist/src/services/registryStore.mjs.map +1 -0
- package/dist/src/services/retryPolicy.cjs +2 -0
- package/dist/src/services/retryPolicy.cjs.map +1 -0
- package/dist/src/services/retryPolicy.mjs +2 -0
- package/dist/src/services/retryPolicy.mjs.map +1 -0
- package/dist/src/services/serveOptions.cjs +2 -0
- package/dist/src/services/serveOptions.cjs.map +1 -0
- package/dist/src/services/serveOptions.d.cts +21 -0
- package/dist/src/services/serveOptions.d.cts.map +1 -0
- package/dist/src/services/serveOptions.d.mts +21 -0
- package/dist/src/services/serveOptions.d.mts.map +1 -0
- package/dist/src/services/serveOptions.mjs +2 -0
- package/dist/src/services/serveOptions.mjs.map +1 -0
- package/dist/src/services/sessionFraming.cjs +4 -0
- package/dist/src/services/sessionFraming.cjs.map +1 -0
- package/dist/src/services/sessionFraming.mjs +4 -0
- package/dist/src/services/sessionFraming.mjs.map +1 -0
- package/dist/src/services/sessionPresence.cjs +2 -0
- package/dist/src/services/sessionPresence.cjs.map +1 -0
- package/dist/src/services/sessionPresence.mjs +2 -0
- package/dist/src/services/sessionPresence.mjs.map +1 -0
- package/dist/src/services/staticAssets.cjs +2 -0
- package/dist/src/services/staticAssets.cjs.map +1 -0
- package/dist/src/services/staticAssets.mjs +2 -0
- package/dist/src/services/staticAssets.mjs.map +1 -0
- package/dist/src/services/webDevRoots.cjs +2 -0
- package/dist/src/services/webDevRoots.cjs.map +1 -0
- package/dist/src/services/webDevRoots.mjs +2 -0
- package/dist/src/services/webDevRoots.mjs.map +1 -0
- package/dist/src/services/webPluginManifest.cjs +2 -0
- package/dist/src/services/webPluginManifest.cjs.map +1 -0
- package/dist/src/services/webPluginManifest.mjs +2 -0
- package/dist/src/services/webPluginManifest.mjs.map +1 -0
- package/dist/src/types/auth.cjs +2 -0
- package/dist/src/types/auth.cjs.map +1 -0
- package/dist/src/types/auth.d.cts +94 -0
- package/dist/src/types/auth.d.cts.map +1 -0
- package/dist/src/types/auth.d.mts +94 -0
- package/dist/src/types/auth.d.mts.map +1 -0
- package/dist/src/types/auth.mjs +2 -0
- package/dist/src/types/auth.mjs.map +1 -0
- package/dist/src/types/bridge.d.cts +31 -0
- package/dist/src/types/bridge.d.cts.map +1 -0
- package/dist/src/types/bridge.d.mts +31 -0
- package/dist/src/types/bridge.d.mts.map +1 -0
- package/dist/src/types/hub.cjs +2 -0
- package/dist/src/types/hub.cjs.map +1 -0
- package/dist/src/types/hub.d.cts +58 -0
- package/dist/src/types/hub.d.cts.map +1 -0
- package/dist/src/types/hub.d.mts +58 -0
- package/dist/src/types/hub.d.mts.map +1 -0
- package/dist/src/types/hub.mjs +2 -0
- package/dist/src/types/hub.mjs.map +1 -0
- package/dist/src/types/media.cjs +2 -0
- package/dist/src/types/media.cjs.map +1 -0
- package/dist/src/types/media.mjs +2 -0
- package/dist/src/types/media.mjs.map +1 -0
- package/dist/src/types/registry.cjs +1 -0
- package/dist/src/types/registry.d.cts +2 -0
- package/dist/src/types/registry.d.mts +2 -0
- package/dist/src/types/registry.mjs +1 -0
- package/dist/src/types/session.cjs +2 -0
- package/dist/src/types/session.cjs.map +1 -0
- package/dist/src/types/session.d.cts +24 -0
- package/dist/src/types/session.d.cts.map +1 -0
- package/dist/src/types/session.d.mts +24 -0
- package/dist/src/types/session.d.mts.map +1 -0
- package/dist/src/types/session.mjs +2 -0
- package/dist/src/types/session.mjs.map +1 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/index-Cg52ngKv.css +2 -0
- package/dist/web/assets/index-DQO7NTIP.js +90 -0
- package/dist/web/assets/index-DQO7NTIP.js.map +1 -0
- package/dist/web/favicon-16.png +0 -0
- package/dist/web/favicon-32.png +0 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/favicon.svg +16 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/index.html +17 -0
- package/llms.txt +8 -0
- package/package.json +106 -0
- package/src/adapters/authRoutes.ts +95 -0
- package/src/adapters/bundledServer.ts +64 -0
- package/src/adapters/directoryListing.ts +127 -0
- package/src/adapters/directorySearch.ts +124 -0
- package/src/adapters/gitStatus.ts +27 -0
- package/src/adapters/httpServer.ts +556 -0
- package/src/adapters/hubProbe.ts +27 -0
- package/src/adapters/packageApiProxy.ts +60 -0
- package/src/adapters/piHubService.ts +129 -0
- package/src/adapters/piWebSocketListener.ts +98 -0
- package/src/adapters/providerAuth.ts +181 -0
- package/src/adapters/registryWatcher.ts +112 -0
- package/src/adapters/serverSpawner.ts +181 -0
- package/src/adapters/sessionFiles.ts +99 -0
- package/src/adapters/sessionHub.ts +567 -0
- package/src/adapters/sessionSocketClient.ts +105 -0
- package/src/adapters/syncGuard.ts +119 -0
- package/src/adapters/threadJournals.ts +184 -0
- package/src/adapters/webBundler.ts +83 -0
- package/src/adapters/webHubPluginLoader.ts +67 -0
- package/src/adapters/webHubPlugins.generated.ts +4 -0
- package/src/adapters/webPluginGenerate.ts +172 -0
- package/src/adapters/webPluginScan.ts +62 -0
- package/src/adapters/webPluginVite.ts +51 -0
- package/src/bin/serve.ts +64 -0
- package/src/exports/bundler.ts +1 -0
- package/src/exports/index.ts +9 -0
- package/src/services/directoryMatch.ts +71 -0
- package/src/services/fileMatch.ts +37 -0
- package/src/services/fileMedia.ts +27 -0
- package/src/services/frameRing.ts +33 -0
- package/src/services/journalTail.ts +47 -0
- package/src/services/loginFlow.ts +144 -0
- package/src/services/registryStore.ts +83 -0
- package/src/services/retryPolicy.ts +15 -0
- package/src/services/serveOptions.ts +63 -0
- package/src/services/sessionFraming.ts +32 -0
- package/src/services/sessionPresence.ts +131 -0
- package/src/services/staticAssets.ts +37 -0
- package/src/services/webDevRoots.ts +36 -0
- package/src/services/webPluginManifest.ts +149 -0
- package/src/types/auth.ts +118 -0
- package/src/types/bridge.ts +41 -0
- package/src/types/hub.ts +290 -0
- package/src/types/media.ts +38 -0
- package/src/types/registry.ts +2 -0
- package/src/types/session.ts +36 -0
- package/src/web/app/Providers.tsx +46 -0
- package/src/web/app/protocolRuntime.ts +106 -0
- package/src/web/app/sessionRuntime.ts +189 -0
- package/src/web/app/webPlugins.generated.ts +4 -0
- package/src/web/components/MascotMark.tsx +46 -0
- package/src/web/components/PluginSurface.tsx +10 -0
- package/src/web/components/ToolRendererBoundary.tsx +32 -0
- package/src/web/features/activity/ActivityDock.tsx +125 -0
- package/src/web/features/connection/RefusedCard.tsx +63 -0
- package/src/web/features/dialogs/DialogOverlay.tsx +176 -0
- package/src/web/features/leader/CommandPalette.tsx +386 -0
- package/src/web/features/selection/SelectionBar.tsx +529 -0
- package/src/web/features/session/Composer.tsx +368 -0
- package/src/web/features/session/MentionPreviews.tsx +58 -0
- package/src/web/features/session/ThreadView.tsx +35 -0
- package/src/web/features/session/Timeline.tsx +382 -0
- package/src/web/features/session/ToolCard.tsx +79 -0
- package/src/web/features/sessions/NewSessionDialog.tsx +222 -0
- package/src/web/features/sessions/SessionRail.tsx +411 -0
- package/src/web/features/settings/AppearanceSettings.tsx +88 -0
- package/src/web/features/settings/LoginFlowDialog.tsx +221 -0
- package/src/web/features/settings/PluginSettings.tsx +92 -0
- package/src/web/features/settings/ProviderSettings.tsx +238 -0
- package/src/web/features/settings/SettingsMenu.tsx +32 -0
- package/src/web/features/status/TopBar.tsx +208 -0
- package/src/web/index.html +16 -0
- package/src/web/lib/authApi.ts +90 -0
- package/src/web/lib/commands.ts +61 -0
- package/src/web/lib/composition.ts +225 -0
- package/src/web/lib/fileMentions.ts +32 -0
- package/src/web/lib/hubApi.ts +86 -0
- package/src/web/lib/leaderTree.ts +159 -0
- package/src/web/lib/piTransport.ts +61 -0
- package/src/web/lib/pluginRegistry.ts +488 -0
- package/src/web/lib/pluginSlotProps.ts +50 -0
- package/src/web/lib/promptFocus.ts +25 -0
- package/src/web/lib/protocolTimeline.ts +73 -0
- package/src/web/lib/sessionModel.ts +673 -0
- package/src/web/lib/sessionSummary.ts +58 -0
- package/src/web/lib/settingsSections.ts +21 -0
- package/src/web/lib/statusLine.ts +90 -0
- package/src/web/lib/threadRenderer.ts +25 -0
- package/src/web/lib/toolMessageProps.ts +21 -0
- package/src/web/lib/transport.ts +30 -0
- package/src/web/lib/wsClient.ts +73 -0
- package/src/web/main.tsx +17 -0
- package/src/web/public/apple-touch-icon.png +0 -0
- package/src/web/public/favicon-16.png +0 -0
- package/src/web/public/favicon-32.png +0 -0
- package/src/web/public/favicon.ico +0 -0
- package/src/web/public/favicon.svg +16 -0
- package/src/web/public/icon-512.png +0 -0
- package/src/web/routes/CockpitPage.tsx +98 -0
- package/src/web/routes/SettingsPage.tsx +60 -0
- package/src/web/routes/routeTree.tsx +49 -0
- package/src/web/stores/menuStore.ts +78 -0
- package/src/web/stores/paletteStore.ts +25 -0
- package/src/web/stores/sessionStore.ts +303 -0
- package/src/web/stores/sessionsStore.ts +155 -0
- package/src/web/stores/themeStore.ts +52 -0
- package/src/web/stores/threadStore.ts +68 -0
- package/src/web/stores/transientTabsStore.ts +63 -0
- package/src/web/stores/uiStore.ts +28 -0
- package/src/web/stores/useOpenTab.ts +19 -0
- package/src/web/stores/usePluginSlotProps.ts +22 -0
- package/src/web/styles/app.css +15 -0
- package/src/web/styles/webPluginSources.generated.css +1 -0
- package/src/web/vite-env.d.ts +1 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describeSyncDrift, readSyncDrift } from '@agimon-ai/doompi/services';
|
|
5
|
+
|
|
6
|
+
const DOOMPI_PACKAGE = '@agimon-ai/doompi';
|
|
7
|
+
const CLI_SEGMENTS = ['dist', 'bin', 'cli.mjs'];
|
|
8
|
+
const SYNC_ARGS = ['sync'];
|
|
9
|
+
/** How often the watcher re-reads the drift inputs. */
|
|
10
|
+
const WATCH_INTERVAL_MS = 2_000;
|
|
11
|
+
|
|
12
|
+
export interface SyncGuardOptions {
|
|
13
|
+
repoRoot: string;
|
|
14
|
+
onNotice?: (message: string) => void;
|
|
15
|
+
/** Test seam for running the sync itself. */
|
|
16
|
+
runSync?: (repoRoot: string) => Promise<void>;
|
|
17
|
+
/** Test seam for the drift read. */
|
|
18
|
+
readDrift?: (repoRoot: string) => { fresh: boolean; reasons: readonly string[] };
|
|
19
|
+
intervalMs?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SyncGuard {
|
|
23
|
+
/**
|
|
24
|
+
* Syncs the repository when anything a session reads has drifted.
|
|
25
|
+
*
|
|
26
|
+
* Serialized: several sessions starting at once wait on one sync rather
|
|
27
|
+
* than racing to rebuild the same artifacts over each other.
|
|
28
|
+
*/
|
|
29
|
+
ensureSynced(): Promise<void>;
|
|
30
|
+
/** Re-syncs on drift and reports when artifacts changed under a running cockpit. */
|
|
31
|
+
watch(onSynced: () => void): void;
|
|
32
|
+
close(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Runs the launcher's own sync, in its own process so the hub keeps serving. */
|
|
36
|
+
function spawnSync(repoRoot: string, onNotice: (message: string) => void): Promise<void> {
|
|
37
|
+
const cli = path.join(
|
|
38
|
+
path.dirname(createRequire(import.meta.url).resolve(`${DOOMPI_PACKAGE}/package.json`)),
|
|
39
|
+
...CLI_SEGMENTS,
|
|
40
|
+
);
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const child = spawn(process.execPath, [cli, ...SYNC_ARGS], { cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
43
|
+
const tail: string[] = [];
|
|
44
|
+
const keep = (chunk: Buffer): void => {
|
|
45
|
+
tail.push(chunk.toString());
|
|
46
|
+
if (tail.length > 20) tail.shift();
|
|
47
|
+
};
|
|
48
|
+
child.stdout?.on('data', keep);
|
|
49
|
+
child.stderr?.on('data', keep);
|
|
50
|
+
child.once('error', (error) => {
|
|
51
|
+
onNotice(`sync could not start: ${error.message}`);
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
54
|
+
child.once('exit', (code) => {
|
|
55
|
+
// A failed sync is reported, not thrown: the session the caller wanted
|
|
56
|
+
// still starts, and it starts against whatever the last good sync left.
|
|
57
|
+
if (code !== 0) onNotice(`sync exited ${String(code)}: ${tail.join('').trim().slice(-400)}`);
|
|
58
|
+
resolve();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Keeps the repository synced under a running cockpit.
|
|
65
|
+
*
|
|
66
|
+
* A session launched from the browser reads what sync produced: the resolved
|
|
67
|
+
* composition, the plugin bundle, and the package API routes. Nothing forces
|
|
68
|
+
* a person clicking "new session" to have run sync first, and a stale one
|
|
69
|
+
* fails quietly rather than loudly, so the hub checks and syncs itself.
|
|
70
|
+
*/
|
|
71
|
+
export function createSyncGuard(options: SyncGuardOptions): SyncGuard {
|
|
72
|
+
const notice = options.onNotice ?? ((): void => {});
|
|
73
|
+
const runSync = options.runSync ?? ((repoRoot: string) => spawnSync(repoRoot, notice));
|
|
74
|
+
const readDrift = options.readDrift ?? ((repoRoot: string) => readSyncDrift({ repoRoot }));
|
|
75
|
+
let inFlight: Promise<void> | undefined;
|
|
76
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
77
|
+
let closed = false;
|
|
78
|
+
|
|
79
|
+
const syncOnce = async (drift: { reasons: readonly string[] }): Promise<void> => {
|
|
80
|
+
notice(`syncing: ${describeSyncDrift({ fresh: false, reasons: drift.reasons as never })}`);
|
|
81
|
+
await runSync(options.repoRoot);
|
|
82
|
+
notice('sync complete');
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const ensureSynced = async (): Promise<void> => {
|
|
86
|
+
if (closed) return;
|
|
87
|
+
// Joining the run already in flight is what makes concurrent launches
|
|
88
|
+
// wait for one sync instead of starting several.
|
|
89
|
+
if (inFlight) return inFlight;
|
|
90
|
+
const drift = readDrift(options.repoRoot);
|
|
91
|
+
if (drift.fresh) return;
|
|
92
|
+
inFlight = syncOnce(drift).finally(() => {
|
|
93
|
+
inFlight = undefined;
|
|
94
|
+
});
|
|
95
|
+
return inFlight;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
ensureSynced,
|
|
100
|
+
watch(onSynced) {
|
|
101
|
+
if (timer) return;
|
|
102
|
+
timer = setInterval(() => {
|
|
103
|
+
if (closed || inFlight) return;
|
|
104
|
+
const drift = readDrift(options.repoRoot);
|
|
105
|
+
if (drift.fresh) return;
|
|
106
|
+
void ensureSynced().then(() => {
|
|
107
|
+
if (!closed) onSynced();
|
|
108
|
+
});
|
|
109
|
+
}, options.intervalMs ?? WATCH_INTERVAL_MS);
|
|
110
|
+
// The hub should not be held open by its own watcher.
|
|
111
|
+
timer.unref?.();
|
|
112
|
+
},
|
|
113
|
+
close() {
|
|
114
|
+
closed = true;
|
|
115
|
+
if (timer) clearInterval(timer);
|
|
116
|
+
timer = undefined;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { journalFrames, retainNewest } from '../services/journalTail.ts';
|
|
4
|
+
import type { SessionFrame } from '../types/session.ts';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_POLL_MS = 1000;
|
|
7
|
+
/** Journalled messages kept per thread, the same allowance an attach restores. */
|
|
8
|
+
const DEFAULT_RETAIN_LIMIT = 300;
|
|
9
|
+
/** How far back a first read goes into a long journal; older lines stay on disk. */
|
|
10
|
+
const DEFAULT_MAX_INITIAL_BYTES = 8 * 1024 * 1024;
|
|
11
|
+
const JOURNAL_EXTENSION = '.jsonl';
|
|
12
|
+
const NEWLINE = 0x0a;
|
|
13
|
+
|
|
14
|
+
export interface ThreadFrameEvent {
|
|
15
|
+
sessionId: string;
|
|
16
|
+
threadId: string;
|
|
17
|
+
frame: SessionFrame;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ThreadJournalsOptions {
|
|
21
|
+
/** Names the journal of one thread, or undefined while no source knows it yet. */
|
|
22
|
+
resolve(sessionId: string, threadId: string): string | undefined;
|
|
23
|
+
pollMs?: number;
|
|
24
|
+
retainLimit?: number;
|
|
25
|
+
maxInitialBytes?: number;
|
|
26
|
+
onNotice?: (message: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ThreadJournals {
|
|
30
|
+
/** Follows the thread for one more subscriber and returns its retained history, oldest first. */
|
|
31
|
+
subscribe(sessionId: string, threadId: string): SessionFrame[];
|
|
32
|
+
unsubscribe(sessionId: string, threadId: string): void;
|
|
33
|
+
/** Live frames of every followed thread; the return value stops listening. */
|
|
34
|
+
onFrame(listener: (event: ThreadFrameEvent) => void): () => void;
|
|
35
|
+
close(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Tail {
|
|
39
|
+
sessionId: string;
|
|
40
|
+
threadId: string;
|
|
41
|
+
refs: number;
|
|
42
|
+
/** Unknown until a source names the journal; asked again on every tick. */
|
|
43
|
+
path: string | undefined;
|
|
44
|
+
/** The byte after the last complete line read, so a torn line is re-read whole. */
|
|
45
|
+
offset: number;
|
|
46
|
+
retained: SessionFrame[];
|
|
47
|
+
warned: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function keyOf(sessionId: string, threadId: string): string {
|
|
51
|
+
return `${sessionId}\n${threadId}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Bytes [start, end) of a file, or undefined when it cannot be read right now. */
|
|
55
|
+
function readRange(file: string, start: number, end: number): Buffer | undefined {
|
|
56
|
+
const buffer = Buffer.alloc(end - start);
|
|
57
|
+
let fd: number | undefined;
|
|
58
|
+
try {
|
|
59
|
+
fd = fs.openSync(file, 'r');
|
|
60
|
+
const read = fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
61
|
+
return buffer.subarray(0, read);
|
|
62
|
+
} catch {
|
|
63
|
+
// The writer may be rotating or the file may have gone; the next tick looks again.
|
|
64
|
+
return undefined;
|
|
65
|
+
} finally {
|
|
66
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Tails Pi session journals for the pages following them.
|
|
72
|
+
*
|
|
73
|
+
* A thread is any journal a data channel can name for a session, such as a
|
|
74
|
+
* subagent run's own session file. The hub reads only what the file gained
|
|
75
|
+
* since the last look and stops at the last newline, so a line the writer is
|
|
76
|
+
* still appending is never folded half-way; a file that shrank is read from
|
|
77
|
+
* the start again, which is harmless because the page dedupes entries by id.
|
|
78
|
+
*/
|
|
79
|
+
export function createThreadJournals(options: ThreadJournalsOptions): ThreadJournals {
|
|
80
|
+
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
81
|
+
const retainLimit = options.retainLimit ?? DEFAULT_RETAIN_LIMIT;
|
|
82
|
+
const maxInitialBytes = options.maxInitialBytes ?? DEFAULT_MAX_INITIAL_BYTES;
|
|
83
|
+
const notice = options.onNotice ?? ((): void => {});
|
|
84
|
+
const tails = new Map<string, Tail>();
|
|
85
|
+
const listeners = new Set<(event: ThreadFrameEvent) => void>();
|
|
86
|
+
let timer: NodeJS.Timeout | undefined;
|
|
87
|
+
|
|
88
|
+
const journalPathOf = (tail: Tail): string | undefined => {
|
|
89
|
+
const candidate = options.resolve(tail.sessionId, tail.threadId);
|
|
90
|
+
if (candidate === undefined) return undefined;
|
|
91
|
+
if (path.isAbsolute(candidate) && candidate.endsWith(JOURNAL_EXTENSION)) return candidate;
|
|
92
|
+
if (!tail.warned) {
|
|
93
|
+
tail.warned = true;
|
|
94
|
+
notice(`thread ${tail.threadId} of session ${tail.sessionId} names '${candidate}', which is not a journal`);
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** Folds what the journal gained since the last look and returns the new frames. */
|
|
100
|
+
const advance = (tail: Tail): SessionFrame[] => {
|
|
101
|
+
tail.path ??= journalPathOf(tail);
|
|
102
|
+
if (tail.path === undefined) return [];
|
|
103
|
+
let size: number;
|
|
104
|
+
try {
|
|
105
|
+
size = fs.statSync(tail.path).size;
|
|
106
|
+
} catch {
|
|
107
|
+
// Not written yet, or gone: the thread stays empty until the next tick finds it.
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
if (size < tail.offset) {
|
|
111
|
+
tail.offset = 0;
|
|
112
|
+
tail.retained = [];
|
|
113
|
+
}
|
|
114
|
+
let start = tail.offset;
|
|
115
|
+
let cut = false;
|
|
116
|
+
if (start === 0 && size > maxInitialBytes) {
|
|
117
|
+
start = size - maxInitialBytes;
|
|
118
|
+
cut = true;
|
|
119
|
+
}
|
|
120
|
+
if (size <= start) return [];
|
|
121
|
+
const chunk = readRange(tail.path, start, size);
|
|
122
|
+
if (chunk === undefined) return [];
|
|
123
|
+
const lastNewline = chunk.lastIndexOf(NEWLINE);
|
|
124
|
+
if (lastNewline === -1) return [];
|
|
125
|
+
// A cut lands mid-line; the line it tore is dropped rather than misread.
|
|
126
|
+
const firstLine = cut ? chunk.indexOf(NEWLINE) + 1 : 0;
|
|
127
|
+
const frames = firstLine > lastNewline ? [] : journalFrames(chunk.toString('utf8', firstLine, lastNewline + 1));
|
|
128
|
+
tail.offset = start + lastNewline + 1;
|
|
129
|
+
if (frames.length > 0) tail.retained = retainNewest([...tail.retained, ...frames], retainLimit);
|
|
130
|
+
return frames;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const tick = (): void => {
|
|
134
|
+
for (const tail of tails.values()) {
|
|
135
|
+
const frames = advance(tail);
|
|
136
|
+
for (const frame of frames) {
|
|
137
|
+
for (const listener of listeners) listener({ sessionId: tail.sessionId, threadId: tail.threadId, frame });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const syncTimer = (): void => {
|
|
143
|
+
if (tails.size > 0 && timer === undefined) timer = setInterval(tick, pollMs);
|
|
144
|
+
if (tails.size === 0 && timer !== undefined) {
|
|
145
|
+
clearInterval(timer);
|
|
146
|
+
timer = undefined;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
subscribe(sessionId, threadId) {
|
|
152
|
+
const key = keyOf(sessionId, threadId);
|
|
153
|
+
let tail = tails.get(key);
|
|
154
|
+
if (tail === undefined) {
|
|
155
|
+
tail = { sessionId, threadId, refs: 0, path: undefined, offset: 0, retained: [], warned: false };
|
|
156
|
+
tails.set(key, tail);
|
|
157
|
+
advance(tail);
|
|
158
|
+
syncTimer();
|
|
159
|
+
}
|
|
160
|
+
tail.refs += 1;
|
|
161
|
+
return [...tail.retained];
|
|
162
|
+
},
|
|
163
|
+
unsubscribe(sessionId, threadId) {
|
|
164
|
+
const key = keyOf(sessionId, threadId);
|
|
165
|
+
const tail = tails.get(key);
|
|
166
|
+
if (tail === undefined) return;
|
|
167
|
+
tail.refs -= 1;
|
|
168
|
+
if (tail.refs <= 0) tails.delete(key);
|
|
169
|
+
syncTimer();
|
|
170
|
+
},
|
|
171
|
+
onFrame(listener) {
|
|
172
|
+
listeners.add(listener);
|
|
173
|
+
return () => {
|
|
174
|
+
listeners.delete(listener);
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
close() {
|
|
178
|
+
if (timer !== undefined) clearInterval(timer);
|
|
179
|
+
timer = undefined;
|
|
180
|
+
tails.clear();
|
|
181
|
+
listeners.clear();
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { PLUGIN_ROOTS_FILE } from '../services/webDevRoots.ts';
|
|
4
|
+
import { SERVER_REGISTRY_FILE, webHostPackageRoot, writeSyncWebPluginModules } from './webPluginGenerate.ts';
|
|
5
|
+
import { scanWebPlugins } from './webPluginScan.ts';
|
|
6
|
+
import { webPluginCssAlias, webPluginOverridePlugin } from './webPluginVite.ts';
|
|
7
|
+
|
|
8
|
+
/** Shared runtime packages every plugin's client code must resolve to the host copies. */
|
|
9
|
+
const DEDUPED_RUNTIMES = [
|
|
10
|
+
'react',
|
|
11
|
+
'react-dom',
|
|
12
|
+
'@tanstack/store',
|
|
13
|
+
'@tanstack/react-store',
|
|
14
|
+
'@agimon-ai/doompi-web-components',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export interface BundleCockpitWebOptions {
|
|
18
|
+
/** Package roots to scan for doompiWeb manifests, usually the installed composition. */
|
|
19
|
+
pluginRoots: readonly string[];
|
|
20
|
+
/** Output directory; receives web/ (the bundle), generated/ (the entry modules), and pluginRoots.json. */
|
|
21
|
+
outDir: string;
|
|
22
|
+
onNotice?: (message: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BundleCockpitWebResult {
|
|
26
|
+
/** The assets directory to serve (contains index.html and the server registry). */
|
|
27
|
+
assetsDir: string;
|
|
28
|
+
pluginIds: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Builds the cockpit SPA against the installed plugin set.
|
|
33
|
+
*
|
|
34
|
+
* This is what `doompi sync` calls: manifests come from the given package
|
|
35
|
+
* roots (no hardcoded plugin dependencies anywhere), the generated entry
|
|
36
|
+
* modules are aliased over the committed builtin registry, and Vite compiles
|
|
37
|
+
* everything (host shell plus each plugin's shipped web source) into one
|
|
38
|
+
* bundle. The server registry written next to the assets tells the hub which
|
|
39
|
+
* built hub entries to load, and the roots file beside the bundle lets the
|
|
40
|
+
* dev server serve the same composition with hot reload.
|
|
41
|
+
*/
|
|
42
|
+
export async function bundleCockpitWeb(options: BundleCockpitWebOptions): Promise<BundleCockpitWebResult> {
|
|
43
|
+
const notice = options.onNotice ?? ((): void => {});
|
|
44
|
+
const hostRoot = webHostPackageRoot();
|
|
45
|
+
const plugins = scanWebPlugins(hostRoot, options.pluginRoots, notice);
|
|
46
|
+
notice(
|
|
47
|
+
`bundling the cockpit with ${String(plugins.length)} web plugin(s): ${plugins.map((p) => p.pluginId).join(', ')}`,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const generatedDir = path.join(options.outDir, 'generated');
|
|
51
|
+
const assetsDir = path.join(options.outDir, 'web');
|
|
52
|
+
const generated = writeSyncWebPluginModules(plugins, generatedDir);
|
|
53
|
+
const roots = [...new Set(options.pluginRoots.map((root) => path.resolve(root)))];
|
|
54
|
+
fs.writeFileSync(path.join(options.outDir, PLUGIN_ROOTS_FILE), `${JSON.stringify(roots, null, 2)}\n`);
|
|
55
|
+
|
|
56
|
+
// Vite and its plugins are regular dependencies so this works from an
|
|
57
|
+
// installed package, not only the workspace.
|
|
58
|
+
const { build } = await import('vite');
|
|
59
|
+
const { default: react } = await import('@vitejs/plugin-react');
|
|
60
|
+
const { default: tailwindcss } = await import('@tailwindcss/vite');
|
|
61
|
+
|
|
62
|
+
const clientRoot = path.join(hostRoot, 'src', 'web');
|
|
63
|
+
|
|
64
|
+
await build({
|
|
65
|
+
configFile: false,
|
|
66
|
+
envDir: false,
|
|
67
|
+
logLevel: 'warn',
|
|
68
|
+
root: clientRoot,
|
|
69
|
+
plugins: [webPluginOverridePlugin(clientRoot, generated), react(), tailwindcss()],
|
|
70
|
+
resolve: {
|
|
71
|
+
dedupe: [...DEDUPED_RUNTIMES],
|
|
72
|
+
alias: [webPluginCssAlias(generated)],
|
|
73
|
+
},
|
|
74
|
+
build: {
|
|
75
|
+
outDir: assetsDir,
|
|
76
|
+
emptyOutDir: true,
|
|
77
|
+
sourcemap: false,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
fs.writeFileSync(path.join(assetsDir, SERVER_REGISTRY_FILE), generated.serverRegistry);
|
|
82
|
+
return { assetsDir, pluginIds: plugins.map((plugin) => plugin.pluginId) };
|
|
83
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import type { WebHubChannel } from '@agimon-ai/doompi-web-contracts';
|
|
5
|
+
import { SERVER_REGISTRY_FILE } from './webPluginGenerate.ts';
|
|
6
|
+
import { BUILTIN_HUB_CHANNELS } from './webHubPlugins.generated.ts';
|
|
7
|
+
|
|
8
|
+
interface RegistryEntry {
|
|
9
|
+
pluginId?: unknown;
|
|
10
|
+
hubEntry?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const UNKNOWN_PLUGIN_ID = 'unknown';
|
|
14
|
+
|
|
15
|
+
/** The synced bundle's server registry, or an empty list when there is none to read. */
|
|
16
|
+
function registryEntries(assetsDir: string, onNotice: (message: string) => void): RegistryEntry[] {
|
|
17
|
+
const registryPath = path.join(assetsDir, SERVER_REGISTRY_FILE);
|
|
18
|
+
if (!fs.existsSync(registryPath)) return [];
|
|
19
|
+
try {
|
|
20
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
21
|
+
return Array.isArray(parsed) ? (parsed as RegistryEntry[]) : [];
|
|
22
|
+
} catch (error) {
|
|
23
|
+
onNotice(`web plugin registry ${registryPath} is unreadable (${String(error)}); plugin channels skipped`);
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Assembles the hub's data channels for the assets being served.
|
|
30
|
+
*
|
|
31
|
+
* A synced bundle carries webPlugins.server.json naming each plugin's built
|
|
32
|
+
* hub entry by absolute path; those are imported lazily so an absent or
|
|
33
|
+
* broken plugin package is a notice and an empty tab, never a crash. Without
|
|
34
|
+
* a registry (the package's own prebuilt bundle, or a custom assets dir) the
|
|
35
|
+
* built-in channels are the whole set.
|
|
36
|
+
*/
|
|
37
|
+
export async function loadHubChannels(
|
|
38
|
+
assetsDir: string,
|
|
39
|
+
onNotice: (message: string) => void,
|
|
40
|
+
): Promise<WebHubChannel[]> {
|
|
41
|
+
const channels: WebHubChannel[] = [...BUILTIN_HUB_CHANNELS];
|
|
42
|
+
for (const entry of registryEntries(assetsDir, onNotice)) {
|
|
43
|
+
const pluginId = typeof entry.pluginId === 'string' ? entry.pluginId : UNKNOWN_PLUGIN_ID;
|
|
44
|
+
if (typeof entry.hubEntry !== 'string') continue;
|
|
45
|
+
try {
|
|
46
|
+
const module = (await import(pathToFileURL(entry.hubEntry).href)) as { webHubChannels?: unknown };
|
|
47
|
+
if (!Array.isArray(module.webHubChannels)) {
|
|
48
|
+
throw new Error(`'${entry.hubEntry}' exports no webHubChannels array`);
|
|
49
|
+
}
|
|
50
|
+
channels.push(...(module.webHubChannels as WebHubChannel[]));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
53
|
+
onNotice(`web plugin '${pluginId}' hub channels unavailable (${cause}); its panels stay empty`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const seen = new Set<string>();
|
|
57
|
+
const unique: WebHubChannel[] = [];
|
|
58
|
+
for (const channel of channels) {
|
|
59
|
+
if (seen.has(channel.frameType)) {
|
|
60
|
+
onNotice(`duplicate web channel '${channel.frameType}' dropped; frame types are global`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
seen.add(channel.frameType);
|
|
64
|
+
unique.push(channel);
|
|
65
|
+
}
|
|
66
|
+
return unique;
|
|
67
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import type { DeclaredWebPlugin } from '../services/webPluginManifest.ts';
|
|
5
|
+
import { scanWebPlugins } from './webPluginScan.ts';
|
|
6
|
+
|
|
7
|
+
const HEADER = '// Generated by src/adapters/webPluginGenerate.ts. Do not edit by hand.';
|
|
8
|
+
|
|
9
|
+
/** Walks up to the owning package.json: correct from src and from the unbundled dist tree alike. */
|
|
10
|
+
function ownPackageRoot(): string {
|
|
11
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
for (;;) {
|
|
13
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
|
|
14
|
+
const parent = path.dirname(dir);
|
|
15
|
+
if (parent === dir) throw new Error('doompi-web could not locate its own package root.');
|
|
16
|
+
dir = parent;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const packageRoot = ownPackageRoot();
|
|
21
|
+
|
|
22
|
+
export const BUILTIN_CLIENT_MODULE = path.join('src', 'web', 'app', 'webPlugins.generated.ts');
|
|
23
|
+
export const BUILTIN_HUB_MODULE = path.join('src', 'adapters', 'webHubPlugins.generated.ts');
|
|
24
|
+
export const BUILTIN_CSS_MODULE = path.join('src', 'web', 'styles', 'webPluginSources.generated.css');
|
|
25
|
+
/** The server-side registry a synced bundle carries next to its assets. */
|
|
26
|
+
export const SERVER_REGISTRY_FILE = 'webPlugins.server.json';
|
|
27
|
+
|
|
28
|
+
export function webHostPackageRoot(): string {
|
|
29
|
+
return packageRoot;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function camel(pluginId: string): string {
|
|
33
|
+
return pluginId.replace(/-([a-z0-9])/g, (_, ch: string) => ch.toUpperCase());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function relativeImport(fromDir: string, toFile: string): string {
|
|
37
|
+
const spec = path.relative(fromDir, toFile).split(path.sep).join('/');
|
|
38
|
+
return spec.startsWith('.') ? spec : `./${spec}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderClientModule(plugins: readonly DeclaredWebPlugin[], style: 'relative' | 'absolute'): string {
|
|
42
|
+
const lines = [HEADER, "import type { WebPluginDefinition } from '@agimon-ai/doompi-web-contracts';"];
|
|
43
|
+
const names: string[] = [];
|
|
44
|
+
const fromDir = path.join(packageRoot, 'src', 'web', 'app');
|
|
45
|
+
for (const plugin of plugins) {
|
|
46
|
+
const name = `${camel(plugin.pluginId)}Plugin`;
|
|
47
|
+
names.push(name);
|
|
48
|
+
const entry = path.join(plugin.packageDir, plugin.client.entry);
|
|
49
|
+
// The committed builtin module keeps machine-neutral relative imports; the
|
|
50
|
+
// sync-generated module lives outside src, is aliased over the committed
|
|
51
|
+
// path, and resolves imports against its real location, so every entry
|
|
52
|
+
// becomes an absolute file URL into the installed packages.
|
|
53
|
+
const specifier = style === 'relative' ? relativeImport(fromDir, entry) : pathToFileURL(entry).href;
|
|
54
|
+
lines.push(`import { webPlugin as ${name} } from '${specifier}';`);
|
|
55
|
+
}
|
|
56
|
+
lines.push('', `export const webPlugins: readonly WebPluginDefinition[] = [${names.join(', ')}];`, '');
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renderBuiltinHubModule(plugins: readonly DeclaredWebPlugin[]): string {
|
|
61
|
+
const lines = [HEADER, "import type { WebHubChannel } from '@agimon-ai/doompi-web-contracts';"];
|
|
62
|
+
const names: string[] = [];
|
|
63
|
+
for (const plugin of plugins) {
|
|
64
|
+
if (plugin.hub === undefined || !plugin.isHost) continue;
|
|
65
|
+
const name = `${camel(plugin.pluginId)}Channels`;
|
|
66
|
+
names.push(name);
|
|
67
|
+
const specifier = relativeImport(
|
|
68
|
+
path.join(packageRoot, 'src', 'adapters'),
|
|
69
|
+
path.join(plugin.packageDir, plugin.hub.entry),
|
|
70
|
+
);
|
|
71
|
+
lines.push(`import { webHubChannels as ${name} } from '${specifier}';`);
|
|
72
|
+
}
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push(
|
|
75
|
+
names.length === 0
|
|
76
|
+
? 'export const BUILTIN_HUB_CHANNELS: readonly WebHubChannel[] = [];'
|
|
77
|
+
: `export const BUILTIN_HUB_CHANNELS: readonly WebHubChannel[] = [${names.map((name) => `...${name}`).join(', ')}];`,
|
|
78
|
+
);
|
|
79
|
+
lines.push('');
|
|
80
|
+
return lines.join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function renderCssModule(plugins: readonly DeclaredWebPlugin[]): string {
|
|
84
|
+
const lines = [`/* ${HEADER.slice(3)} */`];
|
|
85
|
+
for (const plugin of plugins) {
|
|
86
|
+
if (plugin.isHost) continue; // The host tree is already inside Tailwind's scan root.
|
|
87
|
+
lines.push(`@source "${path.dirname(path.join(plugin.packageDir, plugin.client.entry))}";`);
|
|
88
|
+
}
|
|
89
|
+
lines.push('');
|
|
90
|
+
return lines.join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ServerRegistryEntry {
|
|
94
|
+
pluginId: string;
|
|
95
|
+
channels: string[];
|
|
96
|
+
/** Absolute path to the built hub entry exporting webHubChannels. */
|
|
97
|
+
hubEntry: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderServerRegistry(plugins: readonly DeclaredWebPlugin[]): string {
|
|
101
|
+
const entries: ServerRegistryEntry[] = [];
|
|
102
|
+
for (const plugin of plugins) {
|
|
103
|
+
if (plugin.hub === undefined) continue;
|
|
104
|
+
// Host channels are compiled into the server; only externals need loading.
|
|
105
|
+
if (plugin.isHost) continue;
|
|
106
|
+
entries.push({
|
|
107
|
+
pluginId: plugin.pluginId,
|
|
108
|
+
channels: plugin.channels,
|
|
109
|
+
hubEntry: path.join(plugin.packageDir, plugin.hub.dist ?? plugin.hub.entry),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return `${JSON.stringify(entries, null, 2)}\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The committed builtin registry, rendered from the host package alone. */
|
|
116
|
+
export function renderBuiltinWebPluginModules(): Map<string, string> {
|
|
117
|
+
const plugins = scanWebPlugins(packageRoot);
|
|
118
|
+
return new Map([
|
|
119
|
+
[BUILTIN_CLIENT_MODULE, renderClientModule(plugins, 'relative')],
|
|
120
|
+
[BUILTIN_HUB_MODULE, renderBuiltinHubModule(plugins)],
|
|
121
|
+
[BUILTIN_CSS_MODULE, renderCssModule(plugins)],
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Keeps the committed builtin registry fresh: write-if-changed locally,
|
|
127
|
+
* check-only (throw) in CI. Both vite.config.ts and tsdown.config.ts call
|
|
128
|
+
* this at config-load time.
|
|
129
|
+
*/
|
|
130
|
+
export function ensureBuiltinWebPluginModules({ check = false }: { check?: boolean } = {}): void {
|
|
131
|
+
const stale: string[] = [];
|
|
132
|
+
for (const [relativePath, content] of renderBuiltinWebPluginModules()) {
|
|
133
|
+
const filePath = path.join(packageRoot, relativePath);
|
|
134
|
+
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : undefined;
|
|
135
|
+
if (existing === content) continue;
|
|
136
|
+
if (check) {
|
|
137
|
+
stale.push(relativePath);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
141
|
+
fs.writeFileSync(filePath, content);
|
|
142
|
+
}
|
|
143
|
+
if (stale.length > 0) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Stale generated web plugin modules: ${stale.join(', ')}. Run pnpm build in doompi-web and commit them.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface SyncGeneratedModules {
|
|
151
|
+
clientModulePath: string;
|
|
152
|
+
cssModulePath: string;
|
|
153
|
+
serverRegistry: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The sync-time variant: renders the full plugin set (host built-ins plus the
|
|
158
|
+
* installed composition's packages) into a generation directory the bundler
|
|
159
|
+
* aliases over the committed builtin modules, plus the server-side registry
|
|
160
|
+
* the hub loads at startup.
|
|
161
|
+
*/
|
|
162
|
+
export function writeSyncWebPluginModules(
|
|
163
|
+
plugins: readonly DeclaredWebPlugin[],
|
|
164
|
+
generatedDir: string,
|
|
165
|
+
): SyncGeneratedModules {
|
|
166
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
167
|
+
const clientModulePath = path.join(generatedDir, 'webPlugins.generated.ts');
|
|
168
|
+
const cssModulePath = path.join(generatedDir, 'webPluginSources.generated.css');
|
|
169
|
+
fs.writeFileSync(clientModulePath, renderClientModule(plugins, 'absolute'));
|
|
170
|
+
fs.writeFileSync(cssModulePath, renderCssModule(plugins));
|
|
171
|
+
return { clientModulePath, cssModulePath, serverRegistry: renderServerRegistry(plugins) };
|
|
172
|
+
}
|