@neurocode-ai/desktop 1.18.8

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 (253) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +19 -0
  3. package/electron-builder.config.test.ts +48 -0
  4. package/electron-builder.config.ts +146 -0
  5. package/electron.vite.config.ts +106 -0
  6. package/icons/README.md +14 -0
  7. package/icons/beta/128x128.png +0 -0
  8. package/icons/beta/128x128@2x.png +0 -0
  9. package/icons/beta/32x32.png +0 -0
  10. package/icons/beta/64x64.png +0 -0
  11. package/icons/beta/Square107x107Logo.png +0 -0
  12. package/icons/beta/Square142x142Logo.png +0 -0
  13. package/icons/beta/Square150x150Logo.png +0 -0
  14. package/icons/beta/Square284x284Logo.png +0 -0
  15. package/icons/beta/Square30x30Logo.png +0 -0
  16. package/icons/beta/Square310x310Logo.png +0 -0
  17. package/icons/beta/Square44x44Logo.png +0 -0
  18. package/icons/beta/Square71x71Logo.png +0 -0
  19. package/icons/beta/Square89x89Logo.png +0 -0
  20. package/icons/beta/StoreLogo.png +0 -0
  21. package/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  22. package/icons/beta/android/mipmap-hdpi/ic_launcher.png +0 -0
  23. package/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  24. package/icons/beta/android/mipmap-hdpi/ic_launcher_round.png +0 -0
  25. package/icons/beta/android/mipmap-mdpi/ic_launcher.png +0 -0
  26. package/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  27. package/icons/beta/android/mipmap-mdpi/ic_launcher_round.png +0 -0
  28. package/icons/beta/android/mipmap-xhdpi/ic_launcher.png +0 -0
  29. package/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  30. package/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
  31. package/icons/beta/android/mipmap-xxhdpi/ic_launcher.png +0 -0
  32. package/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  33. package/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  34. package/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
  35. package/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  36. package/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  37. package/icons/beta/android/values/ic_launcher_background.xml +4 -0
  38. package/icons/beta/dock.png +0 -0
  39. package/icons/beta/icon.icns +0 -0
  40. package/icons/beta/icon.ico +0 -0
  41. package/icons/beta/icon.png +0 -0
  42. package/icons/beta/ios/AppIcon-20x20@1x.png +0 -0
  43. package/icons/beta/ios/AppIcon-20x20@2x-1.png +0 -0
  44. package/icons/beta/ios/AppIcon-20x20@2x.png +0 -0
  45. package/icons/beta/ios/AppIcon-20x20@3x.png +0 -0
  46. package/icons/beta/ios/AppIcon-29x29@1x.png +0 -0
  47. package/icons/beta/ios/AppIcon-29x29@2x-1.png +0 -0
  48. package/icons/beta/ios/AppIcon-29x29@2x.png +0 -0
  49. package/icons/beta/ios/AppIcon-29x29@3x.png +0 -0
  50. package/icons/beta/ios/AppIcon-40x40@1x.png +0 -0
  51. package/icons/beta/ios/AppIcon-40x40@2x-1.png +0 -0
  52. package/icons/beta/ios/AppIcon-40x40@2x.png +0 -0
  53. package/icons/beta/ios/AppIcon-40x40@3x.png +0 -0
  54. package/icons/beta/ios/AppIcon-512@2x.png +0 -0
  55. package/icons/beta/ios/AppIcon-60x60@2x.png +0 -0
  56. package/icons/beta/ios/AppIcon-60x60@3x.png +0 -0
  57. package/icons/beta/ios/AppIcon-76x76@1x.png +0 -0
  58. package/icons/beta/ios/AppIcon-76x76@2x.png +0 -0
  59. package/icons/beta/ios/AppIcon-83.5x83.5@2x.png +0 -0
  60. package/icons/dev/128x128.png +0 -0
  61. package/icons/dev/128x128@2x.png +0 -0
  62. package/icons/dev/32x32.png +0 -0
  63. package/icons/dev/64x64.png +0 -0
  64. package/icons/dev/Square107x107Logo.png +0 -0
  65. package/icons/dev/Square142x142Logo.png +0 -0
  66. package/icons/dev/Square150x150Logo.png +0 -0
  67. package/icons/dev/Square284x284Logo.png +0 -0
  68. package/icons/dev/Square30x30Logo.png +0 -0
  69. package/icons/dev/Square310x310Logo.png +0 -0
  70. package/icons/dev/Square44x44Logo.png +0 -0
  71. package/icons/dev/Square71x71Logo.png +0 -0
  72. package/icons/dev/Square89x89Logo.png +0 -0
  73. package/icons/dev/StoreLogo.png +0 -0
  74. package/icons/dev/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  75. package/icons/dev/android/mipmap-hdpi/ic_launcher.png +0 -0
  76. package/icons/dev/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  77. package/icons/dev/android/mipmap-hdpi/ic_launcher_round.png +0 -0
  78. package/icons/dev/android/mipmap-mdpi/ic_launcher.png +0 -0
  79. package/icons/dev/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  80. package/icons/dev/android/mipmap-mdpi/ic_launcher_round.png +0 -0
  81. package/icons/dev/android/mipmap-xhdpi/ic_launcher.png +0 -0
  82. package/icons/dev/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  83. package/icons/dev/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
  84. package/icons/dev/android/mipmap-xxhdpi/ic_launcher.png +0 -0
  85. package/icons/dev/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  86. package/icons/dev/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  87. package/icons/dev/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
  88. package/icons/dev/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  89. package/icons/dev/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  90. package/icons/dev/android/values/ic_launcher_background.xml +4 -0
  91. package/icons/dev/dock.png +0 -0
  92. package/icons/dev/icon.icns +0 -0
  93. package/icons/dev/icon.ico +0 -0
  94. package/icons/dev/icon.png +0 -0
  95. package/icons/dev/ios/AppIcon-20x20@1x.png +0 -0
  96. package/icons/dev/ios/AppIcon-20x20@2x-1.png +0 -0
  97. package/icons/dev/ios/AppIcon-20x20@2x.png +0 -0
  98. package/icons/dev/ios/AppIcon-20x20@3x.png +0 -0
  99. package/icons/dev/ios/AppIcon-29x29@1x.png +0 -0
  100. package/icons/dev/ios/AppIcon-29x29@2x-1.png +0 -0
  101. package/icons/dev/ios/AppIcon-29x29@2x.png +0 -0
  102. package/icons/dev/ios/AppIcon-29x29@3x.png +0 -0
  103. package/icons/dev/ios/AppIcon-40x40@1x.png +0 -0
  104. package/icons/dev/ios/AppIcon-40x40@2x-1.png +0 -0
  105. package/icons/dev/ios/AppIcon-40x40@2x.png +0 -0
  106. package/icons/dev/ios/AppIcon-40x40@3x.png +0 -0
  107. package/icons/dev/ios/AppIcon-512@2x.png +0 -0
  108. package/icons/dev/ios/AppIcon-60x60@2x.png +0 -0
  109. package/icons/dev/ios/AppIcon-60x60@3x.png +0 -0
  110. package/icons/dev/ios/AppIcon-76x76@1x.png +0 -0
  111. package/icons/dev/ios/AppIcon-76x76@2x.png +0 -0
  112. package/icons/dev/ios/AppIcon-83.5x83.5@2x.png +0 -0
  113. package/icons/prod/128x128.png +0 -0
  114. package/icons/prod/128x128@2x.png +0 -0
  115. package/icons/prod/32x32.png +0 -0
  116. package/icons/prod/64x64.png +0 -0
  117. package/icons/prod/Square107x107Logo.png +0 -0
  118. package/icons/prod/Square142x142Logo.png +0 -0
  119. package/icons/prod/Square150x150Logo.png +0 -0
  120. package/icons/prod/Square284x284Logo.png +0 -0
  121. package/icons/prod/Square30x30Logo.png +0 -0
  122. package/icons/prod/Square310x310Logo.png +0 -0
  123. package/icons/prod/Square44x44Logo.png +0 -0
  124. package/icons/prod/Square71x71Logo.png +0 -0
  125. package/icons/prod/Square89x89Logo.png +0 -0
  126. package/icons/prod/StoreLogo.png +0 -0
  127. package/icons/prod/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  128. package/icons/prod/android/mipmap-hdpi/ic_launcher.png +0 -0
  129. package/icons/prod/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  130. package/icons/prod/android/mipmap-hdpi/ic_launcher_round.png +0 -0
  131. package/icons/prod/android/mipmap-mdpi/ic_launcher.png +0 -0
  132. package/icons/prod/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  133. package/icons/prod/android/mipmap-mdpi/ic_launcher_round.png +0 -0
  134. package/icons/prod/android/mipmap-xhdpi/ic_launcher.png +0 -0
  135. package/icons/prod/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  136. package/icons/prod/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
  137. package/icons/prod/android/mipmap-xxhdpi/ic_launcher.png +0 -0
  138. package/icons/prod/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  139. package/icons/prod/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  140. package/icons/prod/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
  141. package/icons/prod/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  142. package/icons/prod/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  143. package/icons/prod/android/values/ic_launcher_background.xml +4 -0
  144. package/icons/prod/dock.png +0 -0
  145. package/icons/prod/icon.icns +0 -0
  146. package/icons/prod/icon.ico +0 -0
  147. package/icons/prod/icon.png +0 -0
  148. package/icons/prod/ios/AppIcon-20x20@1x.png +0 -0
  149. package/icons/prod/ios/AppIcon-20x20@2x-1.png +0 -0
  150. package/icons/prod/ios/AppIcon-20x20@2x.png +0 -0
  151. package/icons/prod/ios/AppIcon-20x20@3x.png +0 -0
  152. package/icons/prod/ios/AppIcon-29x29@1x.png +0 -0
  153. package/icons/prod/ios/AppIcon-29x29@2x-1.png +0 -0
  154. package/icons/prod/ios/AppIcon-29x29@2x.png +0 -0
  155. package/icons/prod/ios/AppIcon-29x29@3x.png +0 -0
  156. package/icons/prod/ios/AppIcon-40x40@1x.png +0 -0
  157. package/icons/prod/ios/AppIcon-40x40@2x-1.png +0 -0
  158. package/icons/prod/ios/AppIcon-40x40@2x.png +0 -0
  159. package/icons/prod/ios/AppIcon-40x40@3x.png +0 -0
  160. package/icons/prod/ios/AppIcon-512@2x.png +0 -0
  161. package/icons/prod/ios/AppIcon-60x60@2x.png +0 -0
  162. package/icons/prod/ios/AppIcon-60x60@3x.png +0 -0
  163. package/icons/prod/ios/AppIcon-76x76@1x.png +0 -0
  164. package/icons/prod/ios/AppIcon-76x76@2x.png +0 -0
  165. package/icons/prod/ios/AppIcon-83.5x83.5@2x.png +0 -0
  166. package/package.json +75 -0
  167. package/resources/entitlements.plist +18 -0
  168. package/resources/linux/opencode-desktop.desktop +10 -0
  169. package/scripts/copy-bundles.ts +12 -0
  170. package/scripts/copy-icons.ts +12 -0
  171. package/scripts/copy-metainfo.ts +47 -0
  172. package/scripts/finalize-latest-json.ts +219 -0
  173. package/scripts/finalize-latest-yml.ts +124 -0
  174. package/scripts/prebuild.ts +10 -0
  175. package/scripts/predev.ts +5 -0
  176. package/scripts/prepare.ts +9 -0
  177. package/scripts/utils.ts +77 -0
  178. package/src/main/apps.ts +136 -0
  179. package/src/main/attachment-picker.test.ts +87 -0
  180. package/src/main/attachment-picker.ts +56 -0
  181. package/src/main/constants.ts +7 -0
  182. package/src/main/debug.ts +95 -0
  183. package/src/main/desktop-menu-actions.ts +84 -0
  184. package/src/main/env.d.ts +19 -0
  185. package/src/main/index.test.ts +37 -0
  186. package/src/main/index.ts +399 -0
  187. package/src/main/initialization.ts +6 -0
  188. package/src/main/install-state.test.ts +19 -0
  189. package/src/main/install-state.ts +8 -0
  190. package/src/main/ipc.ts +274 -0
  191. package/src/main/logging.ts +205 -0
  192. package/src/main/markdown.ts +16 -0
  193. package/src/main/menu.ts +67 -0
  194. package/src/main/migrate.ts +91 -0
  195. package/src/main/onboarding.ts +45 -0
  196. package/src/main/server.ts +238 -0
  197. package/src/main/shell-env.test.ts +50 -0
  198. package/src/main/shell-env.ts +101 -0
  199. package/src/main/sidecar.ts +157 -0
  200. package/src/main/store-cleanup.test.ts +93 -0
  201. package/src/main/store-cleanup.ts +94 -0
  202. package/src/main/store-keys.ts +7 -0
  203. package/src/main/store.ts +35 -0
  204. package/src/main/unresponsive.ts +70 -0
  205. package/src/main/updater-controller.test.ts +111 -0
  206. package/src/main/updater-controller.ts +97 -0
  207. package/src/main/updater-subscriptions.test.ts +16 -0
  208. package/src/main/updater-subscriptions.ts +20 -0
  209. package/src/main/updater.ts +85 -0
  210. package/src/main/window-registry.test.ts +91 -0
  211. package/src/main/window-registry.ts +47 -0
  212. package/src/main/window-state.ts +29 -0
  213. package/src/main/windows.ts +496 -0
  214. package/src/main/wsl/ipc.ts +103 -0
  215. package/src/main/wsl/policy.ts +33 -0
  216. package/src/main/wsl/runtime.ts +400 -0
  217. package/src/main/wsl/servers.test.ts +231 -0
  218. package/src/main/wsl/servers.ts +521 -0
  219. package/src/main/wsl/sidecar.ts +129 -0
  220. package/src/main/wsl/startup.ts +31 -0
  221. package/src/preload/index.ts +128 -0
  222. package/src/preload/types.ts +109 -0
  223. package/src/renderer/cli.ts +12 -0
  224. package/src/renderer/env.d.ts +10 -0
  225. package/src/renderer/html.test.ts +62 -0
  226. package/src/renderer/i18n/ar.ts +26 -0
  227. package/src/renderer/i18n/br.ts +27 -0
  228. package/src/renderer/i18n/bs.ts +28 -0
  229. package/src/renderer/i18n/da.ts +28 -0
  230. package/src/renderer/i18n/de.ts +28 -0
  231. package/src/renderer/i18n/en.ts +27 -0
  232. package/src/renderer/i18n/es.ts +27 -0
  233. package/src/renderer/i18n/fr.ts +28 -0
  234. package/src/renderer/i18n/index.ts +194 -0
  235. package/src/renderer/i18n/ja.ts +28 -0
  236. package/src/renderer/i18n/ko.ts +27 -0
  237. package/src/renderer/i18n/no.ts +28 -0
  238. package/src/renderer/i18n/pl.ts +28 -0
  239. package/src/renderer/i18n/ru.ts +27 -0
  240. package/src/renderer/i18n/uk.ts +28 -0
  241. package/src/renderer/i18n/zh.ts +26 -0
  242. package/src/renderer/i18n/zht.ts +26 -0
  243. package/src/renderer/index.html +21 -0
  244. package/src/renderer/index.tsx +464 -0
  245. package/src/renderer/initialization.test.ts +73 -0
  246. package/src/renderer/initialization.ts +22 -0
  247. package/src/renderer/onboarding.tsx +53 -0
  248. package/src/renderer/styles.css +0 -0
  249. package/src/renderer/webview-zoom.ts +138 -0
  250. package/src/renderer/wsl/connections.test.ts +43 -0
  251. package/src/renderer/wsl/connections.ts +28 -0
  252. package/sst-env.d.ts +10 -0
  253. package/tsconfig.json +23 -0
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bun
2
+ import { $ } from "bun"
3
+
4
+ import { resolveChannel } from "./utils"
5
+
6
+ const channel = resolveChannel()
7
+ await $`bun ./scripts/copy-icons.ts ${channel}`
8
+ await $`bun ./scripts/copy-metainfo.ts ${channel}`
9
+
10
+ await $`cd ../opencode && bun script/build-node.ts`
@@ -0,0 +1,5 @@
1
+ import { $ } from "bun"
2
+
3
+ await $`bun ./scripts/copy-icons.ts ${process.env.OPENCODE_CHANNEL ?? "dev"}`
4
+
5
+ await $`cd ../opencode && bun script/build-node.ts`
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bun
2
+ import { Script } from "@neurocode-ai/script"
3
+
4
+ await import("./prebuild")
5
+
6
+ const pkg = await Bun.file("./package.json").json()
7
+ pkg.version = Script.version
8
+ await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n")
9
+ console.log(`Updated package.json version to ${Script.version}`)
@@ -0,0 +1,77 @@
1
+ import { $ } from "bun"
2
+
3
+ export type Channel = "dev" | "beta" | "prod"
4
+
5
+ export function resolveChannel(): Channel {
6
+ const raw = Bun.env.OPENCODE_CHANNEL
7
+ if (raw === "dev" || raw === "beta" || raw === "prod") return raw
8
+ return "dev"
9
+ }
10
+
11
+ export const SIDECAR_BINARIES: Array<{ rustTarget: string; ocBinary: string; assetExt: string }> = [
12
+ {
13
+ rustTarget: "aarch64-apple-darwin",
14
+ ocBinary: "opencode-darwin-arm64",
15
+ assetExt: "zip",
16
+ },
17
+ {
18
+ rustTarget: "x86_64-apple-darwin",
19
+ ocBinary: "opencode-darwin-x64-baseline",
20
+ assetExt: "zip",
21
+ },
22
+ {
23
+ rustTarget: "aarch64-pc-windows-msvc",
24
+ ocBinary: "opencode-windows-arm64",
25
+ assetExt: "zip",
26
+ },
27
+ {
28
+ rustTarget: "x86_64-pc-windows-msvc",
29
+ ocBinary: "opencode-windows-x64-baseline",
30
+ assetExt: "zip",
31
+ },
32
+ {
33
+ rustTarget: "x86_64-unknown-linux-gnu",
34
+ ocBinary: "opencode-linux-x64-baseline",
35
+ assetExt: "tar.gz",
36
+ },
37
+ {
38
+ rustTarget: "aarch64-unknown-linux-gnu",
39
+ ocBinary: "opencode-linux-arm64",
40
+ assetExt: "tar.gz",
41
+ },
42
+ ]
43
+
44
+ export const RUST_TARGET = Bun.env.RUST_TARGET
45
+
46
+ function nativeTarget() {
47
+ const { platform, arch } = process
48
+ if (platform === "darwin") return arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin"
49
+ if (platform === "win32") return arch === "arm64" ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc"
50
+ if (platform === "linux") return arch === "arm64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu"
51
+ throw new Error(`Unsupported platform: ${platform}/${arch}`)
52
+ }
53
+
54
+ export function getCurrentSidecar(target = RUST_TARGET ?? nativeTarget()) {
55
+ const binaryConfig = SIDECAR_BINARIES.find((b) => b.rustTarget === target)
56
+ if (!binaryConfig) throw new Error(`Sidecar configuration not available for Rust target '${target}'`)
57
+
58
+ return binaryConfig
59
+ }
60
+
61
+ export async function copyBinaryToSidecarFolder(source: string) {
62
+ const dir = `resources`
63
+ await $`mkdir -p ${dir}`
64
+ const dest = windowsify(`${dir}/opencode-cli`)
65
+ await $`cp ${source} ${dest}`
66
+ if (process.platform === "win32" && process.env.GITHUB_ACTIONS === "true") {
67
+ await $`pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File ../../script/sign-windows.ps1 ${dest}`
68
+ }
69
+ if (process.platform === "darwin") await $`codesign --force --sign - ${dest}`
70
+
71
+ console.log(`Copied ${source} to ${dest}`)
72
+ }
73
+
74
+ export function windowsify(path: string) {
75
+ if (path.endsWith(".exe")) return path
76
+ return `${path}${process.platform === "win32" ? ".exe" : ""}`
77
+ }
@@ -0,0 +1,136 @@
1
+ import { execFile } from "node:child_process"
2
+ import { access, readFile, readdir } from "node:fs/promises"
3
+ import { dirname, extname, join } from "node:path"
4
+ import util from "node:util"
5
+
6
+ const execFilePromise = util.promisify(execFile)
7
+
8
+ const exists = (path: string) =>
9
+ access(path)
10
+ .then(() => true)
11
+ .catch(() => false)
12
+
13
+ export function checkAppExists(appName: string) {
14
+ if (process.platform === "win32") return true
15
+ if (process.platform === "linux") return true
16
+ return checkMacosApp(appName)
17
+ }
18
+
19
+ export function resolveAppPath(appName: string) {
20
+ if (process.platform !== "win32") return appName
21
+ return resolveWindowsAppPath(appName)
22
+ }
23
+
24
+ async function checkMacosApp(appName: string) {
25
+ const locations = [`/Applications/${appName}.app`, `/System/Applications/${appName}.app`]
26
+
27
+ const home = process.env.HOME
28
+ if (home) locations.push(`${home}/Applications/${appName}.app`)
29
+
30
+ for (const location of locations) {
31
+ if (await exists(location)) return true
32
+ }
33
+
34
+ return execFilePromise("which", [appName])
35
+ .then(() => true)
36
+ .catch(() => false)
37
+ }
38
+
39
+ async function resolveWindowsAppPath(appName: string): Promise<string | null> {
40
+ let output: string
41
+ try {
42
+ output = await execFilePromise("where", [appName]).then((r) => r.stdout.toString())
43
+ } catch {
44
+ return null
45
+ }
46
+
47
+ const paths = output
48
+ .split(/\r?\n/)
49
+ .map((line) => line.trim())
50
+ .filter((line) => line.length > 0)
51
+
52
+ const hasExt = (path: string, ext: string) => extname(path).toLowerCase() === `.${ext}`
53
+
54
+ const exe = paths.find((path) => hasExt(path, "exe"))
55
+ if (exe) return exe
56
+
57
+ const resolveCmd = async (path: string) => {
58
+ const content = await readFile(path, "utf8")
59
+ for (const token of content.split('"').map((value: string) => value.trim())) {
60
+ const lower = token.toLowerCase()
61
+ if (!lower.includes(".exe")) continue
62
+
63
+ const index = lower.indexOf("%~dp0")
64
+ if (index >= 0) {
65
+ const base = dirname(path)
66
+ const suffix = token.slice(index + 5)
67
+ const resolved = suffix
68
+ .replace(/\//g, "\\")
69
+ .split("\\")
70
+ .filter((part: string) => part && part !== ".")
71
+ .reduce((current: string, part: string) => {
72
+ if (part === "..") return dirname(current)
73
+ return join(current, part)
74
+ }, base)
75
+
76
+ if (await exists(resolved)) return resolved
77
+ }
78
+
79
+ if (await exists(token)) return token
80
+ }
81
+
82
+ return null
83
+ }
84
+
85
+ for (const path of paths) {
86
+ if (hasExt(path, "cmd") || hasExt(path, "bat")) {
87
+ const resolved = await resolveCmd(path)
88
+ if (resolved) return resolved
89
+ }
90
+
91
+ if (!extname(path)) {
92
+ const cmd = `${path}.cmd`
93
+ if (await exists(cmd)) {
94
+ const resolved = await resolveCmd(cmd)
95
+ if (resolved) return resolved
96
+ }
97
+
98
+ const bat = `${path}.bat`
99
+ if (await exists(bat)) {
100
+ const resolved = await resolveCmd(bat)
101
+ if (resolved) return resolved
102
+ }
103
+ }
104
+ }
105
+
106
+ const key = appName
107
+ .split("")
108
+ .filter((value: string) => /[a-z0-9]/i.test(value))
109
+ .map((value: string) => value.toLowerCase())
110
+ .join("")
111
+
112
+ if (key) {
113
+ for (const path of paths) {
114
+ const dirs = [dirname(path), dirname(dirname(path)), dirname(dirname(dirname(path)))]
115
+ for (const dir of dirs) {
116
+ try {
117
+ for (const entry of await readdir(dir)) {
118
+ const candidate = join(dir, entry)
119
+ if (!hasExt(candidate, "exe")) continue
120
+ const stem = entry.replace(/\.exe$/i, "")
121
+ const name = stem
122
+ .split("")
123
+ .filter((value: string) => /[a-z0-9]/i.test(value))
124
+ .map((value: string) => value.toLowerCase())
125
+ .join("")
126
+ if (name.includes(key) || key.includes(name)) return candidate
127
+ }
128
+ } catch {
129
+ continue
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ return paths[0] ?? null
136
+ }
@@ -0,0 +1,87 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { mkdtemp, rm, truncate, writeFile } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import { join } from "node:path"
5
+ import {
6
+ assertAttachmentBudget,
7
+ createPickedFileAuthorizations,
8
+ MAX_ATTACHMENT_BYTES,
9
+ readAttachment,
10
+ } from "./attachment-picker"
11
+
12
+ describe("assertAttachmentBudget", () => {
13
+ test("accepts selections within the media ingest limit", () => {
14
+ expect(() =>
15
+ assertAttachmentBudget([{ size: MAX_ATTACHMENT_BYTES / 2 }, { size: MAX_ATTACHMENT_BYTES / 2 }]),
16
+ ).not.toThrow()
17
+ })
18
+
19
+ test("rejects the selection before files are read when its total exceeds the limit", () => {
20
+ expect(() => assertAttachmentBudget([{ size: MAX_ATTACHMENT_BYTES }, { size: 1 }])).toThrow("20 MB limit")
21
+ })
22
+
23
+ test("reads an approved file through a bounded buffer", async () => {
24
+ const directory = await mkdtemp(join(tmpdir(), "opencode-attachment-"))
25
+ const file = join(directory, "example.txt")
26
+ try {
27
+ await writeFile(file, "lorem ipsum")
28
+ expect(new TextDecoder().decode(await readAttachment(file))).toBe("lorem ipsum")
29
+ } finally {
30
+ await rm(directory, { recursive: true, force: true })
31
+ }
32
+ })
33
+
34
+ test("rejects an oversized file before allocating its contents", async () => {
35
+ const directory = await mkdtemp(join(tmpdir(), "opencode-attachment-"))
36
+ const file = join(directory, "oversized.txt")
37
+ try {
38
+ await writeFile(file, "")
39
+ await truncate(file, MAX_ATTACHMENT_BYTES + 1)
40
+ await expect(readAttachment(file)).rejects.toThrow("20 MB limit")
41
+ } finally {
42
+ await rm(directory, { recursive: true, force: true })
43
+ }
44
+ })
45
+ })
46
+
47
+ describe("picked file authorizations", () => {
48
+ const read = async (path: string) => new TextEncoder().encode(path).buffer
49
+
50
+ test("keeps concurrent picker selections isolated", async () => {
51
+ const authorizations = createPickedFileAuthorizations(read)
52
+ const first = authorizations.add(1, ["a.txt", "b.txt"])
53
+ const second = authorizations.add(1, ["c.txt"])
54
+
55
+ expect(new TextDecoder().decode(await authorizations.read(1, first, "a.txt"))).toBe("a.txt")
56
+ expect(new TextDecoder().decode(await authorizations.read(1, second, "c.txt"))).toBe("c.txt")
57
+ expect(new TextDecoder().decode(await authorizations.read(1, first, "b.txt"))).toBe("b.txt")
58
+ })
59
+
60
+ test("releases unread files for one picker without affecting another", async () => {
61
+ const authorizations = createPickedFileAuthorizations(read)
62
+ const first = authorizations.add(1, ["a.txt"])
63
+ const second = authorizations.add(1, ["b.txt"])
64
+ authorizations.release(1, first)
65
+
66
+ await expect(authorizations.read(1, first, "a.txt")).rejects.toThrow("not selected")
67
+ expect(new TextDecoder().decode(await authorizations.read(1, second, "b.txt"))).toBe("b.txt")
68
+ })
69
+
70
+ test("keeps picker tokens scoped to their renderer", async () => {
71
+ const authorizations = createPickedFileAuthorizations(read)
72
+ const token = authorizations.add(1, ["a.txt"])
73
+
74
+ await expect(authorizations.read(2, token, "a.txt")).rejects.toThrow("not selected")
75
+ })
76
+
77
+ test("charges actual reads against the selection budget", async () => {
78
+ const authorizations = createPickedFileAuthorizations(async (_path, maxBytes) => {
79
+ if (6 > maxBytes) throw new Error("budget exceeded")
80
+ return new ArrayBuffer(6)
81
+ }, 10)
82
+ const token = authorizations.add(1, ["a.txt", "b.txt"])
83
+
84
+ await authorizations.read(1, token, "a.txt")
85
+ await expect(authorizations.read(1, token, "b.txt")).rejects.toThrow("budget exceeded")
86
+ })
87
+ })
@@ -0,0 +1,56 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import { open } from "node:fs/promises"
3
+
4
+ export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024
5
+
6
+ export function createPickedFileAuthorizations(
7
+ read: (path: string, maxBytes: number) => Promise<ArrayBuffer> = readAttachment,
8
+ budget = MAX_ATTACHMENT_BYTES,
9
+ ) {
10
+ const selections = new Map<string, { sender: number; paths: Set<string>; remaining: number }>()
11
+
12
+ return {
13
+ add(sender: number, paths: string[]) {
14
+ const token = randomUUID()
15
+ selections.set(token, { sender, paths: new Set(paths), remaining: budget })
16
+ return token
17
+ },
18
+ async read(sender: number, token: string, path: string) {
19
+ const selection = selections.get(token)
20
+ if (selection?.sender !== sender || !selection.paths.delete(path))
21
+ throw new Error("File was not selected by the picker")
22
+ const bytes = await read(path, selection.remaining)
23
+ selection.remaining -= bytes.byteLength
24
+ if (selection.paths.size === 0) selections.delete(token)
25
+ return bytes
26
+ },
27
+ release(sender: number, token: string) {
28
+ if (selections.get(token)?.sender === sender) selections.delete(token)
29
+ },
30
+ }
31
+ }
32
+
33
+ export function assertAttachmentBudget(files: { size: number }[]) {
34
+ const total = files.reduce((sum, file) => sum + file.size, 0)
35
+ if (total <= MAX_ATTACHMENT_BYTES) return
36
+ throw new Error(`Selected attachments exceed the ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB limit`)
37
+ }
38
+
39
+ export async function readAttachment(filePath: string, maxBytes = MAX_ATTACHMENT_BYTES) {
40
+ const file = await open(filePath, "r")
41
+ try {
42
+ const info = await file.stat()
43
+ if (info.size > maxBytes)
44
+ throw new Error(`Selected attachments exceed the ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB limit`)
45
+ const bytes = Buffer.allocUnsafe(info.size)
46
+ let offset = 0
47
+ while (offset < info.size) {
48
+ const result = await file.read(bytes, offset, info.size - offset, offset)
49
+ if (result.bytesRead === 0) break
50
+ offset += result.bytesRead
51
+ }
52
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + offset) as ArrayBuffer
53
+ } finally {
54
+ await file.close()
55
+ }
56
+ }
@@ -0,0 +1,7 @@
1
+ import { app } from "electron"
2
+
3
+ type Channel = "dev" | "beta" | "prod"
4
+ const raw = import.meta.env.NEUROCODE_CHANNEL
5
+ export const CHANNEL: Channel = raw === "dev" || raw === "beta" || raw === "prod" ? raw : "dev"
6
+
7
+ export const UPDATER_ENABLED = app.isPackaged && CHANNEL !== "dev"
@@ -0,0 +1,95 @@
1
+ import type { WebContents } from "electron"
2
+
3
+ const focusDebuggerOwners = new WeakSet<WebContents>()
4
+ const forcedFocusNodes = new WeakMap<WebContents, number[]>()
5
+ const focusableSelector = `
6
+ a[href],
7
+ button:not([disabled]),
8
+ input:not([disabled]),
9
+ select:not([disabled]),
10
+ textarea:not([disabled]),
11
+ summary,
12
+ [contenteditable="true"],
13
+ [tabindex]:not([tabindex="-1"])
14
+ `
15
+
16
+ export async function setForceFocus(contents: WebContents, enabled: boolean) {
17
+ const debuggerApi = contents.debugger
18
+ if (!debuggerApi.isAttached()) {
19
+ if (!enabled) {
20
+ focusDebuggerOwners.delete(contents)
21
+ forcedFocusNodes.delete(contents)
22
+ return
23
+ }
24
+ debuggerApi.attach("1.3")
25
+ focusDebuggerOwners.add(contents)
26
+ debuggerApi.once("detach", () => {
27
+ focusDebuggerOwners.delete(contents)
28
+ forcedFocusNodes.delete(contents)
29
+ })
30
+ }
31
+
32
+ if (!enabled) {
33
+ await Promise.allSettled(
34
+ (forcedFocusNodes.get(contents) ?? []).map((nodeId) =>
35
+ debuggerApi.sendCommand("CSS.forcePseudoState", {
36
+ nodeId,
37
+ forcedPseudoClasses: [],
38
+ }),
39
+ ),
40
+ )
41
+ forcedFocusNodes.delete(contents)
42
+ if (!focusDebuggerOwners.delete(contents)) return
43
+ debuggerApi.detach()
44
+ return
45
+ }
46
+
47
+ await debuggerApi.sendCommand("DOM.enable")
48
+ await debuggerApi.sendCommand("CSS.enable")
49
+ const document: unknown = await debuggerApi.sendCommand("DOM.getDocument", {
50
+ depth: -1,
51
+ pierce: true,
52
+ })
53
+ const nodes: unknown = await debuggerApi.sendCommand("DOM.querySelectorAll", {
54
+ nodeId: readDocumentNodeId(document),
55
+ selector: focusableSelector,
56
+ })
57
+ const nodeIds = readNodeIds(nodes)
58
+ forcedFocusNodes.set(contents, [...new Set([...(forcedFocusNodes.get(contents) ?? []), ...nodeIds])])
59
+ await Promise.allSettled(
60
+ nodeIds.map((nodeId) =>
61
+ debuggerApi.sendCommand("CSS.forcePseudoState", {
62
+ nodeId,
63
+ forcedPseudoClasses: ["focus", "focus-visible"],
64
+ }),
65
+ ),
66
+ )
67
+ }
68
+
69
+ function readDocumentNodeId(value: unknown) {
70
+ if (
71
+ !value ||
72
+ typeof value !== "object" ||
73
+ !("root" in value) ||
74
+ !value.root ||
75
+ typeof value.root !== "object" ||
76
+ !("nodeId" in value.root) ||
77
+ typeof value.root.nodeId !== "number"
78
+ ) {
79
+ throw new Error("Invalid DOM.getDocument response")
80
+ }
81
+ return value.root.nodeId
82
+ }
83
+
84
+ function readNodeIds(value: unknown) {
85
+ if (
86
+ !value ||
87
+ typeof value !== "object" ||
88
+ !("nodeIds" in value) ||
89
+ !Array.isArray(value.nodeIds) ||
90
+ !value.nodeIds.every((nodeId) => typeof nodeId === "number")
91
+ ) {
92
+ throw new Error("Invalid DOM.querySelectorAll response")
93
+ }
94
+ return value.nodeIds
95
+ }
@@ -0,0 +1,84 @@
1
+ import { BrowserWindow } from "electron"
2
+ import type { DesktopMenuAction } from "@neurocode-ai/app/desktop-menu"
3
+ import { createMainWindow, updateTitlebar } from "./windows"
4
+
5
+ export type DesktopMenuActionHandlers = Partial<{
6
+ checkForUpdates: () => void
7
+ relaunch: () => void
8
+ }>
9
+
10
+ export function runDesktopMenuAction(
11
+ win: BrowserWindow | null,
12
+ action: DesktopMenuAction,
13
+ handlers: DesktopMenuActionHandlers = {},
14
+ ) {
15
+ switch (action) {
16
+ case "app.checkForUpdates":
17
+ handlers.checkForUpdates?.()
18
+ return
19
+ case "app.relaunch":
20
+ handlers.relaunch?.()
21
+ return
22
+ case "window.new":
23
+ createMainWindow()
24
+ return
25
+ case "window.close":
26
+ win?.close()
27
+ return
28
+ case "window.minimize":
29
+ win?.minimize()
30
+ return
31
+ case "window.toggleMaximize":
32
+ if (win?.isMaximized()) {
33
+ win.unmaximize()
34
+ return
35
+ }
36
+ win?.maximize()
37
+ return
38
+ case "view.reload":
39
+ win?.reload()
40
+ return
41
+ case "view.toggleDevTools":
42
+ win?.webContents.toggleDevTools()
43
+ return
44
+ case "view.resetZoom":
45
+ setZoom(win, 1)
46
+ return
47
+ case "view.zoomIn":
48
+ setZoom(win, (win?.webContents.getZoomFactor() ?? 1) + 0.2)
49
+ return
50
+ case "view.zoomOut":
51
+ setZoom(win, (win?.webContents.getZoomFactor() ?? 1) - 0.2)
52
+ return
53
+ case "view.toggleFullscreen":
54
+ win?.setFullScreen(!win.isFullScreen())
55
+ return
56
+ case "edit.undo":
57
+ win?.webContents.undo()
58
+ return
59
+ case "edit.redo":
60
+ win?.webContents.redo()
61
+ return
62
+ case "edit.cut":
63
+ win?.webContents.cut()
64
+ return
65
+ case "edit.copy":
66
+ win?.webContents.copy()
67
+ return
68
+ case "edit.paste":
69
+ win?.webContents.paste()
70
+ return
71
+ case "edit.delete":
72
+ win?.webContents.delete()
73
+ return
74
+ case "edit.selectAll":
75
+ win?.webContents.selectAll()
76
+ return
77
+ }
78
+ }
79
+
80
+ function setZoom(win: BrowserWindow | null, value: number) {
81
+ if (!win) return
82
+ win.webContents.setZoomFactor(Math.min(Math.max(value, 0.2), 10))
83
+ updateTitlebar(win)
84
+ }
@@ -0,0 +1,19 @@
1
+ interface ImportMetaEnv {
2
+ readonly NEUROCODE_CHANNEL: string
3
+ }
4
+
5
+ interface ImportMeta {
6
+ readonly env: ImportMetaEnv
7
+ }
8
+
9
+ declare module "virtual:neurocode-server" {
10
+ export namespace Server {
11
+ export const listen: typeof import("../../../opencode/dist/types/src/node").Server.listen
12
+ export type Listener = import("../../../opencode/dist/types/src/node").Server.Listener
13
+ }
14
+ export namespace Config {
15
+ export const get: typeof import("../../../opencode/dist/types/src/node").Config.get
16
+ export type Info = import("../../../opencode/dist/types/src/node").Config.Info
17
+ }
18
+ export const bootstrap: typeof import("../../../opencode/dist/types/src/node").bootstrap
19
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { Cause, Deferred, Effect, Exit, Fiber } from "effect"
3
+ import { forwardInitializationFailure } from "./initialization"
4
+
5
+ describe("desktop initialization", () => {
6
+ const failure = new Error("sidecar startup failed")
7
+ const expectFailure = (exit: Exit.Exit<unknown, unknown>) => {
8
+ expect(Exit.isFailure(exit)).toBe(true)
9
+ if (Exit.isSuccess(exit)) return
10
+ expect(Cause.squash(exit.cause)).toBe(failure)
11
+ }
12
+
13
+ test("forwards loading task failures before renderer initialization", () => {
14
+ const exit = Effect.runSync(
15
+ Effect.gen(function* () {
16
+ const initialization = yield* Deferred.make<never, unknown>()
17
+ yield* forwardInitializationFailure(initialization)(Effect.die(failure)).pipe(Effect.exit)
18
+ return yield* Deferred.await(initialization).pipe(Effect.exit)
19
+ }),
20
+ )
21
+
22
+ expectFailure(exit)
23
+ })
24
+
25
+ test("forwards loading task failures while renderer initialization waits", () => {
26
+ const exit = Effect.runSync(
27
+ Effect.gen(function* () {
28
+ const initialization = yield* Deferred.make<never, unknown>()
29
+ const waiting = yield* Deferred.await(initialization).pipe(Effect.exit, Effect.forkChild)
30
+ yield* forwardInitializationFailure(initialization)(Effect.die(failure)).pipe(Effect.exit)
31
+ return yield* Fiber.join(waiting)
32
+ }),
33
+ )
34
+
35
+ expectFailure(exit)
36
+ })
37
+ })