@neuralnomads/codenomad-dev 0.20.0-dev-20260905-52f0e629 → 0.20.0-dev-20260908-e5115fe8

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.
Files changed (109) hide show
  1. package/dist/api-types.js +1 -0
  2. package/dist/api-types.js.map +1 -1
  3. package/dist/opencode/automation-plugin.test.js +14 -1
  4. package/dist/opencode/automation-plugin.test.js.map +1 -1
  5. package/dist/server/routes/binary-validation.test.js +41 -0
  6. package/dist/server/routes/binary-validation.test.js.map +1 -0
  7. package/dist/server/routes/settings.js +2 -3
  8. package/dist/server/routes/settings.js.map +1 -1
  9. package/dist/workspaces/__tests__/binary-probe-fixture.js +46 -0
  10. package/dist/workspaces/__tests__/binary-probe-fixture.js.map +1 -0
  11. package/dist/workspaces/__tests__/binary-probe.test.js +81 -0
  12. package/dist/workspaces/__tests__/binary-probe.test.js.map +1 -0
  13. package/dist/workspaces/__tests__/spawn.test.js +39 -1
  14. package/dist/workspaces/__tests__/spawn.test.js.map +1 -1
  15. package/dist/workspaces/host-opencode-service.test.js +25 -0
  16. package/dist/workspaces/host-opencode-service.test.js.map +1 -1
  17. package/dist/workspaces/opencode-cli-compatibility.js +32 -0
  18. package/dist/workspaces/opencode-cli-compatibility.js.map +1 -0
  19. package/dist/workspaces/opencode-cli-compatibility.test.js +35 -0
  20. package/dist/workspaces/opencode-cli-compatibility.test.js.map +1 -0
  21. package/dist/workspaces/opencode-cli-service.js +14 -2
  22. package/dist/workspaces/opencode-cli-service.js.map +1 -1
  23. package/dist/workspaces/spawn.js +94 -31
  24. package/dist/workspaces/spawn.js.map +1 -1
  25. package/dist/workspaces/wsl-opencode-service.test.js +18 -0
  26. package/dist/workspaces/wsl-opencode-service.test.js.map +1 -1
  27. package/package.json +1 -1
  28. package/public/assets/{App-CqjB0XCD.js → App-DPj4p3An.js} +16 -16
  29. package/public/assets/App-DPj4p3An.js.map +1 -0
  30. package/public/assets/{FilesTab-5DFefbXn.js → FilesTab-C8fnWsM4.js} +2 -2
  31. package/public/assets/{FilesTab-5DFefbXn.js.map → FilesTab-C8fnWsM4.js.map} +1 -1
  32. package/public/assets/{GitChangesTab-DpT-MHU0.js → GitChangesTab-C3rapWwl.js} +2 -2
  33. package/public/assets/{GitChangesTab-DpT-MHU0.js.map → GitChangesTab-C3rapWwl.js.map} +1 -1
  34. package/public/assets/{SplitFilePanel-BbE1LzCe.js → SplitFilePanel-D3S7Zfcc.js} +2 -2
  35. package/public/assets/{SplitFilePanel-BbE1LzCe.js.map → SplitFilePanel-D3S7Zfcc.js.map} +1 -1
  36. package/public/assets/{StatusTab-BFoX-V9X.js → StatusTab-DHvQ-0to.js} +2 -2
  37. package/public/assets/{StatusTab-BFoX-V9X.js.map → StatusTab-DHvQ-0to.js.map} +1 -1
  38. package/public/assets/{align-justify-qrXM4H3l.js → align-justify-BIN0BnZw.js} +2 -2
  39. package/public/assets/{align-justify-qrXM4H3l.js.map → align-justify-BIN0BnZw.js.map} +1 -1
  40. package/public/assets/{bundle-full-unk5xR1Y.js → bundle-full-BfEvraVO.js} +2 -2
  41. package/public/assets/{bundle-full-unk5xR1Y.js.map → bundle-full-BfEvraVO.js.map} +1 -1
  42. package/public/assets/{client-state-CHCvxsCU.js → client-state-CBKOWfhu.js} +2 -2
  43. package/public/assets/{client-state-CHCvxsCU.js.map → client-state-CBKOWfhu.js.map} +1 -1
  44. package/public/assets/{diff-viewer-RKOl9AXZ.js → diff-viewer-6oxyPi3-.js} +2 -2
  45. package/public/assets/{diff-viewer-RKOl9AXZ.js.map → diff-viewer-6oxyPi3-.js.map} +1 -1
  46. package/public/assets/index-BJrZpSe6.js +2 -0
  47. package/public/assets/index-BJrZpSe6.js.map +1 -0
  48. package/public/assets/index-B_9dhJTl.js +2 -0
  49. package/public/assets/index-B_9dhJTl.js.map +1 -0
  50. package/public/assets/{index-DtTSGKVJ.js → index-BeWVYvfJ.js} +2 -2
  51. package/public/assets/index-BeWVYvfJ.js.map +1 -0
  52. package/public/assets/{index-Cld6pmhN.js → index-BhBg6Tnf.js} +2 -2
  53. package/public/assets/{index-Cld6pmhN.js.map → index-BhBg6Tnf.js.map} +1 -1
  54. package/public/assets/{index-CEAIa6p2.js → index-C9znv1NB.js} +2 -2
  55. package/public/assets/{index-CEAIa6p2.js.map → index-C9znv1NB.js.map} +1 -1
  56. package/public/assets/index-CD9b6FoA.js +2 -0
  57. package/public/assets/{index-CvrBisww.js.map → index-CD9b6FoA.js.map} +1 -1
  58. package/public/assets/index-CmJg6xjr.js +2 -0
  59. package/public/assets/index-CmJg6xjr.js.map +1 -0
  60. package/public/assets/index-DHNlQHw2.js +2 -0
  61. package/public/assets/index-DHNlQHw2.js.map +1 -0
  62. package/public/assets/index-DdklToXY.js +2 -0
  63. package/public/assets/index-DdklToXY.js.map +1 -0
  64. package/public/assets/index-Dr5L4afe.js +2 -0
  65. package/public/assets/index-Dr5L4afe.js.map +1 -0
  66. package/public/assets/index-DsmOPul3.js +2 -0
  67. package/public/assets/index-DsmOPul3.js.map +1 -0
  68. package/public/assets/index-ef9_CbWU.js +3 -0
  69. package/public/assets/index-ef9_CbWU.js.map +1 -0
  70. package/public/assets/{loading-D3uPMfMu.js → loading-CanSHf7z.js} +2 -2
  71. package/public/assets/{loading-D3uPMfMu.js.map → loading-CanSHf7z.js.map} +1 -1
  72. package/public/assets/{main-BwbZb3g5.js → main-COtTRZe6.js} +25 -25
  73. package/public/assets/main-COtTRZe6.js.map +1 -0
  74. package/public/assets/{markdown-BJLcfdun.js → markdown-B04qkFuc.js} +4 -4
  75. package/public/assets/{markdown-BJLcfdun.js.map → markdown-B04qkFuc.js.map} +1 -1
  76. package/public/assets/{startup-state-settings-card-B4gPI-c4.js → startup-state-settings-card-DE11-Je6.js} +2 -2
  77. package/public/assets/{startup-state-settings-card-B4gPI-c4.js.map → startup-state-settings-card-DE11-Je6.js.map} +1 -1
  78. package/public/assets/{terminal-square-B5KdW_c-.js → terminal-square-W8UL0Uzs.js} +2 -2
  79. package/public/assets/{terminal-square-B5KdW_c-.js.map → terminal-square-W8UL0Uzs.js.map} +1 -1
  80. package/public/assets/{tool-call-B9DCPGVo.js → tool-call-irc_6gLg.js} +4 -4
  81. package/public/assets/{tool-call-B9DCPGVo.js.map → tool-call-irc_6gLg.js.map} +1 -1
  82. package/public/assets/{unified-picker-BvxdLAJQ.js → unified-picker-CkVc32ly.js} +2 -2
  83. package/public/assets/{unified-picker-BvxdLAJQ.js.map → unified-picker-CkVc32ly.js.map} +1 -1
  84. package/public/assets/{wrap-text-NfrmWiEu.js → wrap-text-CR-cuiJP.js} +2 -2
  85. package/public/assets/{wrap-text-NfrmWiEu.js.map → wrap-text-CR-cuiJP.js.map} +1 -1
  86. package/public/index.html +2 -2
  87. package/public/loading.html +2 -2
  88. package/public/sw.js +1 -1
  89. package/public/sw.js.map +1 -1
  90. package/public/assets/App-CqjB0XCD.js.map +0 -1
  91. package/public/assets/index-BOrPHA7t.js +0 -2
  92. package/public/assets/index-BOrPHA7t.js.map +0 -1
  93. package/public/assets/index-BaPs8N0o.js +0 -2
  94. package/public/assets/index-BaPs8N0o.js.map +0 -1
  95. package/public/assets/index-BrjHffsb.js +0 -2
  96. package/public/assets/index-BrjHffsb.js.map +0 -1
  97. package/public/assets/index-C-CjhNjl.js +0 -2
  98. package/public/assets/index-C-CjhNjl.js.map +0 -1
  99. package/public/assets/index-CvrBisww.js +0 -2
  100. package/public/assets/index-DDpJlsfl.js +0 -3
  101. package/public/assets/index-DDpJlsfl.js.map +0 -1
  102. package/public/assets/index-DViQvmpk.js +0 -2
  103. package/public/assets/index-DViQvmpk.js.map +0 -1
  104. package/public/assets/index-DtTSGKVJ.js.map +0 -1
  105. package/public/assets/index-DvRzkWiJ.js +0 -2
  106. package/public/assets/index-DvRzkWiJ.js.map +0 -1
  107. package/public/assets/index-HuQOubUd.js +0 -2
  108. package/public/assets/index-HuQOubUd.js.map +0 -1
  109. package/public/assets/main-BwbZb3g5.js.map +0 -1
