fix(desktop): restore managed Node server startup (#348)
## Summary - revert the Bun standalone desktop packaging path and restore the server's original `dist/bin.js` bootstrap flow - add a managed Node runtime for Electron and Tauri that downloads only the current platform/arch artifact into `~/.config/codenomad` - update desktop startup and packaging scripts so packaged apps use the managed runtime consistently, and clean up Electron's expected navigation-abort log noise ## Testing - npm run typecheck --workspace @neuralnomads/codenomad-electron-app - cargo check - npm run build --workspace @neuralnomads/codenomad - npm run build:mac --workspace @neuralnomads/codenomad-electron-app - launch `packages/electron-app/release/mac-arm64/CodeNomad.app/Contents/MacOS/CodeNomad` and verify the packaged server reaches ready with the managed Node runtime
This commit is contained in:
@@ -116,10 +116,20 @@ function loadLoadingScreen(window: BrowserWindow) {
|
||||
: window.loadFile(target.source)
|
||||
|
||||
loader.catch((error) => {
|
||||
if (isIgnorableNavigationError(error)) {
|
||||
return
|
||||
}
|
||||
console.error("[cli] failed to load loading screen:", error)
|
||||
})
|
||||
}
|
||||
|
||||
return loader
|
||||
function isIgnorableNavigationError(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object") {
|
||||
return false
|
||||
}
|
||||
|
||||
const code = "code" in error ? String((error as { code?: unknown }).code ?? "") : ""
|
||||
return code === "ERR_ABORTED" || code === "ERR_FAILED"
|
||||
}
|
||||
|
||||
function getAllowedRendererOrigins(window?: BrowserWindow | null): string[] {
|
||||
@@ -294,7 +304,7 @@ function createWindow() {
|
||||
showingLoadingScreen = true
|
||||
currentCliUrl = null
|
||||
clearWindowAllowedOrigin(window)
|
||||
const loadingReady = loadLoadingScreen(window)
|
||||
loadLoadingScreen(window)
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
window.webContents.openDevTools({ mode: "detach" })
|
||||
@@ -313,7 +323,11 @@ function createWindow() {
|
||||
showingLoadingScreen = false
|
||||
})
|
||||
|
||||
return loadingReady
|
||||
if (pendingCliUrl) {
|
||||
const url = pendingCliUrl
|
||||
pendingCliUrl = null
|
||||
startCliPreload(url)
|
||||
}
|
||||
}
|
||||
|
||||
function showLoadingScreen(force = false) {
|
||||
@@ -384,6 +398,9 @@ function startCliPreload(url: string) {
|
||||
})
|
||||
|
||||
view.webContents.loadURL(url).catch((error) => {
|
||||
if (isIgnorableNavigationError(error)) {
|
||||
return
|
||||
}
|
||||
console.error("[cli] failed to preload CLI view:", error)
|
||||
if (preloadingView === view) {
|
||||
destroyPreloadingView(view)
|
||||
@@ -404,7 +421,12 @@ function finalizeCliSwap(url: string) {
|
||||
currentCliUrl = url
|
||||
setWindowAllowedOrigin(window, url)
|
||||
pendingCliUrl = null
|
||||
window.loadURL(url).catch((error) => console.error("[cli] failed to load CLI view:", error))
|
||||
window.loadURL(url).catch((error) => {
|
||||
if (isIgnorableNavigationError(error)) {
|
||||
return
|
||||
}
|
||||
console.error("[cli] failed to load CLI view:", error)
|
||||
})
|
||||
}
|
||||
|
||||
function buildRemoteWindowTitle(name: string, baseUrl: string) {
|
||||
@@ -620,8 +642,7 @@ app.whenReady().then(() => {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const loadingReady = createWindow()
|
||||
;(mainWindow as BrowserWindow & { __codenomadOpenRemoteWindow?: typeof openRemoteWindow }).__codenomadOpenRemoteWindow = openRemoteWindow
|
||||
startCli()
|
||||
|
||||
if (isMac) {
|
||||
session.defaultSession.setSpellCheckerEnabled(false)
|
||||
@@ -638,11 +659,8 @@ app.whenReady().then(() => {
|
||||
}
|
||||
}
|
||||
|
||||
void loadingReady.finally(() => {
|
||||
setTimeout(() => {
|
||||
void startCli()
|
||||
}, 0)
|
||||
})
|
||||
createWindow()
|
||||
;(mainWindow as BrowserWindow & { __codenomadOpenRemoteWindow?: typeof openRemoteWindow }).__codenomadOpenRemoteWindow = openRemoteWindow
|
||||
|
||||
app.on("certificate-error", (event, _webContents, url, error, _certificate, callback) => {
|
||||
if (isInsecureOriginAllowed(url)) {
|
||||
|
||||
283
packages/electron-app/electron/main/managed-node.ts
Normal file
283
packages/electron-app/electron/main/managed-node.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import { dialog, app } from "electron"
|
||||
import { createHash } from "node:crypto"
|
||||
import fs from "node:fs"
|
||||
import { createWriteStream } from "node:fs"
|
||||
import { mkdir, mkdtemp, rename, rm, stat } from "node:fs/promises"
|
||||
import https from "node:https"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { pipeline } from "node:stream/promises"
|
||||
import { spawn } from "node:child_process"
|
||||
|
||||
const MANAGED_NODE_VERSION = "v22.22.2"
|
||||
const CONFIG_DIR = path.join(app.getPath("home"), ".config", "codenomad")
|
||||
|
||||
interface NodeArtifactSpec {
|
||||
archiveName: string
|
||||
archiveRoot: string
|
||||
binaryRelativePath: string
|
||||
url: string
|
||||
}
|
||||
|
||||
function getNodeArtifactSpec(): NodeArtifactSpec {
|
||||
const platform = process.platform
|
||||
const arch = process.arch
|
||||
|
||||
if (platform === "darwin" && arch === "x64") {
|
||||
return buildTarGzSpec("darwin-x64")
|
||||
}
|
||||
if (platform === "darwin" && arch === "arm64") {
|
||||
return buildTarGzSpec("darwin-arm64")
|
||||
}
|
||||
if (platform === "linux" && arch === "x64") {
|
||||
return buildTarGzSpec("linux-x64")
|
||||
}
|
||||
if (platform === "linux" && arch === "arm64") {
|
||||
return buildTarGzSpec("linux-arm64")
|
||||
}
|
||||
if (platform === "win32" && arch === "x64") {
|
||||
return buildZipSpec("win-x64", "node.exe")
|
||||
}
|
||||
if (platform === "win32" && arch === "arm64") {
|
||||
return buildZipSpec("win-arm64", "node.exe")
|
||||
}
|
||||
|
||||
throw new Error(`Managed Node runtime is not supported on ${platform}-${arch}.`)
|
||||
}
|
||||
|
||||
function buildTarGzSpec(target: string): NodeArtifactSpec {
|
||||
const archiveName = `node-${MANAGED_NODE_VERSION}-${target}.tar.gz`
|
||||
return {
|
||||
archiveName,
|
||||
archiveRoot: archiveName.replace(/\.tar\.gz$/, ""),
|
||||
binaryRelativePath: path.join("bin", "node"),
|
||||
url: `https://nodejs.org/dist/${MANAGED_NODE_VERSION}/${archiveName}`,
|
||||
}
|
||||
}
|
||||
|
||||
function buildZipSpec(target: string, binaryName: string): NodeArtifactSpec {
|
||||
const archiveName = `node-${MANAGED_NODE_VERSION}-${target}.zip`
|
||||
return {
|
||||
archiveName,
|
||||
archiveRoot: archiveName.replace(/\.zip$/, ""),
|
||||
binaryRelativePath: binaryName,
|
||||
url: `https://nodejs.org/dist/${MANAGED_NODE_VERSION}/${archiveName}`,
|
||||
}
|
||||
}
|
||||
|
||||
function getRuntimePlatformDir(): string {
|
||||
return `${process.platform}-${process.arch}`
|
||||
}
|
||||
|
||||
function getManagedNodeRoot(): string {
|
||||
return path.join(CONFIG_DIR, "node", MANAGED_NODE_VERSION, getRuntimePlatformDir())
|
||||
}
|
||||
|
||||
function getManagedNodeBinaryPath(): string {
|
||||
return path.join(getManagedNodeRoot(), getNodeArtifactSpec().binaryRelativePath)
|
||||
}
|
||||
|
||||
function fileExists(filePath: string): boolean {
|
||||
try {
|
||||
return fs.existsSync(filePath)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchText(url: string): Promise<string> {
|
||||
const response = await request(url)
|
||||
return response.toString("utf-8")
|
||||
}
|
||||
|
||||
function request(url: string): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const doRequest = (target: string) => {
|
||||
https
|
||||
.get(target, (response) => {
|
||||
const statusCode = response.statusCode ?? 0
|
||||
const redirect = response.headers.location
|
||||
|
||||
if (statusCode >= 300 && statusCode < 400 && redirect) {
|
||||
response.resume()
|
||||
doRequest(new URL(redirect, target).toString())
|
||||
return
|
||||
}
|
||||
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
response.resume()
|
||||
reject(new Error(`Request failed for ${target} with status ${statusCode}`))
|
||||
return
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = []
|
||||
response.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)))
|
||||
response.on("end", () => resolve(Buffer.concat(chunks)))
|
||||
response.on("error", reject)
|
||||
})
|
||||
.on("error", reject)
|
||||
}
|
||||
|
||||
doRequest(url)
|
||||
})
|
||||
}
|
||||
|
||||
function downloadFile(url: string, destination: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const doDownload = (target: string) => {
|
||||
https
|
||||
.get(target, (response) => {
|
||||
const statusCode = response.statusCode ?? 0
|
||||
const redirect = response.headers.location
|
||||
|
||||
if (statusCode >= 300 && statusCode < 400 && redirect) {
|
||||
response.resume()
|
||||
doDownload(new URL(redirect, target).toString())
|
||||
return
|
||||
}
|
||||
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
response.resume()
|
||||
reject(new Error(`Download failed for ${target} with status ${statusCode}`))
|
||||
return
|
||||
}
|
||||
|
||||
const output = createWriteStream(destination)
|
||||
pipeline(response, output).then(() => resolve()).catch(reject)
|
||||
})
|
||||
.on("error", reject)
|
||||
}
|
||||
|
||||
doDownload(url)
|
||||
})
|
||||
}
|
||||
|
||||
async function sha256File(filePath: string): Promise<string> {
|
||||
const hash = createHash("sha256")
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const stream = fs.createReadStream(filePath)
|
||||
stream.on("data", (chunk) => hash.update(chunk))
|
||||
stream.on("end", () => resolve())
|
||||
stream.on("error", reject)
|
||||
})
|
||||
return hash.digest("hex")
|
||||
}
|
||||
|
||||
async function fetchExpectedSha256(archiveName: string): Promise<string> {
|
||||
const checksums = await fetchText(`https://nodejs.org/dist/${MANAGED_NODE_VERSION}/SHASUMS256.txt`)
|
||||
for (const line of checksums.split(/\r?\n/)) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed) continue
|
||||
const [checksum, fileName] = trimmed.split(/\s+/, 2)
|
||||
if (fileName === archiveName) {
|
||||
return checksum
|
||||
}
|
||||
}
|
||||
throw new Error(`Unable to find checksum for ${archiveName}.`)
|
||||
}
|
||||
|
||||
function runCommand(command: string, args: string[]): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, { stdio: "ignore", shell: false })
|
||||
child.on("error", reject)
|
||||
child.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`${command} ${args.join(" ")} exited with code ${code ?? 1}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function extractArchive(archivePath: string, destination: string): Promise<void> {
|
||||
if (archivePath.endsWith(".zip")) {
|
||||
const command = process.platform === "win32" ? "powershell.exe" : "powershell"
|
||||
await runCommand(command, [
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
"Expand-Archive",
|
||||
"-LiteralPath",
|
||||
archivePath,
|
||||
"-DestinationPath",
|
||||
destination,
|
||||
"-Force",
|
||||
])
|
||||
return
|
||||
}
|
||||
|
||||
await runCommand("tar", ["-xzf", archivePath, "-C", destination])
|
||||
}
|
||||
|
||||
async function promptForManagedNodeDownload(): Promise<boolean> {
|
||||
const result = await dialog.showMessageBox({
|
||||
type: "question",
|
||||
buttons: ["Download", "Cancel"],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
noLink: true,
|
||||
title: "Download Node Runtime",
|
||||
message: "CodeNomad needs its managed Node.js runtime to start the server.",
|
||||
detail: `Download ${MANAGED_NODE_VERSION} for ${process.platform}-${process.arch} into ~/.config/codenomad?`,
|
||||
})
|
||||
|
||||
return result.response === 0
|
||||
}
|
||||
|
||||
async function installManagedNodeRuntime(): Promise<string> {
|
||||
const spec = getNodeArtifactSpec()
|
||||
const runtimeRoot = getManagedNodeRoot()
|
||||
const runtimeParent = path.dirname(runtimeRoot)
|
||||
await mkdir(runtimeParent, { recursive: true })
|
||||
const tempRoot = await mkdtemp(path.join(runtimeParent, ".download-"))
|
||||
const archivePath = path.join(tempRoot, spec.archiveName)
|
||||
const extractRoot = path.join(tempRoot, "extract")
|
||||
|
||||
try {
|
||||
await mkdir(extractRoot, { recursive: true })
|
||||
|
||||
const expectedSha = await fetchExpectedSha256(spec.archiveName)
|
||||
await downloadFile(spec.url, archivePath)
|
||||
|
||||
const actualSha = await sha256File(archivePath)
|
||||
if (actualSha !== expectedSha) {
|
||||
throw new Error(`Checksum mismatch for ${spec.archiveName}.`)
|
||||
}
|
||||
|
||||
await extractArchive(archivePath, extractRoot)
|
||||
|
||||
const extractedRoot = path.join(extractRoot, spec.archiveRoot)
|
||||
const extractedBinary = path.join(extractedRoot, spec.binaryRelativePath)
|
||||
if (!fileExists(extractedBinary)) {
|
||||
throw new Error(`Managed Node binary missing after extraction: ${extractedBinary}`)
|
||||
}
|
||||
|
||||
await rm(runtimeRoot, { recursive: true, force: true })
|
||||
await rename(extractedRoot, runtimeRoot)
|
||||
|
||||
return path.join(runtimeRoot, spec.binaryRelativePath)
|
||||
} finally {
|
||||
await rm(tempRoot, { recursive: true, force: true }).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureManagedNodeBinary(): Promise<string> {
|
||||
const binaryPath = getManagedNodeBinaryPath()
|
||||
if (fileExists(binaryPath)) {
|
||||
return binaryPath
|
||||
}
|
||||
|
||||
const confirmed = await promptForManagedNodeDownload()
|
||||
if (!confirmed) {
|
||||
throw new Error("CodeNomad requires the managed Node.js runtime to start. Download was cancelled.")
|
||||
}
|
||||
|
||||
const installedBinary = await installManagedNodeRuntime()
|
||||
const installedStats = await stat(installedBinary)
|
||||
if (!installedStats.isFile()) {
|
||||
throw new Error(`Managed Node binary is invalid: ${installedBinary}`)
|
||||
}
|
||||
|
||||
return installedBinary
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import os from "os"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { parse as parseYaml } from "yaml"
|
||||
import { ensureManagedNodeBinary } from "./managed-node"
|
||||
import { buildUserShellCommand, getUserShellEnv, supportsUserShell } from "./user-shell"
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url)
|
||||
@@ -38,8 +39,10 @@ interface StartOptions {
|
||||
|
||||
interface CliEntryResolution {
|
||||
entry: string
|
||||
runner: "node" | "tsx" | "standalone"
|
||||
runner: "node" | "tsx"
|
||||
runnerPath?: string
|
||||
nodeBinaryPath: string
|
||||
nodeArgs?: string[]
|
||||
}
|
||||
|
||||
type ManagedChild = ChildProcess | UtilityProcess
|
||||
@@ -148,14 +151,16 @@ export class CliProcessManager extends EventEmitter {
|
||||
const listeningMode = this.resolveListeningMode()
|
||||
const host = resolveHostForMode(listeningMode)
|
||||
const args = this.buildCliArgs(options, host)
|
||||
const cliEntry = this.resolveCliEntry(options)
|
||||
const cliEntry = await this.resolveCliEntry(options)
|
||||
|
||||
let child: ManagedChild
|
||||
|
||||
if (this.shouldUsePackagedShellSupervisor(options, cliEntry)) {
|
||||
if (this.shouldUsePackagedShellSupervisor(options)) {
|
||||
const runtimePath = this.resolveShellNodeCommand()
|
||||
const entryPath = this.resolveBundledProdEntry()
|
||||
const supervisorPath = this.resolveCliSupervisorPath()
|
||||
const shellEnv = supportsUserShell() ? getUserShellEnv() : { ...process.env }
|
||||
const shellTarget = cliEntry.runner === "standalone" ? this.buildExecutableCommand(cliEntry.entry, args) : this.buildCommand(cliEntry, args)
|
||||
const shellTarget = this.buildCommand(cliEntry, args)
|
||||
const shellCommand = buildUserShellCommand(`exec ${shellTarget}`)
|
||||
const supervisorPayload = JSON.stringify({
|
||||
command: shellCommand.command,
|
||||
@@ -164,13 +169,13 @@ export class CliProcessManager extends EventEmitter {
|
||||
})
|
||||
|
||||
console.info(
|
||||
`[cli] launching CodeNomad CLI (${options.dev ? "dev" : "prod"}) via utility supervisor using ${cliEntry.runner} at ${cliEntry.entry} (host=${host})`,
|
||||
`[cli] launching CodeNomad CLI (${options.dev ? "dev" : "prod"}) via utility supervisor using node at ${runtimePath} (host=${host})`,
|
||||
)
|
||||
console.info(`[cli] utility supervisor: ${supervisorPath}`)
|
||||
console.info(`[cli] shell command: ${shellCommand.command} ${shellCommand.args.join(" ")}`)
|
||||
|
||||
child = utilityProcess.fork(supervisorPath, [supervisorPayload], {
|
||||
env: cliEntry.runner === "standalone" ? shellEnv : { ...shellEnv, ELECTRON_RUN_AS_NODE: "1" },
|
||||
env: { ...shellEnv, ELECTRON_RUN_AS_NODE: "1" },
|
||||
stdio: "pipe",
|
||||
serviceName: "CodeNomad CLI Supervisor",
|
||||
})
|
||||
@@ -181,16 +186,10 @@ export class CliProcessManager extends EventEmitter {
|
||||
)
|
||||
|
||||
const env = supportsUserShell() ? getUserShellEnv() : { ...process.env }
|
||||
if (cliEntry.runner !== "standalone") {
|
||||
env.ELECTRON_RUN_AS_NODE = "1"
|
||||
}
|
||||
env.ELECTRON_RUN_AS_NODE = "1"
|
||||
|
||||
const spawnDetails = supportsUserShell()
|
||||
? buildUserShellCommand(
|
||||
`${cliEntry.runner === "standalone" ? "" : "ELECTRON_RUN_AS_NODE=1 "}exec ${
|
||||
cliEntry.runner === "standalone" ? this.buildExecutableCommand(cliEntry.entry, args) : this.buildCommand(cliEntry, args)
|
||||
}`,
|
||||
)
|
||||
? buildUserShellCommand(`ELECTRON_RUN_AS_NODE=1 exec ${this.buildCommand(cliEntry, args)}`)
|
||||
: this.buildDirectSpawn(cliEntry, args)
|
||||
|
||||
const detached = process.platform !== "win32"
|
||||
@@ -568,11 +567,10 @@ export class CliProcessManager extends EventEmitter {
|
||||
}
|
||||
|
||||
private buildCommand(cliEntry: CliEntryResolution, args: string[]): string {
|
||||
if (cliEntry.runner === "standalone") {
|
||||
return this.buildExecutableCommand(cliEntry.entry, args)
|
||||
const parts = [JSON.stringify(cliEntry.nodeBinaryPath)]
|
||||
for (const nodeArg of cliEntry.nodeArgs ?? []) {
|
||||
parts.push(JSON.stringify(nodeArg))
|
||||
}
|
||||
|
||||
const parts = [JSON.stringify(process.execPath)]
|
||||
if (cliEntry.runner === "tsx" && cliEntry.runnerPath) {
|
||||
parts.push(JSON.stringify(cliEntry.runnerPath))
|
||||
}
|
||||
@@ -581,33 +579,30 @@ export class CliProcessManager extends EventEmitter {
|
||||
return parts.join(" ")
|
||||
}
|
||||
|
||||
private buildExecutableCommand(command: string, args: string[]): string {
|
||||
return [JSON.stringify(command), ...args.map((arg) => JSON.stringify(arg))].join(" ")
|
||||
}
|
||||
|
||||
private buildDirectSpawn(cliEntry: CliEntryResolution, args: string[]) {
|
||||
if (cliEntry.runner === "standalone") {
|
||||
return { command: cliEntry.entry, args }
|
||||
}
|
||||
|
||||
if (cliEntry.runner === "tsx") {
|
||||
return { command: process.execPath, args: [cliEntry.runnerPath!, cliEntry.entry, ...args] }
|
||||
return { command: cliEntry.nodeBinaryPath, args: [...(cliEntry.nodeArgs ?? []), cliEntry.runnerPath!, cliEntry.entry, ...args] }
|
||||
}
|
||||
|
||||
return { command: process.execPath, args: [cliEntry.entry, ...args] }
|
||||
return { command: cliEntry.nodeBinaryPath, args: [...(cliEntry.nodeArgs ?? []), cliEntry.entry, ...args] }
|
||||
}
|
||||
|
||||
private resolveCliEntry(options: StartOptions): CliEntryResolution {
|
||||
private async resolveCliEntry(options: StartOptions): Promise<CliEntryResolution> {
|
||||
if (options.dev) {
|
||||
const tsxPath = this.resolveTsx()
|
||||
if (!tsxPath) {
|
||||
throw new Error("tsx is required to run the CLI in development mode. Please install dependencies.")
|
||||
}
|
||||
const devEntry = this.resolveDevEntry()
|
||||
return { entry: devEntry, runner: "tsx", runnerPath: tsxPath }
|
||||
return { entry: devEntry, runner: "tsx", runnerPath: tsxPath, nodeBinaryPath: process.execPath }
|
||||
}
|
||||
|
||||
return { entry: this.resolveStandaloneProdEntry(), runner: "standalone" }
|
||||
return {
|
||||
entry: this.resolveProdEntry(),
|
||||
runner: "node",
|
||||
nodeBinaryPath: await ensureManagedNodeBinary(),
|
||||
nodeArgs: ["--experimental-specifier-resolution=node"],
|
||||
}
|
||||
}
|
||||
|
||||
private resolveTsx(): string | null {
|
||||
@@ -647,12 +642,11 @@ export class CliProcessManager extends EventEmitter {
|
||||
return entry
|
||||
}
|
||||
|
||||
private resolveStandaloneProdEntry(): string {
|
||||
const executableName = process.platform === "win32" ? "codenomad-server.exe" : "codenomad-server"
|
||||
private resolveProdEntry(): string {
|
||||
const candidates = [
|
||||
path.join(process.resourcesPath, "server", "dist", executableName),
|
||||
path.join(mainDirname, "../resources/server/dist", executableName),
|
||||
path.resolve(process.cwd(), "..", "server", "dist", executableName),
|
||||
path.join(process.resourcesPath, "server", "dist", "bin.js"),
|
||||
path.join(mainDirname, "../resources/server/dist/bin.js"),
|
||||
path.resolve(process.cwd(), "..", "server", "dist", "bin.js"),
|
||||
]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
@@ -661,11 +655,11 @@ export class CliProcessManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unable to locate standalone CodeNomad server executable (${executableName}). Run npm run build:standalone --workspace @neuralnomads/codenomad.`)
|
||||
throw new Error("Unable to locate the packaged CodeNomad server entrypoint (dist/bin.js). Rebuild the desktop bundle.")
|
||||
}
|
||||
|
||||
private shouldUsePackagedShellSupervisor(options: StartOptions, cliEntry: CliEntryResolution): boolean {
|
||||
return !options.dev && app.isPackaged && process.platform === "darwin" && cliEntry.runner !== "standalone"
|
||||
private shouldUsePackagedShellSupervisor(options: StartOptions): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
private resolveCliSupervisorPath(): string {
|
||||
@@ -683,6 +677,26 @@ export class CliProcessManager extends EventEmitter {
|
||||
throw new Error("Unable to locate CodeNomad CLI supervisor script.")
|
||||
}
|
||||
|
||||
private resolveShellNodeCommand(): string {
|
||||
const configured = process.env.NODE_BINARY?.trim()
|
||||
return configured && configured.length > 0 ? configured : "node"
|
||||
}
|
||||
|
||||
private resolveBundledProdEntry(): string {
|
||||
const candidates = [
|
||||
path.join(process.resourcesPath, "server", "dist", "bin.js"),
|
||||
path.join(mainDirname, "../resources/server/dist/bin.js"),
|
||||
]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Unable to locate bundled CodeNomad CLI build in app resources.")
|
||||
}
|
||||
|
||||
private describeUtilityProcessError(error: unknown): string {
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from "child_process"
|
||||
import { existsSync, readFileSync } from "fs"
|
||||
import { existsSync } from "fs"
|
||||
import path, { join } from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
@@ -14,46 +14,6 @@ const npxCmd = process.platform === "win32" ? "npx.cmd" : "npx"
|
||||
const nodeModulesPath = join(appDir, "node_modules")
|
||||
const workspaceNodeModulesPath = join(workspaceRoot, "node_modules")
|
||||
|
||||
function getPlatformEsbuildPackage() {
|
||||
const platformKey = `${process.platform}-${process.arch}`
|
||||
const platformPackages = {
|
||||
"linux-x64": "@esbuild/linux-x64",
|
||||
"linux-arm64": "@esbuild/linux-arm64",
|
||||
"darwin-arm64": "@esbuild/darwin-arm64",
|
||||
"darwin-x64": "@esbuild/darwin-x64",
|
||||
"win32-arm64": "@esbuild/win32-arm64",
|
||||
"win32-x64": "@esbuild/win32-x64",
|
||||
}
|
||||
|
||||
return platformPackages[platformKey] ?? null
|
||||
}
|
||||
|
||||
async function ensureEsbuildPlatformBinary() {
|
||||
const pkgName = getPlatformEsbuildPackage()
|
||||
if (!pkgName) {
|
||||
return
|
||||
}
|
||||
|
||||
const platformPackagePath = join(workspaceNodeModulesPath, ...pkgName.split("/"))
|
||||
if (existsSync(platformPackagePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
let esbuildVersion = ""
|
||||
try {
|
||||
esbuildVersion = JSON.parse(readFileSync(join(workspaceNodeModulesPath, "esbuild", "package.json"), "utf-8")).version ?? ""
|
||||
} catch {
|
||||
// leave version empty; fallback install will use latest compatible
|
||||
}
|
||||
|
||||
const packageSpec = esbuildVersion ? `${pkgName}@${esbuildVersion}` : pkgName
|
||||
console.log("📦 Step 0/3: Restoring esbuild platform binary...\n")
|
||||
await run(npmCmd, ["install", packageSpec, "--no-save", "--ignore-scripts", "--fund=false", "--audit=false"], {
|
||||
cwd: workspaceRoot,
|
||||
env: { NODE_PATH: workspaceNodeModulesPath },
|
||||
})
|
||||
}
|
||||
|
||||
const platforms = {
|
||||
mac: {
|
||||
args: ["--mac", "--x64", "--arm64"],
|
||||
@@ -145,8 +105,6 @@ async function build(platform) {
|
||||
console.log(`\n🔨 Building for: ${config.description}\n`)
|
||||
|
||||
try {
|
||||
await ensureEsbuildPlatformBinary()
|
||||
|
||||
console.log("📦 Step 1/3: Building CLI dependency...\n")
|
||||
await run(npmCmd, ["run", "build", "--workspace", "@neuralnomads/codenomad"], {
|
||||
cwd: workspaceRoot,
|
||||
|
||||
@@ -16,7 +16,6 @@ const npmNodeExecPath = process.env.npm_node_execpath
|
||||
|
||||
const serverSources = ["dist", "public", "node_modules", "package.json"]
|
||||
const serverDepsMarker = join(serverRoot, "node_modules", "fastify", "package.json")
|
||||
const standaloneMarker = join(serverRoot, "dist", process.platform === "win32" ? "codenomad-server.exe" : "codenomad-server")
|
||||
|
||||
function log(message) {
|
||||
console.log(`[prepare-resources] ${message}`)
|
||||
@@ -30,34 +29,6 @@ function ensureServerBuild() {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureStandaloneServerBuild() {
|
||||
log("building standalone server executable")
|
||||
const result = spawnSync(
|
||||
"npm",
|
||||
["run", "build:standalone", "--workspace", "@neuralnomads/codenomad"],
|
||||
{
|
||||
cwd: workspaceRoot,
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${join(workspaceRoot, "node_modules", ".bin")}${path.delimiter}${process.env.PATH ?? ""}`,
|
||||
},
|
||||
shell: process.platform === "win32",
|
||||
},
|
||||
)
|
||||
|
||||
if (result.status !== 0) {
|
||||
if (result.error) {
|
||||
throw result.error
|
||||
}
|
||||
throw new Error(`standalone server build exited with code ${result.status ?? 1}`)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(standaloneMarker)) {
|
||||
throw new Error(`Standalone server executable missing after build: ${standaloneMarker}`)
|
||||
}
|
||||
}
|
||||
|
||||
function ensureServerDependencies() {
|
||||
if (fs.existsSync(serverDepsMarker)) {
|
||||
return
|
||||
@@ -94,51 +65,6 @@ function ensureServerDependencies() {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureEsbuildPlatformBinary() {
|
||||
const platformKey = `${process.platform}-${process.arch}`
|
||||
const platformPackages = {
|
||||
"linux-x64": "@esbuild/linux-x64",
|
||||
"linux-arm64": "@esbuild/linux-arm64",
|
||||
"darwin-arm64": "@esbuild/darwin-arm64",
|
||||
"darwin-x64": "@esbuild/darwin-x64",
|
||||
"win32-arm64": "@esbuild/win32-arm64",
|
||||
"win32-x64": "@esbuild/win32-x64",
|
||||
}
|
||||
|
||||
const pkgName = platformPackages[platformKey]
|
||||
if (!pkgName) {
|
||||
return
|
||||
}
|
||||
|
||||
const platformPackagePath = join(workspaceRoot, "node_modules", ...pkgName.split("/"))
|
||||
if (fs.existsSync(platformPackagePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
let esbuildVersion = ""
|
||||
try {
|
||||
esbuildVersion = JSON.parse(fs.readFileSync(join(workspaceRoot, "node_modules", "esbuild", "package.json"), "utf-8")).version ?? ""
|
||||
} catch {
|
||||
// leave version empty; fallback install will use latest compatible
|
||||
}
|
||||
|
||||
const packageSpec = esbuildVersion ? `${pkgName}@${esbuildVersion}` : pkgName
|
||||
log("installing esbuild platform binary (optional dep workaround)")
|
||||
|
||||
const result = spawnSync("npm", ["install", packageSpec, "--no-save", "--ignore-scripts", "--fund=false", "--audit=false"], {
|
||||
cwd: workspaceRoot,
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
})
|
||||
|
||||
if (result.status !== 0) {
|
||||
if (result.error) {
|
||||
throw result.error
|
||||
}
|
||||
throw new Error(`esbuild platform install exited with code ${result.status ?? 1}`)
|
||||
}
|
||||
}
|
||||
|
||||
function copyServerArtifacts() {
|
||||
fs.rmSync(serverDest, { recursive: true, force: true })
|
||||
fs.mkdirSync(serverDest, { recursive: true })
|
||||
@@ -195,9 +121,7 @@ function stripNodeModuleBins() {
|
||||
|
||||
async function main() {
|
||||
ensureServerBuild()
|
||||
ensureStandaloneServerBuild()
|
||||
ensureServerDependencies()
|
||||
ensureEsbuildPlatformBinary()
|
||||
copyServerArtifacts()
|
||||
stripNodeModuleBins()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user