@xenosystem/agent-interface-host 0.1.3
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 +67 -0
- package/dist/acp/AcpAgentRegistryService.d.ts +77 -0
- package/dist/acp/AcpAgentRegistryService.d.ts.map +1 -0
- package/dist/acp/AcpAgentRegistryService.js +754 -0
- package/dist/acp/AcpAgentRegistryService.js.map +1 -0
- package/dist/acp/AcpAgentTurnExecutionAdapter.d.ts +56 -0
- package/dist/acp/AcpAgentTurnExecutionAdapter.d.ts.map +1 -0
- package/dist/acp/AcpAgentTurnExecutionAdapter.js +323 -0
- package/dist/acp/AcpAgentTurnExecutionAdapter.js.map +1 -0
- package/dist/acp/AcpApprovalPortAdapter.d.ts +41 -0
- package/dist/acp/AcpApprovalPortAdapter.d.ts.map +1 -0
- package/dist/acp/AcpApprovalPortAdapter.js +82 -0
- package/dist/acp/AcpApprovalPortAdapter.js.map +1 -0
- package/dist/acp/AcpCoordinationBridge.d.ts +13 -0
- package/dist/acp/AcpCoordinationBridge.d.ts.map +1 -0
- package/dist/acp/AcpCoordinationBridge.js +13 -0
- package/dist/acp/AcpCoordinationBridge.js.map +1 -0
- package/dist/acp/AcpDiagnosticsService.d.ts +61 -0
- package/dist/acp/AcpDiagnosticsService.d.ts.map +1 -0
- package/dist/acp/AcpDiagnosticsService.js +203 -0
- package/dist/acp/AcpDiagnosticsService.js.map +1 -0
- package/dist/acp/AcpExternalCertificationHarness.d.ts +47 -0
- package/dist/acp/AcpExternalCertificationHarness.d.ts.map +1 -0
- package/dist/acp/AcpExternalCertificationHarness.js +176 -0
- package/dist/acp/AcpExternalCertificationHarness.js.map +1 -0
- package/dist/acp/AcpHostBridge.d.ts +81 -0
- package/dist/acp/AcpHostBridge.d.ts.map +1 -0
- package/dist/acp/AcpHostBridge.js +101 -0
- package/dist/acp/AcpHostBridge.js.map +1 -0
- package/dist/acp/AcpInteractivePermissionBridge.d.ts +77 -0
- package/dist/acp/AcpInteractivePermissionBridge.d.ts.map +1 -0
- package/dist/acp/AcpInteractivePermissionBridge.js +127 -0
- package/dist/acp/AcpInteractivePermissionBridge.js.map +1 -0
- package/dist/acp/AcpPermissionBridge.d.ts +19 -0
- package/dist/acp/AcpPermissionBridge.d.ts.map +1 -0
- package/dist/acp/AcpPermissionBridge.js +110 -0
- package/dist/acp/AcpPermissionBridge.js.map +1 -0
- package/dist/acp/AcpPermissionPolicy.d.ts +111 -0
- package/dist/acp/AcpPermissionPolicy.d.ts.map +1 -0
- package/dist/acp/AcpPermissionPolicy.js +167 -0
- package/dist/acp/AcpPermissionPolicy.js.map +1 -0
- package/dist/acp/AcpProcessSupervisor.d.ts +134 -0
- package/dist/acp/AcpProcessSupervisor.d.ts.map +1 -0
- package/dist/acp/AcpProcessSupervisor.js +674 -0
- package/dist/acp/AcpProcessSupervisor.js.map +1 -0
- package/dist/acp/AcpProviderApprovalService.d.ts +27 -0
- package/dist/acp/AcpProviderApprovalService.d.ts.map +1 -0
- package/dist/acp/AcpProviderApprovalService.js +128 -0
- package/dist/acp/AcpProviderApprovalService.js.map +1 -0
- package/dist/acp/AcpProviderCatalogService.d.ts +69 -0
- package/dist/acp/AcpProviderCatalogService.d.ts.map +1 -0
- package/dist/acp/AcpProviderCatalogService.js +707 -0
- package/dist/acp/AcpProviderCatalogService.js.map +1 -0
- package/dist/acp/AcpProviderModelService.d.ts +6 -0
- package/dist/acp/AcpProviderModelService.d.ts.map +1 -0
- package/dist/acp/AcpProviderModelService.js +274 -0
- package/dist/acp/AcpProviderModelService.js.map +1 -0
- package/dist/acp/AcpProviderVersionReadiness.d.ts +6 -0
- package/dist/acp/AcpProviderVersionReadiness.d.ts.map +1 -0
- package/dist/acp/AcpProviderVersionReadiness.js +100 -0
- package/dist/acp/AcpProviderVersionReadiness.js.map +1 -0
- package/dist/acp/AcpRuntimeEventMapper.d.ts +16 -0
- package/dist/acp/AcpRuntimeEventMapper.d.ts.map +1 -0
- package/dist/acp/AcpRuntimeEventMapper.js +326 -0
- package/dist/acp/AcpRuntimeEventMapper.js.map +1 -0
- package/dist/acp/AcpSessionService.d.ts +65 -0
- package/dist/acp/AcpSessionService.d.ts.map +1 -0
- package/dist/acp/AcpSessionService.js +931 -0
- package/dist/acp/AcpSessionService.js.map +1 -0
- package/dist/acp/DurableAcpPermissionPolicyStore.d.ts +25 -0
- package/dist/acp/DurableAcpPermissionPolicyStore.d.ts.map +1 -0
- package/dist/acp/DurableAcpPermissionPolicyStore.js +100 -0
- package/dist/acp/DurableAcpPermissionPolicyStore.js.map +1 -0
- package/dist/acp/acpCommandSafety.d.ts +63 -0
- package/dist/acp/acpCommandSafety.d.ts.map +1 -0
- package/dist/acp/acpCommandSafety.js +90 -0
- package/dist/acp/acpCommandSafety.js.map +1 -0
- package/dist/acp/acpErrors.d.ts +47 -0
- package/dist/acp/acpErrors.d.ts.map +1 -0
- package/dist/acp/acpErrors.js +93 -0
- package/dist/acp/acpErrors.js.map +1 -0
- package/dist/acp/acpRedaction.d.ts +9 -0
- package/dist/acp/acpRedaction.d.ts.map +1 -0
- package/dist/acp/acpRedaction.js +69 -0
- package/dist/acp/acpRedaction.js.map +1 -0
- package/dist/acp/acpSessionResume.d.ts +8 -0
- package/dist/acp/acpSessionResume.d.ts.map +1 -0
- package/dist/acp/acpSessionResume.js +31 -0
- package/dist/acp/acpSessionResume.js.map +1 -0
- package/dist/acp/acpTypes.d.ts +467 -0
- package/dist/acp/acpTypes.d.ts.map +1 -0
- package/dist/acp/acpTypes.js +2 -0
- package/dist/acp/acpTypes.js.map +1 -0
- package/dist/agent/chooseDirectoryBridge.d.ts +29 -0
- package/dist/agent/chooseDirectoryBridge.d.ts.map +1 -0
- package/dist/agent/chooseDirectoryBridge.js +116 -0
- package/dist/agent/chooseDirectoryBridge.js.map +1 -0
- package/dist/agent/permissionProfiles.d.ts +29 -0
- package/dist/agent/permissionProfiles.d.ts.map +1 -0
- package/dist/agent/permissionProfiles.js +107 -0
- package/dist/agent/permissionProfiles.js.map +1 -0
- package/dist/agent/runtimeAuthorityPrompt.d.ts +13 -0
- package/dist/agent/runtimeAuthorityPrompt.d.ts.map +1 -0
- package/dist/agent/runtimeAuthorityPrompt.js +45 -0
- package/dist/agent/runtimeAuthorityPrompt.js.map +1 -0
- package/dist/agent/runtimeModeOutputGuard.d.ts +6 -0
- package/dist/agent/runtimeModeOutputGuard.d.ts.map +1 -0
- package/dist/agent/runtimeModeOutputGuard.js +40 -0
- package/dist/agent/runtimeModeOutputGuard.js.map +1 -0
- package/dist/agent/workspacePlacementBridge.d.ts +29 -0
- package/dist/agent/workspacePlacementBridge.d.ts.map +1 -0
- package/dist/agent/workspacePlacementBridge.js +143 -0
- package/dist/agent/workspacePlacementBridge.js.map +1 -0
- package/dist/agentSearch/AgentSearchService.d.ts +81 -0
- package/dist/agentSearch/AgentSearchService.d.ts.map +1 -0
- package/dist/agentSearch/AgentSearchService.js +1038 -0
- package/dist/agentSearch/AgentSearchService.js.map +1 -0
- package/dist/agentSearch/JsonInvertedIndexAdapter.d.ts +44 -0
- package/dist/agentSearch/JsonInvertedIndexAdapter.d.ts.map +1 -0
- package/dist/agentSearch/JsonInvertedIndexAdapter.js +307 -0
- package/dist/agentSearch/JsonInvertedIndexAdapter.js.map +1 -0
- package/dist/agentSearch/SearchIndexAdapter.d.ts +33 -0
- package/dist/agentSearch/SearchIndexAdapter.d.ts.map +1 -0
- package/dist/agentSearch/SearchIndexAdapter.js +2 -0
- package/dist/agentSearch/SearchIndexAdapter.js.map +1 -0
- package/dist/agentSearch/SearchSourceWatcher.d.ts +29 -0
- package/dist/agentSearch/SearchSourceWatcher.d.ts.map +1 -0
- package/dist/agentSearch/SearchSourceWatcher.js +150 -0
- package/dist/agentSearch/SearchSourceWatcher.js.map +1 -0
- package/dist/agentSearch/SqliteFtsIndexAdapter.d.ts +58 -0
- package/dist/agentSearch/SqliteFtsIndexAdapter.d.ts.map +1 -0
- package/dist/agentSearch/SqliteFtsIndexAdapter.js +420 -0
- package/dist/agentSearch/SqliteFtsIndexAdapter.js.map +1 -0
- package/dist/agentSearch/conversationIndexer.d.ts +69 -0
- package/dist/agentSearch/conversationIndexer.d.ts.map +1 -0
- package/dist/agentSearch/conversationIndexer.js +313 -0
- package/dist/agentSearch/conversationIndexer.js.map +1 -0
- package/dist/agentSearch/searchTypes.d.ts +264 -0
- package/dist/agentSearch/searchTypes.d.ts.map +1 -0
- package/dist/agentSearch/searchTypes.js +2 -0
- package/dist/agentSearch/searchTypes.js.map +1 -0
- package/dist/agentSearch/searchUtils.d.ts +12 -0
- package/dist/agentSearch/searchUtils.d.ts.map +1 -0
- package/dist/agentSearch/searchUtils.js +125 -0
- package/dist/agentSearch/searchUtils.js.map +1 -0
- package/dist/agentSearch/workspaceMetadataIndexer.d.ts +3 -0
- package/dist/agentSearch/workspaceMetadataIndexer.d.ts.map +1 -0
- package/dist/agentSearch/workspaceMetadataIndexer.js +125 -0
- package/dist/agentSearch/workspaceMetadataIndexer.js.map +1 -0
- package/dist/cli/CliControlPlaneService.d.ts +269 -0
- package/dist/cli/CliControlPlaneService.d.ts.map +1 -0
- package/dist/cli/CliControlPlaneService.js +1874 -0
- package/dist/cli/CliControlPlaneService.js.map +1 -0
- package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.d.ts +91 -0
- package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.d.ts.map +1 -0
- package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.js +379 -0
- package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.js.map +1 -0
- package/dist/codebase/CodebaseIntelligenceService.d.ts +68 -0
- package/dist/codebase/CodebaseIntelligenceService.d.ts.map +1 -0
- package/dist/codebase/CodebaseIntelligenceService.js +1717 -0
- package/dist/codebase/CodebaseIntelligenceService.js.map +1 -0
- package/dist/codebase/LanguageServerProcessManager.d.ts +85 -0
- package/dist/codebase/LanguageServerProcessManager.d.ts.map +1 -0
- package/dist/codebase/LanguageServerProcessManager.js +607 -0
- package/dist/codebase/LanguageServerProcessManager.js.map +1 -0
- package/dist/codebase/codeTypes.d.ts +485 -0
- package/dist/codebase/codeTypes.d.ts.map +1 -0
- package/dist/codebase/codeTypes.js +2 -0
- package/dist/codebase/codeTypes.js.map +1 -0
- package/dist/codebase/codeUtils.d.ts +24 -0
- package/dist/codebase/codeUtils.d.ts.map +1 -0
- package/dist/codebase/codeUtils.js +177 -0
- package/dist/codebase/codeUtils.js.map +1 -0
- package/dist/codebase/contextPacks.d.ts +18 -0
- package/dist/codebase/contextPacks.d.ts.map +1 -0
- package/dist/codebase/contextPacks.js +180 -0
- package/dist/codebase/contextPacks.js.map +1 -0
- package/dist/codebase/dependencyGraph.d.ts +3 -0
- package/dist/codebase/dependencyGraph.d.ts.map +1 -0
- package/dist/codebase/dependencyGraph.js +209 -0
- package/dist/codebase/dependencyGraph.js.map +1 -0
- package/dist/codebase/fileIndexer.d.ts +27 -0
- package/dist/codebase/fileIndexer.d.ts.map +1 -0
- package/dist/codebase/fileIndexer.js +167 -0
- package/dist/codebase/fileIndexer.js.map +1 -0
- package/dist/codebase/symbolIndexer.d.ts +3 -0
- package/dist/codebase/symbolIndexer.d.ts.map +1 -0
- package/dist/codebase/symbolIndexer.js +184 -0
- package/dist/codebase/symbolIndexer.js.map +1 -0
- package/dist/codebase/whatChanged.d.ts +4 -0
- package/dist/codebase/whatChanged.d.ts.map +1 -0
- package/dist/codebase/whatChanged.js +138 -0
- package/dist/codebase/whatChanged.js.map +1 -0
- package/dist/coordination/CoordinationService.d.ts +166 -0
- package/dist/coordination/CoordinationService.d.ts.map +1 -0
- package/dist/coordination/CoordinationService.js +247 -0
- package/dist/coordination/CoordinationService.js.map +1 -0
- package/dist/coordination/claims.d.ts +122 -0
- package/dist/coordination/claims.d.ts.map +1 -0
- package/dist/coordination/claims.js +196 -0
- package/dist/coordination/claims.js.map +1 -0
- package/dist/coordination/coordinationTools.d.ts +40 -0
- package/dist/coordination/coordinationTools.d.ts.map +1 -0
- package/dist/coordination/coordinationTools.js +92 -0
- package/dist/coordination/coordinationTools.js.map +1 -0
- package/dist/coordination/laneBridge.d.ts +84 -0
- package/dist/coordination/laneBridge.d.ts.map +1 -0
- package/dist/coordination/laneBridge.js +86 -0
- package/dist/coordination/laneBridge.js.map +1 -0
- package/dist/coordination/messages.d.ts +225 -0
- package/dist/coordination/messages.d.ts.map +1 -0
- package/dist/coordination/messages.js +335 -0
- package/dist/coordination/messages.js.map +1 -0
- package/dist/coordination/permissionWaitRegistry.d.ts +32 -0
- package/dist/coordination/permissionWaitRegistry.d.ts.map +1 -0
- package/dist/coordination/permissionWaitRegistry.js +59 -0
- package/dist/coordination/permissionWaitRegistry.js.map +1 -0
- package/dist/coordination/query.d.ts +187 -0
- package/dist/coordination/query.d.ts.map +1 -0
- package/dist/coordination/query.js +138 -0
- package/dist/coordination/query.js.map +1 -0
- package/dist/coordination/renderInbox.d.ts +15 -0
- package/dist/coordination/renderInbox.d.ts.map +1 -0
- package/dist/coordination/renderInbox.js +19 -0
- package/dist/coordination/renderInbox.js.map +1 -0
- package/dist/coordination/runtimeEvents.d.ts +53 -0
- package/dist/coordination/runtimeEvents.d.ts.map +1 -0
- package/dist/coordination/runtimeEvents.js +112 -0
- package/dist/coordination/runtimeEvents.js.map +1 -0
- package/dist/detachedHostService.d.ts +77 -0
- package/dist/detachedHostService.d.ts.map +1 -0
- package/dist/detachedHostService.js +66 -0
- package/dist/detachedHostService.js.map +1 -0
- package/dist/detachedHostSupervisor.d.ts +102 -0
- package/dist/detachedHostSupervisor.d.ts.map +1 -0
- package/dist/detachedHostSupervisor.js +168 -0
- package/dist/detachedHostSupervisor.js.map +1 -0
- package/dist/durableRuntimeEventStore.d.ts +49 -0
- package/dist/durableRuntimeEventStore.d.ts.map +1 -0
- package/dist/durableRuntimeEventStore.js +364 -0
- package/dist/durableRuntimeEventStore.js.map +1 -0
- package/dist/engineering/EngineeringWorkbenchService.d.ts +142 -0
- package/dist/engineering/EngineeringWorkbenchService.d.ts.map +1 -0
- package/dist/engineering/EngineeringWorkbenchService.js +2301 -0
- package/dist/engineering/EngineeringWorkbenchService.js.map +1 -0
- package/dist/engineering/LandingService.d.ts +112 -0
- package/dist/engineering/LandingService.d.ts.map +1 -0
- package/dist/engineering/LandingService.js +239 -0
- package/dist/engineering/LandingService.js.map +1 -0
- package/dist/engineering/engineeringTypes.d.ts +2 -0
- package/dist/engineering/engineeringTypes.d.ts.map +1 -0
- package/dist/engineering/engineeringTypes.js +2 -0
- package/dist/engineering/engineeringTypes.js.map +1 -0
- package/dist/engineering/patchReview.d.ts +11 -0
- package/dist/engineering/patchReview.d.ts.map +1 -0
- package/dist/engineering/patchReview.js +174 -0
- package/dist/engineering/patchReview.js.map +1 -0
- package/dist/index.d.ts +834 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3242 -0
- package/dist/index.js.map +1 -0
- package/dist/intake/IntakeService.d.ts +84 -0
- package/dist/intake/IntakeService.d.ts.map +1 -0
- package/dist/intake/IntakeService.js +117 -0
- package/dist/intake/IntakeService.js.map +1 -0
- package/dist/intake/RepositoryTodoIntakeSource.d.ts +72 -0
- package/dist/intake/RepositoryTodoIntakeSource.d.ts.map +1 -0
- package/dist/intake/RepositoryTodoIntakeSource.js +168 -0
- package/dist/intake/RepositoryTodoIntakeSource.js.map +1 -0
- package/dist/intake/WorkspaceTodoIntakeSource.d.ts +53 -0
- package/dist/intake/WorkspaceTodoIntakeSource.d.ts.map +1 -0
- package/dist/intake/WorkspaceTodoIntakeSource.js +81 -0
- package/dist/intake/WorkspaceTodoIntakeSource.js.map +1 -0
- package/dist/isolation/CommandExecution.d.ts +132 -0
- package/dist/isolation/CommandExecution.d.ts.map +1 -0
- package/dist/isolation/CommandExecution.js +48 -0
- package/dist/isolation/CommandExecution.js.map +1 -0
- package/dist/isolation/LocalCommandExecutor.d.ts +27 -0
- package/dist/isolation/LocalCommandExecutor.d.ts.map +1 -0
- package/dist/isolation/LocalCommandExecutor.js +97 -0
- package/dist/isolation/LocalCommandExecutor.js.map +1 -0
- package/dist/isolation/RunExecutionEnvironment.d.ts +135 -0
- package/dist/isolation/RunExecutionEnvironment.d.ts.map +1 -0
- package/dist/isolation/RunExecutionEnvironment.js +206 -0
- package/dist/isolation/RunExecutionEnvironment.js.map +1 -0
- package/dist/isolation/RunIsolationService.d.ts +90 -0
- package/dist/isolation/RunIsolationService.d.ts.map +1 -0
- package/dist/isolation/RunIsolationService.js +86 -0
- package/dist/isolation/RunIsolationService.js.map +1 -0
- package/dist/isolation/SandboxCommandExecutor.d.ts +93 -0
- package/dist/isolation/SandboxCommandExecutor.d.ts.map +1 -0
- package/dist/isolation/SandboxCommandExecutor.js +189 -0
- package/dist/isolation/SandboxCommandExecutor.js.map +1 -0
- package/dist/knowledge/WorkspaceKnowledgeService.d.ts +71 -0
- package/dist/knowledge/WorkspaceKnowledgeService.d.ts.map +1 -0
- package/dist/knowledge/WorkspaceKnowledgeService.js +103 -0
- package/dist/knowledge/WorkspaceKnowledgeService.js.map +1 -0
- package/dist/localHostAuthority.d.ts +24 -0
- package/dist/localHostAuthority.d.ts.map +1 -0
- package/dist/localHostAuthority.js +87 -0
- package/dist/localHostAuthority.js.map +1 -0
- package/dist/localHostLease.d.ts +51 -0
- package/dist/localHostLease.d.ts.map +1 -0
- package/dist/localHostLease.js +167 -0
- package/dist/localHostLease.js.map +1 -0
- package/dist/localHostPaths.d.ts +30 -0
- package/dist/localHostPaths.d.ts.map +1 -0
- package/dist/localHostPaths.js +60 -0
- package/dist/localHostPaths.js.map +1 -0
- package/dist/localRpcTransport.d.ts +21 -0
- package/dist/localRpcTransport.d.ts.map +1 -0
- package/dist/localRpcTransport.js +423 -0
- package/dist/localRpcTransport.js.map +1 -0
- package/dist/node.d.ts +104 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +103 -0
- package/dist/node.js.map +1 -0
- package/dist/observation/DirectoryBaselineScanner.d.ts +44 -0
- package/dist/observation/DirectoryBaselineScanner.d.ts.map +1 -0
- package/dist/observation/DirectoryBaselineScanner.js +112 -0
- package/dist/observation/DirectoryBaselineScanner.js.map +1 -0
- package/dist/observation/MountContentReader.d.ts +63 -0
- package/dist/observation/MountContentReader.d.ts.map +1 -0
- package/dist/observation/MountContentReader.js +158 -0
- package/dist/observation/MountContentReader.js.map +1 -0
- package/dist/observation/RunObservationService.d.ts +229 -0
- package/dist/observation/RunObservationService.d.ts.map +1 -0
- package/dist/observation/RunObservationService.js +248 -0
- package/dist/observation/RunObservationService.js.map +1 -0
- package/dist/observation/XenoUseObservationSource.d.ts +42 -0
- package/dist/observation/XenoUseObservationSource.d.ts.map +1 -0
- package/dist/observation/XenoUseObservationSource.js +104 -0
- package/dist/observation/XenoUseObservationSource.js.map +1 -0
- package/dist/observation/XenoUseRunObserver.d.ts +140 -0
- package/dist/observation/XenoUseRunObserver.d.ts.map +1 -0
- package/dist/observation/XenoUseRunObserver.js +256 -0
- package/dist/observation/XenoUseRunObserver.js.map +1 -0
- package/dist/pluginMcp/PluginMcpManagerService.d.ts +141 -0
- package/dist/pluginMcp/PluginMcpManagerService.d.ts.map +1 -0
- package/dist/pluginMcp/PluginMcpManagerService.js +1821 -0
- package/dist/pluginMcp/PluginMcpManagerService.js.map +1 -0
- package/dist/pluginMcp/pluginMcpTypes.d.ts +349 -0
- package/dist/pluginMcp/pluginMcpTypes.d.ts.map +1 -0
- package/dist/pluginMcp/pluginMcpTypes.js +2 -0
- package/dist/pluginMcp/pluginMcpTypes.js.map +1 -0
- package/dist/policy/WorkspacePolicyService.d.ts +83 -0
- package/dist/policy/WorkspacePolicyService.d.ts.map +1 -0
- package/dist/policy/WorkspacePolicyService.js +809 -0
- package/dist/policy/WorkspacePolicyService.js.map +1 -0
- package/dist/policy/policyGuard.d.ts +27 -0
- package/dist/policy/policyGuard.d.ts.map +1 -0
- package/dist/policy/policyGuard.js +143 -0
- package/dist/policy/policyGuard.js.map +1 -0
- package/dist/policy/policyTypes.d.ts +190 -0
- package/dist/policy/policyTypes.d.ts.map +1 -0
- package/dist/policy/policyTypes.js +2 -0
- package/dist/policy/policyTypes.js.map +1 -0
- package/dist/runtime/RuntimeEventStore.d.ts +42 -0
- package/dist/runtime/RuntimeEventStore.d.ts.map +1 -0
- package/dist/runtime/RuntimeEventStore.js +412 -0
- package/dist/runtime/RuntimeEventStore.js.map +1 -0
- package/dist/runtime/RuntimeEventStoreRepositoryAdapter.d.ts +21 -0
- package/dist/runtime/RuntimeEventStoreRepositoryAdapter.d.ts.map +1 -0
- package/dist/runtime/RuntimeEventStoreRepositoryAdapter.js +41 -0
- package/dist/runtime/RuntimeEventStoreRepositoryAdapter.js.map +1 -0
- package/dist/runtime/RuntimeTimelineProjection.d.ts +5 -0
- package/dist/runtime/RuntimeTimelineProjection.d.ts.map +1 -0
- package/dist/runtime/RuntimeTimelineProjection.js +705 -0
- package/dist/runtime/RuntimeTimelineProjection.js.map +1 -0
- package/dist/runtime/runtimeEventTypes.d.ts +73 -0
- package/dist/runtime/runtimeEventTypes.d.ts.map +1 -0
- package/dist/runtime/runtimeEventTypes.js +2 -0
- package/dist/runtime/runtimeEventTypes.js.map +1 -0
- package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.d.ts +117 -0
- package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.d.ts.map +1 -0
- package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.js +214 -0
- package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.js.map +1 -0
- package/dist/session/AgentSessionService.d.ts +285 -0
- package/dist/session/AgentSessionService.d.ts.map +1 -0
- package/dist/session/AgentSessionService.js +444 -0
- package/dist/session/AgentSessionService.js.map +1 -0
- package/dist/session/bufferedSessionPty.d.ts +27 -0
- package/dist/session/bufferedSessionPty.d.ts.map +1 -0
- package/dist/session/bufferedSessionPty.js +49 -0
- package/dist/session/bufferedSessionPty.js.map +1 -0
- package/dist/session/renderSupport.d.ts +53 -0
- package/dist/session/renderSupport.d.ts.map +1 -0
- package/dist/session/renderSupport.js +41 -0
- package/dist/session/renderSupport.js.map +1 -0
- package/dist/session/runtimeEventChannel.d.ts +56 -0
- package/dist/session/runtimeEventChannel.d.ts.map +1 -0
- package/dist/session/runtimeEventChannel.js +53 -0
- package/dist/session/runtimeEventChannel.js.map +1 -0
- package/dist/state/JsonAgentStateRepository.d.ts +29 -0
- package/dist/state/JsonAgentStateRepository.d.ts.map +1 -0
- package/dist/state/JsonAgentStateRepository.js +165 -0
- package/dist/state/JsonAgentStateRepository.js.map +1 -0
- package/dist/state/SqliteAgentStateRepository.d.ts +58 -0
- package/dist/state/SqliteAgentStateRepository.d.ts.map +1 -0
- package/dist/state/SqliteAgentStateRepository.js +777 -0
- package/dist/state/SqliteAgentStateRepository.js.map +1 -0
- package/dist/state/createAgentStateRepository.d.ts +4 -0
- package/dist/state/createAgentStateRepository.d.ts.map +1 -0
- package/dist/state/createAgentStateRepository.js +12 -0
- package/dist/state/createAgentStateRepository.js.map +1 -0
- package/dist/state/stateSanitizers.d.ts +7 -0
- package/dist/state/stateSanitizers.d.ts.map +1 -0
- package/dist/state/stateSanitizers.js +87 -0
- package/dist/state/stateSanitizers.js.map +1 -0
- package/dist/subagents/AcpSubagentExecutionAdapter.d.ts +12 -0
- package/dist/subagents/AcpSubagentExecutionAdapter.d.ts.map +1 -0
- package/dist/subagents/AcpSubagentExecutionAdapter.js +119 -0
- package/dist/subagents/AcpSubagentExecutionAdapter.js.map +1 -0
- package/dist/subagents/CliSubagentExecutionAdapter.d.ts +13 -0
- package/dist/subagents/CliSubagentExecutionAdapter.d.ts.map +1 -0
- package/dist/subagents/CliSubagentExecutionAdapter.js +217 -0
- package/dist/subagents/CliSubagentExecutionAdapter.js.map +1 -0
- package/dist/subagents/HostTurnSubagentExecutionAdapter.d.ts +78 -0
- package/dist/subagents/HostTurnSubagentExecutionAdapter.d.ts.map +1 -0
- package/dist/subagents/HostTurnSubagentExecutionAdapter.js +160 -0
- package/dist/subagents/HostTurnSubagentExecutionAdapter.js.map +1 -0
- package/dist/subagents/SubagentRunService.d.ts +103 -0
- package/dist/subagents/SubagentRunService.d.ts.map +1 -0
- package/dist/subagents/SubagentRunService.js +1183 -0
- package/dist/subagents/SubagentRunService.js.map +1 -0
- package/dist/subagents/SubagentScheduler.d.ts +167 -0
- package/dist/subagents/SubagentScheduler.d.ts.map +1 -0
- package/dist/subagents/SubagentScheduler.js +530 -0
- package/dist/subagents/SubagentScheduler.js.map +1 -0
- package/dist/subagents/TeamAgentRouter.d.ts +104 -0
- package/dist/subagents/TeamAgentRouter.d.ts.map +1 -0
- package/dist/subagents/TeamAgentRouter.js +435 -0
- package/dist/subagents/TeamAgentRouter.js.map +1 -0
- package/dist/subagents/capabilityMatcher.d.ts +3 -0
- package/dist/subagents/capabilityMatcher.d.ts.map +1 -0
- package/dist/subagents/capabilityMatcher.js +127 -0
- package/dist/subagents/capabilityMatcher.js.map +1 -0
- package/dist/subagents/subagentRunPort.d.ts +43 -0
- package/dist/subagents/subagentRunPort.d.ts.map +1 -0
- package/dist/subagents/subagentRunPort.js +24 -0
- package/dist/subagents/subagentRunPort.js.map +1 -0
- package/dist/subagents/subagentTypes.d.ts +330 -0
- package/dist/subagents/subagentTypes.d.ts.map +1 -0
- package/dist/subagents/subagentTypes.js +2 -0
- package/dist/subagents/subagentTypes.js.map +1 -0
- package/dist/subagents/subagentWorkspaceAuthority.d.ts +29 -0
- package/dist/subagents/subagentWorkspaceAuthority.d.ts.map +1 -0
- package/dist/subagents/subagentWorkspaceAuthority.js +173 -0
- package/dist/subagents/subagentWorkspaceAuthority.js.map +1 -0
- package/dist/support/SupportDiagnosticsService.d.ts +87 -0
- package/dist/support/SupportDiagnosticsService.d.ts.map +1 -0
- package/dist/support/SupportDiagnosticsService.js +1199 -0
- package/dist/support/SupportDiagnosticsService.js.map +1 -0
- package/dist/turns/DurableInterruptedTurnStore.d.ts +14 -0
- package/dist/turns/DurableInterruptedTurnStore.d.ts.map +1 -0
- package/dist/turns/DurableInterruptedTurnStore.js +63 -0
- package/dist/turns/DurableInterruptedTurnStore.js.map +1 -0
- package/dist/turns/interruptedTurns.d.ts +50 -0
- package/dist/turns/interruptedTurns.d.ts.map +1 -0
- package/dist/turns/interruptedTurns.js +54 -0
- package/dist/turns/interruptedTurns.js.map +1 -0
- package/dist/turns/turnAdmission.d.ts +113 -0
- package/dist/turns/turnAdmission.d.ts.map +1 -0
- package/dist/turns/turnAdmission.js +136 -0
- package/dist/turns/turnAdmission.js.map +1 -0
- package/dist/workspace/WorkspaceHostService.d.ts +126 -0
- package/dist/workspace/WorkspaceHostService.d.ts.map +1 -0
- package/dist/workspace/WorkspaceHostService.js +841 -0
- package/dist/workspace/WorkspaceHostService.js.map +1 -0
- package/dist/workspace/WorkspaceShellService.d.ts +71 -0
- package/dist/workspace/WorkspaceShellService.d.ts.map +1 -0
- package/dist/workspace/WorkspaceShellService.js +273 -0
- package/dist/workspace/WorkspaceShellService.js.map +1 -0
- package/dist/workspace/fileAssociations.d.ts +12 -0
- package/dist/workspace/fileAssociations.d.ts.map +1 -0
- package/dist/workspace/fileAssociations.js +202 -0
- package/dist/workspace/fileAssociations.js.map +1 -0
- package/dist/workspace/scrollbackBuffer.d.ts +48 -0
- package/dist/workspace/scrollbackBuffer.d.ts.map +1 -0
- package/dist/workspace/scrollbackBuffer.js +94 -0
- package/dist/workspace/scrollbackBuffer.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,2301 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { commandSucceeded } from '../isolation/CommandExecution.js';
|
|
4
|
+
import { LocalCommandExecutor } from '../isolation/LocalCommandExecutor.js';
|
|
5
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from 'fs';
|
|
6
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'path';
|
|
7
|
+
import { isPathInside, hashText, resolveRootDirectory, safeReadJson, safeWriteJson, workspaceIndexDirectory, } from '../codebase/codeUtils.js';
|
|
8
|
+
import { getRuntimeEventStore } from '../runtime/RuntimeEventStore.js';
|
|
9
|
+
import { buildPatchReviewHunks, composePatchSnapshot, effectivePatchDecisions, } from './patchReview.js';
|
|
10
|
+
const MAX_GIT_OUTPUT = 1_500_000;
|
|
11
|
+
const DEFAULT_RUN_OUTPUT = 2_000_000;
|
|
12
|
+
const MAX_STORED_RUNS = 80;
|
|
13
|
+
const MAX_STORED_BACKGROUND_RUNS = 80;
|
|
14
|
+
const MAX_STORED_PATCH_REVIEWS = 120;
|
|
15
|
+
const MAX_CHECKPOINT_COMPARE_PATCHES = 40;
|
|
16
|
+
const MAX_TRANSACTIONAL_SNAPSHOT_BYTES = 1_000_000;
|
|
17
|
+
const MAX_TRANSACTIONAL_SNAPSHOT_CHARS = 250_000;
|
|
18
|
+
export class EngineeringWorkbenchService {
|
|
19
|
+
activeBackgroundRuns = new Map();
|
|
20
|
+
runtimeEventStore;
|
|
21
|
+
/**
|
|
22
|
+
* The landing queue, when the composition mounted one.
|
|
23
|
+
*
|
|
24
|
+
* Optional in the same way the host's observation service is: landing consults
|
|
25
|
+
* observed diffs and verification results, and a host with neither has nothing
|
|
26
|
+
* to gate with. Answering `unavailable` is honest; answering "landed" after
|
|
27
|
+
* consulting nothing is the merge-debt failure with extra steps.
|
|
28
|
+
*/
|
|
29
|
+
landingService;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.runtimeEventStore = options.runtimeEventStore || getRuntimeEventStore();
|
|
32
|
+
this.landingService = options.landingService;
|
|
33
|
+
}
|
|
34
|
+
/** Mounted after construction when the composition builds the queue later. */
|
|
35
|
+
configureLandingService(landingService) {
|
|
36
|
+
this.landingService = landingService;
|
|
37
|
+
}
|
|
38
|
+
configureRuntimeEventStore(runtimeEventStore) {
|
|
39
|
+
this.runtimeEventStore = runtimeEventStore;
|
|
40
|
+
}
|
|
41
|
+
close() {
|
|
42
|
+
for (const active of this.activeBackgroundRuns.values()) {
|
|
43
|
+
active.stopRequested = true;
|
|
44
|
+
killProcessTree(active.child);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async request(method, input) {
|
|
48
|
+
if (method === 'engineering.git.status')
|
|
49
|
+
return await this.gitStatus(input);
|
|
50
|
+
if (method === 'engineering.git.diff')
|
|
51
|
+
return await this.gitDiff(input);
|
|
52
|
+
if (method === 'engineering.git.stage')
|
|
53
|
+
return await this.gitStage(input);
|
|
54
|
+
if (method === 'engineering.git.unstage')
|
|
55
|
+
return await this.gitUnstage(input);
|
|
56
|
+
if (method === 'engineering.git.commit')
|
|
57
|
+
return await this.gitCommit(input);
|
|
58
|
+
if (method === 'engineering.git.branch')
|
|
59
|
+
return await this.gitBranch(input);
|
|
60
|
+
if (method === 'engineering.git.push')
|
|
61
|
+
return await this.gitPush(input);
|
|
62
|
+
if (method === 'engineering.git.pr.create')
|
|
63
|
+
return await this.gitCreatePullRequest(input);
|
|
64
|
+
if (method === 'engineering.landing.enqueue')
|
|
65
|
+
return this.landing('enqueue', input);
|
|
66
|
+
if (method === 'engineering.landing.remove')
|
|
67
|
+
return this.landing('remove', input);
|
|
68
|
+
if (method === 'engineering.landing.status')
|
|
69
|
+
return this.landing('status', input);
|
|
70
|
+
if (method === 'engineering.landing.land')
|
|
71
|
+
return await this.landing('land', input);
|
|
72
|
+
if (method === 'engineering.landing.verify')
|
|
73
|
+
return await this.landing('verify', input);
|
|
74
|
+
if (method === 'engineering.checks.discover')
|
|
75
|
+
return this.discoverChecks(input);
|
|
76
|
+
if (method === 'engineering.checks.run')
|
|
77
|
+
return await this.runCheck(input);
|
|
78
|
+
if (method === 'engineering.background.start')
|
|
79
|
+
return this.startBackgroundCheck(input);
|
|
80
|
+
if (method === 'engineering.background.list')
|
|
81
|
+
return this.listBackgroundRuns(input);
|
|
82
|
+
if (method === 'engineering.background.output')
|
|
83
|
+
return this.readBackgroundRunOutput(input);
|
|
84
|
+
if (method === 'engineering.background.stop')
|
|
85
|
+
return this.stopBackgroundRun(input);
|
|
86
|
+
if (method === 'engineering.runs.list')
|
|
87
|
+
return this.listRuns(input);
|
|
88
|
+
if (method === 'engineering.diagnostics')
|
|
89
|
+
return await this.diagnostics(input);
|
|
90
|
+
if (method === 'engineering.patches.list')
|
|
91
|
+
return this.listPatchReviews(input);
|
|
92
|
+
if (method === 'engineering.patches.enqueue')
|
|
93
|
+
return this.enqueuePatchReview(input);
|
|
94
|
+
if (method === 'engineering.patches.update')
|
|
95
|
+
return this.updatePatchReview(input);
|
|
96
|
+
if (method === 'engineering.patches.review')
|
|
97
|
+
return this.reviewPatchSelection(input);
|
|
98
|
+
if (method === 'engineering.checkpoints.list')
|
|
99
|
+
return this.listCheckpoints(input);
|
|
100
|
+
if (method === 'engineering.checkpoints.compare')
|
|
101
|
+
return this.compareCheckpoints(input);
|
|
102
|
+
if (method === 'engineering.checkpoints.restore')
|
|
103
|
+
return this.restoreCheckpoint(input);
|
|
104
|
+
if (method === 'engineering.sessions.list')
|
|
105
|
+
return this.listSessions(input);
|
|
106
|
+
if (method === 'engineering.runtime.timeline')
|
|
107
|
+
return this.runtimeTimeline(input);
|
|
108
|
+
throw new Error(`Unsupported engineering workbench method: ${String(method)}`);
|
|
109
|
+
}
|
|
110
|
+
async gitStatus(request) {
|
|
111
|
+
try {
|
|
112
|
+
const rootDirectory = this.resolveRoot(request);
|
|
113
|
+
if (!await this.isGitRepo(rootDirectory)) {
|
|
114
|
+
return { success: true, isGitRepo: false, rootDirectory, clean: true, files: [] };
|
|
115
|
+
}
|
|
116
|
+
const [statusResult, numstatResult] = await Promise.all([
|
|
117
|
+
runProcess('git', ['status', '--porcelain=v1', '-b'], rootDirectory, 8000, MAX_GIT_OUTPUT),
|
|
118
|
+
runProcess('git', ['diff', '--numstat'], rootDirectory, 8000, MAX_GIT_OUTPUT),
|
|
119
|
+
]);
|
|
120
|
+
if (!statusResult.success && statusResult.exitCode !== 0) {
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
isGitRepo: true,
|
|
124
|
+
rootDirectory,
|
|
125
|
+
clean: true,
|
|
126
|
+
files: [],
|
|
127
|
+
error: statusResult.error || statusResult.stderr || 'Failed to read git status.',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const branch = parseBranch(statusResult.stdout);
|
|
131
|
+
const statsByPath = parseNumstat(numstatResult.stdout);
|
|
132
|
+
const files = parseGitStatusFiles(rootDirectory, statusResult.stdout);
|
|
133
|
+
for (const file of files) {
|
|
134
|
+
const stats = statsByPath.get(file.relativePath);
|
|
135
|
+
if (stats) {
|
|
136
|
+
file.additions = stats.additions;
|
|
137
|
+
file.deletions = stats.deletions;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
success: true,
|
|
142
|
+
isGitRepo: true,
|
|
143
|
+
rootDirectory,
|
|
144
|
+
branch: branch.branch,
|
|
145
|
+
upstream: branch.upstream,
|
|
146
|
+
ahead: branch.ahead,
|
|
147
|
+
behind: branch.behind,
|
|
148
|
+
clean: files.length === 0,
|
|
149
|
+
files,
|
|
150
|
+
raw: statusResult.stdout,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
isGitRepo: false,
|
|
157
|
+
rootDirectory: request.rootDirectory || '',
|
|
158
|
+
clean: true,
|
|
159
|
+
files: [],
|
|
160
|
+
error: err instanceof Error ? err.message : String(err),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async gitDiff(request) {
|
|
165
|
+
try {
|
|
166
|
+
const rootDirectory = this.resolveRoot(request);
|
|
167
|
+
if (!await this.isGitRepo(rootDirectory)) {
|
|
168
|
+
return { success: true, isGitRepo: false, rootDirectory, diff: '', truncated: false };
|
|
169
|
+
}
|
|
170
|
+
const safePaths = this.validateRelativePaths(rootDirectory, request.paths || []);
|
|
171
|
+
const args = ['diff', ...(request.staged ? ['--staged'] : []), '--', ...safePaths];
|
|
172
|
+
const result = await runProcess('git', args, rootDirectory, 12000, request.maxBytes || MAX_GIT_OUTPUT);
|
|
173
|
+
return {
|
|
174
|
+
success: result.success || result.exitCode === 1 || result.exitCode === 0,
|
|
175
|
+
isGitRepo: true,
|
|
176
|
+
rootDirectory,
|
|
177
|
+
diff: result.stdout,
|
|
178
|
+
truncated: result.truncated,
|
|
179
|
+
error: result.success || result.exitCode === 1 || result.exitCode === 0 ? undefined : result.error || result.stderr,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
return {
|
|
184
|
+
success: false,
|
|
185
|
+
isGitRepo: false,
|
|
186
|
+
rootDirectory: request.rootDirectory || '',
|
|
187
|
+
diff: '',
|
|
188
|
+
truncated: false,
|
|
189
|
+
error: err instanceof Error ? err.message : String(err),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async gitStage(request) {
|
|
194
|
+
return this.runGitMutation(request, ['add', '--', ...this.validateRelativePaths(this.resolveRoot(request), request.paths)]);
|
|
195
|
+
}
|
|
196
|
+
async gitUnstage(request) {
|
|
197
|
+
return this.runGitMutation(request, ['restore', '--staged', '--', ...this.validateRelativePaths(this.resolveRoot(request), request.paths)]);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Create or switch to a branch.
|
|
201
|
+
*
|
|
202
|
+
* Local and reversible, so unlike push and PR creation it needs no
|
|
203
|
+
* confirmation. The NAME is validated rather than trusted: it reaches a
|
|
204
|
+
* command line, and git itself accepts things a caller rarely means โ
|
|
205
|
+
* a leading dash reads as a flag.
|
|
206
|
+
*/
|
|
207
|
+
async gitBranch(request) {
|
|
208
|
+
const name = String(request.name || '').trim();
|
|
209
|
+
const invalid = validateRefName(name);
|
|
210
|
+
if (invalid)
|
|
211
|
+
return this.gitFailure(request, invalid);
|
|
212
|
+
// `git switch`, never `git checkout`. `checkout` is overloaded: it takes
|
|
213
|
+
// branches AND pathspecs, so `checkout -- main` restores a FILE called main
|
|
214
|
+
// rather than switching branch โ which is what the first version of this
|
|
215
|
+
// did, and the test caught. `switch` only ever means a branch, so the
|
|
216
|
+
// ambiguity is not something to be careful about; it is gone.
|
|
217
|
+
if (request.create === false) {
|
|
218
|
+
return this.runGitMutation(request, ['switch', name]);
|
|
219
|
+
}
|
|
220
|
+
const startPoint = String(request.startPoint || '').trim();
|
|
221
|
+
if (startPoint) {
|
|
222
|
+
const invalidStart = validateRefName(startPoint);
|
|
223
|
+
if (invalidStart)
|
|
224
|
+
return this.gitFailure(request, invalidStart);
|
|
225
|
+
return this.runGitMutation(request, ['switch', '-c', name, startPoint]);
|
|
226
|
+
}
|
|
227
|
+
return this.runGitMutation(request, ['switch', '-c', name]);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Push a branch to a remote.
|
|
231
|
+
*
|
|
232
|
+
* ๐ด The first verb here whose effect leaves the machine, and the first that
|
|
233
|
+
* refuses without `confirmed`. The host cannot otherwise distinguish an agent
|
|
234
|
+
* deciding to publish from a human asking it to, and those are different acts
|
|
235
|
+
* with different consequences โ a push is visible to other people and, unlike
|
|
236
|
+
* every other verb on this surface, cannot be undone locally.
|
|
237
|
+
*
|
|
238
|
+
* There is no `--force`, by construction rather than by policy: it destroys
|
|
239
|
+
* history others may already hold, every XENO repo rule forbids it, and a
|
|
240
|
+
* command that cannot express it cannot be talked into it.
|
|
241
|
+
*/
|
|
242
|
+
async gitPush(request) {
|
|
243
|
+
const base = await this.gitStatus(request);
|
|
244
|
+
if (request.confirmed !== true) {
|
|
245
|
+
return { ...base, success: false, pushed: false, error: 'Pushing publishes work to a remote and was not confirmed. Send confirmed: true from a human action.' };
|
|
246
|
+
}
|
|
247
|
+
if (!base.isGitRepo)
|
|
248
|
+
return { ...base, pushed: false };
|
|
249
|
+
const remote = String(request.remote || 'origin').trim();
|
|
250
|
+
const invalidRemote = validateRemoteName(remote);
|
|
251
|
+
if (invalidRemote)
|
|
252
|
+
return { ...base, success: false, pushed: false, error: invalidRemote };
|
|
253
|
+
const branch = String(request.branch || base.branch || '').trim();
|
|
254
|
+
const invalidBranch = validateRefName(branch);
|
|
255
|
+
if (invalidBranch)
|
|
256
|
+
return { ...base, success: false, pushed: false, error: invalidBranch };
|
|
257
|
+
const args = ['push'];
|
|
258
|
+
if (request.setUpstream)
|
|
259
|
+
args.push('--set-upstream');
|
|
260
|
+
args.push(remote, branch);
|
|
261
|
+
const result = await this.runGitMutation(request, args);
|
|
262
|
+
return {
|
|
263
|
+
...result,
|
|
264
|
+
pushed: result.success,
|
|
265
|
+
remote,
|
|
266
|
+
pushedBranch: branch,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Open a pull request, through the `gh` CLI.
|
|
271
|
+
*
|
|
272
|
+
* `gh` rather than a GitHub API client on purpose: it already holds the
|
|
273
|
+
* operator's credential, so this adds no token storage, no scope decisions and
|
|
274
|
+
* no second place a credential can leak. The cost is a dependency on `gh`
|
|
275
|
+
* being installed, which is reported plainly when it is not โ a missing tool
|
|
276
|
+
* is a legible failure, while a stored token is a permanent liability.
|
|
277
|
+
*
|
|
278
|
+
* Outward-facing, so confirmed like a push. `draft` defaults to TRUE: opening
|
|
279
|
+
* a review request notifies people, and an action taken on someone's behalf
|
|
280
|
+
* should default to the quieter form.
|
|
281
|
+
*/
|
|
282
|
+
async gitCreatePullRequest(request) {
|
|
283
|
+
if (request.confirmed !== true) {
|
|
284
|
+
return { success: false, created: false, error: 'Opening a pull request notifies reviewers and was not confirmed. Send confirmed: true from a human action.' };
|
|
285
|
+
}
|
|
286
|
+
const title = String(request.title || '').trim();
|
|
287
|
+
if (title.length < 3)
|
|
288
|
+
return { success: false, created: false, error: 'A pull request title of at least 3 characters is required.' };
|
|
289
|
+
try {
|
|
290
|
+
const rootDirectory = this.resolveRoot(request);
|
|
291
|
+
if (!await this.isGitRepo(rootDirectory)) {
|
|
292
|
+
return { success: false, created: false, error: 'Workspace is not a git repository.' };
|
|
293
|
+
}
|
|
294
|
+
const args = ['pr', 'create', '--title', title, '--body', String(request.body || '')];
|
|
295
|
+
// Draft unless explicitly told otherwise โ see the note above.
|
|
296
|
+
if (request.draft !== false)
|
|
297
|
+
args.push('--draft');
|
|
298
|
+
for (const [flag, value] of [['--base', request.base], ['--head', request.head]]) {
|
|
299
|
+
const ref = String(value || '').trim();
|
|
300
|
+
if (!ref)
|
|
301
|
+
continue;
|
|
302
|
+
const invalid = validateRefName(ref);
|
|
303
|
+
if (invalid)
|
|
304
|
+
return { success: false, created: false, error: invalid };
|
|
305
|
+
args.push(flag, ref);
|
|
306
|
+
}
|
|
307
|
+
const result = await runProcess('gh', args, rootDirectory, 60000, MAX_GIT_OUTPUT);
|
|
308
|
+
if (!result.success) {
|
|
309
|
+
return {
|
|
310
|
+
success: false,
|
|
311
|
+
created: false,
|
|
312
|
+
error: result.error || result.stderr || 'Failed to create the pull request. Is the GitHub CLI installed and authenticated?',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const url = /https:\/\/\S+/.exec(result.stdout)?.[0];
|
|
316
|
+
const number = url ? Number(/\/(\d+)(?:\s|$)/.exec(url)?.[1]) : Number.NaN;
|
|
317
|
+
return {
|
|
318
|
+
success: true,
|
|
319
|
+
created: true,
|
|
320
|
+
...(url ? { url } : {}),
|
|
321
|
+
...(Number.isFinite(number) ? { number } : {}),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
return { success: false, created: false, error: err instanceof Error ? err.message : String(err) };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Routes a landing call, or says plainly that landing is not mounted.
|
|
330
|
+
*
|
|
331
|
+
* ๐ด An unmounted queue reports `success: false` with a reason, never an empty
|
|
332
|
+
* queue. An empty queue reads as "nothing is waiting", which is a fact about
|
|
333
|
+
* the world; "no queue exists here" is a fact about this installation. A
|
|
334
|
+
* caller that cannot tell them apart will wait for a landing that can never
|
|
335
|
+
* be attempted.
|
|
336
|
+
*/
|
|
337
|
+
landing(operation, input) {
|
|
338
|
+
const service = this.landingService;
|
|
339
|
+
if (!service) {
|
|
340
|
+
const unavailable = {
|
|
341
|
+
success: false,
|
|
342
|
+
entries: [],
|
|
343
|
+
fingerprint: 'empty',
|
|
344
|
+
error: 'Landing is not available: this host has no landing queue mounted.',
|
|
345
|
+
};
|
|
346
|
+
if (operation === 'land') {
|
|
347
|
+
return Promise.resolve({ ...unavailable, landed: false, verdict: unavailable.error });
|
|
348
|
+
}
|
|
349
|
+
if (operation === 'verify') {
|
|
350
|
+
return Promise.resolve({ ...unavailable, verified: false, checks: [], problem: unavailable.error });
|
|
351
|
+
}
|
|
352
|
+
return unavailable;
|
|
353
|
+
}
|
|
354
|
+
if (operation === 'enqueue')
|
|
355
|
+
return service.enqueue(input);
|
|
356
|
+
if (operation === 'remove')
|
|
357
|
+
return service.remove(input);
|
|
358
|
+
if (operation === 'status')
|
|
359
|
+
return service.status(input);
|
|
360
|
+
if (operation === 'verify')
|
|
361
|
+
return service.verify(input);
|
|
362
|
+
return service.land(input);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* The merge behind a landing โ deliberately NOT a host method.
|
|
366
|
+
*
|
|
367
|
+
* There is no `engineering.git.merge` in the contract: a free-standing merge
|
|
368
|
+
* verb would sit beside the gate rather than behind it, and a gate that can be
|
|
369
|
+
* walked around is documentation. This is reachable only from
|
|
370
|
+
* `engineering.landing.land`, which runs it after the queue admitted the run.
|
|
371
|
+
*
|
|
372
|
+
* `--no-ff` on purpose. A landing is a decision, and a decision should appear
|
|
373
|
+
* in the history as one revertable commit rather than dissolving into trunk.
|
|
374
|
+
*/
|
|
375
|
+
async gitMergeForLanding(request) {
|
|
376
|
+
const branch = String(request.branch || '').trim();
|
|
377
|
+
const invalid = validateRefName(branch);
|
|
378
|
+
if (invalid)
|
|
379
|
+
return { merged: false, error: invalid };
|
|
380
|
+
const result = await this.runGitMutation(request, ['merge', '--no-ff', '--no-edit', branch]);
|
|
381
|
+
return result.success ? { merged: true } : { merged: false, ...(result.error ? { error: result.error } : {}) };
|
|
382
|
+
}
|
|
383
|
+
/** A failed git response that still reports where it was looking. */
|
|
384
|
+
gitFailure(request, error) {
|
|
385
|
+
return { success: false, isGitRepo: false, rootDirectory: request.rootDirectory || '', clean: true, files: [], error };
|
|
386
|
+
}
|
|
387
|
+
async gitCommit(request) {
|
|
388
|
+
const message = String(request.message || '').trim();
|
|
389
|
+
if (message.length < 3) {
|
|
390
|
+
return {
|
|
391
|
+
success: false,
|
|
392
|
+
isGitRepo: false,
|
|
393
|
+
rootDirectory: request.rootDirectory || '',
|
|
394
|
+
clean: true,
|
|
395
|
+
files: [],
|
|
396
|
+
error: 'Commit message must be at least 3 characters.',
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return this.runGitMutation(request, ['commit', '-m', message]);
|
|
400
|
+
}
|
|
401
|
+
discoverChecks(request) {
|
|
402
|
+
try {
|
|
403
|
+
const rootDirectory = this.resolveRoot(request);
|
|
404
|
+
const packagePath = join(rootDirectory, 'package.json');
|
|
405
|
+
if (!existsSync(packagePath)) {
|
|
406
|
+
return { success: true, rootDirectory, checks: [] };
|
|
407
|
+
}
|
|
408
|
+
const parsed = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
409
|
+
const scripts = parsed.scripts && typeof parsed.scripts === 'object' ? parsed.scripts : {};
|
|
410
|
+
const checks = [];
|
|
411
|
+
for (const [scriptName, rawCommand] of Object.entries(scripts)) {
|
|
412
|
+
if (typeof rawCommand !== 'string')
|
|
413
|
+
continue;
|
|
414
|
+
if (!/^[a-zA-Z0-9:_-]+$/.test(scriptName))
|
|
415
|
+
continue;
|
|
416
|
+
const kind = classifyScript(scriptName, rawCommand);
|
|
417
|
+
if (!kind)
|
|
418
|
+
continue;
|
|
419
|
+
const invocation = npmScriptInvocation(scriptName);
|
|
420
|
+
checks.push({
|
|
421
|
+
id: `script:${scriptName}`,
|
|
422
|
+
kind,
|
|
423
|
+
name: scriptName,
|
|
424
|
+
command: invocation.command,
|
|
425
|
+
args: invocation.args,
|
|
426
|
+
packageScript: scriptName,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
checks.sort((a, b) => checkSortScore(a) - checkSortScore(b) || a.name.localeCompare(b.name));
|
|
430
|
+
return { success: true, rootDirectory, checks, packageManager: 'npm' };
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
return {
|
|
434
|
+
success: false,
|
|
435
|
+
rootDirectory: request.rootDirectory || '',
|
|
436
|
+
checks: [],
|
|
437
|
+
error: err instanceof Error ? err.message : String(err),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async runCheck(request) {
|
|
442
|
+
try {
|
|
443
|
+
const rootDirectory = this.resolveRoot(request);
|
|
444
|
+
const check = this.discoverChecks(request).checks.find((item) => item.id === request.checkId);
|
|
445
|
+
if (!check)
|
|
446
|
+
return { success: false, error: 'Check was not discovered for this workspace.' };
|
|
447
|
+
const startedAt = Date.now();
|
|
448
|
+
const outputLimit = Math.max(20_000, Math.min(request.maxOutputBytes || DEFAULT_RUN_OUTPUT, 5_000_000));
|
|
449
|
+
const result = await runProcess(check.command, check.args, rootDirectory, Math.max(5_000, Math.min(request.timeoutMs || 10 * 60_000, 30 * 60_000)), outputLimit);
|
|
450
|
+
const endedAt = Date.now();
|
|
451
|
+
const run = {
|
|
452
|
+
id: `eng-run-${startedAt}-${Math.random().toString(36).slice(2, 8)}`,
|
|
453
|
+
rootDirectory,
|
|
454
|
+
check,
|
|
455
|
+
startedAt,
|
|
456
|
+
endedAt,
|
|
457
|
+
durationMs: endedAt - startedAt,
|
|
458
|
+
success: result.success,
|
|
459
|
+
exitCode: result.exitCode,
|
|
460
|
+
stdout: result.stdout,
|
|
461
|
+
stderr: result.stderr,
|
|
462
|
+
truncated: result.truncated,
|
|
463
|
+
error: result.error || (result.timedOut ? 'Check timed out.' : undefined),
|
|
464
|
+
};
|
|
465
|
+
this.appendRun(rootDirectory, run);
|
|
466
|
+
return { success: true, run };
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
listRuns(request) {
|
|
473
|
+
try {
|
|
474
|
+
const rootDirectory = this.resolveRoot(request);
|
|
475
|
+
return { success: true, rootDirectory, runs: this.readRunStore(rootDirectory).runs };
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
return {
|
|
479
|
+
success: false,
|
|
480
|
+
rootDirectory: request.rootDirectory || '',
|
|
481
|
+
runs: [],
|
|
482
|
+
error: err instanceof Error ? err.message : String(err),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
startBackgroundCheck(request) {
|
|
487
|
+
try {
|
|
488
|
+
const rootDirectory = this.resolveRoot(request);
|
|
489
|
+
const check = this.discoverChecks(request).checks.find((item) => item.id === request.checkId);
|
|
490
|
+
if (!check)
|
|
491
|
+
return { success: false, rootDirectory, error: 'Check was not discovered for this workspace.' };
|
|
492
|
+
const startedAt = Date.now();
|
|
493
|
+
const outputLimit = Math.max(20_000, Math.min(request.maxOutputBytes || DEFAULT_RUN_OUTPUT, 5_000_000));
|
|
494
|
+
const run = {
|
|
495
|
+
id: `eng-bg-${startedAt}-${Math.random().toString(36).slice(2, 8)}`,
|
|
496
|
+
rootDirectory,
|
|
497
|
+
check,
|
|
498
|
+
status: 'running',
|
|
499
|
+
startedAt,
|
|
500
|
+
updatedAt: startedAt,
|
|
501
|
+
stdout: '',
|
|
502
|
+
stderr: '',
|
|
503
|
+
truncated: false,
|
|
504
|
+
};
|
|
505
|
+
const child = spawn(check.command, check.args, {
|
|
506
|
+
cwd: rootDirectory,
|
|
507
|
+
windowsHide: true,
|
|
508
|
+
shell: false,
|
|
509
|
+
env: process.env,
|
|
510
|
+
});
|
|
511
|
+
const active = {
|
|
512
|
+
run,
|
|
513
|
+
child,
|
|
514
|
+
outputLimit,
|
|
515
|
+
stopRequested: false,
|
|
516
|
+
finalized: false,
|
|
517
|
+
};
|
|
518
|
+
this.activeBackgroundRuns.set(run.id, active);
|
|
519
|
+
this.writeBackgroundRun(rootDirectory, run);
|
|
520
|
+
const append = (target, chunk) => {
|
|
521
|
+
const text = chunk.toString();
|
|
522
|
+
if (target === 'stdout')
|
|
523
|
+
active.run.stdout += text;
|
|
524
|
+
else
|
|
525
|
+
active.run.stderr += text;
|
|
526
|
+
if (active.run.stdout.length + active.run.stderr.length > active.outputLimit) {
|
|
527
|
+
active.run.truncated = true;
|
|
528
|
+
const keep = Math.max(4_000, Math.floor(active.outputLimit / 2));
|
|
529
|
+
active.run.stdout = active.run.stdout.slice(-keep);
|
|
530
|
+
active.run.stderr = active.run.stderr.slice(-keep);
|
|
531
|
+
}
|
|
532
|
+
active.run.updatedAt = Date.now();
|
|
533
|
+
this.writeBackgroundRun(rootDirectory, active.run);
|
|
534
|
+
};
|
|
535
|
+
const finalize = (status, exitCode, error) => {
|
|
536
|
+
if (active.finalized)
|
|
537
|
+
return;
|
|
538
|
+
active.finalized = true;
|
|
539
|
+
const endedAt = Date.now();
|
|
540
|
+
active.run.status = status;
|
|
541
|
+
active.run.exitCode = exitCode;
|
|
542
|
+
active.run.error = error || active.run.error;
|
|
543
|
+
active.run.endedAt = endedAt;
|
|
544
|
+
active.run.updatedAt = endedAt;
|
|
545
|
+
active.run.durationMs = endedAt - active.run.startedAt;
|
|
546
|
+
this.activeBackgroundRuns.delete(active.run.id);
|
|
547
|
+
this.writeBackgroundRun(rootDirectory, active.run);
|
|
548
|
+
this.appendRun(rootDirectory, {
|
|
549
|
+
id: active.run.id,
|
|
550
|
+
rootDirectory,
|
|
551
|
+
check: active.run.check,
|
|
552
|
+
startedAt: active.run.startedAt,
|
|
553
|
+
endedAt,
|
|
554
|
+
durationMs: active.run.durationMs,
|
|
555
|
+
success: status === 'completed',
|
|
556
|
+
exitCode,
|
|
557
|
+
stdout: active.run.stdout,
|
|
558
|
+
stderr: active.run.stderr,
|
|
559
|
+
truncated: active.run.truncated,
|
|
560
|
+
error: active.run.error,
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
child.stdout.on('data', (chunk) => append('stdout', chunk));
|
|
564
|
+
child.stderr.on('data', (chunk) => append('stderr', chunk));
|
|
565
|
+
child.on('error', (err) => finalize('failed', null, err.message));
|
|
566
|
+
child.on('close', (code) => {
|
|
567
|
+
if (active.stopRequested)
|
|
568
|
+
finalize('stopped', code, 'Background check stopped by user.');
|
|
569
|
+
else
|
|
570
|
+
finalize(code === 0 ? 'completed' : 'failed', code);
|
|
571
|
+
});
|
|
572
|
+
return { success: true, rootDirectory, run };
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
return { success: false, rootDirectory: request.rootDirectory || '', error: err instanceof Error ? err.message : String(err) };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
listBackgroundRuns(request) {
|
|
579
|
+
try {
|
|
580
|
+
const rootDirectory = this.resolveRoot(request);
|
|
581
|
+
return { success: true, rootDirectory, runs: this.readBackgroundRunStore(rootDirectory).runs };
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
return {
|
|
585
|
+
success: false,
|
|
586
|
+
rootDirectory: request.rootDirectory || '',
|
|
587
|
+
runs: [],
|
|
588
|
+
error: err instanceof Error ? err.message : String(err),
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
readBackgroundRunOutput(request) {
|
|
593
|
+
try {
|
|
594
|
+
const rootDirectory = this.resolveRoot(request);
|
|
595
|
+
const runId = String(request.runId || '').trim();
|
|
596
|
+
if (!runId)
|
|
597
|
+
return { success: false, rootDirectory, error: 'runId is required.' };
|
|
598
|
+
const run = this.activeBackgroundRuns.get(runId)?.run
|
|
599
|
+
|| this.readBackgroundRunStore(rootDirectory).runs.find((entry) => entry.id === runId);
|
|
600
|
+
if (!run)
|
|
601
|
+
return { success: false, rootDirectory, error: 'Background run was not found.' };
|
|
602
|
+
return { success: true, rootDirectory, run };
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
return { success: false, rootDirectory: request.rootDirectory || '', error: err instanceof Error ? err.message : String(err) };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
stopBackgroundRun(request) {
|
|
609
|
+
try {
|
|
610
|
+
const rootDirectory = this.resolveRoot(request);
|
|
611
|
+
const runId = String(request.runId || '').trim();
|
|
612
|
+
if (!runId)
|
|
613
|
+
return { success: false, rootDirectory, error: 'runId is required.' };
|
|
614
|
+
const active = this.activeBackgroundRuns.get(runId);
|
|
615
|
+
if (active) {
|
|
616
|
+
active.stopRequested = true;
|
|
617
|
+
active.run.status = 'stopped';
|
|
618
|
+
active.run.updatedAt = Date.now();
|
|
619
|
+
active.run.error = 'Background check stopped by user.';
|
|
620
|
+
this.writeBackgroundRun(rootDirectory, active.run);
|
|
621
|
+
killProcessTree(active.child);
|
|
622
|
+
return { success: true, rootDirectory, run: active.run };
|
|
623
|
+
}
|
|
624
|
+
const store = this.readBackgroundRunStore(rootDirectory);
|
|
625
|
+
const run = store.runs.find((entry) => entry.id === runId);
|
|
626
|
+
if (!run)
|
|
627
|
+
return { success: false, rootDirectory, error: 'Background run was not found.' };
|
|
628
|
+
if (run.status === 'running') {
|
|
629
|
+
const now = Date.now();
|
|
630
|
+
run.status = 'stopped';
|
|
631
|
+
run.updatedAt = now;
|
|
632
|
+
run.endedAt = now;
|
|
633
|
+
run.durationMs = now - run.startedAt;
|
|
634
|
+
run.error = 'Background process is no longer attached to this Hub session.';
|
|
635
|
+
this.writeBackgroundRun(rootDirectory, run);
|
|
636
|
+
}
|
|
637
|
+
return { success: true, rootDirectory, run };
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
return { success: false, rootDirectory: request.rootDirectory || '', error: err instanceof Error ? err.message : String(err) };
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
async diagnostics(request) {
|
|
644
|
+
try {
|
|
645
|
+
const rootDirectory = this.resolveRoot(request);
|
|
646
|
+
const [git, discovery] = await Promise.all([
|
|
647
|
+
this.gitStatus(request),
|
|
648
|
+
Promise.resolve(this.discoverChecks(request)),
|
|
649
|
+
]);
|
|
650
|
+
const recentRuns = this.readRunStore(rootDirectory).runs.slice(0, 12);
|
|
651
|
+
const backgroundRuns = this.readBackgroundRunStore(rootDirectory).runs.slice(0, 12);
|
|
652
|
+
const failedRuns = recentRuns.filter((run) => !run.success).length;
|
|
653
|
+
const activeBackgroundRuns = backgroundRuns.filter((run) => run.status === 'running').length;
|
|
654
|
+
const dirtyFiles = git.files?.length || 0;
|
|
655
|
+
const issues = collectDiagnosticIssues(rootDirectory, recentRuns, backgroundRuns);
|
|
656
|
+
const summaryParts = [
|
|
657
|
+
git.isGitRepo ? `${dirtyFiles} changed git file${dirtyFiles === 1 ? '' : 's'}` : 'not a git repository',
|
|
658
|
+
`${discovery.checks.length} runnable check${discovery.checks.length === 1 ? '' : 's'}`,
|
|
659
|
+
recentRuns.length ? `${failedRuns} failing recent run${failedRuns === 1 ? '' : 's'}` : 'no recent runs',
|
|
660
|
+
backgroundRuns.length ? `${activeBackgroundRuns} active background check${activeBackgroundRuns === 1 ? '' : 's'}` : 'no background checks',
|
|
661
|
+
issues.length ? `${issues.length} parsed diagnostic${issues.length === 1 ? '' : 's'}` : 'no parsed diagnostics',
|
|
662
|
+
];
|
|
663
|
+
return {
|
|
664
|
+
success: true,
|
|
665
|
+
rootDirectory,
|
|
666
|
+
summary: summaryParts.join(' / '),
|
|
667
|
+
git,
|
|
668
|
+
checks: discovery.checks,
|
|
669
|
+
recentRuns,
|
|
670
|
+
backgroundRuns,
|
|
671
|
+
issues,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
return {
|
|
676
|
+
success: false,
|
|
677
|
+
rootDirectory: request.rootDirectory || '',
|
|
678
|
+
summary: '',
|
|
679
|
+
checks: [],
|
|
680
|
+
recentRuns: [],
|
|
681
|
+
issues: [],
|
|
682
|
+
error: err instanceof Error ? err.message : String(err),
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
listPatchReviews(request) {
|
|
687
|
+
try {
|
|
688
|
+
const rootDirectory = this.resolveRoot(request);
|
|
689
|
+
return { success: true, rootDirectory, items: this.readPatchReviewStore(rootDirectory).items };
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
return {
|
|
693
|
+
success: false,
|
|
694
|
+
rootDirectory: request.rootDirectory || '',
|
|
695
|
+
items: [],
|
|
696
|
+
error: err instanceof Error ? err.message : String(err),
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
enqueuePatchReview(request) {
|
|
701
|
+
try {
|
|
702
|
+
const rootDirectory = this.resolveRoot(request);
|
|
703
|
+
const diff = String(request.diff || '').trim();
|
|
704
|
+
if (!diff)
|
|
705
|
+
return { success: false, rootDirectory, error: 'Patch review diff is required.' };
|
|
706
|
+
const now = Date.now();
|
|
707
|
+
const store = this.readPatchReviewStore(rootDirectory);
|
|
708
|
+
const id = String(request.id || `patch-review-${now}-${Math.random().toString(36).slice(2, 8)}`);
|
|
709
|
+
const existing = store.items.find((item) => item.id === id);
|
|
710
|
+
const rawFiles = request.files || request.filesChanged || existing?.filesChanged || existing?.files || [];
|
|
711
|
+
const reviewFiles = rawFiles
|
|
712
|
+
.map((file) => String(file || '').trim())
|
|
713
|
+
.filter(Boolean)
|
|
714
|
+
.map((file) => isAbsolute(file) && isPathInside(file, rootDirectory)
|
|
715
|
+
? relative(rootDirectory, file).replace(/\\/g, '/')
|
|
716
|
+
: file.replace(/\\/g, '/'));
|
|
717
|
+
const item = {
|
|
718
|
+
id,
|
|
719
|
+
rootDirectory,
|
|
720
|
+
title: String(request.title || existing?.title || 'Patch review').trim() || 'Patch review',
|
|
721
|
+
description: request.description || existing?.description,
|
|
722
|
+
diff,
|
|
723
|
+
files: this.validateRelativePaths(rootDirectory, reviewFiles),
|
|
724
|
+
filesChanged: this.validateRelativePaths(rootDirectory, reviewFiles),
|
|
725
|
+
summary: String(request.summary || request.title || existing?.summary || existing?.title || 'Patch review').trim() || 'Patch review',
|
|
726
|
+
timestamp: Number.isFinite(request.timestamp) ? Number(request.timestamp) : (existing?.timestamp || now),
|
|
727
|
+
conversationId: normalizeOptionalId(request.conversationId || existing?.conversationId),
|
|
728
|
+
requestId: normalizeOptionalId(request.requestId || existing?.requestId),
|
|
729
|
+
checkpointId: normalizeOptionalId(request.checkpointId || existing?.checkpointId),
|
|
730
|
+
testsRun: normalizeStringList(request.testsRun || existing?.testsRun || []),
|
|
731
|
+
finalSummary: request.finalSummary || existing?.finalSummary,
|
|
732
|
+
fileSnapshots: normalizePatchFileSnapshots(rootDirectory, request.fileSnapshots || existing?.fileSnapshots || []),
|
|
733
|
+
hunks: [],
|
|
734
|
+
decisions: normalizePatchReviewDecisions(existing?.decisions || []),
|
|
735
|
+
status: existing?.status || 'open',
|
|
736
|
+
notes: existing?.notes,
|
|
737
|
+
source: request.source || existing?.source || 'manual',
|
|
738
|
+
createdAt: existing?.createdAt || now,
|
|
739
|
+
updatedAt: now,
|
|
740
|
+
};
|
|
741
|
+
item.hunks = buildPatchReviewHunks(item.fileSnapshots || []);
|
|
742
|
+
store.items = [item, ...store.items.filter((entry) => entry.id !== item.id)].slice(0, MAX_STORED_PATCH_REVIEWS);
|
|
743
|
+
this.writePatchReviewStore(rootDirectory, store);
|
|
744
|
+
return { success: true, rootDirectory, item, items: store.items };
|
|
745
|
+
}
|
|
746
|
+
catch (err) {
|
|
747
|
+
return {
|
|
748
|
+
success: false,
|
|
749
|
+
rootDirectory: request.rootDirectory || '',
|
|
750
|
+
error: err instanceof Error ? err.message : String(err),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
updatePatchReview(request) {
|
|
755
|
+
try {
|
|
756
|
+
const rootDirectory = this.resolveRoot(request);
|
|
757
|
+
const id = String(request.id || '').trim();
|
|
758
|
+
if (!id)
|
|
759
|
+
return { success: false, rootDirectory, error: 'Patch review id is required.' };
|
|
760
|
+
const store = this.readPatchReviewStore(rootDirectory);
|
|
761
|
+
const item = store.items.find((entry) => entry.id === id);
|
|
762
|
+
if (!item)
|
|
763
|
+
return { success: false, rootDirectory, error: 'Patch review was not found.' };
|
|
764
|
+
item.status = normalizePatchReviewStatus(request.status, item.status);
|
|
765
|
+
item.notes = request.notes ?? item.notes;
|
|
766
|
+
item.updatedAt = Date.now();
|
|
767
|
+
this.writePatchReviewStore(rootDirectory, store);
|
|
768
|
+
return { success: true, rootDirectory, item, items: store.items };
|
|
769
|
+
}
|
|
770
|
+
catch (err) {
|
|
771
|
+
return {
|
|
772
|
+
success: false,
|
|
773
|
+
rootDirectory: request.rootDirectory || '',
|
|
774
|
+
error: err instanceof Error ? err.message : String(err),
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
reviewPatchSelection(request) {
|
|
779
|
+
const empty = { selectedHunkIds: [], wouldChangeFiles: [] };
|
|
780
|
+
try {
|
|
781
|
+
const rootDirectory = this.resolveRoot(request);
|
|
782
|
+
const id = normalizeRequiredId(request.id, 'id');
|
|
783
|
+
const relativePath = this.validateRelativePaths(rootDirectory, [String(request.relativePath || '').replace(/\\/g, '/')])[0];
|
|
784
|
+
if (!relativePath)
|
|
785
|
+
return { success: false, rootDirectory, ...empty, error: 'A safe relative patch path is required.' };
|
|
786
|
+
if (request.action !== 'accept' && request.action !== 'reject' && request.action !== 'comment') {
|
|
787
|
+
return { success: false, rootDirectory, ...empty, error: 'Patch decision action must be accept, reject, or comment.' };
|
|
788
|
+
}
|
|
789
|
+
const store = this.readPatchReviewStore(rootDirectory);
|
|
790
|
+
const item = store.items.find((entry) => entry.id === id);
|
|
791
|
+
if (!item)
|
|
792
|
+
return { success: false, rootDirectory, ...empty, error: 'Patch review was not found.' };
|
|
793
|
+
item.fileSnapshots = normalizePatchFileSnapshots(rootDirectory, item.fileSnapshots || []);
|
|
794
|
+
item.hunks = buildPatchReviewHunks(item.fileSnapshots);
|
|
795
|
+
const decisions = normalizePatchReviewDecisions(item.decisions || []);
|
|
796
|
+
item.decisions = decisions;
|
|
797
|
+
const pathKey = reviewPathKey(relativePath);
|
|
798
|
+
const snapshot = item.fileSnapshots.find((entry) => reviewPathKey(entry.relativePath) === pathKey);
|
|
799
|
+
const pathHunks = item.hunks.filter((entry) => reviewPathKey(entry.relativePath) === pathKey);
|
|
800
|
+
const requestedHunk = request.hunkId
|
|
801
|
+
? pathHunks.find((entry) => entry.id === request.hunkId)
|
|
802
|
+
: undefined;
|
|
803
|
+
if (request.action === 'comment') {
|
|
804
|
+
const comment = normalizeReviewComment(request.comment);
|
|
805
|
+
if (!comment)
|
|
806
|
+
return { success: false, rootDirectory, ...empty, error: 'A non-empty review comment is required.' };
|
|
807
|
+
if (request.hunkId && !requestedHunk) {
|
|
808
|
+
return { success: false, rootDirectory, ...empty, error: 'The selected patch hunk was not found.' };
|
|
809
|
+
}
|
|
810
|
+
const now = Date.now();
|
|
811
|
+
decisions.push({
|
|
812
|
+
id: `patch-decision-${now}-${randomUUID()}`,
|
|
813
|
+
relativePath,
|
|
814
|
+
...(requestedHunk ? { hunkId: requestedHunk.id } : {}),
|
|
815
|
+
action: 'comment',
|
|
816
|
+
comment,
|
|
817
|
+
state: 'applied',
|
|
818
|
+
createdAt: now,
|
|
819
|
+
updatedAt: now,
|
|
820
|
+
});
|
|
821
|
+
item.decisions = decisions.slice(-1_000);
|
|
822
|
+
item.updatedAt = now;
|
|
823
|
+
this.writePatchReviewStore(rootDirectory, store);
|
|
824
|
+
return {
|
|
825
|
+
success: true,
|
|
826
|
+
rootDirectory,
|
|
827
|
+
item,
|
|
828
|
+
selectedHunkIds: requestedHunk ? [requestedHunk.id] : [],
|
|
829
|
+
wouldChangeFiles: [],
|
|
830
|
+
applied: true,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
const conflicts = [];
|
|
834
|
+
if (!snapshot) {
|
|
835
|
+
conflicts.push({ relativePath, reason: 'selection_not_found', detail: 'The patch has no immutable snapshot for this file.' });
|
|
836
|
+
}
|
|
837
|
+
if (request.hunkId && !requestedHunk) {
|
|
838
|
+
conflicts.push({ relativePath, reason: 'selection_not_found', detail: 'The selected patch hunk was not found.' });
|
|
839
|
+
}
|
|
840
|
+
const effective = effectivePatchDecisions(decisions);
|
|
841
|
+
const targetHunks = request.hunkId
|
|
842
|
+
? (requestedHunk ? [requestedHunk] : [])
|
|
843
|
+
: pathHunks.filter((hunk) => !effective.has(hunk.id));
|
|
844
|
+
for (const hunk of targetHunks) {
|
|
845
|
+
if (effective.has(hunk.id)) {
|
|
846
|
+
conflicts.push({ relativePath, reason: 'already_decided', detail: `Patch hunk ${hunk.id} already has a final decision.` });
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (targetHunks.length === 0 && conflicts.length === 0) {
|
|
850
|
+
conflicts.push({ relativePath, reason: 'already_decided', detail: 'Every hunk in this file already has a final decision.' });
|
|
851
|
+
}
|
|
852
|
+
let expectedBefore;
|
|
853
|
+
let resulting;
|
|
854
|
+
let inspected;
|
|
855
|
+
if (snapshot && conflicts.length === 0) {
|
|
856
|
+
if (snapshot.originalExists === undefined || snapshot.modifiedExists === undefined) {
|
|
857
|
+
conflicts.push({
|
|
858
|
+
relativePath,
|
|
859
|
+
reason: 'legacy_snapshot_ambiguous',
|
|
860
|
+
detail: 'This patch predates explicit file-existence metadata and cannot be selectively applied.',
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
try {
|
|
865
|
+
expectedBefore = composePatchSnapshot(snapshot, pathHunks, decisions);
|
|
866
|
+
const prospectiveDecisions = [
|
|
867
|
+
...decisions,
|
|
868
|
+
...targetHunks.map((hunk, index) => ({
|
|
869
|
+
id: `preview-${index}`,
|
|
870
|
+
relativePath,
|
|
871
|
+
hunkId: hunk.id,
|
|
872
|
+
action: request.action,
|
|
873
|
+
state: 'applied',
|
|
874
|
+
createdAt: Date.now() + index,
|
|
875
|
+
updatedAt: Date.now() + index,
|
|
876
|
+
})),
|
|
877
|
+
];
|
|
878
|
+
resulting = composePatchSnapshot(snapshot, pathHunks, prospectiveDecisions);
|
|
879
|
+
inspected = inspectRestoreTarget(rootDirectory, relativePath);
|
|
880
|
+
if (inspected.exists !== expectedBefore.exists
|
|
881
|
+
|| (inspected.exists && inspected.sha256 !== expectedBefore.sha256)) {
|
|
882
|
+
conflicts.push({
|
|
883
|
+
relativePath,
|
|
884
|
+
reason: 'current_state_changed',
|
|
885
|
+
expectedExists: expectedBefore.exists,
|
|
886
|
+
actualExists: inspected.exists,
|
|
887
|
+
...(expectedBefore.exists ? { expectedSha256: expectedBefore.sha256 } : {}),
|
|
888
|
+
...(inspected.sha256 ? { actualSha256: inspected.sha256 } : {}),
|
|
889
|
+
detail: 'The workspace file no longer matches the exact reviewed patch state.',
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
catch (error) {
|
|
894
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
895
|
+
conflicts.push({
|
|
896
|
+
relativePath,
|
|
897
|
+
reason: /regular text file|size limit|snapshot/i.test(message) ? 'unsupported_file_type' : 'unsafe_path',
|
|
898
|
+
detail: message,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
const selectedHunkIds = targetHunks.map((hunk) => hunk.id);
|
|
904
|
+
const changesFile = request.action === 'reject'
|
|
905
|
+
&& !!expectedBefore
|
|
906
|
+
&& !!resulting
|
|
907
|
+
&& (expectedBefore.exists !== resulting.exists || expectedBefore.sha256 !== resulting.sha256);
|
|
908
|
+
const wouldChangeFiles = changesFile ? [relativePath] : [];
|
|
909
|
+
const previewToken = hashText(JSON.stringify({
|
|
910
|
+
id,
|
|
911
|
+
rootDirectory: resolve(rootDirectory),
|
|
912
|
+
relativePath,
|
|
913
|
+
action: request.action,
|
|
914
|
+
selectedHunkIds,
|
|
915
|
+
expectedBefore,
|
|
916
|
+
resulting,
|
|
917
|
+
actual: inspected ? { exists: inspected.exists, sha256: inspected.sha256 || null } : null,
|
|
918
|
+
conflicts,
|
|
919
|
+
}));
|
|
920
|
+
if (!request.confirmed) {
|
|
921
|
+
return {
|
|
922
|
+
success: true,
|
|
923
|
+
rootDirectory,
|
|
924
|
+
item,
|
|
925
|
+
selectedHunkIds,
|
|
926
|
+
wouldChangeFiles,
|
|
927
|
+
previewToken,
|
|
928
|
+
conflicts,
|
|
929
|
+
applied: false,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
if (!request.previewToken || request.previewToken !== previewToken) {
|
|
933
|
+
return {
|
|
934
|
+
success: false,
|
|
935
|
+
rootDirectory,
|
|
936
|
+
item,
|
|
937
|
+
selectedHunkIds,
|
|
938
|
+
wouldChangeFiles,
|
|
939
|
+
previewToken,
|
|
940
|
+
conflicts,
|
|
941
|
+
applied: false,
|
|
942
|
+
error: 'Patch review preview is missing or stale. Preview the selection again before confirming.',
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
if (conflicts.length > 0 || !snapshot || !expectedBefore || !resulting || !inspected) {
|
|
946
|
+
return {
|
|
947
|
+
success: false,
|
|
948
|
+
rootDirectory,
|
|
949
|
+
item,
|
|
950
|
+
selectedHunkIds,
|
|
951
|
+
wouldChangeFiles,
|
|
952
|
+
previewToken,
|
|
953
|
+
conflicts,
|
|
954
|
+
applied: false,
|
|
955
|
+
error: 'Patch selection conflicts with the immutable snapshot or current workspace state.',
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
const now = Date.now();
|
|
959
|
+
const pending = targetHunks.map((hunk, index) => ({
|
|
960
|
+
id: `patch-decision-${now}-${index}-${randomUUID()}`,
|
|
961
|
+
relativePath,
|
|
962
|
+
hunkId: hunk.id,
|
|
963
|
+
action: request.action,
|
|
964
|
+
...(normalizeReviewComment(request.comment) ? { comment: normalizeReviewComment(request.comment) } : {}),
|
|
965
|
+
state: changesFile ? 'pending' : 'applied',
|
|
966
|
+
expectedBeforeExists: expectedBefore.exists,
|
|
967
|
+
expectedBeforeSha256: expectedBefore.sha256,
|
|
968
|
+
resultingExists: resulting.exists,
|
|
969
|
+
resultingSha256: resulting.sha256,
|
|
970
|
+
createdAt: now + index,
|
|
971
|
+
updatedAt: now + index,
|
|
972
|
+
}));
|
|
973
|
+
decisions.push(...pending);
|
|
974
|
+
item.decisions = decisions.slice(-1_000);
|
|
975
|
+
item.updatedAt = now;
|
|
976
|
+
if (changesFile)
|
|
977
|
+
this.writePatchReviewStore(rootDirectory, store);
|
|
978
|
+
let transactionId;
|
|
979
|
+
if (changesFile) {
|
|
980
|
+
try {
|
|
981
|
+
const transaction = applyCheckpointRestoreTransaction(rootDirectory, `patch-review-${id}`, [{
|
|
982
|
+
relativePath,
|
|
983
|
+
targetPath: inspected.targetPath,
|
|
984
|
+
originalContent: resulting.content,
|
|
985
|
+
originalExists: resulting.exists,
|
|
986
|
+
originalSha256: resulting.sha256,
|
|
987
|
+
expectedCurrentExists: expectedBefore.exists,
|
|
988
|
+
expectedCurrentSha256: expectedBefore.sha256,
|
|
989
|
+
actualExists: inspected.exists,
|
|
990
|
+
actualSha256: inspected.sha256,
|
|
991
|
+
}]);
|
|
992
|
+
transactionId = transaction.transactionId;
|
|
993
|
+
for (const decision of pending) {
|
|
994
|
+
decision.state = 'applied';
|
|
995
|
+
decision.transactionId = transactionId;
|
|
996
|
+
decision.updatedAt = Date.now();
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
catch (error) {
|
|
1000
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1001
|
+
for (const decision of pending) {
|
|
1002
|
+
decision.state = 'failed';
|
|
1003
|
+
decision.error = message;
|
|
1004
|
+
decision.updatedAt = Date.now();
|
|
1005
|
+
}
|
|
1006
|
+
this.writePatchReviewStore(rootDirectory, store);
|
|
1007
|
+
return {
|
|
1008
|
+
success: false,
|
|
1009
|
+
rootDirectory,
|
|
1010
|
+
item,
|
|
1011
|
+
selectedHunkIds,
|
|
1012
|
+
wouldChangeFiles,
|
|
1013
|
+
previewToken,
|
|
1014
|
+
conflicts,
|
|
1015
|
+
applied: false,
|
|
1016
|
+
error: message,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
item.status = derivePatchReviewStatus(item);
|
|
1021
|
+
item.updatedAt = Date.now();
|
|
1022
|
+
this.writePatchReviewStore(rootDirectory, store);
|
|
1023
|
+
return {
|
|
1024
|
+
success: true,
|
|
1025
|
+
rootDirectory,
|
|
1026
|
+
item,
|
|
1027
|
+
selectedHunkIds,
|
|
1028
|
+
wouldChangeFiles,
|
|
1029
|
+
previewToken,
|
|
1030
|
+
conflicts: [],
|
|
1031
|
+
applied: true,
|
|
1032
|
+
...(transactionId ? { transactionId } : {}),
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
catch (err) {
|
|
1036
|
+
return {
|
|
1037
|
+
success: false,
|
|
1038
|
+
rootDirectory: request.rootDirectory || '',
|
|
1039
|
+
...empty,
|
|
1040
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
listCheckpoints(request) {
|
|
1045
|
+
try {
|
|
1046
|
+
const rootDirectory = this.resolveRoot(request);
|
|
1047
|
+
return { success: true, rootDirectory, checkpoints: this.buildCheckpointSummaries(rootDirectory) };
|
|
1048
|
+
}
|
|
1049
|
+
catch (err) {
|
|
1050
|
+
return {
|
|
1051
|
+
success: false,
|
|
1052
|
+
rootDirectory: request.rootDirectory || '',
|
|
1053
|
+
checkpoints: [],
|
|
1054
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
compareCheckpoints(request) {
|
|
1059
|
+
try {
|
|
1060
|
+
const rootDirectory = this.resolveRoot(request);
|
|
1061
|
+
const fromId = normalizeRequiredId(request.fromCheckpointId, 'fromCheckpointId');
|
|
1062
|
+
const toId = normalizeRequiredId(request.toCheckpointId, 'toCheckpointId');
|
|
1063
|
+
const checkpoints = this.buildCheckpointSummaries(rootDirectory);
|
|
1064
|
+
const from = checkpoints.find((checkpoint) => checkpoint.checkpointId === fromId);
|
|
1065
|
+
const to = checkpoints.find((checkpoint) => checkpoint.checkpointId === toId);
|
|
1066
|
+
if (!from || !to) {
|
|
1067
|
+
return {
|
|
1068
|
+
success: false,
|
|
1069
|
+
rootDirectory,
|
|
1070
|
+
filesChanged: [],
|
|
1071
|
+
diff: '',
|
|
1072
|
+
patches: [],
|
|
1073
|
+
error: 'Both checkpoints must exist in patch history before they can be compared.',
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
const patches = selectPatchesBetweenCheckpoints(this.readPatchReviewStore(rootDirectory).items, fromId, toId);
|
|
1077
|
+
const filesChanged = uniqueStrings(patches.flatMap((patch) => patch.filesChanged.length ? patch.filesChanged : patch.files));
|
|
1078
|
+
return {
|
|
1079
|
+
success: true,
|
|
1080
|
+
rootDirectory,
|
|
1081
|
+
from,
|
|
1082
|
+
to,
|
|
1083
|
+
filesChanged,
|
|
1084
|
+
diff: joinPatchDiffs(patches),
|
|
1085
|
+
patches,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
catch (err) {
|
|
1089
|
+
return {
|
|
1090
|
+
success: false,
|
|
1091
|
+
rootDirectory: request.rootDirectory || '',
|
|
1092
|
+
filesChanged: [],
|
|
1093
|
+
diff: '',
|
|
1094
|
+
patches: [],
|
|
1095
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
restoreCheckpoint(request) {
|
|
1100
|
+
try {
|
|
1101
|
+
const rootDirectory = this.resolveRoot(request);
|
|
1102
|
+
recoverInterruptedRestoreTransactions(rootDirectory);
|
|
1103
|
+
const checkpointId = normalizeRequiredId(request.checkpointId, 'checkpointId');
|
|
1104
|
+
const checkpoint = this.buildCheckpointSummaries(rootDirectory).find((entry) => entry.checkpointId === checkpointId);
|
|
1105
|
+
if (!checkpoint) {
|
|
1106
|
+
return {
|
|
1107
|
+
success: false,
|
|
1108
|
+
rootDirectory,
|
|
1109
|
+
previewDiff: '',
|
|
1110
|
+
wouldRestoreFiles: [],
|
|
1111
|
+
error: 'Checkpoint was not found in engineering patch history.',
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
const patches = this.readPatchReviewStore(rootDirectory).items.filter((patch) => patch.checkpointId === checkpointId);
|
|
1115
|
+
const previewDiff = joinPatchDiffs(patches);
|
|
1116
|
+
const plan = buildCheckpointRestorePlan(rootDirectory, checkpointId, patches);
|
|
1117
|
+
const wouldRestoreFiles = plan.entries.map((entry) => entry.relativePath);
|
|
1118
|
+
if (!request.confirmed) {
|
|
1119
|
+
return {
|
|
1120
|
+
success: true,
|
|
1121
|
+
rootDirectory,
|
|
1122
|
+
checkpoint,
|
|
1123
|
+
previewDiff,
|
|
1124
|
+
wouldRestoreFiles,
|
|
1125
|
+
previewToken: plan.previewToken,
|
|
1126
|
+
conflicts: plan.conflicts,
|
|
1127
|
+
restored: false,
|
|
1128
|
+
restoredFiles: [],
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
if (!request.previewToken || request.previewToken !== plan.previewToken) {
|
|
1132
|
+
return {
|
|
1133
|
+
success: false,
|
|
1134
|
+
rootDirectory,
|
|
1135
|
+
checkpoint,
|
|
1136
|
+
previewDiff,
|
|
1137
|
+
wouldRestoreFiles,
|
|
1138
|
+
previewToken: plan.previewToken,
|
|
1139
|
+
conflicts: plan.conflicts,
|
|
1140
|
+
restored: false,
|
|
1141
|
+
restoredFiles: [],
|
|
1142
|
+
error: 'Checkpoint restore preview is missing or stale. Preview the checkpoint again before confirming.',
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
const nonOverridableConflicts = plan.conflicts.filter((conflict) => (conflict.reason === 'unsafe_path'
|
|
1146
|
+
|| conflict.reason === 'unsupported_file_type'
|
|
1147
|
+
|| conflict.reason === 'legacy_snapshot_ambiguous'
|
|
1148
|
+
|| conflict.reason === 'missing_snapshot_data'
|
|
1149
|
+
|| conflict.reason === 'snapshot_chain_broken'));
|
|
1150
|
+
if (nonOverridableConflicts.length > 0
|
|
1151
|
+
|| (plan.conflicts.length > 0 && request.conflictStrategy !== 'overwrite')) {
|
|
1152
|
+
return {
|
|
1153
|
+
success: false,
|
|
1154
|
+
rootDirectory,
|
|
1155
|
+
checkpoint,
|
|
1156
|
+
previewDiff,
|
|
1157
|
+
wouldRestoreFiles,
|
|
1158
|
+
previewToken: plan.previewToken,
|
|
1159
|
+
conflicts: plan.conflicts,
|
|
1160
|
+
restored: false,
|
|
1161
|
+
restoredFiles: [],
|
|
1162
|
+
error: nonOverridableConflicts.length > 0
|
|
1163
|
+
? 'Checkpoint contains unsafe, unsupported, or ambiguous legacy snapshots and cannot be restored automatically.'
|
|
1164
|
+
: 'Workspace files changed after this checkpoint. Choose an explicit overwrite strategy or keep the current files.',
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
const transaction = applyCheckpointRestoreTransaction(rootDirectory, checkpointId, plan.entries);
|
|
1168
|
+
const conversationId = normalizeOptionalId(request.conversationId || checkpoint.conversationId);
|
|
1169
|
+
const requestId = normalizeOptionalId(request.requestId || checkpoint.requestId) || `checkpoint-restore-${Date.now()}`;
|
|
1170
|
+
const runtimeEvent = {
|
|
1171
|
+
id: `hub-checkpoint-restore-${checkpointId}-${Date.now()}`,
|
|
1172
|
+
type: 'hub.checkpoint.restore',
|
|
1173
|
+
checkpointId,
|
|
1174
|
+
rootDirectory,
|
|
1175
|
+
filesChanged: transaction.restoredFiles,
|
|
1176
|
+
transactionId: transaction.transactionId,
|
|
1177
|
+
restoredHashes: transaction.restoredHashes,
|
|
1178
|
+
summary: `Restored ${transaction.restoredFiles.length} file${transaction.restoredFiles.length === 1 ? '' : 's'} from ${checkpointId}`,
|
|
1179
|
+
};
|
|
1180
|
+
if (conversationId) {
|
|
1181
|
+
this.runtimeEventStore.append({
|
|
1182
|
+
conversationId,
|
|
1183
|
+
requestId,
|
|
1184
|
+
event: runtimeEvent,
|
|
1185
|
+
source: 'engineering-workbench',
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
return {
|
|
1189
|
+
success: true,
|
|
1190
|
+
rootDirectory,
|
|
1191
|
+
checkpoint,
|
|
1192
|
+
previewDiff,
|
|
1193
|
+
wouldRestoreFiles,
|
|
1194
|
+
previewToken: plan.previewToken,
|
|
1195
|
+
conflicts: plan.conflicts,
|
|
1196
|
+
restored: true,
|
|
1197
|
+
restoredFiles: transaction.restoredFiles,
|
|
1198
|
+
transactionId: transaction.transactionId,
|
|
1199
|
+
runtimeEvent,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
catch (err) {
|
|
1203
|
+
return {
|
|
1204
|
+
success: false,
|
|
1205
|
+
rootDirectory: request.rootDirectory || '',
|
|
1206
|
+
previewDiff: '',
|
|
1207
|
+
wouldRestoreFiles: [],
|
|
1208
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
listSessions(request) {
|
|
1213
|
+
try {
|
|
1214
|
+
const rootDirectory = this.resolveRoot(request);
|
|
1215
|
+
return { success: true, rootDirectory, sessions: this.buildEngineeringSessions(rootDirectory) };
|
|
1216
|
+
}
|
|
1217
|
+
catch (err) {
|
|
1218
|
+
return {
|
|
1219
|
+
success: false,
|
|
1220
|
+
rootDirectory: request.rootDirectory || '',
|
|
1221
|
+
sessions: [],
|
|
1222
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
runtimeTimeline(request) {
|
|
1227
|
+
try {
|
|
1228
|
+
const rootDirectory = this.resolveRoot(request);
|
|
1229
|
+
const conversationId = normalizeRequiredId(request.conversationId, 'conversationId');
|
|
1230
|
+
const projection = this.runtimeEventStore.project(conversationId);
|
|
1231
|
+
if (!projection.success) {
|
|
1232
|
+
return { success: false, rootDirectory, conversationId, error: projection.error || 'Failed to project runtime timeline.' };
|
|
1233
|
+
}
|
|
1234
|
+
return { success: true, rootDirectory, conversationId, timeline: projection.projection?.timeline };
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
return {
|
|
1238
|
+
success: false,
|
|
1239
|
+
rootDirectory: request.rootDirectory || '',
|
|
1240
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
resolveRoot(request) {
|
|
1245
|
+
return resolveRootDirectory(request.rootDirectory, request.allowedDirectories);
|
|
1246
|
+
}
|
|
1247
|
+
buildCheckpointSummaries(rootDirectory) {
|
|
1248
|
+
const byCheckpoint = new Map();
|
|
1249
|
+
for (const patch of this.readPatchReviewStore(rootDirectory).items) {
|
|
1250
|
+
if (!patch.checkpointId)
|
|
1251
|
+
continue;
|
|
1252
|
+
const list = byCheckpoint.get(patch.checkpointId) || [];
|
|
1253
|
+
list.push(patch);
|
|
1254
|
+
byCheckpoint.set(patch.checkpointId, list);
|
|
1255
|
+
}
|
|
1256
|
+
return [...byCheckpoint.entries()]
|
|
1257
|
+
.map(([checkpointId, patches]) => {
|
|
1258
|
+
const ordered = patches.slice().sort((a, b) => a.timestamp - b.timestamp || a.id.localeCompare(b.id));
|
|
1259
|
+
const first = ordered[0];
|
|
1260
|
+
const last = ordered[ordered.length - 1];
|
|
1261
|
+
return {
|
|
1262
|
+
id: `checkpoint-summary-${checkpointId}`,
|
|
1263
|
+
checkpointId,
|
|
1264
|
+
rootDirectory,
|
|
1265
|
+
createdAt: first?.timestamp || first?.createdAt || Date.now(),
|
|
1266
|
+
conversationId: first?.conversationId,
|
|
1267
|
+
requestId: first?.requestId,
|
|
1268
|
+
summary: uniqueStrings(ordered.map((patch) => patch.summary || patch.title)).join(' / ') || `Checkpoint ${checkpointId}`,
|
|
1269
|
+
filesChanged: uniqueStrings(ordered.flatMap((patch) => patch.filesChanged.length ? patch.filesChanged : patch.files)),
|
|
1270
|
+
patchIds: ordered.map((patch) => patch.id),
|
|
1271
|
+
patchCount: ordered.length,
|
|
1272
|
+
...(last?.requestId ? { requestId: last.requestId } : {}),
|
|
1273
|
+
};
|
|
1274
|
+
})
|
|
1275
|
+
.sort((a, b) => b.createdAt - a.createdAt || b.checkpointId.localeCompare(a.checkpointId));
|
|
1276
|
+
}
|
|
1277
|
+
buildEngineeringSessions(rootDirectory) {
|
|
1278
|
+
const patches = this.readPatchReviewStore(rootDirectory).items
|
|
1279
|
+
.filter((patch) => patch.conversationId)
|
|
1280
|
+
.slice()
|
|
1281
|
+
.sort((a, b) => a.timestamp - b.timestamp || a.id.localeCompare(b.id));
|
|
1282
|
+
const runs = this.readRunStore(rootDirectory).runs;
|
|
1283
|
+
const checkpoints = this.buildCheckpointSummaries(rootDirectory);
|
|
1284
|
+
const byConversation = new Map();
|
|
1285
|
+
for (const patch of patches) {
|
|
1286
|
+
const key = patch.conversationId || 'manual';
|
|
1287
|
+
const list = byConversation.get(key) || [];
|
|
1288
|
+
list.push(patch);
|
|
1289
|
+
byConversation.set(key, list);
|
|
1290
|
+
}
|
|
1291
|
+
return [...byConversation.entries()].map(([conversationId, sessionPatches]) => {
|
|
1292
|
+
const startedAt = Math.min(...sessionPatches.map((patch) => patch.timestamp || patch.createdAt));
|
|
1293
|
+
const endedAt = Math.max(...sessionPatches.map((patch) => patch.updatedAt || patch.timestamp || patch.createdAt));
|
|
1294
|
+
const sessionCheckpoints = checkpoints.filter((checkpoint) => checkpoint.conversationId === conversationId);
|
|
1295
|
+
const testsRun = runs.filter((run) => run.startedAt >= startedAt && run.startedAt <= Math.max(endedAt, startedAt));
|
|
1296
|
+
const changedFiles = uniqueStrings(sessionPatches.flatMap((patch) => patch.filesChanged.length ? patch.filesChanged : patch.files));
|
|
1297
|
+
const passCount = testsRun.filter((run) => run.success).length;
|
|
1298
|
+
const failCount = testsRun.length - passCount;
|
|
1299
|
+
return {
|
|
1300
|
+
id: `engineering-session-${conversationId}`,
|
|
1301
|
+
rootDirectory,
|
|
1302
|
+
conversationId,
|
|
1303
|
+
startedAt,
|
|
1304
|
+
endedAt,
|
|
1305
|
+
patches: sessionPatches.slice().reverse(),
|
|
1306
|
+
checkpoints: sessionCheckpoints,
|
|
1307
|
+
testsRun,
|
|
1308
|
+
finalSummary: `${sessionPatches.length} patch${sessionPatches.length === 1 ? '' : 'es'} / ${changedFiles.length} file${changedFiles.length === 1 ? '' : 's'} changed / ${testsRun.length} test run${testsRun.length === 1 ? '' : 's'} (${passCount} pass, ${failCount} fail)`,
|
|
1309
|
+
};
|
|
1310
|
+
}).sort((a, b) => b.startedAt - a.startedAt);
|
|
1311
|
+
}
|
|
1312
|
+
async isGitRepo(rootDirectory) {
|
|
1313
|
+
const result = await runProcess('git', ['rev-parse', '--is-inside-work-tree'], rootDirectory, 5000, 8000);
|
|
1314
|
+
return result.success && result.stdout.trim() === 'true';
|
|
1315
|
+
}
|
|
1316
|
+
validateRelativePaths(rootDirectory, paths) {
|
|
1317
|
+
const safe = paths
|
|
1318
|
+
.map((item) => String(item || '').trim())
|
|
1319
|
+
.filter(Boolean)
|
|
1320
|
+
.map((item) => item.replace(/\\/g, '/'))
|
|
1321
|
+
.filter((item) => !item.startsWith('/') && !/^[A-Za-z]:/.test(item))
|
|
1322
|
+
.filter((item) => {
|
|
1323
|
+
const full = join(rootDirectory, item);
|
|
1324
|
+
return isPathInside(full, rootDirectory);
|
|
1325
|
+
});
|
|
1326
|
+
if (paths.length > 0 && safe.length === 0)
|
|
1327
|
+
throw new Error('No safe relative file paths were provided.');
|
|
1328
|
+
return safe;
|
|
1329
|
+
}
|
|
1330
|
+
async runGitMutation(request, args) {
|
|
1331
|
+
try {
|
|
1332
|
+
const rootDirectory = this.resolveRoot(request);
|
|
1333
|
+
if (!await this.isGitRepo(rootDirectory)) {
|
|
1334
|
+
return { success: false, isGitRepo: false, rootDirectory, clean: true, files: [], error: 'Workspace is not a git repository.' };
|
|
1335
|
+
}
|
|
1336
|
+
const result = await runProcess('git', args, rootDirectory, 120000, MAX_GIT_OUTPUT);
|
|
1337
|
+
if (!result.success) {
|
|
1338
|
+
return {
|
|
1339
|
+
success: false,
|
|
1340
|
+
isGitRepo: true,
|
|
1341
|
+
rootDirectory,
|
|
1342
|
+
clean: true,
|
|
1343
|
+
files: [],
|
|
1344
|
+
error: result.error || result.stderr || 'Git operation failed.',
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
return this.gitStatus(request);
|
|
1348
|
+
}
|
|
1349
|
+
catch (err) {
|
|
1350
|
+
return {
|
|
1351
|
+
success: false,
|
|
1352
|
+
isGitRepo: false,
|
|
1353
|
+
rootDirectory: request.rootDirectory || '',
|
|
1354
|
+
clean: true,
|
|
1355
|
+
files: [],
|
|
1356
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
appendRun(rootDirectory, run) {
|
|
1361
|
+
const store = this.readRunStore(rootDirectory);
|
|
1362
|
+
store.runs = [run, ...store.runs.filter((entry) => entry.id !== run.id)].slice(0, MAX_STORED_RUNS);
|
|
1363
|
+
safeWriteJson(this.runStorePath(rootDirectory), store);
|
|
1364
|
+
}
|
|
1365
|
+
readRunStore(rootDirectory) {
|
|
1366
|
+
return safeReadJson(this.runStorePath(rootDirectory), { version: 1, runs: [] });
|
|
1367
|
+
}
|
|
1368
|
+
runStorePath(rootDirectory) {
|
|
1369
|
+
return join(workspaceIndexDirectory(rootDirectory), 'engineering-runs.json');
|
|
1370
|
+
}
|
|
1371
|
+
readBackgroundRunStore(rootDirectory) {
|
|
1372
|
+
const store = safeReadJson(this.backgroundRunStorePath(rootDirectory), { version: 1, runs: [] });
|
|
1373
|
+
let changed = false;
|
|
1374
|
+
const runs = store.runs
|
|
1375
|
+
.map((run) => {
|
|
1376
|
+
const active = this.activeBackgroundRuns.get(run.id);
|
|
1377
|
+
if (active)
|
|
1378
|
+
return active.run;
|
|
1379
|
+
if (run.status !== 'running')
|
|
1380
|
+
return run;
|
|
1381
|
+
changed = true;
|
|
1382
|
+
const now = Date.now();
|
|
1383
|
+
return {
|
|
1384
|
+
...run,
|
|
1385
|
+
status: 'stopped',
|
|
1386
|
+
updatedAt: run.updatedAt || now,
|
|
1387
|
+
endedAt: run.endedAt || now,
|
|
1388
|
+
durationMs: run.durationMs || Math.max(0, now - run.startedAt),
|
|
1389
|
+
error: run.error || 'Background process is no longer attached to this Hub session.',
|
|
1390
|
+
};
|
|
1391
|
+
})
|
|
1392
|
+
.slice(0, MAX_STORED_BACKGROUND_RUNS);
|
|
1393
|
+
const normalized = { version: 1, runs };
|
|
1394
|
+
if (changed || runs.length !== store.runs.length)
|
|
1395
|
+
safeWriteJson(this.backgroundRunStorePath(rootDirectory), normalized);
|
|
1396
|
+
return normalized;
|
|
1397
|
+
}
|
|
1398
|
+
writeBackgroundRun(rootDirectory, run) {
|
|
1399
|
+
const store = safeReadJson(this.backgroundRunStorePath(rootDirectory), { version: 1, runs: [] });
|
|
1400
|
+
const runs = [run, ...store.runs.filter((entry) => entry.id !== run.id)].slice(0, MAX_STORED_BACKGROUND_RUNS);
|
|
1401
|
+
safeWriteJson(this.backgroundRunStorePath(rootDirectory), { version: 1, runs });
|
|
1402
|
+
}
|
|
1403
|
+
backgroundRunStorePath(rootDirectory) {
|
|
1404
|
+
return join(workspaceIndexDirectory(rootDirectory), 'engineering-background-runs.json');
|
|
1405
|
+
}
|
|
1406
|
+
readPatchReviewStore(rootDirectory) {
|
|
1407
|
+
recoverInterruptedRestoreTransactions(rootDirectory);
|
|
1408
|
+
const store = safeReadJson(this.patchReviewStorePath(rootDirectory), { version: 1, items: [] });
|
|
1409
|
+
let changed = false;
|
|
1410
|
+
store.items = Array.isArray(store.items) ? store.items.slice(0, MAX_STORED_PATCH_REVIEWS) : [];
|
|
1411
|
+
for (const item of store.items) {
|
|
1412
|
+
const snapshots = normalizePatchFileSnapshots(rootDirectory, item.fileSnapshots || []);
|
|
1413
|
+
const hunks = buildPatchReviewHunks(snapshots);
|
|
1414
|
+
const decisions = normalizePatchReviewDecisions(item.decisions || []);
|
|
1415
|
+
item.fileSnapshots = snapshots;
|
|
1416
|
+
item.hunks = hunks;
|
|
1417
|
+
item.decisions = decisions;
|
|
1418
|
+
for (const decision of decisions) {
|
|
1419
|
+
if (decision.state !== 'pending')
|
|
1420
|
+
continue;
|
|
1421
|
+
try {
|
|
1422
|
+
const inspected = inspectRestoreTarget(rootDirectory, decision.relativePath);
|
|
1423
|
+
if (matchesReviewDecisionState(inspected, decision.resultingExists, decision.resultingSha256)) {
|
|
1424
|
+
decision.state = 'applied';
|
|
1425
|
+
decision.error = 'Recovered a committed selective-review filesystem transaction.';
|
|
1426
|
+
}
|
|
1427
|
+
else if (matchesReviewDecisionState(inspected, decision.expectedBeforeExists, decision.expectedBeforeSha256)) {
|
|
1428
|
+
decision.state = 'failed';
|
|
1429
|
+
decision.error = 'Recovered a selective-review decision before its filesystem transaction committed.';
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
decision.state = 'conflict';
|
|
1433
|
+
decision.error = 'Selective-review recovery found a file state matching neither the pre-decision nor resulting snapshot.';
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
catch (error) {
|
|
1437
|
+
decision.state = 'conflict';
|
|
1438
|
+
decision.error = error instanceof Error ? error.message : String(error);
|
|
1439
|
+
}
|
|
1440
|
+
decision.updatedAt = Date.now();
|
|
1441
|
+
changed = true;
|
|
1442
|
+
}
|
|
1443
|
+
const status = derivePatchReviewStatus(item);
|
|
1444
|
+
if (status !== item.status) {
|
|
1445
|
+
item.status = status;
|
|
1446
|
+
changed = true;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (changed)
|
|
1450
|
+
this.writePatchReviewStore(rootDirectory, store);
|
|
1451
|
+
return store;
|
|
1452
|
+
}
|
|
1453
|
+
writePatchReviewStore(rootDirectory, store) {
|
|
1454
|
+
safeWriteJson(this.patchReviewStorePath(rootDirectory), {
|
|
1455
|
+
version: 1,
|
|
1456
|
+
items: store.items.slice(0, MAX_STORED_PATCH_REVIEWS),
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
patchReviewStorePath(rootDirectory) {
|
|
1460
|
+
return join(workspaceIndexDirectory(rootDirectory), 'patch-review-queue.json');
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
function buildCheckpointRestorePlan(rootDirectory, checkpointId, patches) {
|
|
1464
|
+
const entriesByPath = new Map();
|
|
1465
|
+
const conflicts = [];
|
|
1466
|
+
const orderedPatches = patches.slice().sort((left, right) => (left.timestamp - right.timestamp || left.id.localeCompare(right.id)));
|
|
1467
|
+
for (const patch of orderedPatches) {
|
|
1468
|
+
for (const snapshot of patch.fileSnapshots || []) {
|
|
1469
|
+
const relativePath = snapshot.relativePath.replace(/\\/g, '/');
|
|
1470
|
+
const key = process.platform === 'win32' ? relativePath.toLowerCase() : relativePath;
|
|
1471
|
+
const prior = entriesByPath.get(key);
|
|
1472
|
+
const originalSha256 = snapshot.originalSha256 || hashText(snapshot.originalContent);
|
|
1473
|
+
const modifiedSha256 = snapshot.modifiedSha256 || hashText(snapshot.modifiedContent);
|
|
1474
|
+
if (!prior) {
|
|
1475
|
+
entriesByPath.set(key, {
|
|
1476
|
+
relativePath,
|
|
1477
|
+
targetPath: resolve(rootDirectory, relativePath),
|
|
1478
|
+
originalContent: snapshot.originalContent,
|
|
1479
|
+
originalExists: snapshot.originalExists,
|
|
1480
|
+
originalSha256,
|
|
1481
|
+
expectedCurrentExists: snapshot.modifiedExists,
|
|
1482
|
+
expectedCurrentSha256: modifiedSha256,
|
|
1483
|
+
actualExists: false,
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
else {
|
|
1487
|
+
if (prior.expectedCurrentExists !== undefined
|
|
1488
|
+
&& snapshot.originalExists !== undefined
|
|
1489
|
+
&& (prior.expectedCurrentExists !== snapshot.originalExists
|
|
1490
|
+
|| (snapshot.originalExists && prior.expectedCurrentSha256 !== originalSha256))) {
|
|
1491
|
+
conflicts.push({
|
|
1492
|
+
relativePath,
|
|
1493
|
+
reason: 'snapshot_chain_broken',
|
|
1494
|
+
expectedExists: prior.expectedCurrentExists,
|
|
1495
|
+
actualExists: snapshot.originalExists,
|
|
1496
|
+
...(prior.expectedCurrentExists ? { expectedSha256: prior.expectedCurrentSha256 } : {}),
|
|
1497
|
+
...(snapshot.originalExists ? { actualSha256: originalSha256 } : {}),
|
|
1498
|
+
detail: 'Consecutive snapshots for this checkpoint do not form one continuous file history.',
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
prior.expectedCurrentExists = snapshot.modifiedExists;
|
|
1502
|
+
prior.expectedCurrentSha256 = modifiedSha256;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
const entries = [...entriesByPath.values()].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
1507
|
+
if (entries.length === 0) {
|
|
1508
|
+
conflicts.push({
|
|
1509
|
+
relativePath: '(checkpoint)',
|
|
1510
|
+
reason: 'missing_snapshot_data',
|
|
1511
|
+
detail: 'This checkpoint has no restorable file snapshots.',
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
for (const entry of entries) {
|
|
1515
|
+
if (entry.originalExists === undefined || entry.expectedCurrentExists === undefined) {
|
|
1516
|
+
conflicts.push({
|
|
1517
|
+
relativePath: entry.relativePath,
|
|
1518
|
+
reason: 'legacy_snapshot_ambiguous',
|
|
1519
|
+
detail: 'This snapshot predates explicit file-existence metadata, so automatic deletion or recreation would be unsafe.',
|
|
1520
|
+
});
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
try {
|
|
1524
|
+
const inspected = inspectRestoreTarget(rootDirectory, entry.relativePath);
|
|
1525
|
+
entry.targetPath = inspected.targetPath;
|
|
1526
|
+
entry.actualExists = inspected.exists;
|
|
1527
|
+
entry.actualSha256 = inspected.sha256;
|
|
1528
|
+
if (entry.actualExists !== entry.expectedCurrentExists
|
|
1529
|
+
|| (entry.actualExists && entry.actualSha256 !== entry.expectedCurrentSha256)) {
|
|
1530
|
+
conflicts.push({
|
|
1531
|
+
relativePath: entry.relativePath,
|
|
1532
|
+
reason: 'current_state_changed',
|
|
1533
|
+
expectedExists: entry.expectedCurrentExists,
|
|
1534
|
+
actualExists: entry.actualExists,
|
|
1535
|
+
...(entry.expectedCurrentExists ? { expectedSha256: entry.expectedCurrentSha256 } : {}),
|
|
1536
|
+
...(entry.actualSha256 ? { actualSha256: entry.actualSha256 } : {}),
|
|
1537
|
+
detail: 'The current file no longer matches the exact post-edit snapshot.',
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
catch (error) {
|
|
1542
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1543
|
+
conflicts.push({
|
|
1544
|
+
relativePath: entry.relativePath,
|
|
1545
|
+
reason: /regular text file|size limit/i.test(message) ? 'unsupported_file_type' : 'unsafe_path',
|
|
1546
|
+
detail: message,
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
const previewToken = hashText(JSON.stringify({
|
|
1551
|
+
checkpointId,
|
|
1552
|
+
rootDirectory: resolve(rootDirectory),
|
|
1553
|
+
files: entries.map((entry) => ({
|
|
1554
|
+
relativePath: entry.relativePath,
|
|
1555
|
+
originalExists: entry.originalExists,
|
|
1556
|
+
originalSha256: entry.originalSha256,
|
|
1557
|
+
expectedCurrentExists: entry.expectedCurrentExists,
|
|
1558
|
+
expectedCurrentSha256: entry.expectedCurrentSha256,
|
|
1559
|
+
actualExists: entry.actualExists,
|
|
1560
|
+
actualSha256: entry.actualSha256 || null,
|
|
1561
|
+
})),
|
|
1562
|
+
conflicts,
|
|
1563
|
+
}));
|
|
1564
|
+
return { entries, conflicts, previewToken };
|
|
1565
|
+
}
|
|
1566
|
+
function inspectRestoreTarget(rootDirectory, relativePath) {
|
|
1567
|
+
const targetPath = assertSafeRestorePath(rootDirectory, relativePath);
|
|
1568
|
+
if (!existsSync(targetPath))
|
|
1569
|
+
return { targetPath, exists: false };
|
|
1570
|
+
const stats = lstatSync(targetPath);
|
|
1571
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
1572
|
+
throw new Error('Checkpoint targets must be regular text files, never symlinks or directories.');
|
|
1573
|
+
}
|
|
1574
|
+
if (stats.size > MAX_TRANSACTIONAL_SNAPSHOT_BYTES) {
|
|
1575
|
+
throw new Error(`Current file exceeds the ${MAX_TRANSACTIONAL_SNAPSHOT_BYTES}-byte transactional snapshot size limit.`);
|
|
1576
|
+
}
|
|
1577
|
+
const content = readFileSync(targetPath, 'utf8');
|
|
1578
|
+
if (content.length > MAX_TRANSACTIONAL_SNAPSHOT_CHARS) {
|
|
1579
|
+
throw new Error(`Current file exceeds the ${MAX_TRANSACTIONAL_SNAPSHOT_CHARS}-character transactional snapshot size limit.`);
|
|
1580
|
+
}
|
|
1581
|
+
return { targetPath, exists: true, sha256: hashText(content) };
|
|
1582
|
+
}
|
|
1583
|
+
function assertSafeRestorePath(rootDirectory, relativePath) {
|
|
1584
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
1585
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
1586
|
+
if (!normalized || isAbsolute(normalized) || parts.some((part) => part === '.' || part === '..')) {
|
|
1587
|
+
throw new Error('Checkpoint contains an unsafe relative path.');
|
|
1588
|
+
}
|
|
1589
|
+
const targetPath = resolve(rootDirectory, normalized);
|
|
1590
|
+
if (!isPathInside(targetPath, rootDirectory))
|
|
1591
|
+
throw new Error('Checkpoint target escapes the workspace root.');
|
|
1592
|
+
const canonicalRoot = realpathSync(rootDirectory);
|
|
1593
|
+
let cursor = resolve(rootDirectory);
|
|
1594
|
+
for (const part of parts.slice(0, -1)) {
|
|
1595
|
+
cursor = join(cursor, part);
|
|
1596
|
+
if (!existsSync(cursor))
|
|
1597
|
+
continue;
|
|
1598
|
+
const stats = lstatSync(cursor);
|
|
1599
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
1600
|
+
throw new Error('Checkpoint target crosses a symlink or non-directory path component.');
|
|
1601
|
+
}
|
|
1602
|
+
if (!isPathInside(realpathSync(cursor), canonicalRoot)) {
|
|
1603
|
+
throw new Error('Checkpoint target resolves outside the canonical workspace root.');
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return targetPath;
|
|
1607
|
+
}
|
|
1608
|
+
function applyCheckpointRestoreTransaction(rootDirectory, checkpointId, entries) {
|
|
1609
|
+
const transactionId = `checkpoint-restore-${Date.now()}-${randomUUID()}`;
|
|
1610
|
+
const transactionDirectory = checkpointTransactionDirectory(rootDirectory);
|
|
1611
|
+
mkdirSync(transactionDirectory, { recursive: true });
|
|
1612
|
+
const manifestPath = join(transactionDirectory, `${transactionId}.json`);
|
|
1613
|
+
const transactionFiles = [];
|
|
1614
|
+
for (const entry of entries) {
|
|
1615
|
+
if (entry.originalExists === undefined)
|
|
1616
|
+
throw new Error(`Checkpoint snapshot is ambiguous: ${entry.relativePath}`);
|
|
1617
|
+
const targetPath = assertSafeRestorePath(rootDirectory, entry.relativePath);
|
|
1618
|
+
const inspected = inspectRestoreTarget(rootDirectory, entry.relativePath);
|
|
1619
|
+
if (inspected.exists !== entry.actualExists
|
|
1620
|
+
|| (inspected.exists && inspected.sha256 !== entry.actualSha256)) {
|
|
1621
|
+
throw new Error(`Checkpoint restore preview became stale for ${entry.relativePath}.`);
|
|
1622
|
+
}
|
|
1623
|
+
const parentDirectory = dirname(targetPath);
|
|
1624
|
+
mkdirSync(parentDirectory, { recursive: true });
|
|
1625
|
+
const suffix = `.xeno-${transactionId}`;
|
|
1626
|
+
const stagePath = entry.originalExists ? `${targetPath}${suffix}.stage` : null;
|
|
1627
|
+
const backupPath = `${targetPath}${suffix}.backup`;
|
|
1628
|
+
if (stagePath)
|
|
1629
|
+
writeFileSync(stagePath, entry.originalContent, { encoding: 'utf8', flag: 'wx' });
|
|
1630
|
+
transactionFiles.push({
|
|
1631
|
+
relativePath: entry.relativePath,
|
|
1632
|
+
targetPath,
|
|
1633
|
+
stagePath,
|
|
1634
|
+
backupPath,
|
|
1635
|
+
hadCurrent: inspected.exists,
|
|
1636
|
+
expectedCurrentExists: inspected.exists,
|
|
1637
|
+
expectedCurrentSha256: inspected.sha256,
|
|
1638
|
+
restoreExists: entry.originalExists,
|
|
1639
|
+
restoreSha256: entry.originalSha256,
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
const manifest = {
|
|
1643
|
+
version: 1,
|
|
1644
|
+
transactionId,
|
|
1645
|
+
checkpointId,
|
|
1646
|
+
rootDirectory,
|
|
1647
|
+
phase: 'prepared',
|
|
1648
|
+
createdAt: Date.now(),
|
|
1649
|
+
updatedAt: Date.now(),
|
|
1650
|
+
files: transactionFiles,
|
|
1651
|
+
};
|
|
1652
|
+
safeWriteJson(manifestPath, manifest);
|
|
1653
|
+
try {
|
|
1654
|
+
manifest.phase = 'applying';
|
|
1655
|
+
manifest.updatedAt = Date.now();
|
|
1656
|
+
safeWriteJson(manifestPath, manifest);
|
|
1657
|
+
for (const file of transactionFiles) {
|
|
1658
|
+
validateRestoreTransactionFile(rootDirectory, transactionId, file);
|
|
1659
|
+
const inspected = inspectRestoreTarget(rootDirectory, file.relativePath);
|
|
1660
|
+
if (inspected.exists !== file.expectedCurrentExists
|
|
1661
|
+
|| (inspected.exists && inspected.sha256 !== file.expectedCurrentSha256)) {
|
|
1662
|
+
throw new Error(`Checkpoint restore target changed during the transaction: ${file.relativePath}`);
|
|
1663
|
+
}
|
|
1664
|
+
if (existsSync(file.backupPath))
|
|
1665
|
+
throw new Error(`Checkpoint backup path already exists: ${file.relativePath}`);
|
|
1666
|
+
if (existsSync(file.targetPath))
|
|
1667
|
+
renameSync(file.targetPath, file.backupPath);
|
|
1668
|
+
if (file.restoreExists && file.stagePath)
|
|
1669
|
+
renameSync(file.stagePath, file.targetPath);
|
|
1670
|
+
}
|
|
1671
|
+
const restoredHashes = {};
|
|
1672
|
+
for (const file of transactionFiles) {
|
|
1673
|
+
if (!file.restoreExists) {
|
|
1674
|
+
if (existsSync(file.targetPath))
|
|
1675
|
+
throw new Error(`Checkpoint verification failed: ${file.relativePath} still exists.`);
|
|
1676
|
+
restoredHashes[file.relativePath] = null;
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
if (!existsSync(file.targetPath))
|
|
1680
|
+
throw new Error(`Checkpoint verification failed: ${file.relativePath} is missing.`);
|
|
1681
|
+
const restoredHash = hashText(readFileSync(file.targetPath, 'utf8'));
|
|
1682
|
+
if (restoredHash !== file.restoreSha256) {
|
|
1683
|
+
throw new Error(`Checkpoint verification failed for ${file.relativePath}.`);
|
|
1684
|
+
}
|
|
1685
|
+
restoredHashes[file.relativePath] = restoredHash;
|
|
1686
|
+
}
|
|
1687
|
+
manifest.phase = 'committed';
|
|
1688
|
+
manifest.updatedAt = Date.now();
|
|
1689
|
+
safeWriteJson(manifestPath, manifest);
|
|
1690
|
+
cleanupTransactionArtifacts(transactionFiles);
|
|
1691
|
+
return {
|
|
1692
|
+
transactionId,
|
|
1693
|
+
restoredFiles: transactionFiles.map((file) => file.relativePath),
|
|
1694
|
+
restoredHashes,
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
catch (error) {
|
|
1698
|
+
const rolledBack = rollbackTransactionFiles(rootDirectory, transactionId, transactionFiles);
|
|
1699
|
+
manifest.phase = rolledBack ? 'rolled_back' : 'rollback_failed';
|
|
1700
|
+
manifest.updatedAt = Date.now();
|
|
1701
|
+
manifest.error = `${error instanceof Error ? error.message : String(error)}${rolledBack ? '' : ' Automatic rollback was incomplete; recovery artifacts were retained.'}`;
|
|
1702
|
+
safeWriteJson(manifestPath, manifest);
|
|
1703
|
+
throw error;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
function recoverInterruptedRestoreTransactions(rootDirectory) {
|
|
1707
|
+
const directory = checkpointTransactionDirectory(rootDirectory);
|
|
1708
|
+
if (!existsSync(directory))
|
|
1709
|
+
return;
|
|
1710
|
+
for (const name of readdirSync(directory)) {
|
|
1711
|
+
if (!name.endsWith('.json'))
|
|
1712
|
+
continue;
|
|
1713
|
+
const manifestPath = join(directory, name);
|
|
1714
|
+
const manifest = safeReadJson(manifestPath, null);
|
|
1715
|
+
if (!manifest || manifest.version !== 1 || !sameResolvedPath(manifest.rootDirectory, rootDirectory))
|
|
1716
|
+
continue;
|
|
1717
|
+
if (name !== `${manifest.transactionId}.json` || !/^checkpoint-restore-\d+-[0-9a-f-]{36}$/i.test(manifest.transactionId))
|
|
1718
|
+
continue;
|
|
1719
|
+
try {
|
|
1720
|
+
validateRestoreTransactionFiles(rootDirectory, manifest.transactionId, manifest.files);
|
|
1721
|
+
if (manifest.phase === 'committed') {
|
|
1722
|
+
cleanupTransactionArtifacts(manifest.files);
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
if (manifest.phase !== 'prepared' && manifest.phase !== 'applying' && manifest.phase !== 'rollback_failed')
|
|
1726
|
+
continue;
|
|
1727
|
+
const recovered = manifest.phase === 'prepared'
|
|
1728
|
+
? (cleanupTransactionArtifacts(manifest.files), true)
|
|
1729
|
+
: rollbackTransactionFiles(rootDirectory, manifest.transactionId, manifest.files);
|
|
1730
|
+
manifest.phase = recovered ? 'recovered_rollback' : 'rollback_failed';
|
|
1731
|
+
manifest.updatedAt = Date.now();
|
|
1732
|
+
manifest.error = recovered
|
|
1733
|
+
? 'Recovered and rolled back an interrupted checkpoint restore transaction.'
|
|
1734
|
+
: 'Checkpoint restore recovery was incomplete; retained backup artifacts require another recovery pass.';
|
|
1735
|
+
safeWriteJson(manifestPath, manifest);
|
|
1736
|
+
}
|
|
1737
|
+
catch (error) {
|
|
1738
|
+
manifest.phase = 'rollback_failed';
|
|
1739
|
+
manifest.updatedAt = Date.now();
|
|
1740
|
+
manifest.error = `Checkpoint restore recovery rejected unsafe transaction metadata: ${error instanceof Error ? error.message : String(error)}`;
|
|
1741
|
+
safeWriteJson(manifestPath, manifest);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
function rollbackTransactionFiles(rootDirectory, transactionId, files) {
|
|
1746
|
+
validateRestoreTransactionFiles(rootDirectory, transactionId, files);
|
|
1747
|
+
let complete = true;
|
|
1748
|
+
for (const file of files.slice().reverse()) {
|
|
1749
|
+
try {
|
|
1750
|
+
if (existsSync(file.backupPath)) {
|
|
1751
|
+
if (existsSync(file.targetPath))
|
|
1752
|
+
unlinkSync(file.targetPath);
|
|
1753
|
+
renameSync(file.backupPath, file.targetPath);
|
|
1754
|
+
}
|
|
1755
|
+
else if (!file.hadCurrent && existsSync(file.targetPath)) {
|
|
1756
|
+
unlinkSync(file.targetPath);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
catch {
|
|
1760
|
+
complete = false;
|
|
1761
|
+
// Keep the transaction manifest and remaining artifacts for the next recovery pass.
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
cleanupTransactionStageArtifacts(files);
|
|
1765
|
+
return complete && files.every((file) => !existsSync(file.backupPath));
|
|
1766
|
+
}
|
|
1767
|
+
function validateRestoreTransactionFiles(rootDirectory, transactionId, files) {
|
|
1768
|
+
for (const file of files)
|
|
1769
|
+
validateRestoreTransactionFile(rootDirectory, transactionId, file);
|
|
1770
|
+
}
|
|
1771
|
+
function validateRestoreTransactionFile(rootDirectory, transactionId, file) {
|
|
1772
|
+
const targetPath = assertSafeRestorePath(rootDirectory, file.relativePath);
|
|
1773
|
+
const suffix = `.xeno-${transactionId}`;
|
|
1774
|
+
const expectedStagePath = file.restoreExists ? `${targetPath}${suffix}.stage` : null;
|
|
1775
|
+
const expectedBackupPath = `${targetPath}${suffix}.backup`;
|
|
1776
|
+
if (!sameResolvedPath(file.targetPath, targetPath)
|
|
1777
|
+
|| (file.stagePath === null ? expectedStagePath !== null : expectedStagePath === null || !sameResolvedPath(file.stagePath, expectedStagePath))
|
|
1778
|
+
|| !sameResolvedPath(file.backupPath, expectedBackupPath)) {
|
|
1779
|
+
throw new Error(`Transaction artifact paths do not match the workspace target for ${file.relativePath}.`);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
function sameResolvedPath(left, right) {
|
|
1783
|
+
const normalizedLeft = resolve(left);
|
|
1784
|
+
const normalizedRight = resolve(right);
|
|
1785
|
+
return process.platform === 'win32'
|
|
1786
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
1787
|
+
: normalizedLeft === normalizedRight;
|
|
1788
|
+
}
|
|
1789
|
+
function cleanupTransactionStageArtifacts(files) {
|
|
1790
|
+
for (const file of files) {
|
|
1791
|
+
if (!file.stagePath || !existsSync(file.stagePath))
|
|
1792
|
+
continue;
|
|
1793
|
+
try {
|
|
1794
|
+
unlinkSync(file.stagePath);
|
|
1795
|
+
}
|
|
1796
|
+
catch { /* retained for a later recovery/repair pass */ }
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
function cleanupTransactionArtifacts(files) {
|
|
1800
|
+
for (const file of files) {
|
|
1801
|
+
for (const artifact of [file.stagePath, file.backupPath]) {
|
|
1802
|
+
if (!artifact || !existsSync(artifact))
|
|
1803
|
+
continue;
|
|
1804
|
+
try {
|
|
1805
|
+
unlinkSync(artifact);
|
|
1806
|
+
}
|
|
1807
|
+
catch { /* retained for a later recovery/repair pass */ }
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
function checkpointTransactionDirectory(rootDirectory) {
|
|
1812
|
+
return join(workspaceIndexDirectory(rootDirectory), 'snapshots', 'transactions');
|
|
1813
|
+
}
|
|
1814
|
+
function normalizeRequiredId(value, label) {
|
|
1815
|
+
const normalized = normalizeOptionalId(value);
|
|
1816
|
+
if (!normalized)
|
|
1817
|
+
throw new Error(`${label} is required.`);
|
|
1818
|
+
return normalized;
|
|
1819
|
+
}
|
|
1820
|
+
function normalizeOptionalId(value) {
|
|
1821
|
+
return typeof value === 'string' && value.trim() ? value.trim().slice(0, 240) : undefined;
|
|
1822
|
+
}
|
|
1823
|
+
function normalizeStringList(value) {
|
|
1824
|
+
if (!Array.isArray(value))
|
|
1825
|
+
return [];
|
|
1826
|
+
return uniqueStrings(value.map((entry) => typeof entry === 'string' ? entry.trim() : '').filter(Boolean));
|
|
1827
|
+
}
|
|
1828
|
+
function normalizePatchFileSnapshots(rootDirectory, value) {
|
|
1829
|
+
if (!Array.isArray(value))
|
|
1830
|
+
return [];
|
|
1831
|
+
const snapshots = [];
|
|
1832
|
+
for (const entry of value) {
|
|
1833
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
1834
|
+
continue;
|
|
1835
|
+
const record = entry;
|
|
1836
|
+
const rawPath = typeof record.path === 'string' ? record.path.trim() : '';
|
|
1837
|
+
const rawRelativePath = typeof record.relativePath === 'string' ? record.relativePath.trim() : '';
|
|
1838
|
+
const relativePath = (rawRelativePath || (isAbsolute(rawPath) && isPathInside(rawPath, rootDirectory)
|
|
1839
|
+
? relative(rootDirectory, rawPath)
|
|
1840
|
+
: rawPath)).replace(/\\/g, '/');
|
|
1841
|
+
if (!relativePath || relativePath.startsWith('/') || /^[A-Za-z]:/.test(relativePath))
|
|
1842
|
+
continue;
|
|
1843
|
+
const absolutePath = join(rootDirectory, relativePath);
|
|
1844
|
+
if (!isPathInside(absolutePath, rootDirectory))
|
|
1845
|
+
continue;
|
|
1846
|
+
const originalContent = typeof record.originalContent === 'string' ? record.originalContent : '';
|
|
1847
|
+
const modifiedContent = typeof record.modifiedContent === 'string' ? record.modifiedContent : '';
|
|
1848
|
+
const originalSha256 = hashText(originalContent);
|
|
1849
|
+
const modifiedSha256 = hashText(modifiedContent);
|
|
1850
|
+
persistSnapshotBlob(rootDirectory, originalSha256, originalContent);
|
|
1851
|
+
persistSnapshotBlob(rootDirectory, modifiedSha256, modifiedContent);
|
|
1852
|
+
snapshots.push({
|
|
1853
|
+
path: absolutePath,
|
|
1854
|
+
relativePath,
|
|
1855
|
+
originalContent,
|
|
1856
|
+
modifiedContent,
|
|
1857
|
+
...(typeof record.originalExists === 'boolean' ? { originalExists: record.originalExists } : {}),
|
|
1858
|
+
...(typeof record.modifiedExists === 'boolean' ? { modifiedExists: record.modifiedExists } : {}),
|
|
1859
|
+
originalSha256,
|
|
1860
|
+
modifiedSha256,
|
|
1861
|
+
originalBlobId: `sha256:${originalSha256}`,
|
|
1862
|
+
modifiedBlobId: `sha256:${modifiedSha256}`,
|
|
1863
|
+
language: typeof record.language === 'string' ? record.language : undefined,
|
|
1864
|
+
addedLines: typeof record.addedLines === 'number' && Number.isFinite(record.addedLines) ? record.addedLines : undefined,
|
|
1865
|
+
removedLines: typeof record.removedLines === 'number' && Number.isFinite(record.removedLines) ? record.removedLines : undefined,
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
return snapshots.slice(0, 80);
|
|
1869
|
+
}
|
|
1870
|
+
function persistSnapshotBlob(rootDirectory, sha256, content) {
|
|
1871
|
+
const directory = join(workspaceIndexDirectory(rootDirectory), 'snapshots', 'blobs');
|
|
1872
|
+
const blobPath = join(directory, sha256);
|
|
1873
|
+
if (existsSync(blobPath))
|
|
1874
|
+
return;
|
|
1875
|
+
mkdirSync(directory, { recursive: true });
|
|
1876
|
+
const temporaryPath = `${blobPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1877
|
+
try {
|
|
1878
|
+
writeFileSync(temporaryPath, content, 'utf8');
|
|
1879
|
+
if (!existsSync(blobPath))
|
|
1880
|
+
renameSync(temporaryPath, blobPath);
|
|
1881
|
+
}
|
|
1882
|
+
finally {
|
|
1883
|
+
if (existsSync(temporaryPath)) {
|
|
1884
|
+
try {
|
|
1885
|
+
unlinkSync(temporaryPath);
|
|
1886
|
+
}
|
|
1887
|
+
catch { /* best-effort cleanup */ }
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
function uniqueStrings(values) {
|
|
1892
|
+
const seen = new Set();
|
|
1893
|
+
const out = [];
|
|
1894
|
+
for (const value of values) {
|
|
1895
|
+
const normalized = String(value || '').trim().replace(/\\/g, '/');
|
|
1896
|
+
const key = normalized.toLowerCase();
|
|
1897
|
+
if (!normalized || seen.has(key))
|
|
1898
|
+
continue;
|
|
1899
|
+
seen.add(key);
|
|
1900
|
+
out.push(normalized);
|
|
1901
|
+
}
|
|
1902
|
+
return out;
|
|
1903
|
+
}
|
|
1904
|
+
function joinPatchDiffs(patches) {
|
|
1905
|
+
return patches
|
|
1906
|
+
.slice(0, MAX_CHECKPOINT_COMPARE_PATCHES)
|
|
1907
|
+
.map((patch) => [
|
|
1908
|
+
`# ${patch.id} / ${patch.summary || patch.title}`,
|
|
1909
|
+
patch.diff.trim(),
|
|
1910
|
+
].filter(Boolean).join('\n'))
|
|
1911
|
+
.filter(Boolean)
|
|
1912
|
+
.join('\n\n');
|
|
1913
|
+
}
|
|
1914
|
+
function selectPatchesBetweenCheckpoints(items, fromCheckpointId, toCheckpointId) {
|
|
1915
|
+
const ordered = items
|
|
1916
|
+
.filter((patch) => patch.checkpointId)
|
|
1917
|
+
.slice()
|
|
1918
|
+
.sort((a, b) => a.timestamp - b.timestamp || a.id.localeCompare(b.id));
|
|
1919
|
+
const fromIndex = ordered.findIndex((patch) => patch.checkpointId === fromCheckpointId);
|
|
1920
|
+
const toIndex = ordered.findIndex((patch) => patch.checkpointId === toCheckpointId);
|
|
1921
|
+
if (fromIndex === -1 || toIndex === -1)
|
|
1922
|
+
return [];
|
|
1923
|
+
const start = Math.min(fromIndex, toIndex);
|
|
1924
|
+
const end = Math.max(fromIndex, toIndex);
|
|
1925
|
+
return ordered.slice(start, end + 1);
|
|
1926
|
+
}
|
|
1927
|
+
function normalizePatchReviewStatus(value, fallback) {
|
|
1928
|
+
return value === 'open'
|
|
1929
|
+
|| value === 'approved'
|
|
1930
|
+
|| value === 'changes-requested'
|
|
1931
|
+
|| value === 'applied'
|
|
1932
|
+
|| value === 'dismissed'
|
|
1933
|
+
? value
|
|
1934
|
+
: fallback;
|
|
1935
|
+
}
|
|
1936
|
+
function normalizePatchReviewDecisions(value) {
|
|
1937
|
+
if (!Array.isArray(value))
|
|
1938
|
+
return [];
|
|
1939
|
+
const decisions = [];
|
|
1940
|
+
for (const entry of value.slice(-1_000)) {
|
|
1941
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
1942
|
+
continue;
|
|
1943
|
+
const record = entry;
|
|
1944
|
+
const id = normalizeOptionalId(record.id);
|
|
1945
|
+
const relativePath = typeof record.relativePath === 'string'
|
|
1946
|
+
? record.relativePath.trim().replace(/\\/g, '/').slice(0, 2_000)
|
|
1947
|
+
: '';
|
|
1948
|
+
const action = record.action === 'accept' || record.action === 'reject' || record.action === 'comment'
|
|
1949
|
+
? record.action
|
|
1950
|
+
: undefined;
|
|
1951
|
+
const state = record.state === 'pending' || record.state === 'applied' || record.state === 'failed' || record.state === 'conflict'
|
|
1952
|
+
? record.state
|
|
1953
|
+
: undefined;
|
|
1954
|
+
if (!id || !relativePath || !action || !state)
|
|
1955
|
+
continue;
|
|
1956
|
+
const createdAt = typeof record.createdAt === 'number' && Number.isFinite(record.createdAt) ? record.createdAt : Date.now();
|
|
1957
|
+
const updatedAt = typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt) ? record.updatedAt : createdAt;
|
|
1958
|
+
decisions.push({
|
|
1959
|
+
id,
|
|
1960
|
+
relativePath,
|
|
1961
|
+
...(normalizeOptionalId(record.hunkId) ? { hunkId: normalizeOptionalId(record.hunkId) } : {}),
|
|
1962
|
+
action,
|
|
1963
|
+
...(normalizeReviewComment(record.comment) ? { comment: normalizeReviewComment(record.comment) } : {}),
|
|
1964
|
+
state,
|
|
1965
|
+
...(typeof record.expectedBeforeExists === 'boolean' ? { expectedBeforeExists: record.expectedBeforeExists } : {}),
|
|
1966
|
+
...(normalizeSha256(record.expectedBeforeSha256) ? { expectedBeforeSha256: normalizeSha256(record.expectedBeforeSha256) } : {}),
|
|
1967
|
+
...(typeof record.resultingExists === 'boolean' ? { resultingExists: record.resultingExists } : {}),
|
|
1968
|
+
...(normalizeSha256(record.resultingSha256) ? { resultingSha256: normalizeSha256(record.resultingSha256) } : {}),
|
|
1969
|
+
...(normalizeOptionalId(record.transactionId) ? { transactionId: normalizeOptionalId(record.transactionId) } : {}),
|
|
1970
|
+
...(typeof record.error === 'string' && record.error.trim() ? { error: record.error.trim().slice(0, 4_000) } : {}),
|
|
1971
|
+
createdAt,
|
|
1972
|
+
updatedAt,
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
return decisions;
|
|
1976
|
+
}
|
|
1977
|
+
function normalizeReviewComment(value) {
|
|
1978
|
+
return typeof value === 'string' && value.trim() ? value.trim().slice(0, 20_000) : undefined;
|
|
1979
|
+
}
|
|
1980
|
+
function normalizeSha256(value) {
|
|
1981
|
+
return typeof value === 'string' && /^[0-9a-f]{64}$/i.test(value) ? value.toLowerCase() : undefined;
|
|
1982
|
+
}
|
|
1983
|
+
function reviewPathKey(value) {
|
|
1984
|
+
const normalized = value.replace(/\\/g, '/');
|
|
1985
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
1986
|
+
}
|
|
1987
|
+
function derivePatchReviewStatus(item) {
|
|
1988
|
+
const hunks = item.hunks || [];
|
|
1989
|
+
if (hunks.length === 0)
|
|
1990
|
+
return item.status;
|
|
1991
|
+
const effective = effectivePatchDecisions(item.decisions || []);
|
|
1992
|
+
if (effective.size >= hunks.length) {
|
|
1993
|
+
return [...effective.values()].every((action) => action === 'accept') ? 'approved' : 'applied';
|
|
1994
|
+
}
|
|
1995
|
+
return [...effective.values()].some((action) => action === 'reject') ? 'changes-requested' : 'open';
|
|
1996
|
+
}
|
|
1997
|
+
function matchesReviewDecisionState(inspected, expectedExists, expectedSha256) {
|
|
1998
|
+
if (expectedExists === undefined || inspected.exists !== expectedExists)
|
|
1999
|
+
return false;
|
|
2000
|
+
if (!expectedExists)
|
|
2001
|
+
return true;
|
|
2002
|
+
return !!expectedSha256 && inspected.sha256 === expectedSha256;
|
|
2003
|
+
}
|
|
2004
|
+
function npmScriptInvocation(scriptName) {
|
|
2005
|
+
if (process.platform === 'win32') {
|
|
2006
|
+
return { command: 'cmd.exe', args: ['/d', '/s', '/c', 'npm', 'run', scriptName] };
|
|
2007
|
+
}
|
|
2008
|
+
return { command: 'npm', args: ['run', scriptName] };
|
|
2009
|
+
}
|
|
2010
|
+
function classifyScript(name, command) {
|
|
2011
|
+
const haystack = `${name} ${command}`.toLowerCase();
|
|
2012
|
+
if (haystack.includes('typecheck') || haystack.includes('tsc '))
|
|
2013
|
+
return 'typecheck';
|
|
2014
|
+
if (haystack.includes('test') || haystack.includes('vitest') || haystack.includes('jest') || haystack.includes('playwright'))
|
|
2015
|
+
return 'test';
|
|
2016
|
+
if (haystack.includes('lint') || haystack.includes('eslint'))
|
|
2017
|
+
return 'lint';
|
|
2018
|
+
if (haystack.includes('build') || haystack.includes('vite build') || haystack.includes('electron-vite build'))
|
|
2019
|
+
return 'build';
|
|
2020
|
+
return null;
|
|
2021
|
+
}
|
|
2022
|
+
function checkSortScore(check) {
|
|
2023
|
+
if (check.kind === 'typecheck')
|
|
2024
|
+
return 1;
|
|
2025
|
+
if (check.kind === 'test')
|
|
2026
|
+
return 2;
|
|
2027
|
+
if (check.kind === 'lint')
|
|
2028
|
+
return 3;
|
|
2029
|
+
if (check.kind === 'build')
|
|
2030
|
+
return 4;
|
|
2031
|
+
return 5;
|
|
2032
|
+
}
|
|
2033
|
+
function collectDiagnosticIssues(rootDirectory, recentRuns, backgroundRuns) {
|
|
2034
|
+
const issues = [];
|
|
2035
|
+
const seen = new Set();
|
|
2036
|
+
for (const run of recentRuns) {
|
|
2037
|
+
const output = [run.error, run.stderr, run.stdout].filter(Boolean).join('\n');
|
|
2038
|
+
for (const issue of parseDiagnosticOutput(rootDirectory, output, run.check.name, run.id)) {
|
|
2039
|
+
const key = diagnosticIssueKey(issue);
|
|
2040
|
+
if (seen.has(key))
|
|
2041
|
+
continue;
|
|
2042
|
+
seen.add(key);
|
|
2043
|
+
issues.push(issue);
|
|
2044
|
+
if (issues.length >= 100)
|
|
2045
|
+
return issues;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
for (const run of backgroundRuns) {
|
|
2049
|
+
const output = [run.error, run.stderr, run.stdout].filter(Boolean).join('\n');
|
|
2050
|
+
for (const issue of parseDiagnosticOutput(rootDirectory, output, run.check.name, run.id)) {
|
|
2051
|
+
const key = diagnosticIssueKey(issue);
|
|
2052
|
+
if (seen.has(key))
|
|
2053
|
+
continue;
|
|
2054
|
+
seen.add(key);
|
|
2055
|
+
issues.push(issue);
|
|
2056
|
+
if (issues.length >= 100)
|
|
2057
|
+
return issues;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
return issues;
|
|
2061
|
+
}
|
|
2062
|
+
function parseDiagnosticOutput(rootDirectory, output, source, runId) {
|
|
2063
|
+
const issues = [];
|
|
2064
|
+
const lines = output.split(/\r?\n/);
|
|
2065
|
+
for (const rawLine of lines) {
|
|
2066
|
+
const line = rawLine.trim();
|
|
2067
|
+
if (!line)
|
|
2068
|
+
continue;
|
|
2069
|
+
const parsed = parseDiagnosticLine(rootDirectory, line, source, runId);
|
|
2070
|
+
if (parsed)
|
|
2071
|
+
issues.push(parsed);
|
|
2072
|
+
if (issues.length >= 80)
|
|
2073
|
+
break;
|
|
2074
|
+
}
|
|
2075
|
+
return issues;
|
|
2076
|
+
}
|
|
2077
|
+
function parseDiagnosticLine(rootDirectory, line, source, runId) {
|
|
2078
|
+
const filePattern = String.raw `(?<file>(?:[A-Za-z]:)?[^:\r\n()]+?\.(?:ts|tsx|js|jsx|mjs|cjs|css|scss|sass|less|json|md|mdx|py|rs|go|c|cc|cpp|h|hpp|cs|java|kt|html|htm))`;
|
|
2079
|
+
const tsMatch = line.match(new RegExp(`^${filePattern}\\((?<line>\\d+),(?<column>\\d+)\\):\\s*(?<severity>error|warning|info)?\\s*(?<code>[A-Za-z]+\\d+)?\\s*:?\\s*(?<message>.+)$`, 'i'));
|
|
2080
|
+
const colonMatch = line.match(new RegExp(`^${filePattern}:(?<line>\\d+):(?<column>\\d+)\\s*(?:[-:]\\s*)?(?<severity>error|warning|warn|info)?\\s*(?<code>[A-Za-z]+\\d+)?\\s*:?\\s*(?<message>.+)$`, 'i'));
|
|
2081
|
+
const failMatch = line.match(new RegExp(`^(?:FAIL|ERROR|Error:)\\s+${filePattern}(?:\\s|$)(?<message>.*)$`, 'i'));
|
|
2082
|
+
const groups = tsMatch?.groups || colonMatch?.groups || failMatch?.groups;
|
|
2083
|
+
if (!groups?.file)
|
|
2084
|
+
return null;
|
|
2085
|
+
const pathInfo = resolveDiagnosticPath(rootDirectory, groups.file);
|
|
2086
|
+
const severityText = (groups.severity || '').toLowerCase();
|
|
2087
|
+
const severity = severityText === 'warning' || severityText === 'warn'
|
|
2088
|
+
? 'warning'
|
|
2089
|
+
: severityText === 'info'
|
|
2090
|
+
? 'info'
|
|
2091
|
+
: 'error';
|
|
2092
|
+
const lineNumber = Number(groups.line);
|
|
2093
|
+
const columnNumber = Number(groups.column);
|
|
2094
|
+
const message = String(groups.message || line).trim();
|
|
2095
|
+
return {
|
|
2096
|
+
id: `diag-${runId}-${hashDiagnosticLine(line)}`,
|
|
2097
|
+
rootDirectory,
|
|
2098
|
+
...pathInfo,
|
|
2099
|
+
line: Number.isFinite(lineNumber) && lineNumber > 0 ? lineNumber : undefined,
|
|
2100
|
+
column: Number.isFinite(columnNumber) && columnNumber > 0 ? columnNumber : undefined,
|
|
2101
|
+
severity,
|
|
2102
|
+
source,
|
|
2103
|
+
code: groups.code,
|
|
2104
|
+
message,
|
|
2105
|
+
runId,
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
function resolveDiagnosticPath(rootDirectory, rawPath) {
|
|
2109
|
+
const cleanPath = rawPath.trim().replace(/^"|"$/g, '').replace(/\\/g, '/');
|
|
2110
|
+
const absolutePath = isAbsolute(cleanPath) ? cleanPath : join(rootDirectory, cleanPath);
|
|
2111
|
+
if (isPathInside(absolutePath, rootDirectory)) {
|
|
2112
|
+
return {
|
|
2113
|
+
filePath: absolutePath,
|
|
2114
|
+
relativePath: relative(rootDirectory, absolutePath).replace(/\\/g, '/'),
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
return { relativePath: cleanPath };
|
|
2118
|
+
}
|
|
2119
|
+
function diagnosticIssueKey(issue) {
|
|
2120
|
+
return [
|
|
2121
|
+
issue.relativePath || issue.filePath || '',
|
|
2122
|
+
issue.line || 0,
|
|
2123
|
+
issue.column || 0,
|
|
2124
|
+
issue.code || '',
|
|
2125
|
+
issue.message,
|
|
2126
|
+
].join(':');
|
|
2127
|
+
}
|
|
2128
|
+
function hashDiagnosticLine(line) {
|
|
2129
|
+
let hash = 0;
|
|
2130
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
2131
|
+
hash = ((hash << 5) - hash + line.charCodeAt(i)) | 0;
|
|
2132
|
+
}
|
|
2133
|
+
return Math.abs(hash).toString(36);
|
|
2134
|
+
}
|
|
2135
|
+
function parseBranch(status) {
|
|
2136
|
+
const line = status.split(/\r?\n/).find((entry) => entry.startsWith('## '));
|
|
2137
|
+
if (!line)
|
|
2138
|
+
return {};
|
|
2139
|
+
const content = line.slice(3).trim();
|
|
2140
|
+
const match = content.match(/^([^.\s]+)(?:\.\.\.([^\s\[]+))?(?:\s+\[(.+)\])?/);
|
|
2141
|
+
if (!match)
|
|
2142
|
+
return { branch: content };
|
|
2143
|
+
const flags = match[3] || '';
|
|
2144
|
+
const aheadMatch = flags.match(/ahead\s+(\d+)/);
|
|
2145
|
+
const behindMatch = flags.match(/behind\s+(\d+)/);
|
|
2146
|
+
return {
|
|
2147
|
+
branch: match[1],
|
|
2148
|
+
upstream: match[2],
|
|
2149
|
+
ahead: aheadMatch ? Number(aheadMatch[1]) : 0,
|
|
2150
|
+
behind: behindMatch ? Number(behindMatch[1]) : 0,
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
function parseGitStatusFiles(rootDirectory, status) {
|
|
2154
|
+
const lines = status.split(/\r?\n/).filter((line) => line && !line.startsWith('## '));
|
|
2155
|
+
return lines.map((line) => {
|
|
2156
|
+
const indexStatus = line[0] || ' ';
|
|
2157
|
+
const workTreeStatus = line[1] || ' ';
|
|
2158
|
+
const statusCode = `${indexStatus}${workTreeStatus}`.trim() || 'M';
|
|
2159
|
+
let body = line.slice(3).trim();
|
|
2160
|
+
let renamedFrom;
|
|
2161
|
+
if (body.includes(' -> ')) {
|
|
2162
|
+
const [from, to] = body.split(' -> ');
|
|
2163
|
+
if (from !== undefined && to !== undefined) {
|
|
2164
|
+
renamedFrom = cleanGitPath(from);
|
|
2165
|
+
body = to;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
const relativePath = cleanGitPath(body);
|
|
2169
|
+
return {
|
|
2170
|
+
path: join(rootDirectory, relativePath),
|
|
2171
|
+
relativePath,
|
|
2172
|
+
status: statusCode,
|
|
2173
|
+
indexStatus: indexStatus.trim() || ' ',
|
|
2174
|
+
workTreeStatus: workTreeStatus.trim() || ' ',
|
|
2175
|
+
renamedFrom,
|
|
2176
|
+
};
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
function cleanGitPath(value) {
|
|
2180
|
+
return value.trim().replace(/^"|"$/g, '').replace(/\\/g, '/');
|
|
2181
|
+
}
|
|
2182
|
+
function parseNumstat(numstat) {
|
|
2183
|
+
const map = new Map();
|
|
2184
|
+
for (const line of numstat.split(/\r?\n/)) {
|
|
2185
|
+
const parts = line.split(/\t/);
|
|
2186
|
+
if (parts.length < 3)
|
|
2187
|
+
continue;
|
|
2188
|
+
const additions = Number(parts[0]);
|
|
2189
|
+
const deletions = Number(parts[1]);
|
|
2190
|
+
const relativePath = cleanGitPath(parts.slice(2).join('\t'));
|
|
2191
|
+
if (!relativePath)
|
|
2192
|
+
continue;
|
|
2193
|
+
map.set(relativePath, {
|
|
2194
|
+
additions: Number.isFinite(additions) ? additions : 0,
|
|
2195
|
+
deletions: Number.isFinite(deletions) ? deletions : 0,
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
return map;
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* The first adopter of `CommandExecutionPort` โ ADE ยง5.3.
|
|
2202
|
+
*
|
|
2203
|
+
* ๐ด A thin adapter now, deliberately. The spawning moved to
|
|
2204
|
+
* `LocalCommandExecutor` unchanged; what remains here is the mapping from the
|
|
2205
|
+
* port's result to `ProcessResult`, which differs by exactly one field:
|
|
2206
|
+
* `success`. Seven call sites read `result.success`, and rewriting them was not
|
|
2207
|
+
* the point of this change โ proving the port's shape against real code was.
|
|
2208
|
+
*
|
|
2209
|
+
* `success` now comes from `commandSucceeded` rather than being recomputed
|
|
2210
|
+
* here. The old expression was `!timedOut && code === 0`, which is equivalent
|
|
2211
|
+
* for every case this site can produce, and the shared one additionally refuses
|
|
2212
|
+
* success when `error` is set โ the case the spawn-failure branch used to
|
|
2213
|
+
* handle by writing `success: false` by hand. One spelling, and the site that
|
|
2214
|
+
* had it right stops being the one that has its own copy.
|
|
2215
|
+
*
|
|
2216
|
+
* This is why engineering went first: its `ProcessResult` WAS the model for
|
|
2217
|
+
* `CommandExecutionResult`, so nothing had to bend to fit. An adoption that has
|
|
2218
|
+
* to contort the caller is evidence the port is wrong.
|
|
2219
|
+
*/
|
|
2220
|
+
const commandExecutor = new LocalCommandExecutor();
|
|
2221
|
+
function runProcess(command, args, cwd, timeoutMs, outputLimit) {
|
|
2222
|
+
return commandExecutor
|
|
2223
|
+
.execute({
|
|
2224
|
+
// The port takes argv as a LIST, and this site already had it split โ
|
|
2225
|
+
// which is the shape the sandbox implementation needs and the reason the
|
|
2226
|
+
// rule cost nothing here.
|
|
2227
|
+
argv: [command, ...args],
|
|
2228
|
+
cwd,
|
|
2229
|
+
timeoutMs,
|
|
2230
|
+
maxOutputBytes: outputLimit,
|
|
2231
|
+
})
|
|
2232
|
+
.then((result) => ({
|
|
2233
|
+
stdout: result.stdout,
|
|
2234
|
+
stderr: result.stderr,
|
|
2235
|
+
exitCode: result.exitCode,
|
|
2236
|
+
success: commandSucceeded(result),
|
|
2237
|
+
timedOut: result.timedOut,
|
|
2238
|
+
truncated: result.truncated,
|
|
2239
|
+
...(result.error === undefined ? {} : { error: result.error }),
|
|
2240
|
+
}));
|
|
2241
|
+
}
|
|
2242
|
+
function killProcessTree(child) {
|
|
2243
|
+
if (process.platform === 'win32' && child.pid) {
|
|
2244
|
+
const killer = spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], {
|
|
2245
|
+
windowsHide: true,
|
|
2246
|
+
shell: false,
|
|
2247
|
+
stdio: 'ignore',
|
|
2248
|
+
});
|
|
2249
|
+
killer.on('error', () => {
|
|
2250
|
+
try {
|
|
2251
|
+
child.kill();
|
|
2252
|
+
}
|
|
2253
|
+
catch { /* process already exited */ }
|
|
2254
|
+
});
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
try {
|
|
2258
|
+
child.kill();
|
|
2259
|
+
}
|
|
2260
|
+
catch { /* process already exited */ }
|
|
2261
|
+
}
|
|
2262
|
+
let singleton = null;
|
|
2263
|
+
export function getEngineeringWorkbenchService() {
|
|
2264
|
+
if (!singleton)
|
|
2265
|
+
singleton = new EngineeringWorkbenchService();
|
|
2266
|
+
return singleton;
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Refuses a ref name git would accept but a caller rarely means.
|
|
2270
|
+
*
|
|
2271
|
+
* These names reach a command line. `--upload-pack=...` is a git OPTION, not a
|
|
2272
|
+
* branch, and a name beginning with a dash is read as one wherever the argument
|
|
2273
|
+
* is not guarded by `--`. Rejecting the shape is simpler and stronger than
|
|
2274
|
+
* remembering to place `--` correctly at every call site, and the failure is
|
|
2275
|
+
* legible rather than mysterious.
|
|
2276
|
+
*/
|
|
2277
|
+
function validateRefName(name) {
|
|
2278
|
+
if (!name)
|
|
2279
|
+
return 'A branch name is required.';
|
|
2280
|
+
if (name.length > 255)
|
|
2281
|
+
return 'Branch names are limited to 255 characters.';
|
|
2282
|
+
if (name.startsWith('-'))
|
|
2283
|
+
return 'A branch name may not begin with a dash; git would read it as an option.';
|
|
2284
|
+
// git check-ref-format's own rules, the ones that matter here.
|
|
2285
|
+
if (/[\s~^:?*\[\\]/.test(name))
|
|
2286
|
+
return 'Branch names may not contain whitespace or any of ~ ^ : ? * [ \\';
|
|
2287
|
+
if (name.includes('..') || name.endsWith('.lock') || name.endsWith('/') || name.startsWith('/')) {
|
|
2288
|
+
return 'That is not a valid git ref name.';
|
|
2289
|
+
}
|
|
2290
|
+
return undefined;
|
|
2291
|
+
}
|
|
2292
|
+
function validateRemoteName(name) {
|
|
2293
|
+
if (!name)
|
|
2294
|
+
return 'A remote name is required.';
|
|
2295
|
+
if (name.startsWith('-'))
|
|
2296
|
+
return 'A remote name may not begin with a dash; git would read it as an option.';
|
|
2297
|
+
if (!/^[A-Za-z0-9._\/-]+$/.test(name))
|
|
2298
|
+
return 'That is not a valid git remote name.';
|
|
2299
|
+
return undefined;
|
|
2300
|
+
}
|
|
2301
|
+
//# sourceMappingURL=EngineeringWorkbenchService.js.map
|