package/dist/api-types.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export const WINDOWS_DRIVES_ROOT = "__drives__";
2
+ export const OPENCODE_V2_REQUIRED_ERROR_CODE = "opencode_v2_required";
2
3
  //# sourceMappingURL=api-types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-types.js","sourceRoot":"","sources":["../src/api-types.ts"],"names":[],"mappings":"AAqQA,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAAA","sourcesContent":["import type {\n AgentModelSelection,\n AgentModelSelections,\n ModelPreference,\n OpenCodeBinary,\n Preferences,\n RecentFolder,\n} from \"./config/schema\"\nimport type { OpenCodeEvent } from \"@opencode-ai/client\"\n\n/**\n * Canonical HTTP/SSE contract for the CLI server.\n * These types are consumed by both the CLI implementation and any UI clients.\n */\n\nexport type WorkspaceStatus = \"starting\" | \"ready\" | \"stopped\" | \"error\"\n\nexport interface WorkspaceDescriptor {\n id: string\n /** Correlates creation events with the client request that initiated them. */\n requestId?: string\n /** Absolute path on the server host. */\n path: string\n name?: string\n status: WorkspaceStatus\n /** PID/port are populated when the workspace is running. */\n pid?: number\n port?: number\n /** Canonical proxy path the CLI exposes for this instance. */\n proxyPath: string\n /** Identifier of the binary resolved from config. */\n binaryId: string\n binaryLabel: string\n binaryVersion?: string\n createdAt: string\n updatedAt: string\n /** Present when `status` is \"error\". */\n error?: string\n}\n\nexport interface WorkspaceCreateRequest {\n path: string\n name?: string\n requestId?: string\n}\n\nexport interface WorkspaceCloneRequest {\n repositoryUrl: string\n destinationPath: string\n cleanup?: boolean\n}\n\nexport interface WorkspaceCloneResponse {\n path: string\n}\n\nexport type WorkspaceCreateResponse = WorkspaceDescriptor & {\n /** True when this request did not own creation of the returned workspace. */\n reused?: true\n}\nexport type WorkspaceListResponse = WorkspaceDescriptor[]\nexport type WorkspaceDetailResponse = WorkspaceDescriptor\n\nexport interface WorkspaceDeleteResponse {\n id: string\n status: WorkspaceStatus\n}\n\nexport interface ProviderUsageWindow {\n usedPercent: number | null\n remainingPercent: number | null\n windowSeconds: number | null\n resetAt: number | null\n valueLabel?: string\n}\n\nexport interface ProviderUsageResponse {\n requestedProviderId: string\n providerId: string | null\n providerName: string\n modelId?: string\n supported: boolean\n configured: boolean\n ok: boolean\n windows: Record<string, ProviderUsageWindow>\n fetchedAt: number\n}\n\nexport type WorktreeKind = \"root\" | \"worktree\"\n\nexport interface WorktreeDescriptor {\n /** Stable identifier used by CodeNomad + clients (\"root\" for the selected workspace folder). */\n slug: string\n /** Absolute directory path on the server host. */\n directory: string\n kind: WorktreeKind\n /** Optional VCS branch name when available. */\n branch?: string\n}\n\nexport interface WorktreeListResponse {\n worktrees: WorktreeDescriptor[]\n /** True when the workspace folder resolves to a Git repository. */\n isGitRepo?: boolean\n}\n\nexport interface WorktreeCreateRequest {\n slug: string\n /** Optional branch name (defaults to slug). */\n branch?: string\n}\n\nexport type GitChangeKind = \"added\" | \"modified\" | \"deleted\" | \"renamed\" | \"copied\" | \"untracked\" | \"unmerged\"\n\nexport interface WorktreeGitStatusEntry {\n path: string\n originalPath?: string | null\n stagedStatus: GitChangeKind | null\n stagedAdditions: number\n stagedDeletions: number\n unstagedStatus: GitChangeKind | null\n unstagedAdditions: number\n unstagedDeletions: number\n}\n\nexport type WorktreeGitStatusResponse = WorktreeGitStatusEntry[]\n\nexport type WorktreeGitDiffScope = \"staged\" | \"unstaged\"\n\nexport interface WorktreeGitPathsRequest {\n paths: string[]\n}\n\nexport interface WorktreeGitMutationResponse {\n ok: true\n}\n\nexport interface WorktreeGitCommitRequest {\n message: string\n}\n\nexport interface WorktreeGitCommitResponse {\n ok: true\n commitSha?: string\n}\n\nexport interface WorktreeGitDiffResponse {\n path: string\n originalPath?: string | null\n scope: WorktreeGitDiffScope\n before: string\n after: string\n isBinary?: boolean\n}\n\nexport interface WorktreeGitDiffRequest {\n path: string\n originalPath?: string | null\n scope: WorktreeGitDiffScope\n}\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\"\n\nexport interface WorkspaceLogEntry {\n workspaceId: string\n timestamp: string\n level: LogLevel\n message: string\n}\n\nexport interface FileSystemEntry {\n name: string\n /**\n * Path identifier for the entry. Relative to the server root in restricted\n * single-root listings (\".\" represents the root itself); absolute in\n * unrestricted, drives, and multi-root top-level listings.\n */\n path: string\n /** Absolute path when available (unrestricted and multi-root listings). */\n absolutePath?: string\n type: \"file\" | \"directory\"\n size?: number\n /** ISO timestamp of last modification when available. */\n modifiedAt?: string\n}\n\nexport type FileSystemScope = \"restricted\" | \"unrestricted\"\nexport type FileSystemPathKind = \"relative\" | \"absolute\" | \"drives\"\n\nexport interface FileSystemListingMetadata {\n scope: FileSystemScope\n /**\n * Canonical identifier of the current view:\n * - \".\" for restricted single-root listings\n * - WINDOWS_DRIVES_ROOT for the Windows drives pseudo-root\n * - absolute path otherwise\n */\n currentPath: string\n /** Optional parent path if navigation upward is allowed. */\n parentPath?: string\n /** Absolute path representing the root or origin point for this listing. */\n rootPath: string\n /** Absolute home directory of the CLI host (useful defaults for unrestricted mode). */\n homePath: string\n /** Human-friendly label for the current path. */\n displayPath: string\n /** Indicates whether entry paths are relative, absolute, or represent the drive pseudo-view. */\n pathKind: FileSystemPathKind\n}\n\nexport interface FileSystemListResponse {\n entries: FileSystemEntry[]\n metadata: FileSystemListingMetadata\n}\n\nexport interface FileSystemCreateFolderRequest {\n /**\n * Path identifier for the currently browsed directory.\n * Matches the `path` parameter used for `/api/filesystem`.\n */\n parentPath?: string\n /** Single folder name (no separators). */\n name: string\n}\n\nexport interface FileSystemCreateFolderResponse {\n /**\n * Path identifier that can be passed back to `/api/filesystem` to browse the new folder.\n * Relative for restricted listings and absolute for unrestricted listings.\n */\n path: string\n /** Absolute folder path on the server host. */\n absolutePath: string\n}\n\nexport interface FileSystemFileContentResponse {\n path: string\n contents: string\n encoding: \"utf-8\" | \"base64\"\n}\n\nexport interface ConfigFileDescriptor {\n id: string\n label: string\n path: string\n language: string\n}\n\nexport type ConfigFileListResponse = ConfigFileDescriptor[]\n\nexport interface ConfigFileContentResponse {\n id: string\n path: string\n contents: string\n exists: boolean\n}\n\nexport interface ConfigFileContentRequest {\n contents: string\n}\n\nexport const WINDOWS_DRIVES_ROOT = \"__drives__\"\n\nexport interface WorkspaceFileResponse {\n workspaceId: string\n relativePath: string\n /** UTF-8 file contents; binary files should be base64 encoded by the caller. */\n contents: string\n encoding?: \"utf-8\" | \"base64\"\n}\n\nexport type WorkspaceFileSearchResponse = FileSystemEntry[]\n\nexport interface InstanceData {\n messageHistory: string[]\n agentModelSelections: AgentModelSelection\n}\n\nexport type InstanceStreamStatus = \"connecting\" | \"connected\" | \"error\" | \"disconnected\"\n\nexport type InstanceStreamEvent = OpenCodeEvent\n\nexport type SideCarKind = \"port\"\n\nexport type SideCarPrefixMode = \"strip\" | \"preserve\"\n\nexport type SideCarStatus = \"running\" | \"stopped\"\n\nexport interface SideCar {\n id: string\n kind: SideCarKind\n name: string\n port: number\n insecure: boolean\n prefixMode: SideCarPrefixMode\n status: SideCarStatus\n createdAt: string\n updatedAt: string\n}\n\nexport interface PreviewSession {\n token: string\n sessionId: string\n targetUrl: string\n proxyUrl: string\n createdAt: string\n}\n\nexport interface BinaryRecord {\n id: string\n path: string\n label: string\n version?: string\n\n /** Indicates that this binary will be picked when workspaces omit an explicit choice. */\n isDefault: boolean\n lastValidatedAt?: string\n validationError?: string\n}\n\nexport type SettingsOwner = string\nexport type SettingsBucket = Record<string, unknown>\nexport type SettingsDoc = Record<string, unknown>\n\nexport interface BinaryListResponse {\n binaries: BinaryRecord[]\n}\n\nexport interface BinaryCreateRequest {\n path: string\n label?: string\n makeDefault?: boolean\n}\n\nexport interface BinaryUpdateRequest {\n label?: string\n makeDefault?: boolean\n}\n\nexport interface BinaryValidationResult {\n valid: boolean\n version?: string\n error?: string\n}\n\nexport interface OpenCodeUpdateStatus {\n currentVersion: string\n latestVersion: string | null\n updateAvailable: boolean | null\n canUpgrade: boolean\n checkError?: \"update_check_failed\"\n}\n\nexport interface OpenCodeUpdateResponse {\n success: boolean\n version: string\n}\n\nexport interface SpeechSegment {\n startMs: number\n endMs: number\n text: string\n}\n\nexport interface SpeechCapabilitiesResponse {\n available: boolean\n configured: boolean\n provider: string\n supportsStt: boolean\n supportsTts: boolean\n supportsStreamingTts: boolean\n baseUrl?: string\n sttModel: string\n ttsModel: string\n ttsVoice: string\n ttsFormats: string[]\n streamingTtsFormats: string[]\n separateProviders?: boolean\n sttConfigured?: boolean\n ttsConfigured?: boolean\n sttBaseUrl?: string\n ttsBaseUrl?: string\n}\n\nexport interface SpeechTranscriptionResponse {\n text: string\n language?: string\n durationMs?: number\n segments?: SpeechSegment[]\n}\n\nexport interface SpeechSynthesisResponse {\n audioBase64: string\n mimeType: string\n}\n\nexport interface YoloStateResponse {\n enabled: boolean\n}\n\nexport interface RemoteServerProfile {\n id: string\n name: string\n baseUrl: string\n skipTlsVerify: boolean\n createdAt: string\n updatedAt: string\n lastConnectedAt?: string\n}\n\nexport interface RemoteServerProbeRequest {\n baseUrl: string\n skipTlsVerify?: boolean\n}\n\nexport interface RemoteServerProbeResponse {\n ok: boolean\n reachable: boolean\n normalizedUrl: string\n skipTlsVerify: boolean\n requiresAuth: boolean\n authenticated: boolean\n error?: string\n errorCode?: string\n}\n\nexport interface RemoteProxySessionCreateRequest {\n baseUrl: string\n skipTlsVerify?: boolean\n}\n\nexport interface RemoteProxySessionCreateResponse {\n sessionId: string\n windowUrl: string\n}\n\nexport type WorkspaceEventType =\n | \"workspace.created\"\n | \"workspace.started\"\n | \"workspace.error\"\n | \"workspace.stopped\"\n | \"workspace.log\"\n | \"sidecar.updated\"\n | \"sidecar.removed\"\n | \"storage.configChanged\"\n | \"storage.stateChanged\"\n | \"instance.dataChanged\"\n | \"instance.event\"\n | \"instance.eventStatus\"\n | \"yolo.stateChanged\"\n | \"yolo.autoAccepted\"\n\nexport type WorkspaceEventPayload =\n | { type: \"workspace.created\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.started\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.error\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.stopped\"; workspaceId: string; reason?: \"deleted\" | \"stopped\" }\n | { type: \"workspace.log\"; entry: WorkspaceLogEntry }\n | { type: \"sidecar.updated\"; sidecar: SideCar }\n | { type: \"sidecar.removed\"; sidecarId: string }\n | { type: \"storage.configChanged\"; owner: SettingsOwner; value: SettingsBucket }\n | { type: \"storage.stateChanged\"; owner: SettingsOwner; value: SettingsBucket }\n | { type: \"instance.dataChanged\"; instanceId: string; data: InstanceData }\n | { type: \"instance.event\"; instanceId: string; event: InstanceStreamEvent }\n | { type: \"instance.eventStatus\"; instanceId: string; status: InstanceStreamStatus; generation: number; reason?: string }\n | { type: \"yolo.stateChanged\"; instanceId: string; sessionId: string; enabled: boolean }\n | { type: \"yolo.autoAccepted\"; instanceId: string; sessionId: string; permissionId: string }\n\nexport interface NetworkAddress {\n ip: string\n family: \"ipv4\" | \"ipv6\"\n scope: \"external\" | \"internal\" | \"loopback\"\n /** Remote URL using the server's remote protocol/port for this IP. */\n remoteUrl: string\n}\n\nexport interface LatestReleaseInfo {\n version: string\n tag: string\n url: string\n channel: \"stable\" | \"dev\"\n publishedAt?: string\n notes?: string\n}\n\nexport interface UiMeta {\n version?: string\n source: \"bundled\" | \"downloaded\" | \"previous\" | \"override\" | \"dev-proxy\" | \"missing\"\n}\n\nexport interface SupportMeta {\n supported: boolean\n message?: string\n minServerVersion?: string\n latestServerVersion?: string\n latestServerUrl?: string\n}\n\nexport interface ServerMeta {\n /** URL desktop apps should use to connect (prefers loopback HTTP when enabled). */\n localUrl: string\n /** URL remote clients should use (prefers HTTPS when enabled). */\n remoteUrl?: string\n /** SSE endpoint advertised to clients (`/api/events` by default). */\n eventsUrl: string\n /** Host the server is bound to (e.g., 127.0.0.1 or 0.0.0.0). */\n host: string\n /** Listening mode derived from host binding. */\n listeningMode: \"local\" | \"all\"\n /** Actual local port in use after binding. */\n localPort: number\n /** Actual remote port in use after binding (when remoteUrl is set). */\n remotePort?: number\n /** Display label for the host (e.g., hostname or friendly name). */\n hostLabel: string\n /** Absolute path of the filesystem root exposed to clients. */\n workspaceRoot: string\n /** Reachable addresses for this server, external first. */\n addresses: NetworkAddress[]\n serverVersion?: string\n ui?: UiMeta\n support?: SupportMeta\n /** Optional update info (dev channel only). */\n update?: LatestReleaseInfo | null\n}\n\nexport type {\n Preferences,\n ModelPreference,\n AgentModelSelections,\n RecentFolder,\n OpenCodeBinary,\n}\n"]}
