@neuralis/brain-core 0.3.1 → 0.4.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/README.md +88 -1417
- package/app/README.md +4 -4
- package/app/cards/FilesystemCardShell.tsx +42 -4
- package/app/cards/FilesystemChangeCard.tsx +34 -11
- package/app/cards/README.md +6 -5
- package/app/cards/diffUtils.ts +99 -1
- package/app/filesystem/FilesystemHost.tsx +24 -2
- package/app/filesystem/README.md +1 -1
- package/app/filesystem/api/README.md +3 -3
- package/app/filesystem/api/index.ts +38 -14
- package/app/filesystem/api/projectPackages.ts +13 -102
- package/app/filesystem/domain/types.ts +30 -10
- package/app/filesystem/hooks/README.md +4 -4
- package/app/filesystem/hooks/useDiscoverableSources.ts +19 -30
- package/app/filesystem/hooks/useFilesystemSSE.ts +42 -2
- package/app/filesystem/host/README.md +6 -4
- package/app/filesystem/host/buildFilesystemHostValue.ts +28 -2
- package/app/filesystem/host/installFilesystemHostComponents.tsx +1 -1
- package/app/filesystem/lib/sourceKindForm.ts +210 -27
- package/app/filesystem/lib/treeVisualState.tsx +98 -4
- package/app/filesystem/store/README.md +1 -0
- package/app/filesystem/store/filesystemStore.ts +99 -8
- package/app/filesystem/store/gitSlice.ts +5 -0
- package/app/filesystem/store/helpers.ts +17 -0
- package/app/filesystem/store/listSlice.ts +8 -0
- package/app/filesystem/ui/CodeEditor.tsx +18 -3
- package/app/filesystem/ui/DiscoverDock.tsx +5 -1
- package/app/filesystem/ui/FileEditor.tsx +15 -10
- package/app/filesystem/ui/FileTree.tsx +75 -24
- package/app/filesystem/ui/FilesystemWidget.tsx +10 -2
- package/app/filesystem/ui/FolderPicker.tsx +251 -0
- package/app/filesystem/ui/GitControlPanel.tsx +7 -2
- package/app/filesystem/ui/PackagesPanel.tsx +52 -374
- package/app/filesystem/ui/PendingChangesPanel.tsx +1 -1
- package/app/filesystem/ui/PolicyEditor.tsx +109 -24
- package/app/filesystem/ui/README.md +30 -23
- package/app/filesystem/ui/RawMediaPreview.tsx +58 -0
- package/app/filesystem/ui/SourceBranchHeader.tsx +4 -4
- package/app/filesystem/ui/SourcesPanel.tsx +502 -342
- package/app/filesystem/ui/Toolbar.tsx +1 -2
- package/app/filesystem/ui/VectorHealthPanel.tsx +2 -2
- package/app/shared/filesystem.png +0 -0
- package/dist/app/host.js +58 -46
- package/dist/scripts/sourceRootPairV1.d.ts +47 -0
- package/dist/scripts/sourceRootPairV1.d.ts.map +1 -0
- package/dist/scripts/sourceRootPairV1.js +147 -0
- package/dist/scripts/sourceRootPairV1.js.map +1 -0
- package/dist/scripts/sourceToFolderV1.d.ts +56 -0
- package/dist/scripts/sourceToFolderV1.d.ts.map +1 -0
- package/dist/scripts/sourceToFolderV1.js +152 -0
- package/dist/scripts/sourceToFolderV1.js.map +1 -0
- package/dist/scripts/sourceToHostV1.d.ts +56 -0
- package/dist/scripts/sourceToHostV1.d.ts.map +1 -0
- package/dist/scripts/sourceToHostV1.js +132 -0
- package/dist/scripts/sourceToHostV1.js.map +1 -0
- package/dist/src/channels/filesystem.d.ts +1 -1
- package/dist/src/channels/filesystem.js +1 -1
- package/dist/src/connectors/AppSourceConfigStore.d.ts +17 -3
- package/dist/src/connectors/AppSourceConfigStore.d.ts.map +1 -1
- package/dist/src/connectors/AppSourceConfigStore.js +46 -6
- package/dist/src/connectors/AppSourceConfigStore.js.map +1 -1
- package/dist/src/connectors/HostConnector.d.ts +201 -0
- package/dist/src/connectors/HostConnector.d.ts.map +1 -0
- package/dist/src/connectors/HostConnector.js +903 -0
- package/dist/src/connectors/HostConnector.js.map +1 -0
- package/dist/src/connectors/inProcessTransport.d.ts +131 -0
- package/dist/src/connectors/inProcessTransport.d.ts.map +1 -0
- package/dist/src/connectors/inProcessTransport.js +447 -0
- package/dist/src/connectors/inProcessTransport.js.map +1 -0
- package/dist/src/domain/access/callerPermissions.d.ts +29 -0
- package/dist/src/domain/access/callerPermissions.d.ts.map +1 -1
- package/dist/src/domain/access/callerPermissions.js +85 -5
- package/dist/src/domain/access/callerPermissions.js.map +1 -1
- package/dist/src/domain/access/nativeLocalFence.d.ts +65 -0
- package/dist/src/domain/access/nativeLocalFence.d.ts.map +1 -0
- package/dist/src/domain/access/nativeLocalFence.js +171 -0
- package/dist/src/domain/access/nativeLocalFence.js.map +1 -0
- package/dist/src/domain/access/uriPermission.js +3 -3
- package/dist/src/domain/access/uriPermission.js.map +1 -1
- package/dist/src/domain/byteRange.d.ts +15 -0
- package/dist/src/domain/byteRange.d.ts.map +1 -0
- package/dist/src/domain/byteRange.js +30 -0
- package/dist/src/domain/byteRange.js.map +1 -0
- package/dist/src/domain/defaultPermissions.d.ts.map +1 -1
- package/dist/src/domain/defaultPermissions.js +29 -0
- package/dist/src/domain/defaultPermissions.js.map +1 -1
- package/dist/src/domain/mime.d.ts +11 -19
- package/dist/src/domain/mime.d.ts.map +1 -1
- package/dist/src/domain/mime.js +13 -102
- package/dist/src/domain/mime.js.map +1 -1
- package/dist/src/domain/paths.d.ts.map +1 -1
- package/dist/src/domain/paths.js +5 -3
- package/dist/src/domain/paths.js.map +1 -1
- package/dist/src/domain/scopeAccessCtx.d.ts +2 -2
- package/dist/src/domain/scopeAccessCtx.js +2 -2
- package/dist/src/domain/sourceConfig.d.ts +52 -19
- package/dist/src/domain/sourceConfig.d.ts.map +1 -1
- package/dist/src/domain/sourceConfig.js +103 -19
- package/dist/src/domain/sourceConfig.js.map +1 -1
- package/dist/src/domain/sourcePackages.d.ts +8 -8
- package/dist/src/domain/sourcePackages.d.ts.map +1 -1
- package/dist/src/domain/sourceRootPair.d.ts +88 -0
- package/dist/src/domain/sourceRootPair.d.ts.map +1 -0
- package/dist/src/domain/sourceRootPair.js +108 -0
- package/dist/src/domain/sourceRootPair.js.map +1 -0
- package/dist/src/domain/syncTypes.d.ts +8 -1
- package/dist/src/domain/syncTypes.d.ts.map +1 -1
- package/dist/src/domain/syncTypes.js +1 -0
- package/dist/src/domain/syncTypes.js.map +1 -1
- package/dist/src/domain/types.d.ts +32 -2
- package/dist/src/domain/types.d.ts.map +1 -1
- package/dist/src/domain/uri.d.ts.map +1 -1
- package/dist/src/domain/uri.js +3 -2
- package/dist/src/domain/uri.js.map +1 -1
- package/dist/src/embeddings/clients/GeminiEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/GeminiEmbedder.js +2 -5
- package/dist/src/embeddings/clients/GeminiEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/OpenAIEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/OpenAIEmbedder.js +2 -3
- package/dist/src/embeddings/clients/OpenAIEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/QwenEmbedder.d.ts +2 -0
- package/dist/src/embeddings/clients/QwenEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/QwenEmbedder.js +10 -2
- package/dist/src/embeddings/clients/QwenEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.d.ts +2 -0
- package/dist/src/embeddings/clients/VoyageEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.js +8 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.js.map +1 -1
- package/dist/src/embeddings/factory.d.ts +5 -3
- package/dist/src/embeddings/factory.d.ts.map +1 -1
- package/dist/src/embeddings/factory.js +7 -3
- package/dist/src/embeddings/factory.js.map +1 -1
- package/dist/src/embeddings/models.d.ts +1 -1
- package/dist/src/embeddings/models.d.ts.map +1 -1
- package/dist/src/embeddings/retry.js +1 -1
- package/dist/src/embeddings/retry.js.map +1 -1
- package/dist/src/embeddings/types.d.ts +6 -0
- package/dist/src/embeddings/types.d.ts.map +1 -1
- package/dist/src/embeddings/types.js +11 -0
- package/dist/src/embeddings/types.js.map +1 -1
- package/dist/src/events/pending.d.ts +1 -1
- package/dist/src/events/pending.js +1 -1
- package/dist/src/git/GitRunner.d.ts +110 -30
- package/dist/src/git/GitRunner.d.ts.map +1 -1
- package/dist/src/git/GitRunner.js +511 -199
- package/dist/src/git/GitRunner.js.map +1 -1
- package/dist/src/git/GitWatchRegistry.d.ts +41 -35
- package/dist/src/git/GitWatchRegistry.d.ts.map +1 -1
- package/dist/src/git/GitWatchRegistry.js +61 -79
- package/dist/src/git/GitWatchRegistry.js.map +1 -1
- package/dist/src/git/gitPlace.d.ts +61 -0
- package/dist/src/git/gitPlace.d.ts.map +1 -0
- package/dist/src/git/gitPlace.js +74 -0
- package/dist/src/git/gitPlace.js.map +1 -0
- package/dist/src/git/sourceGitControl.d.ts +33 -21
- package/dist/src/git/sourceGitControl.d.ts.map +1 -1
- package/dist/src/git/sourceGitControl.js +211 -95
- package/dist/src/git/sourceGitControl.js.map +1 -1
- package/dist/src/git/sourceGitStatus.js +1 -1
- package/dist/src/git/sourceGitStatus.js.map +1 -1
- package/dist/src/git/untrackedStats.js +1 -1
- package/dist/src/git/untrackedStats.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lifecycle.d.ts +9 -12
- package/dist/src/lifecycle.d.ts.map +1 -1
- package/dist/src/lifecycle.js +2 -1
- package/dist/src/lifecycle.js.map +1 -1
- package/dist/src/routes/approve.d.ts +1 -1
- package/dist/src/routes/approve.js +1 -1
- package/dist/src/routes/events.d.ts +1 -1
- package/dist/src/routes/events.js +1 -1
- package/dist/src/routes/git.d.ts +7 -7
- package/dist/src/routes/git.d.ts.map +1 -1
- package/dist/src/routes/git.js +201 -72
- package/dist/src/routes/git.js.map +1 -1
- package/dist/src/routes/health.d.ts +6 -6
- package/dist/src/routes/health.js +12 -12
- package/dist/src/routes/helpers/sourceNotConfigured.d.ts +1 -1
- package/dist/src/routes/helpers/sourceNotConfigured.js +1 -1
- package/dist/src/routes/helpers/uriGuard.d.ts +1 -1
- package/dist/src/routes/helpers/uriGuard.d.ts.map +1 -1
- package/dist/src/routes/helpers/uriGuard.js +3 -2
- package/dist/src/routes/helpers/uriGuard.js.map +1 -1
- package/dist/src/routes/helpers.d.ts +11 -4
- package/dist/src/routes/helpers.d.ts.map +1 -1
- package/dist/src/routes/helpers.js +13 -6
- package/dist/src/routes/helpers.js.map +1 -1
- package/dist/src/routes/list.d.ts +1 -1
- package/dist/src/routes/list.d.ts.map +1 -1
- package/dist/src/routes/list.js +3 -4
- package/dist/src/routes/list.js.map +1 -1
- package/dist/src/routes/pending.d.ts +4 -4
- package/dist/src/routes/pending.js +9 -9
- package/dist/src/routes/policies.d.ts +10 -8
- package/dist/src/routes/policies.d.ts.map +1 -1
- package/dist/src/routes/policies.js +31 -25
- package/dist/src/routes/policies.js.map +1 -1
- package/dist/src/routes/raw.d.ts +1 -1
- package/dist/src/routes/raw.d.ts.map +1 -1
- package/dist/src/routes/raw.js +66 -12
- package/dist/src/routes/raw.js.map +1 -1
- package/dist/src/routes/read.d.ts +1 -1
- package/dist/src/routes/read.js +1 -1
- package/dist/src/routes/search.d.ts +1 -1
- package/dist/src/routes/search.js +1 -1
- package/dist/src/routes/sources.d.ts +63 -6
- package/dist/src/routes/sources.d.ts.map +1 -1
- package/dist/src/routes/sources.js +494 -382
- package/dist/src/routes/sources.js.map +1 -1
- package/dist/src/routes/upload.d.ts +1 -1
- package/dist/src/routes/upload.js +1 -1
- package/dist/src/routes/write.d.ts +2 -2
- package/dist/src/routes/write.js +2 -2
- package/dist/src/services/BridgeService.d.ts +21 -35
- package/dist/src/services/BridgeService.d.ts.map +1 -1
- package/dist/src/services/BridgeService.js +42 -88
- package/dist/src/services/BridgeService.js.map +1 -1
- package/dist/src/services/FilesystemService.d.ts +4 -3
- package/dist/src/services/FilesystemService.d.ts.map +1 -1
- package/dist/src/services/FilesystemService.js +112 -34
- package/dist/src/services/FilesystemService.js.map +1 -1
- package/dist/src/services/FilesystemUiService.d.ts +4 -1
- package/dist/src/services/FilesystemUiService.d.ts.map +1 -1
- package/dist/src/services/FilesystemUiService.js +66 -25
- package/dist/src/services/FilesystemUiService.js.map +1 -1
- package/dist/src/services/SyncScheduler.d.ts +12 -4
- package/dist/src/services/SyncScheduler.d.ts.map +1 -1
- package/dist/src/services/SyncScheduler.js +37 -8
- package/dist/src/services/SyncScheduler.js.map +1 -1
- package/dist/src/services/baselineRepair.d.ts +13 -3
- package/dist/src/services/baselineRepair.d.ts.map +1 -1
- package/dist/src/services/baselineRepair.js +13 -3
- package/dist/src/services/baselineRepair.js.map +1 -1
- package/dist/src/services/declaredSources.d.ts +132 -43
- package/dist/src/services/declaredSources.d.ts.map +1 -1
- package/dist/src/services/declaredSources.js +118 -45
- package/dist/src/services/declaredSources.js.map +1 -1
- package/dist/src/store/dataFormats.d.ts +17 -2
- package/dist/src/store/dataFormats.d.ts.map +1 -1
- package/dist/src/store/dataFormats.js +18 -3
- package/dist/src/store/dataFormats.js.map +1 -1
- package/dist/src/sync/DeltaSyncRunner.d.ts.map +1 -1
- package/dist/src/sync/DeltaSyncRunner.js +144 -4
- package/dist/src/sync/DeltaSyncRunner.js.map +1 -1
- package/dist/src/sync/SyncJobQueue.d.ts +7 -2
- package/dist/src/sync/SyncJobQueue.d.ts.map +1 -1
- package/dist/src/sync/SyncJobQueue.js +16 -10
- package/dist/src/sync/SyncJobQueue.js.map +1 -1
- package/dist/src/sync/SyncWatchService.d.ts +21 -4
- package/dist/src/sync/SyncWatchService.d.ts.map +1 -1
- package/dist/src/sync/SyncWatchService.js +115 -10
- package/dist/src/sync/SyncWatchService.js.map +1 -1
- package/dist/src/sync/categoryInference.d.ts +1 -1
- package/dist/src/sync/categoryInference.js +1 -1
- package/dist/src/tools/execCtx.d.ts +2 -2
- package/dist/src/tools/execCtx.js +2 -2
- package/dist/src/tools/fs_read.d.ts.map +1 -1
- package/dist/src/tools/fs_read.js +5 -0
- package/dist/src/tools/fs_read.js.map +1 -1
- package/dist/src/usecases/admitSourceRoot.d.ts +65 -0
- package/dist/src/usecases/admitSourceRoot.d.ts.map +1 -0
- package/dist/src/usecases/admitSourceRoot.js +85 -0
- package/dist/src/usecases/admitSourceRoot.js.map +1 -0
- package/dist/src/usecases/browseSourceFolders.d.ts +85 -0
- package/dist/src/usecases/browseSourceFolders.d.ts.map +1 -0
- package/dist/src/usecases/browseSourceFolders.js +210 -0
- package/dist/src/usecases/browseSourceFolders.js.map +1 -0
- package/dist/src/usecases/createDeclaredSources.d.ts +13 -2
- package/dist/src/usecases/createDeclaredSources.d.ts.map +1 -1
- package/dist/src/usecases/createDeclaredSources.js +39 -0
- package/dist/src/usecases/createDeclaredSources.js.map +1 -1
- package/dist/src/usecases/embeddingBacklog.js +1 -1
- package/dist/src/usecases/embeddingBacklog.js.map +1 -1
- package/dist/src/usecases/list.d.ts.map +1 -1
- package/dist/src/usecases/list.js +83 -26
- package/dist/src/usecases/list.js.map +1 -1
- package/dist/src/usecases/listAttachableDevices.d.ts +13 -0
- package/dist/src/usecases/listAttachableDevices.d.ts.map +1 -0
- package/dist/src/usecases/listAttachableDevices.js +10 -0
- package/dist/src/usecases/listAttachableDevices.js.map +1 -0
- package/dist/src/usecases/listSourcePackages.d.ts.map +1 -1
- package/dist/src/usecases/listSourcePackages.js +2 -5
- package/dist/src/usecases/listSourcePackages.js.map +1 -1
- package/dist/src/usecases/purgeExcludedPoints.d.ts.map +1 -1
- package/dist/src/usecases/purgeExcludedPoints.js +3 -2
- package/dist/src/usecases/purgeExcludedPoints.js.map +1 -1
- package/dist/src/usecases/readFiles.d.ts.map +1 -1
- package/dist/src/usecases/readFiles.js +99 -1
- package/dist/src/usecases/readFiles.js.map +1 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.d.ts +1 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.d.ts.map +1 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.js +2 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.js.map +1 -1
- package/dist/src/usecases/revokePrincipal.d.ts +7 -5
- package/dist/src/usecases/revokePrincipal.d.ts.map +1 -1
- package/dist/src/usecases/revokePrincipal.js +29 -7
- package/dist/src/usecases/revokePrincipal.js.map +1 -1
- package/dist/src/usecases/scheduleSourceSync.d.ts +7 -0
- package/dist/src/usecases/scheduleSourceSync.d.ts.map +1 -1
- package/dist/src/usecases/scheduleSourceSync.js +6 -5
- package/dist/src/usecases/scheduleSourceSync.js.map +1 -1
- package/dist/src/usecases/syncPointWrites.d.ts +2 -0
- package/dist/src/usecases/syncPointWrites.d.ts.map +1 -1
- package/dist/src/usecases/syncPointWrites.js +5 -1
- package/dist/src/usecases/syncPointWrites.js.map +1 -1
- package/dist/src/usecases/triggerSourceSync.d.ts +46 -0
- package/dist/src/usecases/triggerSourceSync.d.ts.map +1 -0
- package/dist/src/usecases/triggerSourceSync.js +75 -0
- package/dist/src/usecases/triggerSourceSync.js.map +1 -0
- package/dist/src/usecases/uploadBinary.d.ts +2 -1
- package/dist/src/usecases/uploadBinary.d.ts.map +1 -1
- package/dist/src/usecases/uploadBinary.js +2 -2
- package/dist/src/usecases/uploadBinary.js.map +1 -1
- package/dist/src/usecases/vector/endpointGuard.d.ts +1 -1
- package/dist/src/usecases/vector/endpointGuard.js +1 -1
- package/dist/src/usecases/vectorMaintenance.js +1 -1
- package/dist/src/usecases/vectorMaintenance.js.map +1 -1
- package/dist/src/usecases/write/content.js +1 -1
- package/dist/src/vector/qdrantClient.d.ts +1 -1
- package/dist/src/vector/qdrantClient.js +1 -1
- package/dist/src/wiring.d.ts +3 -0
- package/dist/src/wiring.d.ts.map +1 -1
- package/dist/src/wiring.js +72 -6
- package/dist/src/wiring.js.map +1 -1
- package/dist/tools/fs_list.json +1 -1
- package/dist/tools/fs_read.json +1 -1
- package/dist/tools/fs_search.json +1 -1
- package/dist/tools/fs_write.json +1 -1
- package/instructions/brain-model.md +1 -1
- package/package.json +212 -114
- package/rules/brain-rules.md +2 -2
- package/skills/brain-start/SKILL.md +1 -1
- package/skills/connect-business-data/SKILL.md +3 -3
- package/skills/create-package/SKILL.md +26 -26
- package/skills/create-package/references/config-and-credentials.md +16 -13
- package/skills/create-package/references/connectors-and-uri-policies.md +39 -25
- package/skills/create-package/references/manifest-and-discovery.md +12 -12
- package/skills/create-package/references/runtime-and-build.md +19 -19
- package/skills/create-package/references/skills-and-context.md +36 -20
- package/skills/create-package/references/surfaces-and-design.md +16 -16
- package/skills/create-package/references/team-and-workflows.md +2 -2
- package/skills/create-package/references/test-and-ship.md +21 -21
- package/skills/create-package/references/tools-and-schemas.md +5 -5
- package/skills/create-package/scripts/{check-package.sh → check-package.mjs} +79 -65
- package/skills/create-package/scripts/{verify-loaded.sh → verify-loaded.mjs} +156 -162
- package/skills/git/SKILL.md +53 -51
- package/skills/manage-files/SKILL.md +22 -25
- package/skills/manage-sources/SKILL.md +128 -87
- package/skills/manage-uri-policy/SKILL.md +18 -15
- package/skills/organize/SKILL.md +1 -1
- package/skills/sync/SKILL.md +3 -3
- package/tools/fs_list.json +1 -1
- package/tools/fs_read.json +1 -1
- package/tools/fs_search.json +1 -1
- package/tools/fs_write.json +1 -1
- package/workflows/sources-digest.json +1 -1
- package/dist/src/connectors/LocalDiskConnector.d.ts +0 -171
- package/dist/src/connectors/LocalDiskConnector.d.ts.map +0 -1
- package/dist/src/connectors/LocalDiskConnector.js +0 -1233
- package/dist/src/connectors/LocalDiskConnector.js.map +0 -1
- package/docs/drive-overview.md +0 -59
- package/skills/manage-files/scripts/copy.sh +0 -7
- package/skills/manage-files/scripts/create.sh +0 -7
- package/skills/manage-files/scripts/mkdir.sh +0 -7
- package/skills/manage-files/scripts/modify.sh +0 -7
- package/skills/manage-files/scripts/upload.sh +0 -16
- package/skills/manage-files/scripts/write.sh +0 -29
- package/skills/manage-sources/scripts/reembed.sh +0 -23
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* a `.git` stat pre-gate, a FRESH `GitRunner` per cache miss (never reuse — the
|
|
7
7
|
* `#assertRepoInsideSource` memoised-rejection trap), a short TTL cache, and
|
|
8
8
|
* fail-soft `null` on any git problem so a slow/broken repo never blocks the list
|
|
9
|
-
* or panel path
|
|
9
|
+
* or panel path — save ONE: a paired device that refuses the spawn
|
|
10
|
+
* (`GitDeviceUnavailableError`) is thrown by every panel reader for the route
|
|
11
|
+
* to name, never folded into a clean repo's empty answer (the list-path
|
|
12
|
+
* summary never spawns on a device, so it never meets one).
|
|
10
13
|
*
|
|
11
14
|
* THREE readers, each with its own cost/scope:
|
|
12
15
|
* - `getSourceGitStatus` — the WHOLE-repo change-set (staged / unstaged /
|
|
@@ -31,7 +34,7 @@
|
|
|
31
34
|
* listing and the watcher expansion — consume the discovered set WITHOUT ever
|
|
32
35
|
* passing the route validation ladder. A symlinked child pointing at a foreign
|
|
33
36
|
* repo would therefore (1) leak the foreign repo's existence + branch in the
|
|
34
|
-
* listing and (2) plant a host-side
|
|
37
|
+
* listing and (2) plant a host-side file watcher on a foreign `.git`. So
|
|
35
38
|
* every candidate child must pass `realpath(child)` containment under
|
|
36
39
|
* `realpath(sourceRoot)` BEFORE inclusion — at DISCOVERY itself, not only in
|
|
37
40
|
* the route ladder — and the `containsRepos` probe applies the SAME predicate
|
|
@@ -44,7 +47,8 @@
|
|
|
44
47
|
import { opendir, readFile, realpath, stat } from 'node:fs/promises';
|
|
45
48
|
import path from 'node:path';
|
|
46
49
|
import { hasFeature } from '@neuralis/package-system/access';
|
|
47
|
-
import {
|
|
50
|
+
import { childPlace, placeKey, placePath, readDeviceHeadBranch, readDeviceSmallFile, toGitPlace, } from './gitPlace.js';
|
|
51
|
+
import { GitRunner, GitDeviceUnavailableError, GitOutputLimitError, EMPTY_TREE_SHA, } from './GitRunner.js';
|
|
48
52
|
import { xyCode } from './sourceGitStatus.js';
|
|
49
53
|
import { resolvePermissions } from '../domain/permissionResolver.js';
|
|
50
54
|
import { getBrainConfigNumber } from '../config/env.js';
|
|
@@ -74,18 +78,18 @@ const GIT_DISCOVERY_MAX_REPOS = 25;
|
|
|
74
78
|
* demand; a repo too slow to answer within the cap fails soft to `null` (empty
|
|
75
79
|
* panel), never blocking the request.
|
|
76
80
|
*/
|
|
77
|
-
const GIT_CONTROL_STATUS_TIMEOUT_MS =
|
|
81
|
+
const GIT_CONTROL_STATUS_TIMEOUT_MS = 15000;
|
|
78
82
|
/**
|
|
79
83
|
* Inactivity cap for the HEAVY on-demand readers — the per-file diff, the
|
|
80
84
|
* commit DAG, and blob content at a rev (`show`) — so a 2 MB diff or a
|
|
81
|
-
* 2000-commit log on a slow volume doesn't die at the
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* continuously-STREAMING pathological command trips
|
|
85
|
-
* by the engine's output accumulation bound
|
|
86
|
-
* `
|
|
85
|
+
* 2000-commit log on a slow volume doesn't die at the status cap. NOTE:
|
|
86
|
+
* locally this is an INACTIVITY timer — it resets on every output chunk,
|
|
87
|
+
* capping SILENCE, not wall-clock (on a device the broker's timer is
|
|
88
|
+
* wall-clock). A continuously-STREAMING pathological command never trips it;
|
|
89
|
+
* that is stopped by the engine's output accumulation bound
|
|
90
|
+
* (`GIT_MAX_OUTPUT_BYTES`, on both transports, MINOR-6).
|
|
87
91
|
*/
|
|
88
|
-
const GIT_CONTROL_HEAVY_TIMEOUT_MS =
|
|
92
|
+
const GIT_CONTROL_HEAVY_TIMEOUT_MS = 60000;
|
|
89
93
|
/** Max blob bytes `getSourceGitShow` serves — matches the route's 2 MB UI diff cap. */
|
|
90
94
|
const GIT_SHOW_MAX_BLOB_BYTES = 2 * 1024 * 1024;
|
|
91
95
|
const statusCache = new Map();
|
|
@@ -96,6 +100,23 @@ const summaryCache = new Map();
|
|
|
96
100
|
// is shared by the summary's containsRepos/repoNames derivation, the route, and
|
|
97
101
|
// the watcher expansion.
|
|
98
102
|
const discoveryCache = new Map();
|
|
103
|
+
/**
|
|
104
|
+
* A device that could not run a read (offline, devices off, the owner's
|
|
105
|
+
* "others" level, its ceiling) is a REFUSAL the route names — never the empty
|
|
106
|
+
* answer of a clean repo. Every other read failure stays fail-soft.
|
|
107
|
+
*/
|
|
108
|
+
function rethrowDeviceRefusal(error) {
|
|
109
|
+
if (error instanceof GitDeviceUnavailableError)
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The cache key of a read that SPAWNS git: on a device the caller's class
|
|
114
|
+
* rides it, so the owner's answer is never served to another member, whom
|
|
115
|
+
* the device judges again at every spawn (its per-folder "others" level).
|
|
116
|
+
*/
|
|
117
|
+
function spawnKey(place) {
|
|
118
|
+
return place.device && place.device.owner !== true ? `${placeKey(place)}\0other` : placeKey(place);
|
|
119
|
+
}
|
|
99
120
|
async function withCache(cache, key, ttlMs, compute) {
|
|
100
121
|
const now = Date.now();
|
|
101
122
|
const hit = cache.get(key);
|
|
@@ -105,16 +126,65 @@ async function withCache(cache, key, ttlMs, compute) {
|
|
|
105
126
|
cache.set(key, { result, expiresAt: now + ttlMs });
|
|
106
127
|
return result;
|
|
107
128
|
}
|
|
108
|
-
/** True when the source root has its OWN `.git` (dir OR linked-worktree file). */
|
|
109
|
-
async function hasDotGit(
|
|
129
|
+
/** True when the source root has its OWN `.git` (dir OR linked-worktree file) — on its own machine. */
|
|
130
|
+
async function hasDotGit(place) {
|
|
131
|
+
const at = placePath(place).join(place.root, '.git');
|
|
110
132
|
try {
|
|
111
|
-
|
|
133
|
+
if (place.device)
|
|
134
|
+
return (await place.device.transport.stat(at)) !== null;
|
|
135
|
+
await stat(at);
|
|
112
136
|
return true;
|
|
113
137
|
}
|
|
114
138
|
catch {
|
|
115
139
|
return false;
|
|
116
140
|
}
|
|
117
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The device half of the depth-1 scan. The device's listing never names a
|
|
144
|
+
* symlink (the transport filters them), so every candidate child is a real
|
|
145
|
+
* directory under the root — containment holds by construction, with no
|
|
146
|
+
* `realpath` this process could not run there. A `.git` found by `stat` is
|
|
147
|
+
* admitted only when the child's own listing shows it as a real entry: `stat`
|
|
148
|
+
* follows links (inside the device's ceiling), the listing does not, so a
|
|
149
|
+
* symlinked `.git` onto a foreign repo drops exactly as the local `realpath`
|
|
150
|
+
* tail drops it. Cost: one listing of the root, one `stat` per examined child
|
|
151
|
+
* directory, one listing per find (≤ `limit`), at most 8 RPCs in flight.
|
|
152
|
+
*/
|
|
153
|
+
async function scanDeviceDepth1RepoChildren(place, limit, maxDirents) {
|
|
154
|
+
const seat = place.device;
|
|
155
|
+
if (!seat)
|
|
156
|
+
return [];
|
|
157
|
+
const p = placePath(place);
|
|
158
|
+
let entries;
|
|
159
|
+
try {
|
|
160
|
+
entries = await seat.transport.list(place.root);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
const queue = entries.slice(0, maxDirents).filter((e) => e.isDirectory).map((e) => e.name);
|
|
166
|
+
const workers = Math.min(8, queue.length);
|
|
167
|
+
const found = [];
|
|
168
|
+
const worker = async () => {
|
|
169
|
+
for (let name = queue.shift(); name !== undefined && found.length < limit; name = queue.shift()) {
|
|
170
|
+
const child = p.join(place.root, name);
|
|
171
|
+
try {
|
|
172
|
+
if ((await seat.transport.stat(p.join(child, '.git'))) === null)
|
|
173
|
+
continue;
|
|
174
|
+
const inner = await seat.transport.list(child);
|
|
175
|
+
if (!inner.some((e) => e.name === '.git'))
|
|
176
|
+
continue; // a symlinked `.git` — never admitted
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (found.length < limit)
|
|
182
|
+
found.push({ rel: name, name, root: child, place: childPlace(place, name) });
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
await Promise.all(Array.from({ length: workers }, () => worker()));
|
|
186
|
+
return found;
|
|
187
|
+
}
|
|
118
188
|
/**
|
|
119
189
|
* The ONE bounded depth-1 scan the discovery and the `containsRepos` probe
|
|
120
190
|
* share (E1). STREAMING `fs.opendir` iteration — never a full `readdir`
|
|
@@ -133,7 +203,7 @@ async function hasDotGit(resolvedRoot) {
|
|
|
133
203
|
* containment too: step 3 STAT-FOLLOWS the `.git` entry itself, so a
|
|
134
204
|
* contained child whose `.git` is a SYMLINK onto a foreign repo would
|
|
135
205
|
* otherwise pass — re-opening exactly the exposure this filter closes
|
|
136
|
-
* (foreign
|
|
206
|
+
* (foreign file watch, foreign listing/history). A regular `.git`
|
|
137
207
|
* dir or linked-worktree FILE realpaths inside the child (NIT-4's
|
|
138
208
|
* `.git`-file children stay admitted); only an escaping link drops.
|
|
139
209
|
* Residual (G2.5): a `.git` FILE whose `gitdir:` line points outside —
|
|
@@ -142,7 +212,11 @@ async function hasDotGit(resolvedRoot) {
|
|
|
142
212
|
* Fail-soft throughout: an unreadable root yields `[]`, a mid-stream error
|
|
143
213
|
* yields the entries found so far — a broken volume never breaks the caller.
|
|
144
214
|
*/
|
|
145
|
-
async function scanDepth1RepoChildren(
|
|
215
|
+
async function scanDepth1RepoChildren(place, limit) {
|
|
216
|
+
const maxDirents = getBrainConfigNumber('brainGitDiscoveryMaxDirents', undefined, GIT_DISCOVERY_MAX_DIRENTS);
|
|
217
|
+
if (place.device)
|
|
218
|
+
return scanDeviceDepth1RepoChildren(place, limit, maxDirents);
|
|
219
|
+
const resolvedRoot = place.root;
|
|
146
220
|
let realRoot;
|
|
147
221
|
try {
|
|
148
222
|
realRoot = await realpath(resolvedRoot);
|
|
@@ -164,7 +238,7 @@ async function scanDepth1RepoChildren(resolvedRoot, limit) {
|
|
|
164
238
|
// Breaking out of for-await invokes the iterator's return() → the Dir
|
|
165
239
|
// handle closes itself; no manual close (a second close would throw).
|
|
166
240
|
for await (const dirent of dir) {
|
|
167
|
-
if (examined >=
|
|
241
|
+
if (examined >= maxDirents)
|
|
168
242
|
break;
|
|
169
243
|
examined += 1;
|
|
170
244
|
if (!dirent.isDirectory() && !dirent.isSymbolicLink())
|
|
@@ -179,7 +253,7 @@ async function scanDepth1RepoChildren(resolvedRoot, limit) {
|
|
|
179
253
|
}
|
|
180
254
|
if (real !== realRoot && !real.startsWith(rootPrefix))
|
|
181
255
|
continue; // MAJOR-A
|
|
182
|
-
if (!(await hasDotGit(child)))
|
|
256
|
+
if (!(await hasDotGit({ root: child })))
|
|
183
257
|
continue;
|
|
184
258
|
// Step 4 (MAJOR-A tail): contain the `.git` TARGET as well — hasDotGit
|
|
185
259
|
// stat-followed it, so a symlinked `.git` onto a foreign repo passed.
|
|
@@ -192,7 +266,7 @@ async function scanDepth1RepoChildren(resolvedRoot, limit) {
|
|
|
192
266
|
catch {
|
|
193
267
|
continue; // raced away between the stat and the realpath — fail-soft skip
|
|
194
268
|
}
|
|
195
|
-
found.push({ rel: dirent.name, name: dirent.name, root: child });
|
|
269
|
+
found.push({ rel: dirent.name, name: dirent.name, root: child, place: { root: child } });
|
|
196
270
|
if (found.length >= limit)
|
|
197
271
|
break;
|
|
198
272
|
}
|
|
@@ -217,18 +291,19 @@ async function scanDepth1RepoChildren(resolvedRoot, limit) {
|
|
|
217
291
|
* `invalidateSourceGitCache(sourceRoot)` / `invalidateSourceGitDiscovery`
|
|
218
292
|
* drop the entry.
|
|
219
293
|
*/
|
|
220
|
-
export function discoverSourceRepoRoots(
|
|
221
|
-
|
|
294
|
+
export function discoverSourceRepoRoots(target) {
|
|
295
|
+
const place = toGitPlace(target);
|
|
296
|
+
return withCache(discoveryCache, placeKey(place), GIT_DISCOVERY_CACHE_TTL_MS, () => computeRepoRootsUncached(place));
|
|
222
297
|
}
|
|
223
|
-
async function computeRepoRootsUncached(
|
|
224
|
-
if (await hasDotGit(
|
|
225
|
-
return [{ rel: '', name: (await repoNameFromOrigin(
|
|
298
|
+
async function computeRepoRootsUncached(place) {
|
|
299
|
+
if (await hasDotGit(place)) {
|
|
300
|
+
return [{ rel: '', name: (await repoNameFromOrigin(place)) ?? repoNameFromRoot(place) ?? '', root: place.root, place }];
|
|
226
301
|
}
|
|
227
|
-
const found = await scanDepth1RepoChildren(
|
|
302
|
+
const found = await scanDepth1RepoChildren(place, getBrainConfigNumber('brainGitDiscoveryMaxRepos', undefined, GIT_DISCOVERY_MAX_REPOS));
|
|
228
303
|
// `name` is the DISPLAY name (origin basename, else the dir), the summary's order;
|
|
229
304
|
// `rel` stays the dir and is the only one a path gate may read. The cap grabs the
|
|
230
305
|
// FIRST ≤25 in dirent order; the REPORT is name-sorted (codepoint, locale-free).
|
|
231
|
-
const named = await Promise.all(found.map(async (r) => ({ ...r, name: (await repoNameFromOrigin(r.
|
|
306
|
+
const named = await Promise.all(found.map(async (r) => ({ ...r, name: (await repoNameFromOrigin(r.place)) ?? r.name })));
|
|
232
307
|
return named.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
233
308
|
}
|
|
234
309
|
/**
|
|
@@ -265,23 +340,31 @@ export function filterVisibleRepoNames(repoNames, permissions, source, createdBy
|
|
|
265
340
|
* origin (fall back to the dir basename), the config is unreadable (a linked
|
|
266
341
|
* worktree's `.git` is a FILE, no sibling config), or parsing yields nothing.
|
|
267
342
|
*/
|
|
268
|
-
export async function repoNameFromOrigin(
|
|
269
|
-
const
|
|
343
|
+
export async function repoNameFromOrigin(target) {
|
|
344
|
+
const place = toGitPlace(target);
|
|
345
|
+
const configPath = placePath(place).join(place.root, '.git', 'config');
|
|
270
346
|
let text;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// pathologically large config must never force a full-file allocation on a
|
|
275
|
-
// co-member's uncached summary. `.git/config` is normally a few hundred
|
|
276
|
-
// bytes; anything past 256 KB is not a real config → skip (basename fallback).
|
|
277
|
-
const info = await stat(configPath);
|
|
278
|
-
if (info.size > 256 * 1024)
|
|
347
|
+
if (place.device) {
|
|
348
|
+
const read = await readDeviceSmallFile(place.device, configPath, 256 * 1024);
|
|
349
|
+
if (read === null)
|
|
279
350
|
return undefined;
|
|
280
|
-
text =
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
351
|
+
text = read;
|
|
352
|
+
}
|
|
353
|
+
else
|
|
354
|
+
try {
|
|
355
|
+
// Bound the READ, not a post-read slice: a project member can write the
|
|
356
|
+
// shared source's `.git/config`, and this runs on the summary hot path — a
|
|
357
|
+
// pathologically large config must never force a full-file allocation on a
|
|
358
|
+
// co-member's uncached summary. `.git/config` is normally a few hundred
|
|
359
|
+
// bytes; anything past 256 KB is not a real config → skip (basename fallback).
|
|
360
|
+
const info = await stat(configPath);
|
|
361
|
+
if (info.size > 256 * 1024)
|
|
362
|
+
return undefined;
|
|
363
|
+
text = await readFile(configPath, 'utf8');
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
285
368
|
let inOrigin = false;
|
|
286
369
|
for (const raw of text.split('\n')) {
|
|
287
370
|
const line = raw.trim();
|
|
@@ -313,17 +396,18 @@ export async function repoNameFromOrigin(resolvedRoot) {
|
|
|
313
396
|
* user action (panel open / repo select), NEVER on the `fs_list` hot path (use
|
|
314
397
|
* `getSourceGitSummary` there).
|
|
315
398
|
*/
|
|
316
|
-
export function getSourceGitStatus(
|
|
317
|
-
|
|
399
|
+
export function getSourceGitStatus(target, gitBin) {
|
|
400
|
+
const place = toGitPlace(target);
|
|
401
|
+
return withCache(statusCache, spawnKey(place), GIT_CONTROL_CACHE_TTL_MS, () => computeStatusUncached(place, gitBin));
|
|
318
402
|
}
|
|
319
|
-
async function computeStatusUncached(
|
|
320
|
-
if (!(await hasDotGit(
|
|
403
|
+
async function computeStatusUncached(place, gitBin) {
|
|
404
|
+
if (!(await hasDotGit(place)))
|
|
321
405
|
return null;
|
|
322
406
|
let runner;
|
|
323
407
|
try {
|
|
324
408
|
// Fresh runner per miss — NEVER cache a runner (memoised-rejection trap).
|
|
325
409
|
runner = await GitRunner.create({
|
|
326
|
-
|
|
410
|
+
root: place,
|
|
327
411
|
gitBin,
|
|
328
412
|
timeoutMs: getBrainConfigNumber('brainGitPanelTimeoutMs', undefined, GIT_CONTROL_STATUS_TIMEOUT_MS),
|
|
329
413
|
});
|
|
@@ -384,7 +468,8 @@ async function computeStatusUncached(resolvedRoot, gitBin) {
|
|
|
384
468
|
untracked,
|
|
385
469
|
};
|
|
386
470
|
}
|
|
387
|
-
catch {
|
|
471
|
+
catch (error) {
|
|
472
|
+
rethrowDeviceRefusal(error);
|
|
388
473
|
// GitNotARepoError / GitOutsideSourceError / GitTimeoutError / any → fail-soft.
|
|
389
474
|
return null;
|
|
390
475
|
}
|
|
@@ -409,13 +494,14 @@ async function computeStatusUncached(resolvedRoot, gitBin) {
|
|
|
409
494
|
* yields an empty primary diff but is NOT untracked, so it correctly stays blank
|
|
410
495
|
* (never fabricated as fully-added).
|
|
411
496
|
*/
|
|
412
|
-
export async function getSourceGitDiff(
|
|
413
|
-
|
|
497
|
+
export async function getSourceGitDiff(at, repoRelPath, target, gitBin) {
|
|
498
|
+
const place = toGitPlace(at);
|
|
499
|
+
if (!(await hasDotGit(place)))
|
|
414
500
|
return null;
|
|
415
501
|
let runner;
|
|
416
502
|
try {
|
|
417
503
|
runner = await GitRunner.create({
|
|
418
|
-
|
|
504
|
+
root: place,
|
|
419
505
|
gitBin,
|
|
420
506
|
timeoutMs: getBrainConfigNumber('brainGitHeavyTimeoutMs', undefined, GIT_CONTROL_HEAVY_TIMEOUT_MS),
|
|
421
507
|
});
|
|
@@ -445,6 +531,7 @@ export async function getSourceGitDiff(resolvedRoot, repoRelPath, target, gitBin
|
|
|
445
531
|
return { ok: true, diff: primary };
|
|
446
532
|
}
|
|
447
533
|
catch (error) {
|
|
534
|
+
rethrowDeviceRefusal(error);
|
|
448
535
|
if (error instanceof GitOutputLimitError)
|
|
449
536
|
return { ok: false, code: 'too_large' };
|
|
450
537
|
// GitNotARepoError / GitOutsideSourceError / GitArgumentError / GitTimeoutError / GitApplyError → fail-soft.
|
|
@@ -493,13 +580,14 @@ async function withNumstat(runner, files, from, to) {
|
|
|
493
580
|
return { ...f, added: n.added, removed: n.removed, ...(n.binary ? { binary: true } : {}) };
|
|
494
581
|
});
|
|
495
582
|
}
|
|
496
|
-
export async function getSourceGitCommitFiles(
|
|
497
|
-
|
|
583
|
+
export async function getSourceGitCommitFiles(target, sha, gitBin) {
|
|
584
|
+
const place = toGitPlace(target);
|
|
585
|
+
if (!(await hasDotGit(place)))
|
|
498
586
|
return null;
|
|
499
587
|
let runner;
|
|
500
588
|
try {
|
|
501
589
|
runner = await GitRunner.create({
|
|
502
|
-
|
|
590
|
+
root: place,
|
|
503
591
|
gitBin,
|
|
504
592
|
timeoutMs: getBrainConfigNumber('brainGitHeavyTimeoutMs', undefined, GIT_CONTROL_HEAVY_TIMEOUT_MS),
|
|
505
593
|
});
|
|
@@ -512,7 +600,8 @@ export async function getSourceGitCommitFiles(resolvedRoot, sha, gitBin) {
|
|
|
512
600
|
const files = await runner.commitFiles(sha, { hasParent });
|
|
513
601
|
return await withNumstat(runner, files, hasParent ? `${sha}^` : EMPTY_TREE_SHA, sha);
|
|
514
602
|
}
|
|
515
|
-
catch {
|
|
603
|
+
catch (error) {
|
|
604
|
+
rethrowDeviceRefusal(error);
|
|
516
605
|
// Unknown sha / GitNotARepoError / GitTimeoutError / GitOutputLimitError / any → fail-soft.
|
|
517
606
|
return null;
|
|
518
607
|
}
|
|
@@ -533,13 +622,14 @@ export async function getSourceGitCommitFiles(resolvedRoot, sha, gitBin) {
|
|
|
533
622
|
* - non-repo / unborn HEAD / timeout / any git error → `null`.
|
|
534
623
|
* ON DEMAND, UNcached, fresh runner, heavy timeout.
|
|
535
624
|
*/
|
|
536
|
-
export async function getSourceGitOutgoing(
|
|
537
|
-
|
|
625
|
+
export async function getSourceGitOutgoing(target, gitBin) {
|
|
626
|
+
const place = toGitPlace(target);
|
|
627
|
+
if (!(await hasDotGit(place)))
|
|
538
628
|
return null;
|
|
539
629
|
let runner;
|
|
540
630
|
try {
|
|
541
631
|
runner = await GitRunner.create({
|
|
542
|
-
|
|
632
|
+
root: place,
|
|
543
633
|
gitBin,
|
|
544
634
|
timeoutMs: getBrainConfigNumber('brainGitHeavyTimeoutMs', undefined, GIT_CONTROL_HEAVY_TIMEOUT_MS),
|
|
545
635
|
});
|
|
@@ -556,17 +646,19 @@ export async function getSourceGitOutgoing(resolvedRoot, gitBin) {
|
|
|
556
646
|
const files = await withNumstat(runner, await runner.treeDiffFiles(resolved.baseSha, resolved.headSha), resolved.baseSha, resolved.headSha);
|
|
557
647
|
return { baseSha: resolved.baseSha, headSha: resolved.headSha, files };
|
|
558
648
|
}
|
|
559
|
-
catch {
|
|
649
|
+
catch (error) {
|
|
650
|
+
rethrowDeviceRefusal(error);
|
|
560
651
|
return null;
|
|
561
652
|
}
|
|
562
653
|
}
|
|
563
|
-
export async function getSourceGitLogStats(
|
|
564
|
-
|
|
654
|
+
export async function getSourceGitLogStats(target, opts, gitBin) {
|
|
655
|
+
const place = toGitPlace(target);
|
|
656
|
+
if (!(await hasDotGit(place)))
|
|
565
657
|
return null;
|
|
566
658
|
let runner;
|
|
567
659
|
try {
|
|
568
660
|
runner = await GitRunner.create({
|
|
569
|
-
|
|
661
|
+
root: place,
|
|
570
662
|
gitBin,
|
|
571
663
|
timeoutMs: getBrainConfigNumber('brainGitHeavyTimeoutMs', undefined, GIT_CONTROL_HEAVY_TIMEOUT_MS),
|
|
572
664
|
});
|
|
@@ -579,7 +671,8 @@ export async function getSourceGitLogStats(resolvedRoot, opts, gitBin) {
|
|
|
579
671
|
const outgoing = await runner.outgoingShas();
|
|
580
672
|
return { byCommit, ...(outgoing ? { outgoing } : {}) };
|
|
581
673
|
}
|
|
582
|
-
catch {
|
|
674
|
+
catch (error) {
|
|
675
|
+
rethrowDeviceRefusal(error);
|
|
583
676
|
// Includes an older git rejecting `--diff-merges` — fail-soft to no stats.
|
|
584
677
|
return null;
|
|
585
678
|
}
|
|
@@ -599,13 +692,14 @@ export async function getSourceGitLogStats(resolvedRoot, opts, gitBin) {
|
|
|
599
692
|
* log rather than a separate route so the graph's page fetch and its
|
|
600
693
|
* decorations can never disagree about the ref state they were read under.
|
|
601
694
|
*/
|
|
602
|
-
export async function getSourceGitLog(
|
|
603
|
-
|
|
695
|
+
export async function getSourceGitLog(target, opts, gitBin) {
|
|
696
|
+
const place = toGitPlace(target);
|
|
697
|
+
if (!(await hasDotGit(place)))
|
|
604
698
|
return null;
|
|
605
699
|
let runner;
|
|
606
700
|
try {
|
|
607
701
|
runner = await GitRunner.create({
|
|
608
|
-
|
|
702
|
+
root: place,
|
|
609
703
|
gitBin,
|
|
610
704
|
timeoutMs: getBrainConfigNumber('brainGitHeavyTimeoutMs', undefined, GIT_CONTROL_HEAVY_TIMEOUT_MS),
|
|
611
705
|
});
|
|
@@ -618,7 +712,8 @@ export async function getSourceGitLog(resolvedRoot, opts, gitBin) {
|
|
|
618
712
|
const outgoing = await runner.outgoingShas();
|
|
619
713
|
return { commits, ...(outgoing ? { outgoing } : {}) };
|
|
620
714
|
}
|
|
621
|
-
catch {
|
|
715
|
+
catch (error) {
|
|
716
|
+
rethrowDeviceRefusal(error);
|
|
622
717
|
return null;
|
|
623
718
|
}
|
|
624
719
|
}
|
|
@@ -637,13 +732,14 @@ export async function getSourceGitLog(resolvedRoot, opts, gitBin) {
|
|
|
637
732
|
* `null` (fail-soft) for a non-git source, a path missing at the rev, a
|
|
638
733
|
* timeout, or any git error.
|
|
639
734
|
*/
|
|
640
|
-
export async function getSourceGitShow(
|
|
641
|
-
|
|
735
|
+
export async function getSourceGitShow(target, repoRelPath, rev, gitBin) {
|
|
736
|
+
const place = toGitPlace(target);
|
|
737
|
+
if (!(await hasDotGit(place)))
|
|
642
738
|
return null;
|
|
643
739
|
let runner;
|
|
644
740
|
try {
|
|
645
741
|
runner = await GitRunner.create({
|
|
646
|
-
|
|
742
|
+
root: place,
|
|
647
743
|
gitBin,
|
|
648
744
|
timeoutMs: getBrainConfigNumber('brainGitHeavyTimeoutMs', undefined, GIT_CONTROL_HEAVY_TIMEOUT_MS),
|
|
649
745
|
});
|
|
@@ -663,7 +759,8 @@ export async function getSourceGitShow(resolvedRoot, repoRelPath, rev, gitBin) {
|
|
|
663
759
|
return { ok: false, code: 'binary' };
|
|
664
760
|
return { ok: true, content, path: repoRelPath, rev };
|
|
665
761
|
}
|
|
666
|
-
catch {
|
|
762
|
+
catch (error) {
|
|
763
|
+
rethrowDeviceRefusal(error);
|
|
667
764
|
// GitNotARepoError / GitOutsideSourceError / GitArgumentError / GitTimeoutError /
|
|
668
765
|
// GitApplyError (path missing at the rev) / GitOutputLimitError → fail-soft.
|
|
669
766
|
return null;
|
|
@@ -675,7 +772,13 @@ export async function getSourceGitShow(resolvedRoot, repoRelPath, rev, gitBin) {
|
|
|
675
772
|
* The `.git/HEAD` badge watcher (G2.2) calls this the instant an EXTERNAL git
|
|
676
773
|
* op fires so the next panel/list read recomputes from the live repo instead
|
|
677
774
|
* of serving up to `GIT_CONTROL_CACHE_TTL_MS` of stale state. The caches key
|
|
678
|
-
* on the resolved root (see `withCache
|
|
775
|
+
* on the resolved root (see `withCache`; the two that spawn on a device —
|
|
776
|
+
* status and branches — also on the caller's class, `spawnKey`, and both are
|
|
777
|
+
* dropped; the summary stays on the root because on a device it is FILE reads
|
|
778
|
+
* of `.git/HEAD` + `.git/config`, the same answer for every caller, and the
|
|
779
|
+
* discovery is the raw repo-root SET every consumer filters per caller through
|
|
780
|
+
* `filterVisibleRepoNames` — a spawn added to either moves it to `spawnKey`),
|
|
781
|
+
* so a keyed delete is exact — and
|
|
679
782
|
* that keying is what keeps the watcher's existing call correct for NESTED
|
|
680
783
|
* roots (E1): a nested-root invalidation drops that repo's own summary/status
|
|
681
784
|
* entries, while the discovery/probe entries (keyed by the SOURCE root) are
|
|
@@ -683,11 +786,14 @@ export async function getSourceGitShow(resolvedRoot, repoRelPath, rev, gitBin) {
|
|
|
683
786
|
* that DO change the set (source config mutation) invalidate at the source
|
|
684
787
|
* root, which sweeps discovery too via `invalidateSourceGitDiscovery`.
|
|
685
788
|
*/
|
|
686
|
-
export function invalidateSourceGitCache(
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
789
|
+
export function invalidateSourceGitCache(target) {
|
|
790
|
+
const key = placeKey(target);
|
|
791
|
+
for (const spawned of [key, `${key}\0other`]) {
|
|
792
|
+
statusCache.delete(spawned);
|
|
793
|
+
branchCache.delete(spawned);
|
|
794
|
+
}
|
|
795
|
+
summaryCache.delete(key);
|
|
796
|
+
discoveryCache.delete(key);
|
|
691
797
|
}
|
|
692
798
|
/**
|
|
693
799
|
* Source-root-keyed drop of ONLY the E1 nested-checkout discovery entry (the
|
|
@@ -696,7 +802,7 @@ export function invalidateSourceGitCache(resolvedRoot) {
|
|
|
696
802
|
* without also flushing the source's summary/status caches.
|
|
697
803
|
*/
|
|
698
804
|
export function invalidateSourceGitDiscovery(sourceRoot) {
|
|
699
|
-
discoveryCache.delete(sourceRoot);
|
|
805
|
+
discoveryCache.delete(placeKey(sourceRoot));
|
|
700
806
|
}
|
|
701
807
|
/**
|
|
702
808
|
* The local branch list for the branch-switcher. On demand, TTL-cached per root.
|
|
@@ -704,18 +810,19 @@ export function invalidateSourceGitDiscovery(sourceRoot) {
|
|
|
704
810
|
* `getSourceGitSummary` (enumerates every local branch) — the switcher fetches it
|
|
705
811
|
* on demand, never the list path.
|
|
706
812
|
*/
|
|
707
|
-
export function getSourceBranches(
|
|
708
|
-
|
|
813
|
+
export function getSourceBranches(target, gitBin) {
|
|
814
|
+
const place = toGitPlace(target);
|
|
815
|
+
return withCache(branchCache, spawnKey(place), GIT_CONTROL_CACHE_TTL_MS, () => computeBranchesUncached(place, gitBin));
|
|
709
816
|
}
|
|
710
|
-
async function computeBranchesUncached(
|
|
711
|
-
if (!(await hasDotGit(
|
|
817
|
+
async function computeBranchesUncached(place, gitBin) {
|
|
818
|
+
if (!(await hasDotGit(place)))
|
|
712
819
|
return null;
|
|
713
820
|
let runner;
|
|
714
821
|
try {
|
|
715
822
|
// MINOR-9: the branch list carries the SAME 5 s cap as status — it was the
|
|
716
823
|
// one panel reader built with NO timeout.
|
|
717
824
|
runner = await GitRunner.create({
|
|
718
|
-
|
|
825
|
+
root: place,
|
|
719
826
|
gitBin,
|
|
720
827
|
timeoutMs: getBrainConfigNumber('brainGitPanelTimeoutMs', undefined, GIT_CONTROL_STATUS_TIMEOUT_MS),
|
|
721
828
|
});
|
|
@@ -726,7 +833,8 @@ async function computeBranchesUncached(resolvedRoot, gitBin) {
|
|
|
726
833
|
try {
|
|
727
834
|
return await runner.branchList();
|
|
728
835
|
}
|
|
729
|
-
catch {
|
|
836
|
+
catch (error) {
|
|
837
|
+
rethrowDeviceRefusal(error);
|
|
730
838
|
return null;
|
|
731
839
|
}
|
|
732
840
|
}
|
|
@@ -736,8 +844,9 @@ async function computeBranchesUncached(resolvedRoot, gitBin) {
|
|
|
736
844
|
* `undefined` when the basename is empty (a source rooted at `/`), so the
|
|
737
845
|
* `repoName` field is omitted rather than stamped as `''`.
|
|
738
846
|
*/
|
|
739
|
-
export function repoNameFromRoot(
|
|
740
|
-
const
|
|
847
|
+
export function repoNameFromRoot(target) {
|
|
848
|
+
const place = toGitPlace(target);
|
|
849
|
+
const name = placePath(place).basename(place.root);
|
|
741
850
|
return name.length > 0 ? name : undefined;
|
|
742
851
|
}
|
|
743
852
|
/**
|
|
@@ -758,28 +867,35 @@ export function repoNameFromRoot(resolvedRoot) {
|
|
|
758
867
|
* `repoNames` is the RAW set — consumers MUST `filterVisibleRepoNames` before
|
|
759
868
|
* stamping (per-file name-visibility). Root-repos never pay the scan.
|
|
760
869
|
*/
|
|
761
|
-
export function getSourceGitSummary(
|
|
762
|
-
|
|
870
|
+
export function getSourceGitSummary(target, gitBin) {
|
|
871
|
+
const place = toGitPlace(target);
|
|
872
|
+
return withCache(summaryCache, placeKey(place), GIT_CONTROL_CACHE_TTL_MS, () => computeSummaryUncached(place, gitBin));
|
|
763
873
|
}
|
|
764
|
-
async function computeSummaryUncached(
|
|
765
|
-
if (!(await hasDotGit(
|
|
874
|
+
async function computeSummaryUncached(place, gitBin) {
|
|
875
|
+
if (!(await hasDotGit(place))) {
|
|
766
876
|
// E1/F2 — the ONLY new hot-path cost (MAJOR-4b budget): the shared 30 s
|
|
767
877
|
// `discoverSourceRepoRoots` (streaming opendir ≤500 + ≤25 realpath-contained
|
|
768
878
|
// finds; cache shared with the route + watcher so no double scan), fail-soft
|
|
769
879
|
// to []. ZERO git spawn. A non-containing source pays the same dirent walk a
|
|
770
880
|
// short-circuit probe would; a containing source pays ≤25 bounded stats.
|
|
771
|
-
const nested = (await discoverSourceRepoRoots(
|
|
881
|
+
const nested = (await discoverSourceRepoRoots(place)).filter((r) => r.rel !== '');
|
|
772
882
|
return nested.length > 0
|
|
773
883
|
? { isRepo: false, containsRepos: true, repoNames: nested.map((r) => r.rel) }
|
|
774
884
|
: { isRepo: false };
|
|
775
885
|
}
|
|
776
886
|
// F2: the REAL repo name = the origin basename (mount-independent), falling
|
|
777
887
|
// back to the checkout-dir basename for a no-remote repo / linked worktree.
|
|
778
|
-
const repoName = (await repoNameFromOrigin(
|
|
888
|
+
const repoName = (await repoNameFromOrigin(place)) ?? repoNameFromRoot(place);
|
|
779
889
|
const named = repoName ? { repoName } : {};
|
|
890
|
+
if (place.device) {
|
|
891
|
+
// The list path never spawns on a device (the caller may not run programs
|
|
892
|
+
// there): the branch is read from `.git/HEAD` as a file.
|
|
893
|
+
const branch = await readDeviceHeadBranch(place.device, place.root, placePath(place));
|
|
894
|
+
return { isRepo: true, ...named, ...(branch ? { branch } : {}) };
|
|
895
|
+
}
|
|
780
896
|
let runner;
|
|
781
897
|
try {
|
|
782
|
-
runner = await GitRunner.create({
|
|
898
|
+
runner = await GitRunner.create({ root: place, gitBin });
|
|
783
899
|
}
|
|
784
900
|
catch {
|
|
785
901
|
// `.git` exists but the runner could not be built — still a repo, no branch.
|
|
@@ -802,12 +918,12 @@ async function computeSummaryUncached(resolvedRoot, gitBin) {
|
|
|
802
918
|
* owner gains write via `byRole`/`byUser`). PURE (no git I/O) → O(1)/source, safe on
|
|
803
919
|
* the hot list path. Deny-by-default: false unless BOTH planes allow.
|
|
804
920
|
*
|
|
805
|
-
* canWrite = caller HAS `
|
|
921
|
+
* canWrite = caller HAS `files.write` (the file-level FEATURE, same predicate as
|
|
806
922
|
* the route `denyIfNoWrite`) ∧ the caller's RESOLVED uri-policy
|
|
807
923
|
* `write` on the source ROOT `<slug>://` is allowed.
|
|
808
924
|
*/
|
|
809
925
|
export function resolveGitCanWrite(permissions, source, createdBy, caller) {
|
|
810
|
-
if (!hasFeature({ grantedFeatures: caller.grantedFeatures ?? [] }, '
|
|
926
|
+
if (!hasFeature({ grantedFeatures: caller.grantedFeatures ?? [] }, 'files.write'))
|
|
811
927
|
return false;
|
|
812
928
|
return (resolvePermissions(permissions, `${source}://`, {
|
|
813
929
|
userId: caller.userId,
|
|
@@ -825,7 +941,7 @@ export function resolveGitCanWrite(permissions, source, createdBy, caller) {
|
|
|
825
941
|
* ROOT `<slug>://`, exactly what the remote-op route gates on
|
|
826
942
|
* (`resolveGitSource({capability:'exec'})`). Drives whether the panel enables
|
|
827
943
|
* the fetch/pull/push popover vs the "grant exec" affordance. Unlike canWrite it
|
|
828
|
-
* does NOT gate the `
|
|
944
|
+
* does NOT gate the `files.write` FEATURE (the route + the panel's canWrite
|
|
829
945
|
* trigger gate that separately) — it reflects ONLY the exec policy so the
|
|
830
946
|
* affordance is precise. PURE (no git I/O). Deny-by-default: `exec:false`
|
|
831
947
|
* baseline out of the box.
|