@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.
- package/AGENTS.md +4 -0
- package/README.md +19 -0
- package/electron-builder.config.test.ts +48 -0
- package/electron-builder.config.ts +146 -0
- package/electron.vite.config.ts +106 -0
- package/icons/README.md +14 -0
- package/icons/beta/128x128.png +0 -0
- package/icons/beta/128x128@2x.png +0 -0
- package/icons/beta/32x32.png +0 -0
- package/icons/beta/64x64.png +0 -0
- package/icons/beta/Square107x107Logo.png +0 -0
- package/icons/beta/Square142x142Logo.png +0 -0
- package/icons/beta/Square150x150Logo.png +0 -0
- package/icons/beta/Square284x284Logo.png +0 -0
- package/icons/beta/Square30x30Logo.png +0 -0
- package/icons/beta/Square310x310Logo.png +0 -0
- package/icons/beta/Square44x44Logo.png +0 -0
- package/icons/beta/Square71x71Logo.png +0 -0
- package/icons/beta/Square89x89Logo.png +0 -0
- package/icons/beta/StoreLogo.png +0 -0
- package/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/icons/beta/android/mipmap-hdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/mipmap-mdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/mipmap-xhdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/values/ic_launcher_background.xml +4 -0
- package/icons/beta/dock.png +0 -0
- package/icons/beta/icon.icns +0 -0
- package/icons/beta/icon.ico +0 -0
- package/icons/beta/icon.png +0 -0
- package/icons/beta/ios/AppIcon-20x20@1x.png +0 -0
- package/icons/beta/ios/AppIcon-20x20@2x-1.png +0 -0
- package/icons/beta/ios/AppIcon-20x20@2x.png +0 -0
- package/icons/beta/ios/AppIcon-20x20@3x.png +0 -0
- package/icons/beta/ios/AppIcon-29x29@1x.png +0 -0
- package/icons/beta/ios/AppIcon-29x29@2x-1.png +0 -0
- package/icons/beta/ios/AppIcon-29x29@2x.png +0 -0
- package/icons/beta/ios/AppIcon-29x29@3x.png +0 -0
- package/icons/beta/ios/AppIcon-40x40@1x.png +0 -0
- package/icons/beta/ios/AppIcon-40x40@2x-1.png +0 -0
- package/icons/beta/ios/AppIcon-40x40@2x.png +0 -0
- package/icons/beta/ios/AppIcon-40x40@3x.png +0 -0
- package/icons/beta/ios/AppIcon-512@2x.png +0 -0
- package/icons/beta/ios/AppIcon-60x60@2x.png +0 -0
- package/icons/beta/ios/AppIcon-60x60@3x.png +0 -0
- package/icons/beta/ios/AppIcon-76x76@1x.png +0 -0
- package/icons/beta/ios/AppIcon-76x76@2x.png +0 -0
- package/icons/beta/ios/AppIcon-83.5x83.5@2x.png +0 -0
- package/icons/dev/128x128.png +0 -0
- package/icons/dev/128x128@2x.png +0 -0
- package/icons/dev/32x32.png +0 -0
- package/icons/dev/64x64.png +0 -0
- package/icons/dev/Square107x107Logo.png +0 -0
- package/icons/dev/Square142x142Logo.png +0 -0
- package/icons/dev/Square150x150Logo.png +0 -0
- package/icons/dev/Square284x284Logo.png +0 -0
- package/icons/dev/Square30x30Logo.png +0 -0
- package/icons/dev/Square310x310Logo.png +0 -0
- package/icons/dev/Square44x44Logo.png +0 -0
- package/icons/dev/Square71x71Logo.png +0 -0
- package/icons/dev/Square89x89Logo.png +0 -0
- package/icons/dev/StoreLogo.png +0 -0
- package/icons/dev/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/icons/dev/android/mipmap-hdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/mipmap-mdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/mipmap-xhdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/values/ic_launcher_background.xml +4 -0
- package/icons/dev/dock.png +0 -0
- package/icons/dev/icon.icns +0 -0
- package/icons/dev/icon.ico +0 -0
- package/icons/dev/icon.png +0 -0
- package/icons/dev/ios/AppIcon-20x20@1x.png +0 -0
- package/icons/dev/ios/AppIcon-20x20@2x-1.png +0 -0
- package/icons/dev/ios/AppIcon-20x20@2x.png +0 -0
- package/icons/dev/ios/AppIcon-20x20@3x.png +0 -0
- package/icons/dev/ios/AppIcon-29x29@1x.png +0 -0
- package/icons/dev/ios/AppIcon-29x29@2x-1.png +0 -0
- package/icons/dev/ios/AppIcon-29x29@2x.png +0 -0
- package/icons/dev/ios/AppIcon-29x29@3x.png +0 -0
- package/icons/dev/ios/AppIcon-40x40@1x.png +0 -0
- package/icons/dev/ios/AppIcon-40x40@2x-1.png +0 -0
- package/icons/dev/ios/AppIcon-40x40@2x.png +0 -0
- package/icons/dev/ios/AppIcon-40x40@3x.png +0 -0
- package/icons/dev/ios/AppIcon-512@2x.png +0 -0
- package/icons/dev/ios/AppIcon-60x60@2x.png +0 -0
- package/icons/dev/ios/AppIcon-60x60@3x.png +0 -0
- package/icons/dev/ios/AppIcon-76x76@1x.png +0 -0
- package/icons/dev/ios/AppIcon-76x76@2x.png +0 -0
- package/icons/dev/ios/AppIcon-83.5x83.5@2x.png +0 -0
- package/icons/prod/128x128.png +0 -0
- package/icons/prod/128x128@2x.png +0 -0
- package/icons/prod/32x32.png +0 -0
- package/icons/prod/64x64.png +0 -0
- package/icons/prod/Square107x107Logo.png +0 -0
- package/icons/prod/Square142x142Logo.png +0 -0
- package/icons/prod/Square150x150Logo.png +0 -0
- package/icons/prod/Square284x284Logo.png +0 -0
- package/icons/prod/Square30x30Logo.png +0 -0
- package/icons/prod/Square310x310Logo.png +0 -0
- package/icons/prod/Square44x44Logo.png +0 -0
- package/icons/prod/Square71x71Logo.png +0 -0
- package/icons/prod/Square89x89Logo.png +0 -0
- package/icons/prod/StoreLogo.png +0 -0
- package/icons/prod/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/icons/prod/android/mipmap-hdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/mipmap-mdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/mipmap-xhdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/values/ic_launcher_background.xml +4 -0
- package/icons/prod/dock.png +0 -0
- package/icons/prod/icon.icns +0 -0
- package/icons/prod/icon.ico +0 -0
- package/icons/prod/icon.png +0 -0
- package/icons/prod/ios/AppIcon-20x20@1x.png +0 -0
- package/icons/prod/ios/AppIcon-20x20@2x-1.png +0 -0
- package/icons/prod/ios/AppIcon-20x20@2x.png +0 -0
- package/icons/prod/ios/AppIcon-20x20@3x.png +0 -0
- package/icons/prod/ios/AppIcon-29x29@1x.png +0 -0
- package/icons/prod/ios/AppIcon-29x29@2x-1.png +0 -0
- package/icons/prod/ios/AppIcon-29x29@2x.png +0 -0
- package/icons/prod/ios/AppIcon-29x29@3x.png +0 -0
- package/icons/prod/ios/AppIcon-40x40@1x.png +0 -0
- package/icons/prod/ios/AppIcon-40x40@2x-1.png +0 -0
- package/icons/prod/ios/AppIcon-40x40@2x.png +0 -0
- package/icons/prod/ios/AppIcon-40x40@3x.png +0 -0
- package/icons/prod/ios/AppIcon-512@2x.png +0 -0
- package/icons/prod/ios/AppIcon-60x60@2x.png +0 -0
- package/icons/prod/ios/AppIcon-60x60@3x.png +0 -0
- package/icons/prod/ios/AppIcon-76x76@1x.png +0 -0
- package/icons/prod/ios/AppIcon-76x76@2x.png +0 -0
- package/icons/prod/ios/AppIcon-83.5x83.5@2x.png +0 -0
- package/package.json +75 -0
- package/resources/entitlements.plist +18 -0
- package/resources/linux/opencode-desktop.desktop +10 -0
- package/scripts/copy-bundles.ts +12 -0
- package/scripts/copy-icons.ts +12 -0
- package/scripts/copy-metainfo.ts +47 -0
- package/scripts/finalize-latest-json.ts +219 -0
- package/scripts/finalize-latest-yml.ts +124 -0
- package/scripts/prebuild.ts +10 -0
- package/scripts/predev.ts +5 -0
- package/scripts/prepare.ts +9 -0
- package/scripts/utils.ts +77 -0
- package/src/main/apps.ts +136 -0
- package/src/main/attachment-picker.test.ts +87 -0
- package/src/main/attachment-picker.ts +56 -0
- package/src/main/constants.ts +7 -0
- package/src/main/debug.ts +95 -0
- package/src/main/desktop-menu-actions.ts +84 -0
- package/src/main/env.d.ts +19 -0
- package/src/main/index.test.ts +37 -0
- package/src/main/index.ts +399 -0
- package/src/main/initialization.ts +6 -0
- package/src/main/install-state.test.ts +19 -0
- package/src/main/install-state.ts +8 -0
- package/src/main/ipc.ts +274 -0
- package/src/main/logging.ts +205 -0
- package/src/main/markdown.ts +16 -0
- package/src/main/menu.ts +67 -0
- package/src/main/migrate.ts +91 -0
- package/src/main/onboarding.ts +45 -0
- package/src/main/server.ts +238 -0
- package/src/main/shell-env.test.ts +50 -0
- package/src/main/shell-env.ts +101 -0
- package/src/main/sidecar.ts +157 -0
- package/src/main/store-cleanup.test.ts +93 -0
- package/src/main/store-cleanup.ts +94 -0
- package/src/main/store-keys.ts +7 -0
- package/src/main/store.ts +35 -0
- package/src/main/unresponsive.ts +70 -0
- package/src/main/updater-controller.test.ts +111 -0
- package/src/main/updater-controller.ts +97 -0
- package/src/main/updater-subscriptions.test.ts +16 -0
- package/src/main/updater-subscriptions.ts +20 -0
- package/src/main/updater.ts +85 -0
- package/src/main/window-registry.test.ts +91 -0
- package/src/main/window-registry.ts +47 -0
- package/src/main/window-state.ts +29 -0
- package/src/main/windows.ts +496 -0
- package/src/main/wsl/ipc.ts +103 -0
- package/src/main/wsl/policy.ts +33 -0
- package/src/main/wsl/runtime.ts +400 -0
- package/src/main/wsl/servers.test.ts +231 -0
- package/src/main/wsl/servers.ts +521 -0
- package/src/main/wsl/sidecar.ts +129 -0
- package/src/main/wsl/startup.ts +31 -0
- package/src/preload/index.ts +128 -0
- package/src/preload/types.ts +109 -0
- package/src/renderer/cli.ts +12 -0
- package/src/renderer/env.d.ts +10 -0
- package/src/renderer/html.test.ts +62 -0
- package/src/renderer/i18n/ar.ts +26 -0
- package/src/renderer/i18n/br.ts +27 -0
- package/src/renderer/i18n/bs.ts +28 -0
- package/src/renderer/i18n/da.ts +28 -0
- package/src/renderer/i18n/de.ts +28 -0
- package/src/renderer/i18n/en.ts +27 -0
- package/src/renderer/i18n/es.ts +27 -0
- package/src/renderer/i18n/fr.ts +28 -0
- package/src/renderer/i18n/index.ts +194 -0
- package/src/renderer/i18n/ja.ts +28 -0
- package/src/renderer/i18n/ko.ts +27 -0
- package/src/renderer/i18n/no.ts +28 -0
- package/src/renderer/i18n/pl.ts +28 -0
- package/src/renderer/i18n/ru.ts +27 -0
- package/src/renderer/i18n/uk.ts +28 -0
- package/src/renderer/i18n/zh.ts +26 -0
- package/src/renderer/i18n/zht.ts +26 -0
- package/src/renderer/index.html +21 -0
- package/src/renderer/index.tsx +464 -0
- package/src/renderer/initialization.test.ts +73 -0
- package/src/renderer/initialization.ts +22 -0
- package/src/renderer/onboarding.tsx +53 -0
- package/src/renderer/styles.css +0 -0
- package/src/renderer/webview-zoom.ts +138 -0
- package/src/renderer/wsl/connections.test.ts +43 -0
- package/src/renderer/wsl/connections.ts +28 -0
- package/sst-env.d.ts +10 -0
- 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,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}`)
|
package/scripts/utils.ts
ADDED
|
@@ -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
|
+
}
|
package/src/main/apps.ts
ADDED
|
@@ -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
|
+
})
|