1
+ {"version":3,"file":"api-types.js","sourceRoot":"","sources":["../src/api-types.ts"],"names":[],"mappings":"AAqQA,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAAA;AA8E/C,MAAM,CAAC,MAAM,+BAA+B,GAAG,sBAA+B,CAAA","sourcesContent":["import type {\n AgentModelSelection,\n AgentModelSelections,\n ModelPreference,\n OpenCodeBinary,\n Preferences,\n RecentFolder,\n} from \"./config/schema\"\nimport type { OpenCodeEvent } from \"@opencode-ai/client\"\n\n/**\n * Canonical HTTP/SSE contract for the CLI server.\n * These types are consumed by both the CLI implementation and any UI clients.\n */\n\nexport type WorkspaceStatus = \"starting\" | \"ready\" | \"stopped\" | \"error\"\n\nexport interface WorkspaceDescriptor {\n id: string\n /** Correlates creation events with the client request that initiated them. */\n requestId?: string\n /** Absolute path on the server host. */\n path: string\n name?: string\n status: WorkspaceStatus\n /** PID/port are populated when the workspace is running. */\n pid?: number\n port?: number\n /** Canonical proxy path the CLI exposes for this instance. */\n proxyPath: string\n /** Identifier of the binary resolved from config. */\n binaryId: string\n binaryLabel: string\n binaryVersion?: string\n createdAt: string\n updatedAt: string\n /** Present when `status` is \"error\". */\n error?: string\n}\n\nexport interface WorkspaceCreateRequest {\n path: string\n name?: string\n requestId?: string\n}\n\nexport interface WorkspaceCloneRequest {\n repositoryUrl: string\n destinationPath: string\n cleanup?: boolean\n}\n\nexport interface WorkspaceCloneResponse {\n path: string\n}\n\nexport type WorkspaceCreateResponse = WorkspaceDescriptor & {\n /** True when this request did not own creation of the returned workspace. */\n reused?: true\n}\nexport type WorkspaceListResponse = WorkspaceDescriptor[]\nexport type WorkspaceDetailResponse = WorkspaceDescriptor\n\nexport interface WorkspaceDeleteResponse {\n id: string\n status: WorkspaceStatus\n}\n\nexport interface ProviderUsageWindow {\n usedPercent: number | null\n remainingPercent: number | null\n windowSeconds: number | null\n resetAt: number | null\n valueLabel?: string\n}\n\nexport interface ProviderUsageResponse {\n requestedProviderId: string\n providerId: string | null\n providerName: string\n modelId?: string\n supported: boolean\n configured: boolean\n ok: boolean\n windows: Record<string, ProviderUsageWindow>\n fetchedAt: number\n}\n\nexport type WorktreeKind = \"root\" | \"worktree\"\n\nexport interface WorktreeDescriptor {\n /** Stable identifier used by CodeNomad + clients (\"root\" for the selected workspace folder). */\n slug: string\n /** Absolute directory path on the server host. */\n directory: string\n kind: WorktreeKind\n /** Optional VCS branch name when available. */\n branch?: string\n}\n\nexport interface WorktreeListResponse {\n worktrees: WorktreeDescriptor[]\n /** True when the workspace folder resolves to a Git repository. */\n isGitRepo?: boolean\n}\n\nexport interface WorktreeCreateRequest {\n slug: string\n /** Optional branch name (defaults to slug). */\n branch?: string\n}\n\nexport type GitChangeKind = \"added\" | \"modified\" | \"deleted\" | \"renamed\" | \"copied\" | \"untracked\" | \"unmerged\"\n\nexport interface WorktreeGitStatusEntry {\n path: string\n originalPath?: string | null\n stagedStatus: GitChangeKind | null\n stagedAdditions: number\n stagedDeletions: number\n unstagedStatus: GitChangeKind | null\n unstagedAdditions: number\n unstagedDeletions: number\n}\n\nexport type WorktreeGitStatusResponse = WorktreeGitStatusEntry[]\n\nexport type WorktreeGitDiffScope = \"staged\" | \"unstaged\"\n\nexport interface WorktreeGitPathsRequest {\n paths: string[]\n}\n\nexport interface WorktreeGitMutationResponse {\n ok: true\n}\n\nexport interface WorktreeGitCommitRequest {\n message: string\n}\n\nexport interface WorktreeGitCommitResponse {\n ok: true\n commitSha?: string\n}\n\nexport interface WorktreeGitDiffResponse {\n path: string\n originalPath?: string | null\n scope: WorktreeGitDiffScope\n before: string\n after: string\n isBinary?: boolean\n}\n\nexport interface WorktreeGitDiffRequest {\n path: string\n originalPath?: string | null\n scope: WorktreeGitDiffScope\n}\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\"\n\nexport interface WorkspaceLogEntry {\n workspaceId: string\n timestamp: string\n level: LogLevel\n message: string\n}\n\nexport interface FileSystemEntry {\n name: string\n /**\n * Path identifier for the entry. Relative to the server root in restricted\n * single-root listings (\".\" represents the root itself); absolute in\n * unrestricted, drives, and multi-root top-level listings.\n */\n path: string\n /** Absolute path when available (unrestricted and multi-root listings). */\n absolutePath?: string\n type: \"file\" | \"directory\"\n size?: number\n /** ISO timestamp of last modification when available. */\n modifiedAt?: string\n}\n\nexport type FileSystemScope = \"restricted\" | \"unrestricted\"\nexport type FileSystemPathKind = \"relative\" | \"absolute\" | \"drives\"\n\nexport interface FileSystemListingMetadata {\n scope: FileSystemScope\n /**\n * Canonical identifier of the current view:\n * - \".\" for restricted single-root listings\n * - WINDOWS_DRIVES_ROOT for the Windows drives pseudo-root\n * - absolute path otherwise\n */\n currentPath: string\n /** Optional parent path if navigation upward is allowed. */\n parentPath?: string\n /** Absolute path representing the root or origin point for this listing. */\n rootPath: string\n /** Absolute home directory of the CLI host (useful defaults for unrestricted mode). */\n homePath: string\n /** Human-friendly label for the current path. */\n displayPath: string\n /** Indicates whether entry paths are relative, absolute, or represent the drive pseudo-view. */\n pathKind: FileSystemPathKind\n}\n\nexport interface FileSystemListResponse {\n entries: FileSystemEntry[]\n metadata: FileSystemListingMetadata\n}\n\nexport interface FileSystemCreateFolderRequest {\n /**\n * Path identifier for the currently browsed directory.\n * Matches the `path` parameter used for `/api/filesystem`.\n */\n parentPath?: string\n /** Single folder name (no separators). */\n name: string\n}\n\nexport interface FileSystemCreateFolderResponse {\n /**\n * Path identifier that can be passed back to `/api/filesystem` to browse the new folder.\n * Relative for restricted listings and absolute for unrestricted listings.\n */\n path: string\n /** Absolute folder path on the server host. */\n absolutePath: string\n}\n\nexport interface FileSystemFileContentResponse {\n path: string\n contents: string\n encoding: \"utf-8\" | \"base64\"\n}\n\nexport interface ConfigFileDescriptor {\n id: string\n label: string\n path: string\n language: string\n}\n\nexport type ConfigFileListResponse = ConfigFileDescriptor[]\n\nexport interface ConfigFileContentResponse {\n id: string\n path: string\n contents: string\n exists: boolean\n}\n\nexport interface ConfigFileContentRequest {\n contents: string\n}\n\nexport const WINDOWS_DRIVES_ROOT = \"__drives__\"\n\nexport interface WorkspaceFileResponse {\n workspaceId: string\n relativePath: string\n /** UTF-8 file contents; binary files should be base64 encoded by the caller. */\n contents: string\n encoding?: \"utf-8\" | \"base64\"\n}\n\nexport type WorkspaceFileSearchResponse = FileSystemEntry[]\n\nexport interface InstanceData {\n messageHistory: string[]\n agentModelSelections: AgentModelSelection\n}\n\nexport type InstanceStreamStatus = \"connecting\" | \"connected\" | \"error\" | \"disconnected\"\n\nexport type InstanceStreamEvent = OpenCodeEvent\n\nexport type SideCarKind = \"port\"\n\nexport type SideCarPrefixMode = \"strip\" | \"preserve\"\n\nexport type SideCarStatus = \"running\" | \"stopped\"\n\nexport interface SideCar {\n id: string\n kind: SideCarKind\n name: string\n port: number\n insecure: boolean\n prefixMode: SideCarPrefixMode\n status: SideCarStatus\n createdAt: string\n updatedAt: string\n}\n\nexport interface PreviewSession {\n token: string\n sessionId: string\n targetUrl: string\n proxyUrl: string\n createdAt: string\n}\n\nexport interface BinaryRecord {\n id: string\n path: string\n label: string\n version?: string\n\n /** Indicates that this binary will be picked when workspaces omit an explicit choice. */\n isDefault: boolean\n lastValidatedAt?: string\n validationError?: string\n}\n\nexport type SettingsOwner = string\nexport type SettingsBucket = Record<string, unknown>\nexport type SettingsDoc = Record<string, unknown>\n\nexport interface BinaryListResponse {\n binaries: BinaryRecord[]\n}\n\nexport interface BinaryCreateRequest {\n path: string\n label?: string\n makeDefault?: boolean\n}\n\nexport interface BinaryUpdateRequest {\n label?: string\n makeDefault?: boolean\n}\n\nexport const OPENCODE_V2_REQUIRED_ERROR_CODE = \"opencode_v2_required\" as const\n\nexport interface BinaryValidationResult {\n valid: boolean\n version?: string\n error?: string\n errorCode?: typeof OPENCODE_V2_REQUIRED_ERROR_CODE\n}\n\nexport interface OpenCodeUpdateStatus {\n currentVersion: string\n latestVersion: string | null\n updateAvailable: boolean | null\n canUpgrade: boolean\n checkError?: \"update_check_failed\"\n}\n\nexport interface OpenCodeUpdateResponse {\n success: boolean\n version: string\n}\n\nexport interface SpeechSegment {\n startMs: number\n endMs: number\n text: string\n}\n\nexport interface SpeechCapabilitiesResponse {\n available: boolean\n configured: boolean\n provider: string\n supportsStt: boolean\n supportsTts: boolean\n supportsStreamingTts: boolean\n baseUrl?: string\n sttModel: string\n ttsModel: string\n ttsVoice: string\n ttsFormats: string[]\n streamingTtsFormats: string[]\n separateProviders?: boolean\n sttConfigured?: boolean\n ttsConfigured?: boolean\n sttBaseUrl?: string\n ttsBaseUrl?: string\n}\n\nexport interface SpeechTranscriptionResponse {\n text: string\n language?: string\n durationMs?: number\n segments?: SpeechSegment[]\n}\n\nexport interface SpeechSynthesisResponse {\n audioBase64: string\n mimeType: string\n}\n\nexport interface YoloStateResponse {\n enabled: boolean\n}\n\nexport interface RemoteServerProfile {\n id: string\n name: string\n baseUrl: string\n skipTlsVerify: boolean\n createdAt: string\n updatedAt: string\n lastConnectedAt?: string\n}\n\nexport interface RemoteServerProbeRequest {\n baseUrl: string\n skipTlsVerify?: boolean\n}\n\nexport interface RemoteServerProbeResponse {\n ok: boolean\n reachable: boolean\n normalizedUrl: string\n skipTlsVerify: boolean\n requiresAuth: boolean\n authenticated: boolean\n error?: string\n errorCode?: string\n}\n\nexport interface RemoteProxySessionCreateRequest {\n baseUrl: string\n skipTlsVerify?: boolean\n}\n\nexport interface RemoteProxySessionCreateResponse {\n sessionId: string\n windowUrl: string\n}\n\nexport type WorkspaceEventType =\n | \"workspace.created\"\n | \"workspace.started\"\n | \"workspace.error\"\n | \"workspace.stopped\"\n | \"workspace.log\"\n | \"sidecar.updated\"\n | \"sidecar.removed\"\n | \"storage.configChanged\"\n | \"storage.stateChanged\"\n | \"instance.dataChanged\"\n | \"instance.event\"\n | \"instance.eventStatus\"\n | \"yolo.stateChanged\"\n | \"yolo.autoAccepted\"\n\nexport type WorkspaceEventPayload =\n | { type: \"workspace.created\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.started\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.error\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.stopped\"; workspaceId: string; reason?: \"deleted\" | \"stopped\" }\n | { type: \"workspace.log\"; entry: WorkspaceLogEntry }\n | { type: \"sidecar.updated\"; sidecar: SideCar }\n | { type: \"sidecar.removed\"; sidecarId: string }\n | { type: \"storage.configChanged\"; owner: SettingsOwner; value: SettingsBucket }\n | { type: \"storage.stateChanged\"; owner: SettingsOwner; value: SettingsBucket }\n | { type: \"instance.dataChanged\"; instanceId: string; data: InstanceData }\n | { type: \"instance.event\"; instanceId: string; event: InstanceStreamEvent }\n | { type: \"instance.eventStatus\"; instanceId: string; status: InstanceStreamStatus; generation: number; reason?: string }\n | { type: \"yolo.stateChanged\"; instanceId: string; sessionId: string; enabled: boolean }\n | { type: \"yolo.autoAccepted\"; instanceId: string; sessionId: string; permissionId: string }\n\nexport interface NetworkAddress {\n ip: string\n family: \"ipv4\" | \"ipv6\"\n scope: \"external\" | \"internal\" | \"loopback\"\n /** Remote URL using the server's remote protocol/port for this IP. */\n remoteUrl: string\n}\n\nexport interface LatestReleaseInfo {\n version: string\n tag: string\n url: string\n channel: \"stable\" | \"dev\"\n publishedAt?: string\n notes?: string\n}\n\nexport interface UiMeta {\n version?: string\n source: \"bundled\" | \"downloaded\" | \"previous\" | \"override\" | \"dev-proxy\" | \"missing\"\n}\n\nexport interface SupportMeta {\n supported: boolean\n message?: string\n minServerVersion?: string\n latestServerVersion?: string\n latestServerUrl?: string\n}\n\nexport interface ServerMeta {\n /** URL desktop apps should use to connect (prefers loopback HTTP when enabled). */\n localUrl: string\n /** URL remote clients should use (prefers HTTPS when enabled). */\n remoteUrl?: string\n /** SSE endpoint advertised to clients (`/api/events` by default). */\n eventsUrl: string\n /** Host the server is bound to (e.g., 127.0.0.1 or 0.0.0.0). */\n host: string\n /** Listening mode derived from host binding. */\n listeningMode: \"local\" | \"all\"\n /** Actual local port in use after binding. */\n localPort: number\n /** Actual remote port in use after binding (when remoteUrl is set). */\n remotePort?: number\n /** Display label for the host (e.g., hostname or friendly name). */\n hostLabel: string\n /** Absolute path of the filesystem root exposed to clients. */\n workspaceRoot: string\n /** Reachable addresses for this server, external first. */\n addresses: NetworkAddress[]\n serverVersion?: string\n ui?: UiMeta\n support?: SupportMeta\n /** Optional update info (dev channel only). */\n update?: LatestReleaseInfo | null\n}\n\nexport type {\n Preferences,\n ModelPreference,\n AgentModelSelections,\n RecentFolder,\n OpenCodeBinary,\n}\n"]}
@@ -209,7 +209,11 @@ test("pins parallel sessions to their independently inspected bridges", async ()
209
209
  test("prunes stale registry pressure before limiting discovery", async () => {
210
210
  const root = await mkdtemp(path.join(os.tmpdir(), "codenomad-automation-stale-"));
211
211
  const previousLocalAppData = process.env.LOCALAPPDATA;
212
+ const previousXdgRuntimeDir = process.env.XDG_RUNTIME_DIR;
213
+ const previousWslDistroName = process.env.WSL_DISTRO_NAME;
212
214
  process.env.LOCALAPPDATA = root;
215
+ process.env.XDG_RUNTIME_DIR = root;
216
+ delete process.env.WSL_DISTRO_NAME;
213
217
  let removeBridge;
214
218
  let server;
215
219
  try {
@@ -218,7 +222,8 @@ test("prunes stale registry pressure before limiting discovery", async () => {
218
222
  : { result: { target: { id: "live", title: "Live", url: "http://app.test" }, nodes: [], diagnostics: [] } });
219
223
  server = bridge.server;
220
224
  removeBridge = await publishAutomationBridge(createAutomationBridgeRegistration(bridge.url));
221
- const directory = path.join(root, "CodeNomad", "automation-bridges");
225
+ const directory = automationBridgeDirectories()[0];
226
+ assert.equal(path.dirname(path.dirname(directory)), root);
222
227
  const base = Date.now() + 10_000;
223
228
  for (let index = 0; index < 70; index += 1) {
224
229
  const startedAt = base + index;
@@ -242,6 +247,14 @@ test("prunes stale registry pressure before limiting discovery", async () => {
242
247
  delete process.env.LOCALAPPDATA;
243
248
  else
244
249
  process.env.LOCALAPPDATA = previousLocalAppData;
250
+ if (previousXdgRuntimeDir === undefined)
251
+ delete process.env.XDG_RUNTIME_DIR;
252
+ else
253
+ process.env.XDG_RUNTIME_DIR = previousXdgRuntimeDir;
254
+ if (previousWslDistroName === undefined)
255
+ delete process.env.WSL_DISTRO_NAME;
256
+ else
257
+ process.env.WSL_DISTRO_NAME = previousWslDistroName;
245
258
  await rm(root, { recursive: true, force: true });
246
259
  }
247
260
  });
@@ -1 +1 @@
1
- {"version":3,"file":"automation-plugin.test.js","sourceRoot":"","sources":["../../src/opencode/automation-plugin.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from \"node:fs/promises\"\nimport http from \"node:http\"\nimport os from \"node:os\"\nimport path from \"node:path\"\nimport test from \"node:test\"\nimport {\n AUTOMATION_BRIDGE_PATH,\n automationBridgeDirectories,\n createAutomationBridgeRegistration,\n parseDeveloperAction,\n publishAutomationBridge,\n removeLegacyAutomationPlugin,\n setupAutomationPlugin,\n} from \"./automation-plugin\"\n\ntype ToolDefinition = {\n name: string\n execute(input: unknown, context: { sessionID: string }): Promise<{ content: unknown }>\n}\n\nfunction listen(handler: (body: Record<string, unknown>) => Promise<Record<string, unknown>>) {\n return new Promise<{ server: http.Server; url: string }>((resolve) => {\n const server = http.createServer((request, response) => {\n const chunks: Buffer[] = []\n request.on(\"data\", (chunk) => chunks.push(Buffer.from(chunk)))\n request.on(\"end\", () => {\n void handler(JSON.parse(Buffer.concat(chunks).toString(\"utf8\"))).then((body) => {\n response.setHeader(\"content-type\", \"application/json\")\n response.end(JSON.stringify(body))\n })\n })\n })\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address() as { port: number }\n resolve({ server, url: `http://127.0.0.1:${address.port}` })\n })\n })\n}\n\nasync function collectTools(): Promise<ToolDefinition[]> {\n const definitions: ToolDefinition[] = []\n await setupAutomationPlugin({\n tool: { transform: async (callback) => callback({ add: (value) => definitions.push(value as ToolDefinition) }) },\n })\n return definitions\n}\n\nfunction closeServer(server: http.Server | undefined): Promise<void> {\n return new Promise((resolve) => server?.close(() => resolve()) ?? resolve())\n}\n\ntest(\"validates Developer Mode actions\", () => {\n assert.deepEqual(parseDeveloperAction({ action: \"type\", ref: \"e4\", text: \"CodeNomad\" }), {\n action: \"type\",\n ref: \"e4\",\n text: \"CodeNomad\",\n })\n assert.deepEqual(parseDeveloperAction({ action: \"restart\" }), { action: \"restart\" })\n assert.throws(() => parseDeveloperAction({ action: \"click\" }), /click requires ref/)\n})\n\ntest(\"registers developer tools while execution remains session-gated\", async () => {\n const tools: string[] = []\n await setupAutomationPlugin({\n tool: { transform: async (callback) => callback({ add: (value) => tools.push(value.name) }) },\n })\n\n assert.deepEqual(tools, [\"inspect\", \"act\", \"screenshot\"])\n})\n\ntest(\"discovers the Windows bridge registry from a WSL plugin\", () => {\n assert.deepEqual(automationBridgeDirectories({\n platform: \"linux\",\n env: { WSL_DISTRO_NAME: \"Ubuntu\", XDG_RUNTIME_DIR: \"/run/user/1000\" },\n home: \"/home/dev\",\n windowsLocalAppData: \"/mnt/c/Users/dev/AppData/Local\",\n }), [\n \"/run/user/1000/codenomad/automation-bridges\",\n \"/mnt/c/Users/dev/AppData/Local/CodeNomad/automation-bridges\",\n ])\n})\n\ntest(\"removes only the generated legacy global plugin shim\", async () => {\n const root = await mkdtemp(path.join(os.tmpdir(), \"codenomad-legacy-plugin-\"))\n const plugin = path.join(root, \"opencode\", \"plugins\", \"codenomad-automation.ts\")\n await mkdir(path.dirname(plugin), { recursive: true })\n try {\n await writeFile(plugin, 'export { default } from \"file:///C:/CodeNomad/resources/server/dist/opencode/automation-plugin.js\"\\n')\n assert.equal(await removeLegacyAutomationPlugin(root), true)\n await assert.rejects(readFile(plugin, \"utf8\"), { code: \"ENOENT\" })\n\n await writeFile(plugin, \"export default { id: 'user-plugin' }\\n\")\n assert.equal(await removeLegacyAutomationPlugin(root), false)\n assert.match(await readFile(plugin, \"utf8\"), /user-plugin/)\n } finally {\n await rm(root, { recursive: true, force: true })\n }\n})\n\ntest(\"restart waits for a new native generation and returns a fresh inspection\", async () => {\n const root = await mkdtemp(path.join(os.tmpdir(), \"codenomad-automation-restart-\"))\n const previousLocalAppData = process.env.LOCALAPPDATA\n process.env.LOCALAPPDATA = root\n const definitions: ToolDefinition[] = []\n let removeOld: (() => Promise<void>) | undefined\n let removeNew: (() => Promise<void>) | undefined\n let removePreexisting: (() => Promise<void>) | undefined\n const removeDistractors: Array<() => Promise<void>> = []\n let oldServer: http.Server | undefined\n let newServer: http.Server | undefined\n let preexistingServer: http.Server | undefined\n const distractorServers: http.Server[] = []\n let restarting = false\n\n try {\n await setupAutomationPlugin({\n tool: { transform: async (callback) => callback({ add: (value) => definitions.push(value as typeof definitions[number]) }) },\n })\n const replacement = await listen(async (body) => body.mode === \"developer-probe\"\n ? { result: { available: true, nativeIdentity: \"electron:test\", runId: \"run-new\" } }\n : { result: { target: { id: \"page-new\", title: \"Restarted\", url: \"http://app.test\" }, nodes: [], diagnostics: [] } })\n newServer = replacement.server\n const preexisting = await listen(async (body) => body.mode === \"developer-probe\" && restarting\n ? { result: { available: true, nativeIdentity: \"electron:test\", runId: \"run-preexisting\" } }\n : body.mode === \"developer-probe\"\n ? { result: { available: false } }\n : { result: { target: { id: \"page-wrong\", title: \"Pre-existing\", url: \"http://app.test\" }, nodes: [], diagnostics: [] } })\n preexistingServer = preexisting.server\n removePreexisting = await publishAutomationBridge(createAutomationBridgeRegistration(preexisting.url))\n const distractors = await Promise.all(Array.from({ length: 5 }, (_, index) => listen(async (body) => body.mode === \"developer-probe\"\n ? { result: { available: true, nativeIdentity: `electron:distractor-${index}`, runId: `run-distractor-${index}` } }\n : { result: { target: { id: `page-${index}`, title: \"Distractor\", url: \"http://app.test\" }, nodes: [], diagnostics: [] } })))\n distractorServers.push(...distractors.map((value) => value.server))\n const old = await listen(async (body) => {\n if (body.mode === \"developer-probe\") return { result: { available: true, nativeIdentity: \"electron:test\", runId: \"run-old\" } }\n if (body.command && (body.command as { action?: unknown }).action === \"inspect\") {\n return { result: { target: { id: \"page-old\", title: \"Original\", url: \"http://app.test\" }, nodes: [], diagnostics: [] } }\n }\n restarting = true\n removeNew = await publishAutomationBridge(createAutomationBridgeRegistration(replacement.url))\n for (const distractor of distractors) {\n await new Promise((resolve) => setTimeout(resolve, 2))\n removeDistractors.push(await publishAutomationBridge(createAutomationBridgeRegistration(distractor.url)))\n }\n return { result: { state: \"starting\" } }\n })\n oldServer = old.server\n removeOld = await publishAutomationBridge(createAutomationBridgeRegistration(old.url))\n\n const inspect = definitions.find((definition) => definition.name === \"inspect\")!\n await inspect.execute({}, { sessionID: \"session-1\" })\n const act = definitions.find((definition) => definition.name === \"act\")!\n const result = await act.execute({ action: \"restart\" }, { sessionID: \"session-1\" })\n assert.match(String(result.content), /Restarted/)\n } finally {\n await removeOld?.()\n await removeNew?.()\n await removePreexisting?.()\n await Promise.all(removeDistractors.map((remove) => remove()))\n await closeServer(oldServer)\n await closeServer(newServer)\n await closeServer(preexistingServer)\n await Promise.all(distractorServers.map(closeServer))\n if (previousLocalAppData === undefined) delete process.env.LOCALAPPDATA\n else process.env.LOCALAPPDATA = previousLocalAppData\n await rm(root, { recursive: true, force: true })\n }\n})\n\ntest(\"keeps inspected targets isolated per plugin setup\", async () => {\n const root = await mkdtemp(path.join(os.tmpdir(), \"codenomad-automation-isolation-\"))\n const previousLocalAppData = process.env.LOCALAPPDATA\n process.env.LOCALAPPDATA = root\n let removeBridge: (() => Promise<void>) | undefined\n let server: http.Server | undefined\n try {\n const bridge = await listen(async (body) => body.mode === \"developer-probe\"\n ? { result: { available: true, nativeIdentity: \"electron:isolation\", runId: \"run-isolation\" } }\n : { result: { target: { id: \"isolated\", title: \"Isolated\", url: \"http://app.test\" }, nodes: [], diagnostics: [] } })\n server = bridge.server\n removeBridge = await publishAutomationBridge(createAutomationBridgeRegistration(bridge.url))\n const first = await collectTools()\n const second = await collectTools()\n await first.find((definition) => definition.name === \"inspect\")!.execute({}, { sessionID: \"session-isolation\" })\n await assert.rejects(\n second.find((definition) => definition.name === \"screenshot\")!.execute({}, { sessionID: \"session-isolation\" }),\n /Run codenomad\\.inspect/,\n )\n } finally {\n await removeBridge?.()\n await closeServer(server)\n if (previousLocalAppData === undefined) delete process.env.LOCALAPPDATA\n else process.env.LOCALAPPDATA = previousLocalAppData\n await rm(root, { recursive: true, force: true })\n }\n})\n\ntest(\"pins parallel sessions to their independently inspected bridges\", async () => {\n const root = await mkdtemp(path.join(os.tmpdir(), \"codenomad-automation-sessions-\"))\n const previousLocalAppData = process.env.LOCALAPPDATA\n process.env.LOCALAPPDATA = root\n const removals: Array<() => Promise<void>> = []\n const servers: http.Server[] = []\n try {\n for (const sessionID of [\"session-a\", \"session-b\"]) {\n const bridge = await listen(async (body) => body.mode === \"developer-probe\"\n ? { result: { available: body.sessionID === sessionID, nativeIdentity: `electron:${sessionID}`, runId: `run-${sessionID}` } }\n : { result: { target: { id: sessionID, title: sessionID, url: \"http://app.test\" }, nodes: [], diagnostics: [] } })\n servers.push(bridge.server)\n removals.push(await publishAutomationBridge(createAutomationBridgeRegistration(bridge.url)))\n }\n const tools = await collectTools()\n const inspect = tools.find((definition) => definition.name === \"inspect\")!\n const screenshot = tools.find((definition) => definition.name === \"screenshot\")!\n await inspect.execute({}, { sessionID: \"session-a\" })\n await inspect.execute({}, { sessionID: \"session-b\" })\n assert.match(String((await screenshot.execute({}, { sessionID: \"session-a\" })).content), /session-a/)\n assert.match(String((await screenshot.execute({}, { sessionID: \"session-b\" })).content), /session-b/)\n } finally {\n await Promise.all(removals.map((remove) => remove()))\n await Promise.all(servers.map(closeServer))\n if (previousLocalAppData === undefined) delete process.env.LOCALAPPDATA\n else process.env.LOCALAPPDATA = previousLocalAppData\n await rm(root, { recursive: true, force: true })\n }\n})\n\ntest(\"prunes stale registry pressure before limiting discovery\", async () => {\n const root = await mkdtemp(path.join(os.tmpdir(), \"codenomad-automation-stale-\"))\n const previousLocalAppData = process.env.LOCALAPPDATA\n process.env.LOCALAPPDATA = root\n let removeBridge: (() => Promise<void>) | undefined\n let server: http.Server | undefined\n try {\n const bridge = await listen(async (body) => body.mode === \"developer-probe\"\n ? { result: { available: true, nativeIdentity: \"electron:live\", runId: \"run-live\" } }\n : { result: { target: { id: \"live\", title: \"Live\", url: \"http://app.test\" }, nodes: [], diagnostics: [] } })\n server = bridge.server\n removeBridge = await publishAutomationBridge(createAutomationBridgeRegistration(bridge.url))\n const directory = path.join(root, \"CodeNomad\", \"automation-bridges\")\n const base = Date.now() + 10_000\n for (let index = 0; index < 70; index += 1) {\n const startedAt = base + index\n const token = `dead${String(index).padStart(8, \"0\")}`\n await writeFile(path.join(directory, `${startedAt}-99999999-${token}.json`), JSON.stringify({\n version: 1,\n url: new URL(AUTOMATION_BRIDGE_PATH, bridge.url).href,\n token,\n pid: 99_999_999,\n startedAt,\n }))\n }\n const inspect = (await collectTools()).find((definition) => definition.name === \"inspect\")!\n assert.match(String((await inspect.execute({}, { sessionID: \"session-live\" })).content), /Live/)\n assert.equal((await readdir(directory)).length, 1)\n } finally {\n await removeBridge?.()\n await closeServer(server)\n if (previousLocalAppData === undefined) delete process.env.LOCALAPPDATA\n else process.env.LOCALAPPDATA = previousLocalAppData\n await rm(root, { recursive: true, force: true })\n }\n})\n"]}
1
+ {"version":3,"file":"automation-plugin.test.js","sourceRoot":"","sources":["../../src/opencode/automation-plugin.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from \"node:fs/promises\"\nimport http from \"node:http\"\nimport os from \"node:os\"\nimport path from \"node:path\"\nimport test from \"node:test\"\nimport {\n AUTOMATION_BRIDGE_PATH,\n automationBridgeDirectories,\n createAutomationBridgeRegistration,\n parseDeveloperAction,\n publishAutomationBridge,\n removeLegacyAutomationPlugin,\n setupAutomationPlugin,\n} from \"./automation-plugin\"\n\ntype ToolDefinition = {\n name: string\n execute(input: unknown, context: { sessionID: string }): Promise<{ content: unknown }>\n}\n\nfunction listen(handler: (body: Record<string, unknown>) => Promise<Record<string, unknown>>) {\n return new Promise<{ server: http.Server; url: string }>((resolve) => {\n const server = http.createServer((request, response) => {\n const chunks: Buffer[] = []\n request.on(\"data\", (chunk) => chunks.push(Buffer.from(chunk)))\n request.on(\"end\", () => {\n void handler(JSON.parse(Buffer.concat(chunks).toString(\"utf8\"))).then((body) => {\n response.setHeader(\"content-type\", \"application/json\")\n response.end(JSON.stringify(body))\n })\n })\n })\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address() as { port: number }\n resolve({ server, url: `http://127.0.0.1:${address.port}` })\n })\n })\n}\n\nasync function collectTools(): Promise<ToolDefinition[]> {\n const definitions: ToolDefinition[] = []\n await setupAutomationPlugin({\n tool: { transform: async (callback) => callback({ add: (value) => definitions.push(value as ToolDefinition) }) },\n })\n return definitions\n}\n\nfunction closeServer(server: http.Server | undefined): Promise<void> {\n return new Promise((resolve) => server?.close(() => resolve()) ?? resolve())\n}\n\ntest(\"validates Developer Mode actions\", () => {\n assert.deepEqual(parseDeveloperAction({ action: \"type\", ref: \"e4\", text: \"CodeNomad\" }), {\n action: \"type\",\n ref: \"e4\",\n text: \"CodeNomad\",\n })\n assert.deepEqual(parseDeveloperAction({ action: \"restart\" }), { action: \"restart\" })\n assert.throws(() => parseDeveloperAction({ action: \"click\" }), /click requires ref/)\n})\n\ntest(\"registers developer tools while execution remains session-gated\", async () => {\n const tools: string[] = []\n await setupAutomationPlugin({\n tool: { transform: async (callback) => callback({ add: (value) => tools.push(value.name) }) },\n })\n\n assert.deepEqual(tools, [\"inspect\", \"act\", \"screenshot\"])\n})\n\ntest(\"discovers the Windows bridge registry from a WSL plugin\", () => {\n assert.deepEqual(automationBridgeDirectories({\n platform: \"linux\",\n env: { WSL_DISTRO_NAME: \"Ubuntu\", XDG_RUNTIME_DIR: \"/run/user/1000\" },\n home: \"/home/dev\",\n windowsLocalAppData: \"/mnt/c/Users/dev/AppData/Local\",\n }), [\n \"/run/user/1000/codenomad/automation-bridges\",\n \"/mnt/c/Users/dev/AppData/Local/CodeNomad/automation-bridges\",\n ])\n})\n\ntest(\"removes only the generated legacy global plugin shim\", async () => {\n const root = await mkdtemp(path.join(os.tmpdir(), \"codenomad-legacy-plugin-\"))\n const plugin = path.join(root, \"opencode\", \"plugins\", \"codenomad-automation.ts\")\n await mkdir(path.dirname(plugin), { recursive: true })\n try {\n await writeFile(plugin, 'export { default } from \"file:///C:/CodeNomad/resources/server/dist/opencode/automation-plugin.js\"\\n')\n assert.equal(await removeLegacyAutomationPlugin(root), true)\n await assert.rejects(readFile(plugin, \"utf8\"), { code: \"ENOENT\" })\n\n await writeFile(plugin, \"export default { id: 'user-plugin' }\\n\")\n assert.equal(await removeLegacyAutomationPlugin(root), false)\n assert.match(await readFile(plugin, \"utf8\"), /user-plugin/)\n } finally {\n await rm(root, { recursive: true, force: true })\n }\n})\n\ntest(\"restart waits for a new native generation and returns a fresh inspection\", async () => {\n const root = await mkdtemp(path.join(os.tmpdir(), \"codenomad-automation-restart-\"))\n const previousLocalAppData = process.env.LOCALAPPDATA\n process.env.LOCALAPPDATA = root\n const definitions: ToolDefinition[] = []\n let removeOld: (() => Promise<void>) | undefined\n let removeNew: (() => Promise<void>) | undefined\n let removePreexisting: (() => Promise<void>) | undefined\n const removeDistractors: Array<() => Promise<void>> = []\n let oldServer: http.Server | undefined\n let newServer: http.Server | undefined\n let preexistingServer: http.Server | undefined\n const distractorServers: http.Server[] = []\n let restarting = false\n\n try {\n await setupAutomationPlugin({\n tool: { transform: async (callback) => callback({ add: (value) => definitions.push(value as typeof definitions[number]) }) },\n })\n const replacement = await listen(async (body) => body.mode === \"developer-probe\"\n ? { result: { available: true, nativeIdentity: \"electron:test\", runId: \"run-new\" } }\n : { result: { target: { id: \"page-new\", title: \"Restarted\", url: \"http://app.test\" }, nodes: [], diagnostics: [] } })\n newServer = replacement.server\n const preexisting = await listen(async (body) => body.mode === \"developer-probe\" && restarting\n ? { result: { available: true, nativeIdentity: \"electron:test\", runId: \"run-preexisting\" } }\n : body.mode === \"developer-probe\"\n ? { result: { available: false } }\n : { result: { target: { id: \"page-wrong\", title: \"Pre-existing\", url: \"http://app.test\" }, nodes: [], diagnostics: [] } })\n preexistingServer = preexisting.server\n removePreexisting = await publishAutomationBridge(createAutomationBridgeRegistration(preexisting.url))\n const distractors = await Promise.all(Array.from({ length: 5 }, (_, index) => listen(async (body) => body.mode === \"developer-probe\"\n ? { result: { available: true, nativeIdentity: `electron:distractor-${index}`, runId: `run-distractor-${index}` } }\n : { result: { target: { id: `page-${index}`, title: \"Distractor\", url: \"http://app.test\" }, nodes: [], diagnostics: [] } })))\n distractorServers.push(...distractors.map((value) => value.server))\n const old = await listen(async (body) => {\n if (body.mode === \"developer-probe\") return { result: { available: true, nativeIdentity: \"electron:test\", runId: \"run-old\" } }\n if (body.command && (body.command as { action?: unknown }).action === \"inspect\") {\n return { result: { target: { id: \"page-old\", title: \"Original\", url: \"http://app.test\" }, nodes: [], diagnostics: [] } }\n }\n restarting = true\n removeNew = await publishAutomationBridge(createAutomationBridgeRegistration(replacement.url))\n for (const distractor of distractors) {\n await new Promise((resolve) => setTimeout(resolve, 2))\n removeDistractors.push(await publishAutomationBridge(createAutomationBridgeRegistration(distractor.url)))\n }\n return { result: { state: \"starting\" } }\n })\n oldServer = old.server\n removeOld = await publishAutomationBridge(createAutomationBridgeRegistration(old.url))\n\n const inspect = definitions.find((definition) => definition.name === \"inspect\")!\n await inspect.execute({}, { sessionID: \"session-1\" })\n const act = definitions.find((definition) => definition.name === \"act\")!\n const result = await act.execute({ action: \"restart\" }, { sessionID: \"session-1\" })\n assert.match(String(result.content), /Restarted/)\n } finally {\n await removeOld?.()\n await removeNew?.()\n await removePreexisting?.()\n await Promise.all(removeDistractors.map((remove) => remove()))\n await closeServer(oldServer)\n await closeServer(newServer)\n await closeServer(preexistingServer)\n await Promise.all(distractorServers.map(closeServer))\n if (previousLocalAppData === undefined) delete process.env.LOCALAPPDATA\n else process.env.LOCALAPPDATA = previousLocalAppData\n await rm(root, { recursive: true, force: true })\n }\n})\n\ntest(\"keeps inspected targets isolated per plugin setup\", async () => {\n const root = await mkdtemp(path.join(os.tmpdir(), \"codenomad-automation-isolation-\"))\n const previousLocalAppData = process.env.LOCALAPPDATA\n process.env.LOCALAPPDATA = root\n let removeBridge: (() => Promise<void>) | undefined\n let server: http.Server | undefined\n try {\n const bridge = await listen(async (body) => body.mode === \"developer-probe\"\n ? { result: { available: true, nativeIdentity: \"electron:isolation\", runId: \"run-isolation\" } }\n : { result: { target: { id: \"isolated\", title: \"Isolated\", url: \"http://app.test\" }, nodes: [], diagnostics: [] } })\n server = bridge.server\n removeBridge = await publishAutomationBridge(createAutomationBridgeRegistration(bridge.url))\n const first = await collectTools()\n const second = await collectTools()\n await first.find((definition) => definition.name === \"inspect\")!.execute({}, { sessionID: \"session-isolation\" })\n await assert.rejects(\n second.find((definition) => definition.name === \"screenshot\")!.execute({}, { sessionID: \"session-isolation\" }),\n /Run codenomad\\.inspect/,\n )\n } finally {\n await removeBridge?.()\n await closeServer(server)\n if (previousLocalAppData === undefined) delete process.env.LOCALAPPDATA\n else process.env.LOCALAPPDATA = previousLocalAppData\n await rm(root, { recursive: true, force: true })\n }\n})\n\ntest(\"pins parallel sessions to their independently inspected bridges\", async () => {\n const root = await mkdtemp(path.join(os.tmpdir(), \"codenomad-automation-sessions-\"))\n const previousLocalAppData = process.env.LOCALAPPDATA\n process.env.LOCALAPPDATA = root\n const removals: Array<() => Promise<void>> = []\n const servers: http.Server[] = []\n try {\n for (const sessionID of [\"session-a\", \"session-b\"]) {\n const bridge = await listen(async (body) => body.mode === \"developer-probe\"\n ? { result: { available: body.sessionID === sessionID, nativeIdentity: `electron:${sessionID}`, runId: `run-${sessionID}` } }\n : { result: { target: { id: sessionID, title: sessionID, url: \"http://app.test\" }, nodes: [], diagnostics: [] } })\n servers.push(bridge.server)\n removals.push(await publishAutomationBridge(createAutomationBridgeRegistration(bridge.url)))\n }\n const tools = await collectTools()\n const inspect = tools.find((definition) => definition.name === \"inspect\")!\n const screenshot = tools.find((definition) => definition.name === \"screenshot\")!\n await inspect.execute({}, { sessionID: \"session-a\" })\n await inspect.execute({}, { sessionID: \"session-b\" })\n assert.match(String((await screenshot.execute({}, { sessionID: \"session-a\" })).content), /session-a/)\n assert.match(String((await screenshot.execute({}, { sessionID: \"session-b\" })).content), /session-b/)\n } finally {\n await Promise.all(removals.map((remove) => remove()))\n await Promise.all(servers.map(closeServer))\n if (previousLocalAppData === undefined) delete process.env.LOCALAPPDATA\n else process.env.LOCALAPPDATA = previousLocalAppData\n await rm(root, { recursive: true, force: true })\n }\n})\n\ntest(\"prunes stale registry pressure before limiting discovery\", async () => {\n const root = await mkdtemp(path.join(os.tmpdir(), \"codenomad-automation-stale-\"))\n const previousLocalAppData = process.env.LOCALAPPDATA\n const previousXdgRuntimeDir = process.env.XDG_RUNTIME_DIR\n const previousWslDistroName = process.env.WSL_DISTRO_NAME\n process.env.LOCALAPPDATA = root\n process.env.XDG_RUNTIME_DIR = root\n delete process.env.WSL_DISTRO_NAME\n let removeBridge: (() => Promise<void>) | undefined\n let server: http.Server | undefined\n try {\n const bridge = await listen(async (body) => body.mode === \"developer-probe\"\n ? { result: { available: true, nativeIdentity: \"electron:live\", runId: \"run-live\" } }\n : { result: { target: { id: \"live\", title: \"Live\", url: \"http://app.test\" }, nodes: [], diagnostics: [] } })\n server = bridge.server\n removeBridge = await publishAutomationBridge(createAutomationBridgeRegistration(bridge.url))\n const directory = automationBridgeDirectories()[0]\n assert.equal(path.dirname(path.dirname(directory)), root)\n const base = Date.now() + 10_000\n for (let index = 0; index < 70; index += 1) {\n const startedAt = base + index\n const token = `dead${String(index).padStart(8, \"0\")}`\n await writeFile(path.join(directory, `${startedAt}-99999999-${token}.json`), JSON.stringify({\n version: 1,\n url: new URL(AUTOMATION_BRIDGE_PATH, bridge.url).href,\n token,\n pid: 99_999_999,\n startedAt,\n }))\n }\n const inspect = (await collectTools()).find((definition) => definition.name === \"inspect\")!\n assert.match(String((await inspect.execute({}, { sessionID: \"session-live\" })).content), /Live/)\n assert.equal((await readdir(directory)).length, 1)\n } finally {\n await removeBridge?.()\n await closeServer(server)\n if (previousLocalAppData === undefined) delete process.env.LOCALAPPDATA\n else process.env.LOCALAPPDATA = previousLocalAppData\n if (previousXdgRuntimeDir === undefined) delete process.env.XDG_RUNTIME_DIR\n else process.env.XDG_RUNTIME_DIR = previousXdgRuntimeDir\n if (previousWslDistroName === undefined) delete process.env.WSL_DISTRO_NAME\n else process.env.WSL_DISTRO_NAME = previousWslDistroName\n await rm(root, { recursive: true, force: true })\n }\n})\n"]}
@@ -0,0 +1,41 @@
1
+ import assert from "node:assert/strict";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { it } from "node:test";
4
+ import Fastify from "fastify";
5
+ import pino from "pino";
6
+ import { registerSettingsRoutes } from "./settings";
7
+ import { binaryProbeFixture, legacyHelp } from "../../workspaces/__tests__/binary-probe-fixture";
8
+ it("validates through HTTP without writing preferences or blocking other requests", async () => {
9
+ const app = Fastify();
10
+ registerSettingsRoutes(app, {
11
+ settings: new Proxy({}, { get() { throw new Error("Validation must not read or mutate settings"); } }),
12
+ logger: pino({ level: "silent" }),
13
+ });
14
+ app.get("/ping", async () => ({ ok: true }));
15
+ await app.ready();
16
+ const compatible = binaryProbeFixture({ delayMs: 250 });
17
+ const legacy = binaryProbeFixture({ help: legacyHelp, stderr: true });
18
+ try {
19
+ let completed = false;
20
+ const validating = app.inject({ method: "POST", url: "/api/storage/binaries/validate", payload: { path: compatible.binary } })
21
+ .then((response) => { completed = true; return response; });
22
+ await delay(50);
23
+ assert.equal((await app.inject("/ping")).statusCode, 200);
24
+ assert.equal(completed, false, "HTTP must remain available while the CLI is slow");
25
+ const valid = await validating;
26
+ assert.equal(valid.statusCode, 200);
27
+ assert.deepEqual(valid.json(), { valid: true });
28
+ const invalid = await app.inject({ method: "POST", url: "/api/storage/binaries/validate", payload: { path: legacy.binary } });
29
+ assert.equal(invalid.statusCode, 200);
30
+ assert.deepEqual(invalid.json(), { valid: false, errorCode: "opencode_v2_required" });
31
+ assert.deepEqual(compatible.calls(), [["--version"], ["service", "--help"]]);
32
+ assert.deepEqual(legacy.calls(), [["--version"], ["service", "--help"]]);
33
+ assert.equal((await app.inject({ method: "POST", url: "/api/storage/binaries/validate", payload: { path: 42 } })).statusCode, 400);
34
+ }
35
+ finally {
36
+ await app.close();
37
+ compatible.dispose();
38
+ legacy.dispose();
39
+ }
40
+ });
41
+ //# sourceMappingURL=binary-validation.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binary-validation.test.js","sourceRoot":"","sources":["../../../src/server/routes/binary-validation.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { setTimeout as delay } from \"node:timers/promises\"\nimport { it } from \"node:test\"\nimport Fastify from \"fastify\"\nimport pino from \"pino\"\nimport { registerSettingsRoutes } from \"./settings\"\nimport type { SettingsService } from \"../../settings/service\"\nimport { binaryProbeFixture, legacyHelp } from \"../../workspaces/__tests__/binary-probe-fixture\"\n\nit(\"validates through HTTP without writing preferences or blocking other requests\", async () => {\n const app = Fastify()\n registerSettingsRoutes(app, {\n settings: new Proxy({} as SettingsService, { get() { throw new Error(\"Validation must not read or mutate settings\") } }),\n logger: pino({ level: \"silent\" }),\n })\n app.get(\"/ping\", async () => ({ ok: true }))\n await app.ready()\n const compatible = binaryProbeFixture({ delayMs: 250 })\n const legacy = binaryProbeFixture({ help: legacyHelp, stderr: true })\n try {\n let completed = false\n const validating = app.inject({ method: \"POST\", url: \"/api/storage/binaries/validate\", payload: { path: compatible.binary } })\n .then((response) => { completed = true; return response })\n await delay(50)\n assert.equal((await app.inject(\"/ping\")).statusCode, 200)\n assert.equal(completed, false, \"HTTP must remain available while the CLI is slow\")\n const valid = await validating\n assert.equal(valid.statusCode, 200)\n assert.deepEqual(valid.json(), { valid: true })\n const invalid = await app.inject({ method: \"POST\", url: \"/api/storage/binaries/validate\", payload: { path: legacy.binary } })\n assert.equal(invalid.statusCode, 200)\n assert.deepEqual(invalid.json(), { valid: false, errorCode: \"opencode_v2_required\" })\n assert.deepEqual(compatible.calls(), [[\"--version\"], [\"service\", \"--help\"]])\n assert.deepEqual(legacy.calls(), [[\"--version\"], [\"service\", \"--help\"]])\n assert.equal((await app.inject({ method: \"POST\", url: \"/api/storage/binaries/validate\", payload: { path: 42 } })).statusCode, 400)\n } finally {\n await app.close()\n compatible.dispose()\n legacy.dispose()\n }\n})\n"]}
@@ -1,12 +1,11 @@
1
1
  import { z } from "zod";
2
- import { probeBinaryVersion } from "../../workspaces/spawn";
2
+ import { probeOpenCodeBinary } from "../../workspaces/spawn";
3
3
  import { sanitizeConfigDoc, sanitizeConfigOwner } from "../../settings/public-config";
4
4
  const ValidateBinarySchema = z.object({
5
5
  path: z.string(),
6
6
  });
7
7
  function validateBinaryPath(binaryPath) {
8
- const result = probeBinaryVersion(binaryPath);
9
- return { valid: result.valid, version: result.version, error: result.error };
8
+ return probeOpenCodeBinary(binaryPath);
10
9
  }
11
10
  export function enforceSpeechCredentialPairing(body, currentSpeech) {
12
11
  if (!body || typeof body !== "object")
@@ -1 +1 @@
1
- {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../../src/server/routes/settings.ts"],"names":[],"mappings":"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","sourcesContent":["import { FastifyInstance } from \"fastify\"\nimport { z } from \"zod\"\nimport { probeBinaryVersion } from \"../../workspaces/spawn\"\nimport type { SettingsService } from \"../../settings/service\"\nimport type { Logger } from \"../../logger\"\nimport { sanitizeConfigDoc, sanitizeConfigOwner } from \"../../settings/public-config\"\n\ninterface RouteDeps {\n settings: SettingsService\n logger: Logger\n}\n\nconst ValidateBinarySchema = z.object({\n path: z.string(),\n})\n\nfunction validateBinaryPath(binaryPath: string): { valid: boolean; version?: string; error?: string } {\n const result = probeBinaryVersion(binaryPath)\n return { valid: result.valid, version: result.version, error: result.error }\n}\n\nexport function enforceSpeechCredentialPairing(body: unknown, currentSpeech?: unknown): unknown {\n if (!body || typeof body !== \"object\") return body\n const patch = { ...(body as Record<string, unknown>) }\n const speech = patch.speech\n if (!speech || typeof speech !== \"object\") return patch\n const speechPatch = { ...(speech as Record<string, unknown>) }\n const cur = (currentSpeech && typeof currentSpeech === \"object\") ? currentSpeech as Record<string, unknown> : {}\n const curSpeech = (cur.speech && typeof cur.speech === \"object\") ? cur.speech as Record<string, unknown> : cur\n\n if (\"baseUrl\" in speechPatch && !(\"apiKey\" in speechPatch)) {\n if ((speechPatch.baseUrl ?? \"\") !== (curSpeech.baseUrl ?? \"\")) {\n speechPatch.apiKey = null\n }\n }\n for (const dir of [\"stt\", \"tts\"] as const) {\n if (dir in speechPatch) {\n const dirPatch = { ...(speechPatch[dir] as Record<string, unknown>) }\n if (\"baseUrl\" in dirPatch && !(\"apiKey\" in dirPatch)) {\n const curDir = (curSpeech[dir] && typeof curSpeech[dir] === \"object\") ? curSpeech[dir] as Record<string, unknown> : {}\n if ((dirPatch.baseUrl ?? \"\") !== (curDir.baseUrl ?? \"\")) {\n dirPatch.apiKey = null\n }\n speechPatch[dir] = dirPatch\n }\n }\n }\n patch.speech = speechPatch\n return patch\n}\n\nexport function registerSettingsRoutes(app: FastifyInstance, deps: RouteDeps) {\n // Full-document access\n app.get(\"/api/storage/config\", async () => sanitizeConfigDoc(deps.settings.getDoc(\"config\")))\n app.patch(\"/api/storage/config\", async (request, reply) => {\n try {\n let body = request.body ?? {}\n if (body && typeof body === \"object\" && \"server\" in body) {\n const bodyObj = { ...(body as Record<string, unknown>) }\n const serverPatch = bodyObj.server\n if (serverPatch && typeof serverPatch === \"object\") {\n const currentServer = deps.settings.getOwner(\"config\", \"server\")\n bodyObj.server = enforceSpeechCredentialPairing(serverPatch, currentServer)\n }\n body = bodyObj\n }\n return sanitizeConfigDoc(deps.settings.mergePatchDoc(\"config\", body))\n } catch (error) {\n reply.code(400)\n return { error: error instanceof Error ? error.message : \"Invalid patch\" }\n }\n })\n\n app.get<{ Params: { owner: string } }>(\"/api/storage/config/:owner\", async (request) => {\n return sanitizeConfigOwner(request.params.owner, deps.settings.getOwner(\"config\", request.params.owner))\n })\n\n app.patch<{ Params: { owner: string } }>(\"/api/storage/config/:owner\", async (request, reply) => {\n try {\n const currentOwner = request.params.owner === \"server\"\n ? deps.settings.getOwner(\"config\", \"server\")\n : undefined\n const processed = request.params.owner === \"server\"\n ? enforceSpeechCredentialPairing(request.body ?? {}, currentOwner)\n : request.body ?? {}\n return sanitizeConfigOwner(\n request.params.owner,\n deps.settings.mergePatchOwner(\"config\", request.params.owner, processed),\n )\n } catch (error) {\n reply.code(400)\n return { error: error instanceof Error ? error.message : \"Invalid patch\" }\n }\n })\n\n app.get(\"/api/storage/state\", async () => deps.settings.getDoc(\"state\"))\n app.patch(\"/api/storage/state\", async (request, reply) => {\n try {\n return deps.settings.mergePatchDoc(\"state\", request.body ?? {})\n } catch (error) {\n reply.code(400)\n return { error: error instanceof Error ? error.message : \"Invalid patch\" }\n }\n })\n\n app.get<{ Params: { owner: string } }>(\"/api/storage/state/:owner\", async (request) => {\n return deps.settings.getOwner(\"state\", request.params.owner)\n })\n\n app.patch<{ Params: { owner: string } }>(\"/api/storage/state/:owner\", async (request, reply) => {\n try {\n return deps.settings.mergePatchOwner(\"state\", request.params.owner, request.body ?? {})\n } catch (error) {\n reply.code(400)\n return { error: error instanceof Error ? error.message : \"Invalid patch\" }\n }\n })\n\n // Binary validation helper (used by UI when adding binaries)\n app.post(\"/api/storage/binaries/validate\", async (request, reply) => {\n try {\n const body = ValidateBinarySchema.parse(request.body ?? {})\n return validateBinaryPath(body.path)\n } catch (error) {\n deps.logger.warn({ err: error }, \"Failed to validate binary\")\n reply.code(400)\n return { valid: false, error: error instanceof Error ? error.message : \"Invalid request\" }\n }\n })\n}\n"]}
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../../src/server/routes/settings.ts"],"names":[],"mappings":"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","sourcesContent":["import { FastifyInstance } from \"fastify\"\nimport { z } from \"zod\"\nimport type { BinaryValidationResult } from \"../../api-types\"\nimport { probeOpenCodeBinary } from \"../../workspaces/spawn\"\nimport type { SettingsService } from \"../../settings/service\"\nimport type { Logger } from \"../../logger\"\nimport { sanitizeConfigDoc, sanitizeConfigOwner } from \"../../settings/public-config\"\n\ninterface RouteDeps {\n settings: SettingsService\n logger: Logger\n}\n\nconst ValidateBinarySchema = z.object({\n path: z.string(),\n})\n\nfunction validateBinaryPath(binaryPath: string): Promise<BinaryValidationResult> {\n return probeOpenCodeBinary(binaryPath)\n}\n\nexport function enforceSpeechCredentialPairing(body: unknown, currentSpeech?: unknown): unknown {\n if (!body || typeof body !== \"object\") return body\n const patch = { ...(body as Record<string, unknown>) }\n const speech = patch.speech\n if (!speech || typeof speech !== \"object\") return patch\n const speechPatch = { ...(speech as Record<string, unknown>) }\n const cur = (currentSpeech && typeof currentSpeech === \"object\") ? currentSpeech as Record<string, unknown> : {}\n const curSpeech = (cur.speech && typeof cur.speech === \"object\") ? cur.speech as Record<string, unknown> : cur\n\n if (\"baseUrl\" in speechPatch && !(\"apiKey\" in speechPatch)) {\n if ((speechPatch.baseUrl ?? \"\") !== (curSpeech.baseUrl ?? \"\")) {\n speechPatch.apiKey = null\n }\n }\n for (const dir of [\"stt\", \"tts\"] as const) {\n if (dir in speechPatch) {\n const dirPatch = { ...(speechPatch[dir] as Record<string, unknown>) }\n if (\"baseUrl\" in dirPatch && !(\"apiKey\" in dirPatch)) {\n const curDir = (curSpeech[dir] && typeof curSpeech[dir] === \"object\") ? curSpeech[dir] as Record<string, unknown> : {}\n if ((dirPatch.baseUrl ?? \"\") !== (curDir.baseUrl ?? \"\")) {\n dirPatch.apiKey = null\n }\n speechPatch[dir] = dirPatch\n }\n }\n }\n patch.speech = speechPatch\n return patch\n}\n\nexport function registerSettingsRoutes(app: FastifyInstance, deps: RouteDeps) {\n // Full-document access\n app.get(\"/api/storage/config\", async () => sanitizeConfigDoc(deps.settings.getDoc(\"config\")))\n app.patch(\"/api/storage/config\", async (request, reply) => {\n try {\n let body = request.body ?? {}\n if (body && typeof body === \"object\" && \"server\" in body) {\n const bodyObj = { ...(body as Record<string, unknown>) }\n const serverPatch = bodyObj.server\n if (serverPatch && typeof serverPatch === \"object\") {\n const currentServer = deps.settings.getOwner(\"config\", \"server\")\n bodyObj.server = enforceSpeechCredentialPairing(serverPatch, currentServer)\n }\n body = bodyObj\n }\n return sanitizeConfigDoc(deps.settings.mergePatchDoc(\"config\", body))\n } catch (error) {\n reply.code(400)\n return { error: error instanceof Error ? error.message : \"Invalid patch\" }\n }\n })\n\n app.get<{ Params: { owner: string } }>(\"/api/storage/config/:owner\", async (request) => {\n return sanitizeConfigOwner(request.params.owner, deps.settings.getOwner(\"config\", request.params.owner))\n })\n\n app.patch<{ Params: { owner: string } }>(\"/api/storage/config/:owner\", async (request, reply) => {\n try {\n const currentOwner = request.params.owner === \"server\"\n ? deps.settings.getOwner(\"config\", \"server\")\n : undefined\n const processed = request.params.owner === \"server\"\n ? enforceSpeechCredentialPairing(request.body ?? {}, currentOwner)\n : request.body ?? {}\n return sanitizeConfigOwner(\n request.params.owner,\n deps.settings.mergePatchOwner(\"config\", request.params.owner, processed),\n )\n } catch (error) {\n reply.code(400)\n return { error: error instanceof Error ? error.message : \"Invalid patch\" }\n }\n })\n\n app.get(\"/api/storage/state\", async () => deps.settings.getDoc(\"state\"))\n app.patch(\"/api/storage/state\", async (request, reply) => {\n try {\n return deps.settings.mergePatchDoc(\"state\", request.body ?? {})\n } catch (error) {\n reply.code(400)\n return { error: error instanceof Error ? error.message : \"Invalid patch\" }\n }\n })\n\n app.get<{ Params: { owner: string } }>(\"/api/storage/state/:owner\", async (request) => {\n return deps.settings.getOwner(\"state\", request.params.owner)\n })\n\n app.patch<{ Params: { owner: string } }>(\"/api/storage/state/:owner\", async (request, reply) => {\n try {\n return deps.settings.mergePatchOwner(\"state\", request.params.owner, request.body ?? {})\n } catch (error) {\n reply.code(400)\n return { error: error instanceof Error ? error.message : \"Invalid patch\" }\n }\n })\n\n // Binary validation helper (used by UI when adding binaries)\n app.post(\"/api/storage/binaries/validate\", async (request, reply) => {\n try {\n const body = ValidateBinarySchema.parse(request.body ?? {})\n return validateBinaryPath(body.path)\n } catch (error) {\n deps.logger.warn({ err: error }, \"Failed to validate binary\")\n reply.code(400)\n return { valid: false, error: error instanceof Error ? error.message : \"Invalid request\" }\n }\n })\n}\n"]}
@@ -0,0 +1,46 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ export const serviceHelp = `DESCRIPTION
5
+ Manage the background server
6
+ USAGE
7
+ opencode2 service <subcommand> [flags]
8
+ SUBCOMMANDS
9
+ start Start the background server
10
+ status Show background server status
11
+ get Get service configuration
12
+ `;
13
+ export const legacyHelp = `Commands:
14
+ opencode completion generate shell completion script
15
+ opencode [project] start opencode tui [default]
16
+ opencode attach <url> attach to a running opencode server
17
+ `;
18
+ // Actual subprocess fixture: exercises host shell/shim quoting and never starts
19
+ // a real daemon or reads user configuration. Every invocation is recorded.
20
+ export function binaryProbeFixture(options = {}) {
21
+ const directory = mkdtempSync(path.join(tmpdir(), "codenomad-binary-é ' space-"));
22
+ const script = path.join(directory, "probe.cjs");
23
+ const log = path.join(directory, "calls.jsonl");
24
+ const binary = path.join(directory, process.platform === "win32" ? "opencode.cmd" : "opencode");
25
+ writeFileSync(script, `
26
+ const fs = require('node:fs');
27
+ const args = process.argv.slice(2);
28
+ fs.appendFileSync(${JSON.stringify(log)}, JSON.stringify(args) + '\\n');
29
+ setTimeout(() => {
30
+ const version = args.join(' ') === '--version';
31
+ if (!version && args.join(' ') !== 'service --help') process.exit(99);
32
+ process[${JSON.stringify(options.stderr ? "stderr" : "stdout")}].write(version ? 'custom-build\\n' : ${JSON.stringify(options.help ?? serviceHelp)});
33
+ process.exitCode = version ? 0 : ${options.helpExit ?? 0};
34
+ }, ${options.delayMs ?? 0});
35
+ `);
36
+ const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
37
+ writeFileSync(binary, process.platform === "win32"
38
+ ? `@echo off\r\n"${process.execPath}" "%~dp0probe.cjs" %*\r\n`
39
+ : `#!/bin/sh\nexec ${quote(process.execPath)} ${quote(script)} "$@"\n`, { mode: 0o700 });
40
+ return {
41
+ binary,
42
+ calls: () => readFileSync(log, "utf8").trim().split("\n").map((line) => JSON.parse(line)),
43
+ dispose: () => rmSync(directory, { recursive: true, force: true }),
44
+ };
45
+ }
46
+ //# sourceMappingURL=binary-probe-fixture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binary-probe-fixture.js","sourceRoot":"","sources":["../../../src/workspaces/__tests__/binary-probe-fixture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;CAQ1B,CAAA;AAED,MAAM,CAAC,MAAM,UAAU,GAAG;;;;CAIzB,CAAA;AAED,gFAAgF;AAChF,2EAA2E;AAC3E,MAAM,UAAU,kBAAkB,CAAC,UAK/B,EAAE;IACJ,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,6BAA6B,CAAC,CAAC,CAAA;IACjF,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAA;IAChD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,CAAA;IAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,UAAU,CAAC,CAAA;IAC/F,aAAa,CAAC,MAAM,EAAE;;;wBAGA,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;;;;gBAI3B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,yCAAyC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,IAAI,WAAW,CAAC;yCAC/G,OAAO,CAAC,QAAQ,IAAI,CAAC;SACrD,OAAO,CAAC,OAAO,IAAI,CAAC;GAC1B,CAAC,CAAA;IACF,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAA;IACpE,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO;QAChD,CAAC,CAAC,iBAAiB,OAAO,CAAC,QAAQ,2BAA2B;QAC9D,CAAC,CAAC,mBAAmB,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAC1F,OAAO;QACL,MAAM;QACN,KAAK,EAAE,GAAe,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACrG,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;KACnE,CAAA;AACH,CAAC","sourcesContent":["import { mkdtempSync, readFileSync, rmSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport path from \"node:path\"\n\nexport const serviceHelp = `DESCRIPTION\n Manage the background server\nUSAGE\n opencode2 service <subcommand> [flags]\nSUBCOMMANDS\n start Start the background server\n status Show background server status\n get Get service configuration\n`\n\nexport const legacyHelp = `Commands:\n opencode completion generate shell completion script\n opencode [project] start opencode tui [default]\n opencode attach <url> attach to a running opencode server\n`\n\n// Actual subprocess fixture: exercises host shell/shim quoting and never starts\n// a real daemon or reads user configuration. Every invocation is recorded.\nexport function binaryProbeFixture(options: {\n help?: string\n delayMs?: number\n helpExit?: number\n stderr?: boolean\n} = {}) {\n const directory = mkdtempSync(path.join(tmpdir(), \"codenomad-binary-é ' space-\"))\n const script = path.join(directory, \"probe.cjs\")\n const log = path.join(directory, \"calls.jsonl\")\n const binary = path.join(directory, process.platform === \"win32\" ? \"opencode.cmd\" : \"opencode\")\n writeFileSync(script, `\n const fs = require('node:fs');\n const args = process.argv.slice(2);\n fs.appendFileSync(${JSON.stringify(log)}, JSON.stringify(args) + '\\\\n');\n setTimeout(() => {\n const version = args.join(' ') === '--version';\n if (!version && args.join(' ') !== 'service --help') process.exit(99);\n process[${JSON.stringify(options.stderr ? \"stderr\" : \"stdout\")}].write(version ? 'custom-build\\\\n' : ${JSON.stringify(options.help ?? serviceHelp)});\n process.exitCode = version ? 0 : ${options.helpExit ?? 0};\n }, ${options.delayMs ?? 0});\n `)\n const quote = (value: string) => `'${value.replace(/'/g, \"'\\\\''\")}'`\n writeFileSync(binary, process.platform === \"win32\"\n ? `@echo off\\r\\n\"${process.execPath}\" \"%~dp0probe.cjs\" %*\\r\\n`\n : `#!/bin/sh\\nexec ${quote(process.execPath)} ${quote(script)} \"$@\"\\n`, { mode: 0o700 })\n return {\n binary,\n calls: (): string[][] => readFileSync(log, \"utf8\").trim().split(\"\\n\").map((line) => JSON.parse(line)),\n dispose: () => rmSync(directory, { recursive: true, force: true }),\n }\n}\n"]}
@@ -0,0 +1,81 @@
1
+ import assert from "node:assert/strict";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { describe, it } from "node:test";
4
+ import { probeBinaryVersion, probeOpenCodeBinary } from "../spawn";
5
+ import { OPENCODE_V2_REQUIRED_ERROR_CODE } from "../../api-types";
6
+ import { binaryProbeFixture, legacyHelp, serviceHelp } from "./binary-probe-fixture";
7
+ describe("bounded read-only binary validation", () => {
8
+ it("keeps the event loop available during real slow version and help subprocesses", async () => {
9
+ const fixture = binaryProbeFixture({ delayMs: 250 });
10
+ try {
11
+ let completed = false;
12
+ const result = probeOpenCodeBinary(fixture.binary).then((value) => { completed = true; return value; });
13
+ await delay(50);
14
+ assert.equal(completed, false, "the probes must not block timers or other requests");
15
+ assert.deepEqual(await result, { valid: true });
16
+ assert.deepEqual(fixture.calls(), [["--version"], ["service", "--help"]]);
17
+ }
18
+ finally {
19
+ fixture.dispose();
20
+ }
21
+ });
22
+ it("rejects real legacy shims on stdout/stderr with both exit-zero and exit-one help", async () => {
23
+ for (const stderr of [false, true])
24
+ for (const helpExit of [0, 1]) {
25
+ const fixture = binaryProbeFixture({ help: legacyHelp, stderr, helpExit });
26
+ try {
27
+ assert.deepEqual(await probeOpenCodeBinary(fixture.binary), { valid: false, errorCode: OPENCODE_V2_REQUIRED_ERROR_CODE });
28
+ assert.deepEqual(fixture.calls(), [["--version"], ["service", "--help"]]);
29
+ }
30
+ finally {
31
+ fixture.dispose();
32
+ }
33
+ }
34
+ });
35
+ it("uses finite bounds for both probes without constraining custom version labels", async () => {
36
+ for (const version of ["custom-build\n", "opencode2 v0.0.0-beta-19192\n", "99.1.0\n", ""]) {
37
+ const calls = [];
38
+ const result = await probeOpenCodeBinary(process.execPath, async (spec, timeout) => {
39
+ assert.equal(timeout, 5_000);
40
+ calls.push(spec.args);
41
+ return { status: 0, stderr: spec.args[0] === "--version" ? version : serviceHelp };
42
+ });
43
+ assert.equal(result.valid, true);
44
+ assert.deepEqual(calls, [["--version"], ["service", "--help"]]);
45
+ }
46
+ });
47
+ it("preserves missing-file, permission, timeout, output-limit and nonzero diagnostics", async () => {
48
+ assert.deepEqual(await probeOpenCodeBinary(""), { valid: false, error: "Missing binary path" });
49
+ for (const stage of ["--version", "service"])
50
+ for (const code of ["ENOENT", "EACCES", "ETIMEDOUT", "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"]) {
51
+ const calls = [];
52
+ const result = await probeOpenCodeBinary(process.execPath, (spec) => {
53
+ calls.push(spec.args);
54
+ return spec.args[0] === stage
55
+ ? { status: null, error: new Error(code), stdout: legacyHelp }
56
+ : { status: 0, stdout: "2.0.0\n" };
57
+ });
58
+ assert.equal(result.valid, false);
59
+ assert.equal(result.error, code);
60
+ assert.equal(result.errorCode, undefined, "partial help must not mask an execution error");
61
+ assert.equal(calls.length, stage === "--version" ? 1 : 2);
62
+ }
63
+ const failed = await probeOpenCodeBinary(process.execPath, (spec) => spec.args[0] === "--version"
64
+ ? { status: 0, stdout: "2.0.0\n" } : { status: 7, stderr: "Permission denied" });
65
+ assert.equal(failed.valid, false);
66
+ assert.match(failed.error ?? "", /code 7: Permission denied/);
67
+ assert.equal(failed.errorCode, undefined);
68
+ const rejected = await probeOpenCodeBinary(process.execPath, async () => { throw new Error("launch failed"); });
69
+ assert.deepEqual(rejected, { valid: false, error: "launch failed" });
70
+ });
71
+ it("retains the version-only updater probe contract without probing service", () => {
72
+ const calls = [];
73
+ const result = probeBinaryVersion(process.execPath, (spec) => {
74
+ calls.push(spec.args);
75
+ return { status: 0, stderr: "1.18.25\n" };
76
+ });
77
+ assert.deepEqual(result, { valid: true, version: "1.18.25", reported: "1.18.25" });
78
+ assert.deepEqual(calls, [["--version"]]);
79
+ });
80
+ });
81
+ //# sourceMappingURL=binary-probe.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binary-probe.test.js","sourceRoot":"","sources":["../../../src/workspaces/__tests__/binary-probe.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAA;AACvC,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAClE,OAAO,EAAE,+BAA+B,EAAE,MAAM,iBAAiB,CAAA;AACjE,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAEpF,QAAQ,CAAC,qCAAqC,EAAE,GAAG,EAAE;IACnD,EAAE,CAAC,+EAA+E,EAAE,KAAK,IAAI,EAAE;QAC7F,MAAM,OAAO,GAAG,kBAAkB,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAA;QACpD,IAAI,CAAC;YACH,IAAI,SAAS,GAAG,KAAK,CAAA;YACrB,MAAM,MAAM,GAAG,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,GAAG,SAAS,GAAG,IAAI,CAAC,CAAC,OAAO,KAAK,CAAA,CAAC,CAAC,CAAC,CAAA;YACtG,MAAM,KAAK,CAAC,EAAE,CAAC,CAAA;YACf,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,EAAE,oDAAoD,CAAC,CAAA;YACpF,MAAM,CAAC,SAAS,CAAC,MAAM,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;YAC/C,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAA;QAC3E,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,OAAO,EAAE,CAAA;QACnB,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kFAAkF,EAAE,KAAK,IAAI,EAAE;QAChG,KAAK,MAAM,MAAM,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC;YAAE,KAAK,MAAM,QAAQ,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBAClE,MAAM,OAAO,GAAG,kBAAkB,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;gBAC1E,IAAI,CAAC;oBACH,MAAM,CAAC,SAAS,CAAC,MAAM,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,+BAA+B,EAAE,CAAC,CAAA;oBACzH,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAA;gBAC3E,CAAC;wBAAS,CAAC;oBACT,OAAO,CAAC,OAAO,EAAE,CAAA;gBACnB,CAAC;YACH,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+EAA+E,EAAE,KAAK,IAAI,EAAE;QAC7F,KAAK,MAAM,OAAO,IAAI,CAAC,gBAAgB,EAAE,+BAA+B,EAAE,UAAU,EAAE,EAAE,CAAC,EAAE,CAAC;YAC1F,MAAM,KAAK,GAAe,EAAE,CAAA;YAC5B,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;gBACjF,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;gBAC5B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBACrB,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;YACpF,CAAC,CAAC,CAAA;YACF,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;YAChC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAA;QACjE,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,mFAAmF,EAAE,KAAK,IAAI,EAAE;QACjG,MAAM,CAAC,SAAS,CAAC,MAAM,mBAAmB,CAAC,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAA;QAC/F,KAAK,MAAM,KAAK,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC;YAAE,KAAK,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,mCAAmC,CAAC,EAAE,CAAC;gBACxI,MAAM,KAAK,GAAe,EAAE,CAAA;gBAC5B,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE;oBAClE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;oBACrB,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,KAAK;wBAC3B,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE;wBAC9D,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;gBACtC,CAAC,CAAC,CAAA;gBACF,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;gBACjC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;gBAChC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,EAAE,+CAA+C,CAAC,CAAA;gBAC1F,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YAC3D,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,WAAW;YAC/F,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAA;QAClF,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;QACjC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,2BAA2B,CAAC,CAAA;QAC7D,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;QACzC,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE,GAAG,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;QAC9G,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAA;IACtE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yEAAyE,EAAE,GAAG,EAAE;QACjF,MAAM,KAAK,GAAe,EAAE,CAAA;QAC5B,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE;YAC3D,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACrB,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;QAC3C,CAAC,CAAC,CAAA;QACF,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAA;QAClF,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import assert from \"node:assert/strict\"\nimport { setTimeout as delay } from \"node:timers/promises\"\nimport { describe, it } from \"node:test\"\nimport { probeBinaryVersion, probeOpenCodeBinary } from \"../spawn\"\nimport { OPENCODE_V2_REQUIRED_ERROR_CODE } from \"../../api-types\"\nimport { binaryProbeFixture, legacyHelp, serviceHelp } from \"./binary-probe-fixture\"\n\ndescribe(\"bounded read-only binary validation\", () => {\n it(\"keeps the event loop available during real slow version and help subprocesses\", async () => {\n const fixture = binaryProbeFixture({ delayMs: 250 })\n try {\n let completed = false\n const result = probeOpenCodeBinary(fixture.binary).then((value) => { completed = true; return value })\n await delay(50)\n assert.equal(completed, false, \"the probes must not block timers or other requests\")\n assert.deepEqual(await result, { valid: true })\n assert.deepEqual(fixture.calls(), [[\"--version\"], [\"service\", \"--help\"]])\n } finally {\n fixture.dispose()\n }\n })\n\n it(\"rejects real legacy shims on stdout/stderr with both exit-zero and exit-one help\", async () => {\n for (const stderr of [false, true]) for (const helpExit of [0, 1]) {\n const fixture = binaryProbeFixture({ help: legacyHelp, stderr, helpExit })\n try {\n assert.deepEqual(await probeOpenCodeBinary(fixture.binary), { valid: false, errorCode: OPENCODE_V2_REQUIRED_ERROR_CODE })\n assert.deepEqual(fixture.calls(), [[\"--version\"], [\"service\", \"--help\"]])\n } finally {\n fixture.dispose()\n }\n }\n })\n\n it(\"uses finite bounds for both probes without constraining custom version labels\", async () => {\n for (const version of [\"custom-build\\n\", \"opencode2 v0.0.0-beta-19192\\n\", \"99.1.0\\n\", \"\"]) {\n const calls: string[][] = []\n const result = await probeOpenCodeBinary(process.execPath, async (spec, timeout) => {\n assert.equal(timeout, 5_000)\n calls.push(spec.args)\n return { status: 0, stderr: spec.args[0] === \"--version\" ? version : serviceHelp }\n })\n assert.equal(result.valid, true)\n assert.deepEqual(calls, [[\"--version\"], [\"service\", \"--help\"]])\n }\n })\n\n it(\"preserves missing-file, permission, timeout, output-limit and nonzero diagnostics\", async () => {\n assert.deepEqual(await probeOpenCodeBinary(\"\"), { valid: false, error: \"Missing binary path\" })\n for (const stage of [\"--version\", \"service\"]) for (const code of [\"ENOENT\", \"EACCES\", \"ETIMEDOUT\", \"ERR_CHILD_PROCESS_STDIO_MAXBUFFER\"]) {\n const calls: string[][] = []\n const result = await probeOpenCodeBinary(process.execPath, (spec) => {\n calls.push(spec.args)\n return spec.args[0] === stage\n ? { status: null, error: new Error(code), stdout: legacyHelp }\n : { status: 0, stdout: \"2.0.0\\n\" }\n })\n assert.equal(result.valid, false)\n assert.equal(result.error, code)\n assert.equal(result.errorCode, undefined, \"partial help must not mask an execution error\")\n assert.equal(calls.length, stage === \"--version\" ? 1 : 2)\n }\n const failed = await probeOpenCodeBinary(process.execPath, (spec) => spec.args[0] === \"--version\"\n ? { status: 0, stdout: \"2.0.0\\n\" } : { status: 7, stderr: \"Permission denied\" })\n assert.equal(failed.valid, false)\n assert.match(failed.error ?? \"\", /code 7: Permission denied/)\n assert.equal(failed.errorCode, undefined)\n const rejected = await probeOpenCodeBinary(process.execPath, async () => { throw new Error(\"launch failed\") })\n assert.deepEqual(rejected, { valid: false, error: \"launch failed\" })\n })\n\n it(\"retains the version-only updater probe contract without probing service\", () => {\n const calls: string[][] = []\n const result = probeBinaryVersion(process.execPath, (spec) => {\n calls.push(spec.args)\n return { status: 0, stderr: \"1.18.25\\n\" }\n })\n assert.deepEqual(result, { valid: true, version: \"1.18.25\", reported: \"1.18.25\" })\n assert.deepEqual(calls, [[\"--version\"]])\n })\n})\n"]}
@@ -4,7 +4,13 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import path from "node:path";
6
6
  import { describe, it } from "node:test";
7
- import { buildServiceLaunchSpec, buildWindowsSpawnSpec, parseWslUncPath, resolveWslHostDirectory, resolveWslServiceDirectory, resolveWslWorkingDirectory, } from "../spawn";
7
+ import { buildServiceLaunchSpec, buildWindowsSpawnSpec, parseWslUncPath, probeOpenCodeBinary, resolveWslHostDirectory, resolveWslServiceDirectory, resolveWslWorkingDirectory, } from "../spawn";
8
+ import { OPENCODE_V2_REQUIRED_ERROR_CODE } from "../../api-types";
9
+ const legacyOpenCodeHelp = `\x1b[31mCommands:\x1b[0m
10
+ opencode completion generate shell completion script
11
+ opencode [project] start opencode tui [default]
12
+ opencode attach <url> attach to a running opencode server
13
+ `;
8
14
  describe("parseWslUncPath", () => {
9
15
  it("parses WSL UNC paths into distro and linux path", () => {
10
16
  assert.deepEqual(parseWslUncPath(String.raw `\\wsl.localhost\Ubuntu\home\dev\.opencode\bin\opencode`), {
@@ -180,6 +186,38 @@ describe("buildServiceLaunchSpec", () => {
180
186
  assert.equal("binary" in launch, true);
181
187
  });
182
188
  });
189
+ describe("probeOpenCodeBinary", () => {
190
+ it("rejects a V1 binary whose root help exits successfully without a service command", async () => {
191
+ const calls = [];
192
+ const result = await probeOpenCodeBinary(process.execPath, (spec) => {
193
+ calls.push(spec.args);
194
+ return spec.args[0] === "--version"
195
+ ? { status: 0, stdout: "1.18.25\n", stderr: "" }
196
+ : { status: 0, stdout: "", stderr: legacyOpenCodeHelp };
197
+ });
198
+ assert.deepEqual(calls, [["--version"], ["service", "--help"]]);
199
+ assert.deepEqual(result, {
200
+ valid: false,
201
+ version: "1.18.25",
202
+ errorCode: OPENCODE_V2_REQUIRED_ERROR_CODE,
203
+ });
204
+ });
205
+ it("accepts a binary that supports the service lifecycle", async () => {
206
+ const result = await probeOpenCodeBinary(process.execPath, (spec) => spec.args[0] === "--version"
207
+ ? { status: 0, stdout: "opencode2 v0.0.0-beta-18999\n", stderr: "" }
208
+ : { status: 0, stdout: "USAGE\n opencode2 service <subcommand> [flags]\nSUBCOMMANDS\n start Start server\n status Server status\n get Get configuration\n", stderr: "" });
209
+ assert.deepEqual(result, { valid: true, version: "0.0.0-beta-18999" });
210
+ });
211
+ it("does not accept an empty, unrelated, or daemon-status response as capability evidence", async () => {
212
+ for (const stdout of ["", "stopped\n", "http://127.0.0.1:1234\n", "arbitrary wrapper help\n"]) {
213
+ const result = await probeOpenCodeBinary(process.execPath, (spec) => ({
214
+ status: 0, stdout: spec.args[0] === "--version" ? "2.0.0\n" : stdout,
215
+ }));
216
+ assert.equal(result.valid, false, JSON.stringify(stdout));
217
+ assert.equal(result.errorCode, undefined, "unknown output must not be mislabeled as V1");
218
+ }
219
+ });
220
+ });
183
221
  function escapeRegex(value) {
184
222
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
185
223
  }