@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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="DoomPi">
|
|
2
|
+
<rect width="32" height="32" rx="7" fill="#1c1f24"/>
|
|
3
|
+
<g fill="#ecbe7b">
|
|
4
|
+
<path d="M7.0 10.0 Q6.2 4.2 9.3 3.0 Q10.4 6.3 12.1 9.6 Z"/>
|
|
5
|
+
<path d="M25.0 10.0 Q25.8 4.2 22.7 3.0 Q21.6 6.3 19.9 9.6 Z"/>
|
|
6
|
+
</g>
|
|
7
|
+
<g fill="#c678dd">
|
|
8
|
+
<rect x="3.2" y="8.6" width="25.6" height="7.6" rx="3.4"/>
|
|
9
|
+
<rect x="6.9" y="14.0" width="5.5" height="13.0" rx="2.5"/>
|
|
10
|
+
<rect x="19.6" y="14.0" width="5.5" height="13.0" rx="2.5"/>
|
|
11
|
+
</g>
|
|
12
|
+
<g fill="#1c1f24">
|
|
13
|
+
<circle cx="10.9" cy="12.3" r="1.9"/>
|
|
14
|
+
<circle cx="21.1" cy="12.3" r="1.9"/>
|
|
15
|
+
</g>
|
|
16
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
7
|
+
<link rel="alternate icon" href="/favicon.ico" sizes="48x48 32x32 16x16" />
|
|
8
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
9
|
+
<meta name="theme-color" content="#1c1f24" />
|
|
10
|
+
<title>DoomPi cockpit</title>
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-DQO7NTIP.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cg52ngKv.css">
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div id="root"></div>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
package/llms.txt
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agimon-ai/doompi-web",
|
|
3
|
+
"version": "0.0.1-alpha.0",
|
|
4
|
+
"description": "Web cockpit for DoomPi: serves a browser session console and bridges it to an authenticated doompi-server socket",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cockpit",
|
|
7
|
+
"coding-agent",
|
|
8
|
+
"doompi",
|
|
9
|
+
"pi-coding-agent",
|
|
10
|
+
"session-console",
|
|
11
|
+
"web"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://agimon.ai",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/AgiFlow/doompi/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
18
|
+
"author": "Vuong Ngo",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/AgiFlow/doompi.git",
|
|
22
|
+
"directory": "packages/clients/doompi-web"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"doompi-web": "./dist/bin/serve.mjs"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"src",
|
|
30
|
+
"llms.txt",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"package.json"
|
|
34
|
+
],
|
|
35
|
+
"type": "module",
|
|
36
|
+
"main": "./dist/index.cjs",
|
|
37
|
+
"types": "./dist/index.d.mts",
|
|
38
|
+
"jsnext:main": "./dist/index.mjs",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/index.d.mts",
|
|
42
|
+
"import": "./dist/index.mjs",
|
|
43
|
+
"require": "./dist/index.cjs"
|
|
44
|
+
},
|
|
45
|
+
"./bundler": {
|
|
46
|
+
"types": "./dist/bundler.d.mts",
|
|
47
|
+
"import": "./dist/bundler.mjs",
|
|
48
|
+
"require": "./dist/bundler.cjs"
|
|
49
|
+
},
|
|
50
|
+
"./package.json": "./package.json"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@earendil-works/pi-client": "0.84.3",
|
|
57
|
+
"@earendil-works/pi-coding-agent": "0.84.3",
|
|
58
|
+
"@earendil-works/pi-protocol": "0.84.3",
|
|
59
|
+
"@earendil-works/pi-server": "0.84.3",
|
|
60
|
+
"@hono/node-server": "2.1.1",
|
|
61
|
+
"@hono/node-ws": "1.3.1",
|
|
62
|
+
"@tailwindcss/vite": "4.3.3",
|
|
63
|
+
"@tanstack/react-router": "1.170.32",
|
|
64
|
+
"@tanstack/react-store": "0.11.1",
|
|
65
|
+
"@tanstack/store": "0.11.1",
|
|
66
|
+
"@vitejs/plugin-react": "6.1.0",
|
|
67
|
+
"hono": "4.13.3",
|
|
68
|
+
"react": "19.2.8",
|
|
69
|
+
"react-dom": "19.2.8",
|
|
70
|
+
"react-markdown": "10.1.0",
|
|
71
|
+
"remark-gfm": "4.0.1",
|
|
72
|
+
"tailwindcss": "4.3.3",
|
|
73
|
+
"vite": "8.2.2",
|
|
74
|
+
"@agimon-ai/doompi": "0.0.1-alpha.39",
|
|
75
|
+
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.39",
|
|
76
|
+
"@agimon-ai/doompi-server": "0.0.1-alpha.2",
|
|
77
|
+
"@agimon-ai/doompi-web-components": "0.0.1-alpha.0",
|
|
78
|
+
"@agimon-ai/doompi-web-contracts": "0.0.1-alpha.2"
|
|
79
|
+
},
|
|
80
|
+
"devDependencies": {
|
|
81
|
+
"@playwright/test": "1.62.1",
|
|
82
|
+
"@types/node": "26.2.0",
|
|
83
|
+
"@types/react": "19.2.7",
|
|
84
|
+
"@types/react-dom": "19.2.4",
|
|
85
|
+
"@types/ws": "8.18.1",
|
|
86
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
87
|
+
"tsdown": "0.22.14",
|
|
88
|
+
"typescript": "7.0.2",
|
|
89
|
+
"vitest": "4.1.11",
|
|
90
|
+
"ws": "8.21.3",
|
|
91
|
+
"@agimon-ai/vibe-lint-plugin-doom-web": "0.0.1-alpha.36"
|
|
92
|
+
},
|
|
93
|
+
"engines": {
|
|
94
|
+
"node": ">=22.19.0"
|
|
95
|
+
},
|
|
96
|
+
"doompiWeb": [],
|
|
97
|
+
"scripts": {
|
|
98
|
+
"build": "tsdown && vite build",
|
|
99
|
+
"dev": "vite",
|
|
100
|
+
"test": "vitest --run",
|
|
101
|
+
"test:e2e": "playwright test",
|
|
102
|
+
"typecheck": "tsc --noEmit",
|
|
103
|
+
"lint": "oxlint . && oxfmt . --check",
|
|
104
|
+
"fixcode": "oxlint . --fix && oxfmt ."
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import { AUTH_LOGINS_API_ROUTE, AUTH_PROVIDERS_API_ROUTE, type AuthMethodType } from '../types/auth.ts';
|
|
3
|
+
import type { ProviderAuth } from './providerAuth.ts';
|
|
4
|
+
|
|
5
|
+
const METHOD_TYPES: readonly AuthMethodType[] = ['api_key', 'oauth'];
|
|
6
|
+
|
|
7
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
8
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isMethodType(value: unknown): value is AuthMethodType {
|
|
12
|
+
return typeof value === 'string' && (METHOD_TYPES as readonly string[]).includes(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function describe(error: unknown): string {
|
|
16
|
+
return error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The provider-auth REST surface. Every handler that reaches the Pi runtime
|
|
21
|
+
* turns a load failure into 502 with the cause, so a machine without a usable
|
|
22
|
+
* Pi install explains itself on the providers page instead of hanging it.
|
|
23
|
+
*/
|
|
24
|
+
export function registerAuthRoutes(app: Hono, auth: ProviderAuth): void {
|
|
25
|
+
app.get(AUTH_PROVIDERS_API_ROUTE, async (context) => {
|
|
26
|
+
try {
|
|
27
|
+
return context.json({ providers: await auth.listProviders() });
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return context.json({ error: `Provider auth is unavailable: ${describe(error)}` }, 502);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
app.delete(`${AUTH_PROVIDERS_API_ROUTE}/:providerId`, async (context) => {
|
|
34
|
+
const providerId = context.req.param('providerId');
|
|
35
|
+
try {
|
|
36
|
+
const outcome = await auth.logout(providerId);
|
|
37
|
+
if (outcome.ok) return context.json({ providerId });
|
|
38
|
+
return context.json({ error: outcome.error }, outcome.code === 'unknown_provider' ? 404 : 502);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return context.json({ error: `Provider auth is unavailable: ${describe(error)}` }, 502);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
app.post(AUTH_LOGINS_API_ROUTE, async (context) => {
|
|
45
|
+
let body: unknown;
|
|
46
|
+
try {
|
|
47
|
+
body = await context.req.json();
|
|
48
|
+
} catch {
|
|
49
|
+
return context.json({ error: 'The request body must be JSON.' }, 400);
|
|
50
|
+
}
|
|
51
|
+
if (!isRecord(body) || typeof body.providerId !== 'string' || body.providerId === '') {
|
|
52
|
+
return context.json({ error: 'A providerId string is required.' }, 400);
|
|
53
|
+
}
|
|
54
|
+
if (!isMethodType(body.type)) {
|
|
55
|
+
return context.json({ error: `type must be one of ${METHOD_TYPES.join(', ')}.` }, 400);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const outcome = await auth.startLogin(body.providerId, body.type);
|
|
59
|
+
if (outcome.ok) return context.json({ flow: outcome.flow }, 201);
|
|
60
|
+
const status = outcome.code === 'unknown_provider' ? 404 : outcome.code === 'busy' ? 409 : 400;
|
|
61
|
+
return context.json({ error: outcome.error }, status);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return context.json({ error: `Provider auth is unavailable: ${describe(error)}` }, 502);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
app.get(`${AUTH_LOGINS_API_ROUTE}/:flowId`, (context) => {
|
|
68
|
+
const flow = auth.getLogin(context.req.param('flowId'));
|
|
69
|
+
if (!flow) return context.json({ error: 'No such login flow.' }, 404);
|
|
70
|
+
return context.json({ flow });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
app.post(`${AUTH_LOGINS_API_ROUTE}/:flowId/answer`, async (context) => {
|
|
74
|
+
const flowId = context.req.param('flowId');
|
|
75
|
+
let body: unknown;
|
|
76
|
+
try {
|
|
77
|
+
body = await context.req.json();
|
|
78
|
+
} catch {
|
|
79
|
+
return context.json({ error: 'The request body must be JSON.' }, 400);
|
|
80
|
+
}
|
|
81
|
+
if (!isRecord(body) || typeof body.promptId !== 'string' || typeof body.value !== 'string') {
|
|
82
|
+
return context.json({ error: 'promptId and value strings are required.' }, 400);
|
|
83
|
+
}
|
|
84
|
+
const outcome = auth.answerLogin(flowId, body.promptId, body.value);
|
|
85
|
+
if (outcome === 'unknown_flow') return context.json({ error: 'No such login flow.' }, 404);
|
|
86
|
+
if (outcome === 'not_waiting') return context.json({ error: 'The flow is not waiting on that prompt.' }, 409);
|
|
87
|
+
return context.json({ flow: auth.getLogin(flowId) });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
app.delete(`${AUTH_LOGINS_API_ROUTE}/:flowId`, (context) => {
|
|
91
|
+
const flow = auth.cancelLogin(context.req.param('flowId'));
|
|
92
|
+
if (!flow) return context.json({ error: 'No such login flow.' }, 404);
|
|
93
|
+
return context.json({ flow }, 202);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const AGENT_COMMAND_ENV = 'DOOMPI_AGENT_COMMAND';
|
|
7
|
+
const WEB_MODULE_ENV = 'DOOMPI_WEB_MODULE';
|
|
8
|
+
const REPOSITORY_CLI = ['node_modules', '@agimon-ai', 'doompi', 'dist', 'bin', 'cli.mjs'];
|
|
9
|
+
|
|
10
|
+
export interface BundledServerLaunch {
|
|
11
|
+
readonly command: string;
|
|
12
|
+
/** Leading arguments the command needs; a caller appends its own after these. */
|
|
13
|
+
readonly args: readonly string[];
|
|
14
|
+
readonly environment: NodeJS.ProcessEnv;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type FileExists = (file: string) => boolean;
|
|
18
|
+
|
|
19
|
+
function packageFile(packageName: string, segments: readonly string[], parentUrl: string): string {
|
|
20
|
+
const manifest = createRequire(parentUrl).resolve(`${packageName}/package.json`);
|
|
21
|
+
return path.join(path.dirname(manifest), ...segments);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Finds the DoomPi CLI pinned by the repository containing the requested session. */
|
|
25
|
+
export function repositoryDoomPiCli(cwd: string, exists: FileExists = fs.existsSync): string | undefined {
|
|
26
|
+
for (let directory = path.resolve(cwd); ; directory = path.dirname(directory)) {
|
|
27
|
+
const cli = path.join(directory, ...REPOSITORY_CLI);
|
|
28
|
+
if (exists(cli)) return cli;
|
|
29
|
+
if (directory === path.dirname(directory)) return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolves the Server and fallback agent shipped by a DoomPi Web installation.
|
|
35
|
+
*
|
|
36
|
+
* This is what makes `doompi-web` need no flags: the hub launches the
|
|
37
|
+
* doompi-server sitting in its own dependency tree rather than whichever one
|
|
38
|
+
* PATH happens to name, and points it at the agent from the same install, so a
|
|
39
|
+
* checkout runs its own build. A repository that pins its own DoomPi still
|
|
40
|
+
* wins, since a session created there should run that repository's agent.
|
|
41
|
+
*/
|
|
42
|
+
export function defaultServerLaunch(
|
|
43
|
+
cwd: string,
|
|
44
|
+
environment: NodeJS.ProcessEnv,
|
|
45
|
+
parentUrl: string = import.meta.url,
|
|
46
|
+
exists: FileExists = fs.existsSync,
|
|
47
|
+
): BundledServerLaunch {
|
|
48
|
+
const server = packageFile('@agimon-ai/doompi-server', ['dist', 'bin', 'serve.mjs'], parentUrl);
|
|
49
|
+
const bundledAgent = packageFile('@agimon-ai/doompi', ['dist', 'bin', 'cli.mjs'], parentUrl);
|
|
50
|
+
const bundledWeb = packageFile('@agimon-ai/doompi-web', ['dist', 'index.mjs'], parentUrl);
|
|
51
|
+
const configuredAgent = environment[AGENT_COMMAND_ENV];
|
|
52
|
+
const localAgent = repositoryDoomPiCli(cwd, exists);
|
|
53
|
+
const agentEnvironment =
|
|
54
|
+
configuredAgent || localAgent ? { ...environment } : { ...environment, [AGENT_COMMAND_ENV]: bundledAgent };
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
command: process.execPath,
|
|
58
|
+
args: [server],
|
|
59
|
+
environment: {
|
|
60
|
+
...agentEnvironment,
|
|
61
|
+
[WEB_MODULE_ENV]: environment[WEB_MODULE_ENV] || pathToFileURL(bundledWeb).href,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { rankDirectories, searchTermFor } from '../services/directoryMatch.ts';
|
|
5
|
+
import { searchDirectoryTree } from './directorySearch.ts';
|
|
6
|
+
|
|
7
|
+
const PATH_SEPARATOR = '/';
|
|
8
|
+
const HIDDEN_PREFIX = '.';
|
|
9
|
+
/** A pattern that starts with a dot, literal or escaped, anchored or not, is asking for dotdirs. */
|
|
10
|
+
const ASKS_FOR_HIDDEN = /^\^?\\?\./;
|
|
11
|
+
/** The picker shows a handful; a longer list is a sign to type more. */
|
|
12
|
+
const DEFAULT_LIMIT = 12;
|
|
13
|
+
|
|
14
|
+
export interface DirectoryQuery {
|
|
15
|
+
/** The directory whose children are listed. */
|
|
16
|
+
parent: string;
|
|
17
|
+
/** The typed trailing segment, applied to child names as a regular expression. */
|
|
18
|
+
pattern: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Splits a typed path into the directory to list and the filter for its
|
|
23
|
+
* children: everything up to the last slash is the parent, the rest is the
|
|
24
|
+
* pattern. Only absolute paths qualify, matching what a session accepts.
|
|
25
|
+
*/
|
|
26
|
+
export function splitDirectoryQuery(typed: string): DirectoryQuery | undefined {
|
|
27
|
+
if (!path.isAbsolute(typed)) return undefined;
|
|
28
|
+
const cut = typed.lastIndexOf(PATH_SEPARATOR);
|
|
29
|
+
return { parent: cut === 0 ? PATH_SEPARATOR : typed.slice(0, cut), pattern: typed.slice(cut + 1) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nameMatcher(pattern: string): (name: string) => boolean {
|
|
33
|
+
if (pattern === '') return () => true;
|
|
34
|
+
try {
|
|
35
|
+
const regex = new RegExp(pattern, 'i');
|
|
36
|
+
return (name) => regex.test(name);
|
|
37
|
+
} catch {
|
|
38
|
+
// A pattern still being typed ("src/[") is not a regex yet; substring
|
|
39
|
+
// matching keeps the list narrowing instead of going blank.
|
|
40
|
+
const lowered = pattern.toLowerCase();
|
|
41
|
+
return (name) => name.toLowerCase().includes(lowered);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function isDirectory(parent: string, entry: fs.Dirent): Promise<boolean> {
|
|
46
|
+
if (!entry.isSymbolicLink()) return entry.isDirectory();
|
|
47
|
+
try {
|
|
48
|
+
return (await fs.promises.stat(path.join(parent, entry.name))).isDirectory();
|
|
49
|
+
} catch {
|
|
50
|
+
return false; // A dangling link is not somewhere a session can start.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Absolute paths of the directories the typed path could complete to, for
|
|
56
|
+
* the new-session picker. Hidden directories stay out of the way unless the
|
|
57
|
+
* pattern itself begins with a dot; an unreadable or missing parent yields
|
|
58
|
+
* nothing rather than an error, since the person is still typing.
|
|
59
|
+
*/
|
|
60
|
+
export async function listDirectories(typed: string, limit = DEFAULT_LIMIT): Promise<string[]> {
|
|
61
|
+
const query = splitDirectoryQuery(typed);
|
|
62
|
+
if (query === undefined) return [];
|
|
63
|
+
let entries: fs.Dirent[];
|
|
64
|
+
try {
|
|
65
|
+
entries = await fs.promises.readdir(query.parent, { withFileTypes: true });
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
const showHidden = ASKS_FOR_HIDDEN.test(query.pattern);
|
|
70
|
+
const matches = nameMatcher(query.pattern);
|
|
71
|
+
const names: string[] = [];
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (entry.name.startsWith(HIDDEN_PREFIX) && !showHidden) continue;
|
|
74
|
+
if (!matches(entry.name)) continue;
|
|
75
|
+
if (await isDirectory(query.parent, entry)) names.push(entry.name);
|
|
76
|
+
}
|
|
77
|
+
names.sort((left, right) => left.localeCompare(right));
|
|
78
|
+
return names.slice(0, limit).map((name) => path.join(query.parent, name));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Where a fuzzy search starts when the typed value names no place that exists.
|
|
83
|
+
* Home is where projects live; a path outside it is still reachable by typing
|
|
84
|
+
* it out, which is what the completion above is for.
|
|
85
|
+
*/
|
|
86
|
+
function searchRoots(homeDirectory: string): string[] {
|
|
87
|
+
return [homeDirectory];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface SuggestDirectoriesOptions {
|
|
91
|
+
limit?: number;
|
|
92
|
+
/** Injectable so a test does not search the machine it runs on. */
|
|
93
|
+
homeDirectory?: string;
|
|
94
|
+
/** Injectable seam over the tree search. */
|
|
95
|
+
search?: (root: string, query: string) => Promise<string[]>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The directories to offer for a typed value, however it was typed.
|
|
100
|
+
*
|
|
101
|
+
* Completion comes first: an absolute path whose parent exists is someone
|
|
102
|
+
* drilling in, and listing that parent's children is exactly right. Anything
|
|
103
|
+
* else is a name, not a location. A bare "agirepo", or a path remembered from
|
|
104
|
+
* another machine where the folder sat somewhere else, both mean the same
|
|
105
|
+
* thing, and answering "No such directory" to either is a dead end when the
|
|
106
|
+
* folder is sitting in the reader's home directory under a different parent.
|
|
107
|
+
*/
|
|
108
|
+
export async function suggestDirectories(typed: string, options: SuggestDirectoriesOptions = {}): Promise<string[]> {
|
|
109
|
+
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
110
|
+
const completions = await listDirectories(typed, limit);
|
|
111
|
+
if (completions.length > 0) return completions;
|
|
112
|
+
|
|
113
|
+
const query = searchTermFor(typed);
|
|
114
|
+
if (query === '') return [];
|
|
115
|
+
const home = options.homeDirectory ?? os.homedir();
|
|
116
|
+
const search = options.search ?? searchDirectoryTree;
|
|
117
|
+
|
|
118
|
+
const found: string[] = [];
|
|
119
|
+
for (const root of searchRoots(home)) {
|
|
120
|
+
try {
|
|
121
|
+
found.push(...(await search(root, query)));
|
|
122
|
+
} catch {
|
|
123
|
+
// A root that cannot be searched simply contributes nothing.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return rankDirectories(found, query, limit);
|
|
127
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/** How deep a search descends; a project someone starts a session in is never far from home. */
|
|
6
|
+
const MAX_DEPTH = 6;
|
|
7
|
+
/** A ceiling on the walk so a pathological tree cannot hold the request open. */
|
|
8
|
+
const MAX_VISITED = 20_000;
|
|
9
|
+
/** `fd` is fast; if it has not answered by now the fallback will beat waiting for it. */
|
|
10
|
+
const FD_TIMEOUT_MS = 1_500;
|
|
11
|
+
const FD_MAX_BUFFER = 4 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Directories nobody starts a session in, and which dwarf everything else in
|
|
15
|
+
* a tree. Pruning them is the difference between a walk that answers now and
|
|
16
|
+
* one that reads a million files.
|
|
17
|
+
*/
|
|
18
|
+
const PRUNED = new Set([
|
|
19
|
+
'node_modules',
|
|
20
|
+
'Library',
|
|
21
|
+
'Applications',
|
|
22
|
+
'dist',
|
|
23
|
+
'build',
|
|
24
|
+
'out',
|
|
25
|
+
'target',
|
|
26
|
+
'vendor',
|
|
27
|
+
'coverage',
|
|
28
|
+
'.git',
|
|
29
|
+
'.nx',
|
|
30
|
+
'.next',
|
|
31
|
+
'.cache',
|
|
32
|
+
'.venv',
|
|
33
|
+
'__pycache__',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const HIDDEN_PREFIX = '.';
|
|
37
|
+
|
|
38
|
+
function skips(name: string, wantsHidden: boolean): boolean {
|
|
39
|
+
if (PRUNED.has(name)) return true;
|
|
40
|
+
return name.startsWith(HIDDEN_PREFIX) && !wantsHidden;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `fd` if this machine has it: it is built for exactly this, prunes ignored
|
|
45
|
+
* paths itself, and is far quicker than walking from Node. A missing binary,
|
|
46
|
+
* a non-zero exit, or a slow answer all fall through to the walk rather than
|
|
47
|
+
* failing the request, because a suggestion list is a convenience.
|
|
48
|
+
*/
|
|
49
|
+
async function searchWithFd(root: string, query: string, wantsHidden: boolean): Promise<string[] | undefined> {
|
|
50
|
+
const args = [
|
|
51
|
+
'--type',
|
|
52
|
+
'd',
|
|
53
|
+
'--absolute-path',
|
|
54
|
+
'--fixed-strings',
|
|
55
|
+
'--color',
|
|
56
|
+
'never',
|
|
57
|
+
'--max-depth',
|
|
58
|
+
String(MAX_DEPTH),
|
|
59
|
+
// The same prune list the walk uses, so which engine answered is invisible
|
|
60
|
+
// to the reader: no caches, no build output, no OS folders nobody codes in.
|
|
61
|
+
...[...PRUNED].flatMap((name) => ['--exclude', name]),
|
|
62
|
+
...(wantsHidden ? ['--hidden'] : []),
|
|
63
|
+
'--',
|
|
64
|
+
query,
|
|
65
|
+
root,
|
|
66
|
+
];
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
execFile('fd', args, { timeout: FD_TIMEOUT_MS, maxBuffer: FD_MAX_BUFFER }, (error, stdout) => {
|
|
69
|
+
// fd exits non-zero when it matches nothing, which is an answer, not a
|
|
70
|
+
// failure; only a missing binary or a timeout sends us to the walk.
|
|
71
|
+
if (error && stdout.length === 0 && error.code !== 1) {
|
|
72
|
+
resolve(undefined);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
resolve(
|
|
76
|
+
stdout
|
|
77
|
+
.split('\n')
|
|
78
|
+
.map((line) => line.trimEnd().replace(/\/$/u, ''))
|
|
79
|
+
.filter((line) => line !== ''),
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every directory under a root, breadth first, bounded in depth and in total work. */
|
|
86
|
+
async function walkDirectories(root: string, wantsHidden: boolean): Promise<string[]> {
|
|
87
|
+
const found: string[] = [];
|
|
88
|
+
let frontier = [root];
|
|
89
|
+
let visited = 0;
|
|
90
|
+
|
|
91
|
+
for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0 && visited < MAX_VISITED; depth += 1) {
|
|
92
|
+
const next: string[] = [];
|
|
93
|
+
for (const directory of frontier) {
|
|
94
|
+
if (visited >= MAX_VISITED) break;
|
|
95
|
+
let entries: fs.Dirent[];
|
|
96
|
+
try {
|
|
97
|
+
entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
|
98
|
+
} catch {
|
|
99
|
+
continue; // Unreadable is not an error here, just nothing to offer.
|
|
100
|
+
}
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (!entry.isDirectory() || skips(entry.name, wantsHidden)) continue;
|
|
103
|
+
visited += 1;
|
|
104
|
+
const child = path.join(directory, entry.name);
|
|
105
|
+
found.push(child);
|
|
106
|
+
next.push(child);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
frontier = next;
|
|
110
|
+
}
|
|
111
|
+
return found;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Directories under a root whose path could answer the query.
|
|
116
|
+
*
|
|
117
|
+
* Matching happens in the caller, against the whole candidate set, so the
|
|
118
|
+
* ranking is the same whichever engine produced it.
|
|
119
|
+
*/
|
|
120
|
+
export async function searchDirectoryTree(root: string, query: string): Promise<string[]> {
|
|
121
|
+
const wantsHidden = query.startsWith(HIDDEN_PREFIX);
|
|
122
|
+
const viaFd = await searchWithFd(root, query, wantsHidden);
|
|
123
|
+
return viaFd ?? (await walkDirectories(root, wantsHidden));
|
|
124
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import type { SessionGitStatus } from '../types/hub.ts';
|
|
3
|
+
|
|
4
|
+
const GIT_TIMEOUT_MS = 2000;
|
|
5
|
+
|
|
6
|
+
function git(cwd: string, args: string[]): Promise<string | undefined> {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
execFile('git', args, { cwd, timeout: GIT_TIMEOUT_MS }, (error, stdout) => {
|
|
9
|
+
// A non-repo cwd, missing git, or timeout all mean the same thing to the
|
|
10
|
+
// rail: no branch chip.
|
|
11
|
+
resolve(error ? undefined : stdout);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reads the branch chip for one session's working directory.
|
|
18
|
+
*
|
|
19
|
+
* Asks git itself rather than parsing .git/HEAD: worktrees (where .git is a
|
|
20
|
+
* file) come free, and only git can answer whether the tree is dirty.
|
|
21
|
+
*/
|
|
22
|
+
export async function readGitStatus(cwd: string): Promise<SessionGitStatus | undefined> {
|
|
23
|
+
const branch = (await git(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']))?.trim();
|
|
24
|
+
if (!branch) return undefined;
|
|
25
|
+
const status = await git(cwd, ['status', '--porcelain']);
|
|
26
|
+
return { branch, dirty: status !== undefined && status.trim().length > 0 };
|
|
27
|
+
}
|