fix: bundle WorkDSH runtime in desktop apps

This commit is contained in:
techflag
2026-09-23 18:26:52 +08:00
parent e9d3e6be2a
commit d528d11bc1
12 changed files with 637 additions and 24 deletions
+3
View File
@@ -1,3 +1,6 @@
dist/
/lib/
node_modules/
build/workdsh-runtime
build/.workdsh-release-*
build/.workdsh-node
+17 -6
View File
@@ -12,7 +12,7 @@
"directory": "dsh-plugin-desktop-beta"
},
"type": "module",
"main": "lib/main.js",
"main": "lib/workdsh-main.js",
"types": "lib/types/index.d.ts",
"bin": {
"dsh-desktop-beta": "lib/bin.js",
@@ -124,16 +124,17 @@
"verify:operations": "node --test scripts/operation-reliability.spec.mjs && node scripts/verify-operation-reliability.mjs",
"verify:notices": "node scripts/verify-licenses.mjs --notices THIRD_PARTY_NOTICES.md",
"prepare:electron-native": "node scripts/prepare-fs-ext.ts",
"prepare:workdsh-runtime": "node scripts/prepare-workdsh-runtime.mjs",
"check": "yarn run build && yarn run typecheck && yarn run test && yarn run verify:closure && yarn run verify:cli && yarn run verify:loader && yarn run verify:profile && yarn run verify:licenses && yarn run verify:operations",
"check:win-package": "yarn workspace dsh-community-market build && yarn run build && yarn run typecheck && vitest run tests/package.spec.ts tests/package-win.spec.ts tests/desktop-installer-quit.spec.ts tests/installer-nsh.spec.ts tests/update-checker.spec.ts tests/update-download.spec.ts tests/verify-win-installer.spec.ts tests/verify-win-portable.spec.ts tests/verify-packaged-runtime.spec.ts tests/windows-pwsh-sandbox.spec.ts tests/windows-volume-diagnostics.spec.ts tests/window-options.spec.ts tests/safe-mode.spec.ts && yarn run verify:closure",
"check:mac-package": "yarn workspace dsh-community-market build && yarn run build && yarn run typecheck && vitest run tests/package.spec.ts tests/package-mac.spec.ts tests/verify-mac-smoke.spec.ts tests/verify-packaged-runtime.spec.ts tests/mac-universal.spec.ts && yarn run verify:closure",
"start": "node lib/bin.js",
"dev": "yarn run build && yarn run prepare:electron-native && node lib/bin.js",
"package:dir": "yarn run build && yarn run prepare:electron-native && node scripts/package-dir.mjs",
"dist:mac": "node scripts/release-mac.ts",
"dist:mac-smoke": "node scripts/package-mac.ts",
"dist:win": "node scripts/package-win.ts",
"dist:win-portable": "node scripts/package-win-portable.ts",
"package:dir": "yarn run build && yarn run prepare:electron-native && yarn run prepare:workdsh-runtime && node scripts/package-dir.mjs",
"dist:mac": "yarn run prepare:workdsh-runtime && node scripts/release-mac.ts",
"dist:mac-smoke": "yarn run prepare:workdsh-runtime && node scripts/package-mac.ts",
"dist:win": "yarn run prepare:workdsh-runtime && node scripts/package-win.ts",
"dist:win-portable": "yarn run prepare:workdsh-runtime && node scripts/package-win-portable.ts",
"prepack": "yarn run check"
},
"dependencies": {
@@ -348,6 +349,16 @@
"output": "dist",
"buildResources": "build"
},
"extraResources": [
{
"from": "build/workdsh-runtime",
"to": "workdsh-runtime",
"filter": [
"profiles/workdsh/**",
"node/**"
]
}
],
"toolsets": {
"nsis": "1.2.1"
},
@@ -0,0 +1,102 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process'
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const WORKDSH_VERSION = '0.1.0-alpha.8'
const DSH_VERSION = '0.1.7-alpha.1'
const desktopRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const output = join(desktopRoot, 'build', 'workdsh-runtime')
const destination = join(output, 'profiles', 'workdsh')
const cli = join(destination, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js')
function run(command, args, options = {}) {
const result = spawnSync(command, args, { stdio: 'inherit', ...options })
if (result.error) throw result.error
if (result.status !== 0) throw new Error(`${command} exited with ${result.status}`)
}
async function download(url, path) {
if (existsSync(path)) return
const response = await fetch(url, { redirect: 'follow' })
if (!response.ok) throw new Error(`Failed to download ${url}: ${response.status}`)
writeFileSync(path, Buffer.from(await response.arrayBuffer()))
}
async function installReleasedProfile() {
const releaseDir = join(desktopRoot, 'build', `.workdsh-release-${WORKDSH_VERSION}`)
mkdirSync(releaseDir, { recursive: true })
const base = `https://github.com/techflag/workdsh/releases/download/v${WORKDSH_VERSION}`
const manifestPath = join(releaseDir, 'release-manifest.json')
await download(`${base}/release-manifest.json`, manifestPath)
const installerPath = join(releaseDir, 'install-workdsh.mjs')
await download(`${base}/install-workdsh.mjs`, installerPath)
if (process.platform === 'win32') {
let installer = readFileSync(installerPath, 'utf8')
installer = installer
.replace("import { spawnSync } from 'node:child_process';", "import { spawnSync as nativeSpawnSync } from 'node:child_process';")
.replace('const argv = process.argv.slice(2);', "const portableSpawn = (command, args, options = {}) => nativeSpawnSync(command, args, { ...options, shell: true });\nconst argv = process.argv.slice(2);")
.replaceAll('spawnSync(', 'portableSpawn(')
writeFileSync(installerPath, installer)
}
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
for (const item of manifest.packages) {
await download(`${base}/${item.filename}`, join(releaseDir, item.filename))
}
mkdirSync(output, { recursive: true })
const shim = join(output, process.platform === 'win32' ? 'dsh-runtime.cmd' : 'dsh-runtime')
if (process.platform === 'win32') {
writeFileSync(shim, `@echo off\r\nnpx --yes @deepseek-ai/dsh@${DSH_VERSION} %*\r\n`)
} else {
writeFileSync(shim, `#!/bin/sh\nexec npx --yes @deepseek-ai/dsh@${DSH_VERSION} \"$@\"\n`)
chmodSync(shim, 0o755)
}
run(process.execPath, [installerPath, '--directory', releaseDir, '--dsh', shim], {
env: { ...process.env, DSH_HOME: output },
})
}
async function prepareNodeExecutable(path) {
mkdirSync(dirname(path), { recursive: true })
if (process.platform !== 'darwin') {
cpSync(process.execPath, path)
if (process.platform !== 'win32') chmodSync(path, 0o755)
return
}
const otherArch = process.arch === 'arm64' ? 'x64' : 'arm64'
const cache = join(desktopRoot, 'build', '.workdsh-node')
const archive = join(cache, `node-v${process.versions.node}-darwin-${otherArch}.tar.gz`)
const extracted = join(cache, `node-v${process.versions.node}-darwin-${otherArch}`, 'bin', 'node')
mkdirSync(cache, { recursive: true })
await download(`https://nodejs.org/dist/v${process.versions.node}/node-v${process.versions.node}-darwin-${otherArch}.tar.gz`, archive)
if (!existsSync(extracted)) run('/usr/bin/tar', ['-xzf', archive, '-C', cache])
run('/usr/bin/lipo', ['-create', process.execPath, extracted, '-output', path])
chmodSync(path, 0o755)
}
const candidates = [
process.env.WORKDSH_BUNDLED_PROFILE,
process.env.WORKDSH_DSH_HOME && join(process.env.WORKDSH_DSH_HOME, 'profiles', 'workdsh'),
'/tmp/workdsh-desktop-profile.IbF6US/profiles/workdsh',
].filter(Boolean)
const source = candidates.find(candidate => existsSync(join(candidate, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js')))
if (source) {
rmSync(output, { recursive: true, force: true })
mkdirSync(dirname(destination), { recursive: true })
// electron-builder intentionally does not follow extra-resource directory links.
// Clone into a real directory; APFS copy-on-write keeps local preparation fast.
if (process.platform === 'darwin') run('/bin/cp', ['-cR', resolve(source), destination])
else cpSync(resolve(source), destination, { recursive: true, dereference: true })
} else if (!existsSync(cli)) {
await installReleasedProfile()
}
if (!existsSync(cli)) throw new Error(`Failed to prepare WorkDSH runtime at ${destination}`)
cpSync(join(destination, 'package.json'), join(output, 'profile-package.json'))
const nodeExecutable = join(output, 'node', process.platform === 'win32' ? 'node.exe' : 'node')
await prepareNodeExecutable(nodeExecutable)
console.log(`Prepared WorkDSH ${WORKDSH_VERSION} runtime at ${output}`)
+188
View File
@@ -0,0 +1,188 @@
/** Minimal Electron carrier for the bundled WorkDSH release profile. */
import { app, BrowserWindow, shell } from 'electron'
import { spawn, type ChildProcess } from 'node:child_process'
import {
cpSync,
existsSync,
lstatSync,
mkdirSync,
readFileSync,
readlinkSync,
symlinkSync,
unlinkSync,
writeFileSync,
} from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
const PROFILE_NAME = 'workdsh'
const RUNTIME_VERSION = '0.1.0-alpha.8'
const READY_PATTERN = /dsh web:\s+(http:\/\/127\.0\.0\.1:\d+\/?\?token=[^\s]+)/u
let runtime: ChildProcess | undefined
let window: BrowserWindow | undefined
let quitting = false
function bundledProfileDirectory(): string {
const overridden = process.env.WORKDSH_BUNDLED_PROFILE
if (overridden !== undefined && overridden.length > 0) return overridden
return join(process.resourcesPath, 'workdsh-runtime', 'profiles', PROFILE_NAME)
}
function runtimeHome(): string {
const overridden = process.env.WORKDSH_DSH_HOME
if (overridden !== undefined && overridden.length > 0) return overridden
return join(app.getPath('userData'), 'dsh-home')
}
function bundledNodeExecutable(): string {
const overridden = process.env.WORKDSH_NODE_EXECUTABLE
if (overridden !== undefined && overridden.length > 0) return overridden
return join(
process.resourcesPath,
'workdsh-runtime',
'node',
process.platform === 'win32' ? 'node.exe' : 'node',
)
}
function materializeRuntimeProfile(home: string): string {
const source = bundledProfileDirectory()
const target = join(home, 'profiles', PROFILE_NAME)
const marker = join(target, '.workdsh-desktop-runtime')
const sourceModules = join(source, 'node_modules')
const targetModules = join(target, 'node_modules')
if (!existsSync(sourceModules)) {
throw new Error(`Bundled WorkDSH runtime is incomplete: ${sourceModules}`)
}
mkdirSync(target, { recursive: true })
for (const name of ['package.json', 'cordis.yml', 'cordis.patch.yml', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']) {
const from = join(source, name)
if (existsSync(from)) cpSync(from, join(target, name), { force: true })
}
const installedVersion = existsSync(marker) ? readFileSync(marker, 'utf8').trim() : undefined
if (existsSync(targetModules) && installedVersion !== RUNTIME_VERSION) {
const stat = lstatSync(targetModules)
if (!stat.isSymbolicLink()) {
throw new Error(
`WorkDSH cannot replace the existing unmanaged runtime at ${targetModules}. `
+ 'Move that directory, then reopen WorkDSH.',
)
}
readlinkSync(targetModules)
unlinkSync(targetModules)
}
if (!existsSync(targetModules)) {
symlinkSync(sourceModules, targetModules, process.platform === 'win32' ? 'junction' : 'dir')
}
writeFileSync(marker, `${RUNTIME_VERSION}\n`, 'utf8')
return target
}
function openWindow(url: string): void {
const icon = fileURLToPath(new URL('../build/app-icon.png', import.meta.url))
window = new BrowserWindow({
width: 1440,
height: 960,
minWidth: 960,
minHeight: 640,
title: 'WorkDSH',
icon,
backgroundColor: '#111113',
show: false,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
})
window.on('page-title-updated', event => {
event.preventDefault()
window?.setTitle('WorkDSH')
})
window.webContents.setWindowOpenHandler(({ url: target }) => {
if (target.startsWith('https://') || target.startsWith('http://')) void shell.openExternal(target)
return { action: 'deny' }
})
void window.loadURL(url).then(() => window?.show())
window.on('closed', () => { window = undefined })
}
function startRuntime(home: string, profileDir: string): void {
const cli = join(profileDir, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js')
if (!existsSync(cli)) throw new Error(`Bundled WorkDSH launcher is missing: ${cli}`)
const executable = bundledNodeExecutable()
if (!existsSync(executable)) throw new Error(`Bundled Node.js runtime is missing: ${executable}`)
const child = spawn(executable, [cli, '--profile', PROFILE_NAME, '--no-open'], {
env: {
...process.env,
DSH_HOME: home,
ELECTRON_RUN_AS_NODE: undefined,
},
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
})
runtime = child
let ready = false
let output = ''
const consume = (chunk: Buffer): void => {
const text = chunk.toString('utf8')
process.stderr.write(text)
if (ready) return
output = `${output}${text}`.slice(-16_384)
const match = READY_PATTERN.exec(output)
if (match?.[1] !== undefined) {
ready = true
openWindow(match[1])
}
}
child.stdout.on('data', consume)
child.stderr.on('data', chunk => process.stderr.write(chunk))
child.once('error', cause => {
if (!quitting) throw cause
})
child.once('exit', code => {
runtime = undefined
if (!quitting && code !== 0) app.quit()
})
}
function stopRuntime(): void {
quitting = true
if (runtime !== undefined && runtime.exitCode === null) runtime.kill('SIGTERM')
runtime = undefined
}
app.setName('WorkDSH')
if (!app.requestSingleInstanceLock()) {
app.quit()
} else {
app.on('second-instance', () => {
if (window === undefined) return
if (window.isMinimized()) window.restore()
window.focus()
})
app.on('before-quit', stopRuntime)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
app.on('activate', () => {
if (window === undefined) {
// A healthy runtime always owns a window. Relaunching keeps the profile
// lifecycle simple after the last macOS window was closed.
app.relaunch()
app.quit()
}
})
void app.whenReady().then(() => {
const home = runtimeHome()
const profile = process.env.WORKDSH_DSH_HOME === home
? join(home, 'profiles', PROFILE_NAME)
: materializeRuntimeProfile(home)
startRuntime(home, profile)
}).catch(cause => {
process.stderr.write(`WorkDSH failed to start: ${cause instanceof Error ? cause.stack ?? cause.message : String(cause)}\n`)
app.quit()
})
}
@@ -912,7 +912,7 @@ describe('published package surface', () => {
expect(manifest.scripts?.['prepare:electron-native']).toBe('node scripts/prepare-fs-ext.ts')
expect(manifest.scripts?.dev).toContain('yarn run prepare:electron-native')
expect(manifest.scripts?.['package:dir'])
.toBe('yarn run build && yarn run prepare:electron-native && node scripts/package-dir.mjs')
.toBe('yarn run build && yarn run prepare:electron-native && yarn run prepare:workdsh-runtime && node scripts/package-dir.mjs')
expect(packageDir).toContain("CSC_IDENTITY_AUTO_DISCOVERY: 'false'")
expect(packageDir).toContain("'--config.forceCodeSigning=false'")
expect(packageDir).toContain("'--config.mac.identity=null'")
@@ -921,10 +921,10 @@ describe('published package surface', () => {
expect(packageDir).toContain("require.resolve('electron/package.json')")
expect(packageDir).toContain('--config.electronDist=')
expect(packageDir).toContain('electronBuilderEnvironment')
expect(manifest.scripts?.['dist:mac']).toBe('node scripts/release-mac.ts')
expect(manifest.scripts?.['dist:mac-smoke']).toBe('node scripts/package-mac.ts')
expect(manifest.scripts?.['dist:win']).toBe('node scripts/package-win.ts')
expect(manifest.scripts?.['dist:win-portable']).toBe('node scripts/package-win-portable.ts')
expect(manifest.scripts?.['dist:mac']).toBe('yarn run prepare:workdsh-runtime && node scripts/release-mac.ts')
expect(manifest.scripts?.['dist:mac-smoke']).toBe('yarn run prepare:workdsh-runtime && node scripts/package-mac.ts')
expect(manifest.scripts?.['dist:win']).toBe('yarn run prepare:workdsh-runtime && node scripts/package-win.ts')
expect(manifest.scripts?.['dist:win-portable']).toBe('yarn run prepare:workdsh-runtime && node scripts/package-win-portable.ts')
expect(manifest.scripts?.['check:win-package']).toContain('yarn workspace dsh-community-market build')
expect(manifest.scripts?.['check:win-package']).toContain('yarn run build')
expect(manifest.scripts?.['check:win-package']).toContain('yarn run typecheck')
+1
View File
@@ -32,6 +32,7 @@ export default defineConfig([
'windows-pwsh-sandbox': 'src/windows-pwsh-sandbox.ts',
'windows-acl-runner': 'src/windows-acl-runner.ts',
main: 'src/main.ts',
'workdsh-main': 'src/workdsh-main.ts',
},
outDir: 'lib',
format: 'esm',
+4
View File
@@ -1,3 +1,7 @@
dist/
/lib/
node_modules/
build/workdsh-runtime
build/.workdsh-release-*
build/.workdsh-node
+20 -8
View File
@@ -12,7 +12,7 @@
"directory": "dsh-plugin-desktop"
},
"type": "module",
"main": "lib/main.js",
"main": "lib/workdsh-main.js",
"types": "lib/types/index.d.ts",
"bin": {
"dsh-desktop": "lib/bin.js",
@@ -129,12 +129,14 @@
"check:mac-package": "yarn workspace dsh-community-market build && yarn run build && yarn run typecheck && vitest run tests/package.spec.ts tests/package-mac.spec.ts tests/verify-mac-smoke.spec.ts tests/verify-packaged-runtime.spec.ts tests/mac-universal.spec.ts && yarn run verify:closure",
"start": "node lib/bin.js",
"dev": "yarn run build && node lib/bin.js",
"package:dir": "yarn run build && node scripts/package-dir.mjs",
"dist:mac": "node scripts/release-mac.ts",
"dist:mac-smoke": "node scripts/package-mac.ts",
"dist:win": "node scripts/package-win.ts",
"dist:win-portable": "node scripts/package-win-portable.ts",
"prepack": "yarn run check"
"package:dir": "yarn run build && yarn run prepare:electron-native && yarn run prepare:workdsh-runtime && node scripts/package-dir.mjs",
"dist:mac": "yarn run prepare:workdsh-runtime && node scripts/release-mac.ts",
"dist:mac-smoke": "yarn run prepare:workdsh-runtime && node scripts/package-mac.ts",
"dist:win": "yarn run prepare:workdsh-runtime && node scripts/package-win.ts",
"dist:win-portable": "yarn run prepare:workdsh-runtime && node scripts/package-win-portable.ts",
"prepack": "yarn run check",
"prepare:electron-native": "node scripts/prepare-fs-ext.ts",
"prepare:workdsh-runtime": "node scripts/prepare-workdsh-runtime.mjs"
},
"dependencies": {
"@deepseek-ai/cordis": "4.0.2",
@@ -430,6 +432,16 @@
"dir"
],
"icon": "build/app-icon.png"
}
},
"extraResources": [
{
"from": "build/workdsh-runtime",
"to": "workdsh-runtime",
"filter": [
"profiles/workdsh/**",
"node/**"
]
}
]
}
}
@@ -0,0 +1,102 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process'
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const WORKDSH_VERSION = '0.1.0-alpha.8'
const DSH_VERSION = '0.1.7-alpha.1'
const desktopRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const output = join(desktopRoot, 'build', 'workdsh-runtime')
const destination = join(output, 'profiles', 'workdsh')
const cli = join(destination, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js')
function run(command, args, options = {}) {
const result = spawnSync(command, args, { stdio: 'inherit', ...options })
if (result.error) throw result.error
if (result.status !== 0) throw new Error(`${command} exited with ${result.status}`)
}
async function download(url, path) {
if (existsSync(path)) return
const response = await fetch(url, { redirect: 'follow' })
if (!response.ok) throw new Error(`Failed to download ${url}: ${response.status}`)
writeFileSync(path, Buffer.from(await response.arrayBuffer()))
}
async function installReleasedProfile() {
const releaseDir = join(desktopRoot, 'build', `.workdsh-release-${WORKDSH_VERSION}`)
mkdirSync(releaseDir, { recursive: true })
const base = `https://github.com/techflag/workdsh/releases/download/v${WORKDSH_VERSION}`
const manifestPath = join(releaseDir, 'release-manifest.json')
await download(`${base}/release-manifest.json`, manifestPath)
const installerPath = join(releaseDir, 'install-workdsh.mjs')
await download(`${base}/install-workdsh.mjs`, installerPath)
if (process.platform === 'win32') {
let installer = readFileSync(installerPath, 'utf8')
installer = installer
.replace("import { spawnSync } from 'node:child_process';", "import { spawnSync as nativeSpawnSync } from 'node:child_process';")
.replace('const argv = process.argv.slice(2);', "const portableSpawn = (command, args, options = {}) => nativeSpawnSync(command, args, { ...options, shell: true });\nconst argv = process.argv.slice(2);")
.replaceAll('spawnSync(', 'portableSpawn(')
writeFileSync(installerPath, installer)
}
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
for (const item of manifest.packages) {
await download(`${base}/${item.filename}`, join(releaseDir, item.filename))
}
mkdirSync(output, { recursive: true })
const shim = join(output, process.platform === 'win32' ? 'dsh-runtime.cmd' : 'dsh-runtime')
if (process.platform === 'win32') {
writeFileSync(shim, `@echo off\r\nnpx --yes @deepseek-ai/dsh@${DSH_VERSION} %*\r\n`)
} else {
writeFileSync(shim, `#!/bin/sh\nexec npx --yes @deepseek-ai/dsh@${DSH_VERSION} \"$@\"\n`)
chmodSync(shim, 0o755)
}
run(process.execPath, [installerPath, '--directory', releaseDir, '--dsh', shim], {
env: { ...process.env, DSH_HOME: output },
})
}
async function prepareNodeExecutable(path) {
mkdirSync(dirname(path), { recursive: true })
if (process.platform !== 'darwin') {
cpSync(process.execPath, path)
if (process.platform !== 'win32') chmodSync(path, 0o755)
return
}
const otherArch = process.arch === 'arm64' ? 'x64' : 'arm64'
const cache = join(desktopRoot, 'build', '.workdsh-node')
const archive = join(cache, `node-v${process.versions.node}-darwin-${otherArch}.tar.gz`)
const extracted = join(cache, `node-v${process.versions.node}-darwin-${otherArch}`, 'bin', 'node')
mkdirSync(cache, { recursive: true })
await download(`https://nodejs.org/dist/v${process.versions.node}/node-v${process.versions.node}-darwin-${otherArch}.tar.gz`, archive)
if (!existsSync(extracted)) run('/usr/bin/tar', ['-xzf', archive, '-C', cache])
run('/usr/bin/lipo', ['-create', process.execPath, extracted, '-output', path])
chmodSync(path, 0o755)
}
const candidates = [
process.env.WORKDSH_BUNDLED_PROFILE,
process.env.WORKDSH_DSH_HOME && join(process.env.WORKDSH_DSH_HOME, 'profiles', 'workdsh'),
'/tmp/workdsh-desktop-profile.IbF6US/profiles/workdsh',
].filter(Boolean)
const source = candidates.find(candidate => existsSync(join(candidate, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js')))
if (source) {
rmSync(output, { recursive: true, force: true })
mkdirSync(dirname(destination), { recursive: true })
// electron-builder intentionally does not follow extra-resource directory links.
// Clone into a real directory; APFS copy-on-write keeps local preparation fast.
if (process.platform === 'darwin') run('/bin/cp', ['-cR', resolve(source), destination])
else cpSync(resolve(source), destination, { recursive: true, dereference: true })
} else if (!existsSync(cli)) {
await installReleasedProfile()
}
if (!existsSync(cli)) throw new Error(`Failed to prepare WorkDSH runtime at ${destination}`)
cpSync(join(destination, 'package.json'), join(output, 'profile-package.json'))
const nodeExecutable = join(output, 'node', process.platform === 'win32' ? 'node.exe' : 'node')
await prepareNodeExecutable(nodeExecutable)
console.log(`Prepared WorkDSH ${WORKDSH_VERSION} runtime at ${output}`)
+188
View File
@@ -0,0 +1,188 @@
/** Minimal Electron carrier for the bundled WorkDSH release profile. */
import { app, BrowserWindow, shell } from 'electron'
import { spawn, type ChildProcess } from 'node:child_process'
import {
cpSync,
existsSync,
lstatSync,
mkdirSync,
readFileSync,
readlinkSync,
symlinkSync,
unlinkSync,
writeFileSync,
} from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
const PROFILE_NAME = 'workdsh'
const RUNTIME_VERSION = '0.1.0-alpha.8'
const READY_PATTERN = /dsh web:\s+(http:\/\/127\.0\.0\.1:\d+\/?\?token=[^\s]+)/u
let runtime: ChildProcess | undefined
let window: BrowserWindow | undefined
let quitting = false
function bundledProfileDirectory(): string {
const overridden = process.env.WORKDSH_BUNDLED_PROFILE
if (overridden !== undefined && overridden.length > 0) return overridden
return join(process.resourcesPath, 'workdsh-runtime', 'profiles', PROFILE_NAME)
}
function runtimeHome(): string {
const overridden = process.env.WORKDSH_DSH_HOME
if (overridden !== undefined && overridden.length > 0) return overridden
return join(app.getPath('userData'), 'dsh-home')
}
function bundledNodeExecutable(): string {
const overridden = process.env.WORKDSH_NODE_EXECUTABLE
if (overridden !== undefined && overridden.length > 0) return overridden
return join(
process.resourcesPath,
'workdsh-runtime',
'node',
process.platform === 'win32' ? 'node.exe' : 'node',
)
}
function materializeRuntimeProfile(home: string): string {
const source = bundledProfileDirectory()
const target = join(home, 'profiles', PROFILE_NAME)
const marker = join(target, '.workdsh-desktop-runtime')
const sourceModules = join(source, 'node_modules')
const targetModules = join(target, 'node_modules')
if (!existsSync(sourceModules)) {
throw new Error(`Bundled WorkDSH runtime is incomplete: ${sourceModules}`)
}
mkdirSync(target, { recursive: true })
for (const name of ['package.json', 'cordis.yml', 'cordis.patch.yml', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']) {
const from = join(source, name)
if (existsSync(from)) cpSync(from, join(target, name), { force: true })
}
const installedVersion = existsSync(marker) ? readFileSync(marker, 'utf8').trim() : undefined
if (existsSync(targetModules) && installedVersion !== RUNTIME_VERSION) {
const stat = lstatSync(targetModules)
if (!stat.isSymbolicLink()) {
throw new Error(
`WorkDSH cannot replace the existing unmanaged runtime at ${targetModules}. `
+ 'Move that directory, then reopen WorkDSH.',
)
}
readlinkSync(targetModules)
unlinkSync(targetModules)
}
if (!existsSync(targetModules)) {
symlinkSync(sourceModules, targetModules, process.platform === 'win32' ? 'junction' : 'dir')
}
writeFileSync(marker, `${RUNTIME_VERSION}\n`, 'utf8')
return target
}
function openWindow(url: string): void {
const icon = fileURLToPath(new URL('../build/app-icon.png', import.meta.url))
window = new BrowserWindow({
width: 1440,
height: 960,
minWidth: 960,
minHeight: 640,
title: 'WorkDSH',
icon,
backgroundColor: '#111113',
show: false,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
})
window.on('page-title-updated', event => {
event.preventDefault()
window?.setTitle('WorkDSH')
})
window.webContents.setWindowOpenHandler(({ url: target }) => {
if (target.startsWith('https://') || target.startsWith('http://')) void shell.openExternal(target)
return { action: 'deny' }
})
void window.loadURL(url).then(() => window?.show())
window.on('closed', () => { window = undefined })
}
function startRuntime(home: string, profileDir: string): void {
const cli = join(profileDir, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js')
if (!existsSync(cli)) throw new Error(`Bundled WorkDSH launcher is missing: ${cli}`)
const executable = bundledNodeExecutable()
if (!existsSync(executable)) throw new Error(`Bundled Node.js runtime is missing: ${executable}`)
const child = spawn(executable, [cli, '--profile', PROFILE_NAME, '--no-open'], {
env: {
...process.env,
DSH_HOME: home,
ELECTRON_RUN_AS_NODE: undefined,
},
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
})
runtime = child
let ready = false
let output = ''
const consume = (chunk: Buffer): void => {
const text = chunk.toString('utf8')
process.stderr.write(text)
if (ready) return
output = `${output}${text}`.slice(-16_384)
const match = READY_PATTERN.exec(output)
if (match?.[1] !== undefined) {
ready = true
openWindow(match[1])
}
}
child.stdout.on('data', consume)
child.stderr.on('data', chunk => process.stderr.write(chunk))
child.once('error', cause => {
if (!quitting) throw cause
})
child.once('exit', code => {
runtime = undefined
if (!quitting && code !== 0) app.quit()
})
}
function stopRuntime(): void {
quitting = true
if (runtime !== undefined && runtime.exitCode === null) runtime.kill('SIGTERM')
runtime = undefined
}
app.setName('WorkDSH')
if (!app.requestSingleInstanceLock()) {
app.quit()
} else {
app.on('second-instance', () => {
if (window === undefined) return
if (window.isMinimized()) window.restore()
window.focus()
})
app.on('before-quit', stopRuntime)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
app.on('activate', () => {
if (window === undefined) {
// A healthy runtime always owns a window. Relaunching keeps the profile
// lifecycle simple after the last macOS window was closed.
app.relaunch()
app.quit()
}
})
void app.whenReady().then(() => {
const home = runtimeHome()
const profile = process.env.WORKDSH_DSH_HOME === home
? join(home, 'profiles', PROFILE_NAME)
: materializeRuntimeProfile(home)
startRuntime(home, profile)
}).catch(cause => {
process.stderr.write(`WorkDSH failed to start: ${cause instanceof Error ? cause.stack ?? cause.message : String(cause)}\n`)
app.quit()
})
}
+6 -5
View File
@@ -831,16 +831,17 @@ describe('published package surface', () => {
expect(manifest.scripts?.build).toContain('node scripts/generate-windows-app-icon.mjs')
expect(manifest.scripts?.build).toContain('node scripts/generate-mac-app-icon.mjs')
expect(manifest.scripts?.['package:dir']).toBe('yarn run build && node scripts/package-dir.mjs')
expect(manifest.scripts?.['package:dir'])
.toBe('yarn run build && yarn run prepare:electron-native && yarn run prepare:workdsh-runtime && node scripts/package-dir.mjs')
expect(packageDir).toContain("CSC_IDENTITY_AUTO_DISCOVERY: 'false'")
expect(packageDir).toContain("'--config.forceCodeSigning=false'")
expect(packageDir).toContain("'--config.mac.identity=null'")
expect(packageDir).toContain("'--config.mac.notarize=false'")
expect(packageDir).toContain("'--config.win.signExecutable=false'")
expect(manifest.scripts?.['dist:mac']).toBe('node scripts/release-mac.ts')
expect(manifest.scripts?.['dist:mac-smoke']).toBe('node scripts/package-mac.ts')
expect(manifest.scripts?.['dist:win']).toBe('node scripts/package-win.ts')
expect(manifest.scripts?.['dist:win-portable']).toBe('node scripts/package-win-portable.ts')
expect(manifest.scripts?.['dist:mac']).toBe('yarn run prepare:workdsh-runtime && node scripts/release-mac.ts')
expect(manifest.scripts?.['dist:mac-smoke']).toBe('yarn run prepare:workdsh-runtime && node scripts/package-mac.ts')
expect(manifest.scripts?.['dist:win']).toBe('yarn run prepare:workdsh-runtime && node scripts/package-win.ts')
expect(manifest.scripts?.['dist:win-portable']).toBe('yarn run prepare:workdsh-runtime && node scripts/package-win-portable.ts')
expect(manifest.scripts?.['check:win-package']).toContain('yarn workspace dsh-community-market build')
expect(manifest.scripts?.['check:win-package']).toContain('yarn run build')
expect(manifest.scripts?.['check:win-package']).toContain('yarn run typecheck')
+1
View File
@@ -32,6 +32,7 @@ export default defineConfig([
'windows-pwsh-sandbox': 'src/windows-pwsh-sandbox.ts',
'windows-acl-runner': 'src/windows-acl-runner.ts',
main: 'src/main.ts',
'workdsh-main': 'src/workdsh-main.ts',
},
outDir: 'lib',
format: 'esm',