diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2a66b0d97..a157327578 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,6 +119,7 @@ jobs: path: ${{ steps.yarn-cache.outputs.dir }} key: yarn-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} - run: yarn install --immutable + - run: yarn upstream:install - run: yarn workspace dsh-plugin-desktop check:win-package - name: Build Windows installer env: @@ -165,6 +166,7 @@ jobs: path: ${{ steps.yarn-cache.outputs.dir }} key: yarn-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} - run: yarn install --immutable + - run: yarn upstream:install - run: yarn workspace dsh-plugin-desktop check:mac-package - name: Build macOS smoke artifact env: diff --git a/dsh-plugin-desktop-beta/.gitignore b/dsh-plugin-desktop-beta/.gitignore index 341008d80a..ba7f665f73 100644 --- a/dsh-plugin-desktop-beta/.gitignore +++ b/dsh-plugin-desktop-beta/.gitignore @@ -4,3 +4,5 @@ node_modules/ build/workdsh-runtime build/.workdsh-release-* build/.workdsh-node +build/.workdsh-primary-runtime-cache +build/official-primary-runtime diff --git a/dsh-plugin-desktop-beta/package.json b/dsh-plugin-desktop-beta/package.json index 1a990c12a5..9c0f1ffd58 100644 --- a/dsh-plugin-desktop-beta/package.json +++ b/dsh-plugin-desktop-beta/package.json @@ -125,16 +125,17 @@ "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", + "prepare:workdsh-primary-runtime": "node scripts/prepare-workdsh-primary-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 && 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", + "package:dir": "yarn run build && yarn run prepare:electron-native && yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/package-dir.mjs", + "dist:mac": "yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/release-mac.ts", + "dist:mac-smoke": "yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/package-mac.ts", + "dist:win": "yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/package-win.ts", + "dist:win-portable": "yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/package-win-portable.ts", "prepack": "yarn run check" }, "dependencies": { @@ -355,7 +356,9 @@ "to": "workdsh-runtime", "filter": [ "profiles/workdsh/**", - "node/**" + "node/**", + "primary-runtime/**", + "office-skills/**" ] } ], diff --git a/dsh-plugin-desktop-beta/scripts/package-mac.ts b/dsh-plugin-desktop-beta/scripts/package-mac.ts index 317aa3efb1..9d2f70a6be 100644 --- a/dsh-plugin-desktop-beta/scripts/package-mac.ts +++ b/dsh-plugin-desktop-beta/scripts/package-mac.ts @@ -1,7 +1,7 @@ /** Build an unsigned macOS DMG smoke artifact on a native macOS host. */ import { spawnSync } from 'node:child_process' -import { rmSync } from 'node:fs' +import { existsSync, rmSync } from 'node:fs' import { createRequire } from 'node:module' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' @@ -32,6 +32,8 @@ export interface MacSmokePackageOptions { readonly prepareRuntime: () => void /** Absolute electron-builder CLI module. */ readonly builderCli: string + /** Local Electron distribution when already installed, avoiding another download. */ + readonly electronDist?: string /** Absolute packaged-DMG verification script. */ readonly verifier: string /** Node executable used to run package-local scripts. */ @@ -65,6 +67,7 @@ function defaultOptions(): MacSmokePackageOptions { const workspaceRoot = resolve(desktopRoot, '..') const require = createRequire(import.meta.url) const outputDir = resolve(desktopRoot, 'dist', 'mac-smoke', process.env.WORKDSH_MAC_ARCH ?? process.arch) + const electronDist = resolve(dirname(require.resolve('electron/package.json')), 'dist') return { env: process.env, platform: process.platform, @@ -80,6 +83,7 @@ function defaultOptions(): MacSmokePackageOptions { prepareInstalledMacUniversalRuntime(desktopRoot) }, builderCli: require.resolve('electron-builder/cli.js'), + ...(existsSync(resolve(electronDist, 'Electron.app')) ? { electronDist } : {}), verifier: fileURLToPath(new URL('./verify-mac-smoke.ts', import.meta.url)), nodeExecutable: process.execPath, run, @@ -141,6 +145,7 @@ export function packageMacSmoke(options: MacSmokePackageOptions = defaultOptions '--config.mac.notarize=false', '--config.npmRebuild=false', `--config.directories.output=${options.outputDir}`, + ...(options.electronDist === undefined ? [] : [`--config.electronDist=${options.electronDist}`]), ], options.desktopRoot, electronBuilderEnvironment({ diff --git a/dsh-plugin-desktop-beta/scripts/prepare-workdsh-primary-runtime.mjs b/dsh-plugin-desktop-beta/scripts/prepare-workdsh-primary-runtime.mjs new file mode 100644 index 0000000000..67d5a39312 --- /dev/null +++ b/dsh-plugin-desktop-beta/scripts/prepare-workdsh-primary-runtime.mjs @@ -0,0 +1,58 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process' +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const desktopRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const upstreamRoot = resolve(desktopRoot, '..', 'deepseek-harness') +const target = process.platform === 'win32' ? 'win-x64' + : process.platform === 'darwin' ? `mac-${process.env.WORKDSH_MAC_ARCH ?? process.arch}` + : undefined +if (target === undefined || !['win-x64', 'mac-arm64', 'mac-x64'].includes(target)) { + throw new Error(`Unsupported WorkDSH primary runtime target: ${target ?? process.platform}`) +} +const output = join(desktopRoot, 'build', 'workdsh-runtime') +const cache = join(desktopRoot, 'build', '.workdsh-primary-runtime-cache') +const corepack = process.platform === 'win32' ? 'corepack.cmd' : 'corepack' +const result = spawnSync(corepack, [ + 'pnpm', 'run', 'prepare:primary-runtime', '--target', target, '--output', output, '--cache', cache, +], { + cwd: upstreamRoot, + env: { ...process.env, CI: 'true' }, + stdio: 'inherit', + shell: process.platform === 'win32', +}) +if (result.error) throw result.error +if (result.status !== 0) throw new Error(`Official primary runtime preparation failed: ${result.status}`) +const manifestPath = join(output, 'primary-runtime', 'runtime.json') +if (!existsSync(manifestPath) || !existsSync(join(output, 'office-skills'))) { + throw new Error('Official primary runtime payload is incomplete') +} +const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) +if (manifest.desktopVersion !== '0.1.7-rc.2' || manifest.platform !== process.platform + || manifest.arch !== target.slice(4)) { + throw new Error(`Official primary runtime metadata does not match ${target} / rc.2`) +} +const profile = join(output, 'profiles', 'workdsh') +const packagePath = join(profile, 'package.json') +if (!existsSync(packagePath)) throw new Error('WorkDSH release profile must be prepared first') +const profilePackage = JSON.parse(readFileSync(packagePath, 'utf8')) +const packages = [ + '@deepseek-ai/dsh-tool-workspace-dependencies', + '@deepseek-ai/dsh-skill-office', +] +if (packages.some(name => profilePackage.dependencies?.[name] !== '0.1.7-rc.2' + || !existsSync(join(profile, 'node_modules', name, 'package.json')))) { + const pnpm = process.platform === 'win32' ? 'npx.cmd' : 'npx' + const install = spawnSync(pnpm, [ + '--yes', 'pnpm@11.8.0', '--dir', profile, 'add', '--save-exact', + ...packages.map(name => `${name}@0.1.7-rc.2`), + ], { env: process.env, stdio: 'inherit', shell: process.platform === 'win32' }) + if (install.error) throw install.error + if (install.status !== 0) throw new Error(`Official Office plugins failed to install: ${install.status}`) +} +const patch = `# Official DeepSeek Harness Desktop workspace dependencies and Office skills.\n- insert:\n - id: workspace-dependencies\n name: '@deepseek-ai/dsh-tool-workspace-dependencies'\n config:\n source: !!js "process.env.DSH_BUNDLED_PRIMARY_RUNTIME"\n root: !!js "process.getBuiltinModule('node:path').join(process.env.DSH_HOME, 'dsh-runtimes', 'dsh-primary-runtime')"\n - id: skill-office\n name: '@deepseek-ai/dsh-skill-office'\n config:\n assetRoot: !!js "process.getBuiltinModule('node:path').join(process.env.DSH_BUNDLED_PRIMARY_RUNTIME, '..', 'office-skills')"\n node: !!js "process.getBuiltinModule('node:path').join(process.env.DSH_BUNDLED_PRIMARY_RUNTIME, 'dependencies', 'node', 'bin', process.platform === 'win32' ? 'node.exe' : 'node')"\n` +writeFileSync(join(profile, 'cordis.patch.yml'), patch) +console.log(`Prepared official DeepSeek Harness ${target} primary runtime at ${output}`) diff --git a/dsh-plugin-desktop-beta/scripts/prepare-workdsh-runtime.mjs b/dsh-plugin-desktop-beta/scripts/prepare-workdsh-runtime.mjs index 0fbd95512e..2f93d75d2f 100644 --- a/dsh-plugin-desktop-beta/scripts/prepare-workdsh-runtime.mjs +++ b/dsh-plugin-desktop-beta/scripts/prepare-workdsh-runtime.mjs @@ -31,9 +31,10 @@ async function installReleasedProfile() { 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 rawInstallerPath = join(releaseDir, 'install-workdsh.original.mjs') const installerPath = join(releaseDir, 'install-workdsh.mjs') - await download(`${base}/install-workdsh.mjs`, installerPath) - let installer = readFileSync(installerPath, 'utf8') + await download(`${base}/install-workdsh.mjs`, rawInstallerPath) + let installer = readFileSync(rawInstallerPath, 'utf8') installer = installer.replace( 'profilePackage.packageManager = manifest.packageManager;', "profilePackage.packageManager = manifest.packageManager;\n profilePackage.devEngines = { ...profilePackage.devEngines, packageManager: { name: 'pnpm', version: manifest.packageManager.slice('pnpm@'.length), onFail: 'ignore' } };", @@ -56,9 +57,24 @@ async function installReleasedProfile() { } } writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n') + const installSpawn = process.platform === 'win32' ? 'portableSpawn' : 'spawnSync' installer = readFileSync(installerPath, 'utf8').replace( " execute(['plugin', '--profile', profile, 'add', join(directory, item.filename)]);", - " execute(['plugin', '--profile', profile, 'allow-version', `${name}@${item.version}`, '--dsh-version', expectedHarness, '--accept-risk']);\n execute(['plugin', '--profile', profile, 'add', join(directory, item.filename)]);", + ` execute(['plugin', '--profile', profile, 'allow-version', \`\${name}@\${item.version}\`, '--dsh-version', expectedHarness, '--accept-risk']); + // The DSH plugin-manager wrapper can retain an idle pnpm process after a + // completed local tarball add. Use the same pinned pnpm directly; the + // release tarball hash and exact compatibility approval were checked above. + const profileDir = join(dshHome, 'profiles', profile); + const added = ${installSpawn}(corepack, ['--dir', profileDir, 'add', '--save-exact', join(directory, item.filename)], { stdio: 'inherit' }); + if (added.error) throw added.error; + if (added.status !== 0) process.exit(added.status ?? 1); + if (name === 'workdsh-bundle') { + const profilePackage = JSON.parse(readFileSync(profileManifest, 'utf8')); + const bundles = profilePackage.dsh?.profile?.bundles ?? []; + if (!bundles.includes(name)) bundles.push(name); + profilePackage.dsh = { ...profilePackage.dsh, profile: { ...profilePackage.dsh?.profile, bundles } }; + writeFileSync(profileManifest, JSON.stringify(profilePackage, null, 2) + '\\n'); + }`, ) writeFileSync(installerPath, installer) for (const item of manifest.packages) { @@ -99,8 +115,8 @@ async function prepareNodeExecutable(path) { if (process.platform !== 'win32') chmodSync(path, 0o755) return } - const targetArch = process.env.WORKDSH_MAC_ARCH - if (targetArch !== undefined && targetArch !== 'x64' && targetArch !== 'arm64') { + const targetArch = process.env.WORKDSH_MAC_ARCH ?? process.arch + if (targetArch !== 'x64' && targetArch !== 'arm64') { throw new Error(`unsupported macOS target architecture: ${targetArch}`) } if (targetArch === process.arch) { @@ -108,15 +124,14 @@ async function prepareNodeExecutable(path) { chmodSync(path, 0o755) return } - const downloadArch = targetArch ?? (process.arch === 'arm64' ? 'x64' : 'arm64') + const downloadArch = targetArch const cache = join(desktopRoot, 'build', '.workdsh-node') const archive = join(cache, `node-v${process.versions.node}-darwin-${downloadArch}.tar.gz`) const extracted = join(cache, `node-v${process.versions.node}-darwin-${downloadArch}`, 'bin', 'node') mkdirSync(cache, { recursive: true }) await download(`https://nodejs.org/dist/v${process.versions.node}/node-v${process.versions.node}-darwin-${downloadArch}.tar.gz`, archive) if (!existsSync(extracted)) run('/usr/bin/tar', ['-xzf', archive, '-C', cache]) - if (targetArch === undefined) run('/usr/bin/lipo', ['-create', process.execPath, extracted, '-output', path]) - else cpSync(extracted, path) + cpSync(extracted, path) chmodSync(path, 0o755) } @@ -132,7 +147,23 @@ const installedDshVersion = candidate => { return undefined } } -const source = candidates.find(candidate => installedDshVersion(candidate) === DSH_VERSION) +const releasePackages = [ + 'workdsh-provider-identity-local', 'workdsh-plugin-audit', 'workdsh-plugin-access', + 'workdsh-plugin-skills', 'workdsh-plugin-experts', 'workdsh-plugin-connectors', + 'workdsh-plugin-activity', 'workdsh-plugin-office', 'workdsh-plugin-library', + 'workdsh-plugin-projects', 'workdsh-bundle', +] +const isPreparedProfile = candidate => { + if (installedDshVersion(candidate) !== DSH_VERSION) return false + if (!releasePackages.every(name => existsSync(join(candidate, 'node_modules', name, 'package.json')))) return false + try { + const profile = JSON.parse(readFileSync(join(candidate, 'package.json'), 'utf8')) + return profile.dsh?.profile?.bundles?.includes('workdsh-bundle') === true + } catch { + return false + } +} +const source = candidates.find(isPreparedProfile) if (source) { rmSync(output, { recursive: true, force: true }) @@ -141,7 +172,7 @@ if (source) { // 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 (installedDshVersion(destination) !== DSH_VERSION || !existsSync(cli)) { +} else if (!isPreparedProfile(destination) || !existsSync(cli)) { rmSync(output, { recursive: true, force: true }) await installReleasedProfile() } diff --git a/dsh-plugin-desktop-beta/src/workdsh-main.ts b/dsh-plugin-desktop-beta/src/workdsh-main.ts index 86cdbac141..78f4b83f5b 100644 --- a/dsh-plugin-desktop-beta/src/workdsh-main.ts +++ b/dsh-plugin-desktop-beta/src/workdsh-main.ts @@ -17,7 +17,7 @@ import { join } from 'node:path' import { fileURLToPath } from 'node:url' const PROFILE_NAME = 'workdsh' -const RUNTIME_VERSION = '0.1.0-alpha.8' +const RUNTIME_VERSION = '0.1.0-alpha.9+dsh-0.1.7-rc.2' const READY_PATTERN = /dsh web:\s+(http:\/\/127\.0\.0\.1:\d+\/?\?token=[^\s]+)/u let runtime: ChildProcess | undefined @@ -47,6 +47,10 @@ function bundledNodeExecutable(): string { ) } +function bundledPrimaryRuntime(): string { + return join(process.resourcesPath, 'workdsh-runtime', 'primary-runtime') +} + function materializeRuntimeProfile(home: string): string { const source = bundledProfileDirectory() const target = join(home, 'profiles', PROFILE_NAME) @@ -126,6 +130,7 @@ function startRuntime(home: string, profileDir: string): void { env: { ...process.env, DSH_HOME: home, + DSH_BUNDLED_PRIMARY_RUNTIME: bundledPrimaryRuntime(), ELECTRON_RUN_AS_NODE: undefined, }, stdio: ['ignore', 'pipe', 'pipe'], diff --git a/dsh-plugin-desktop-beta/tests/package.spec.ts b/dsh-plugin-desktop-beta/tests/package.spec.ts index 394693800e..cb7c6c1036 100644 --- a/dsh-plugin-desktop-beta/tests/package.spec.ts +++ b/dsh-plugin-desktop-beta/tests/package.spec.ts @@ -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 && yarn run prepare:workdsh-runtime && node scripts/package-dir.mjs') + .toBe('yarn run build && yarn run prepare:electron-native && yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-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('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?.['dist:mac']).toBe('yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/release-mac.ts') + expect(manifest.scripts?.['dist:mac-smoke']).toBe('yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/package-mac.ts') + expect(manifest.scripts?.['dist:win']).toBe('yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/package-win.ts') + expect(manifest.scripts?.['dist:win-portable']).toBe('yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-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') diff --git a/dsh-plugin-desktop/.gitignore b/dsh-plugin-desktop/.gitignore index 512bdde121..6936851f57 100644 --- a/dsh-plugin-desktop/.gitignore +++ b/dsh-plugin-desktop/.gitignore @@ -5,3 +5,5 @@ node_modules/ build/workdsh-runtime build/.workdsh-release-* build/.workdsh-node +build/.workdsh-primary-runtime-cache +build/official-primary-runtime diff --git a/dsh-plugin-desktop/package.json b/dsh-plugin-desktop/package.json index 35ce50af22..9bff026bce 100644 --- a/dsh-plugin-desktop/package.json +++ b/dsh-plugin-desktop/package.json @@ -129,14 +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 && 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", + "package:dir": "yarn run build && yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/package-dir.mjs", + "dist:mac": "yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/release-mac.ts", + "dist:mac-smoke": "yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/package-mac.ts", + "dist:win": "yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/package-win.ts", + "dist:win-portable": "yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-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" + "prepare:workdsh-runtime": "node scripts/prepare-workdsh-runtime.mjs", + "prepare:workdsh-primary-runtime": "node scripts/prepare-workdsh-primary-runtime.mjs" }, "dependencies": { "@deepseek-ai/cordis": "4.0.2", @@ -332,6 +332,7 @@ "asar": { "smartUnpack": true }, + "npmRebuild": false, "afterPack": "./scripts/verify-packaged-runtime.ts", "afterAllArtifactBuild": "./scripts/verify-electron-fuses.ts", "electronFuses": { @@ -443,7 +444,9 @@ "to": "workdsh-runtime", "filter": [ "profiles/workdsh/**", - "node/**" + "node/**", + "primary-runtime/**", + "office-skills/**" ] } ] diff --git a/dsh-plugin-desktop/scripts/electron-builder-environment.ts b/dsh-plugin-desktop/scripts/electron-builder-environment.ts new file mode 100644 index 0000000000..89d37d4c46 --- /dev/null +++ b/dsh-plugin-desktop/scripts/electron-builder-environment.ts @@ -0,0 +1,15 @@ +/** Dependency-collection policy shared by Electron Builder entry points. */ + +/** + * Keep the Desktop package on Electron Builder's bounded physical traversal. + * Its Yarn Berry collector delegates to `npm list --all`, which expands the + * outer stable and Beta workspace instead of the selected app's runtime graph. + */ +export function electronBuilderEnvironment( + environment: NodeJS.ProcessEnv, +): NodeJS.ProcessEnv { + return { + ...environment, + DSH_ELECTRON_BUILDER_TRAVERSAL_ONLY: '1', + } +} diff --git a/dsh-plugin-desktop/scripts/package-dir.mjs b/dsh-plugin-desktop/scripts/package-dir.mjs index f91cbcb155..5f74537bc9 100644 --- a/dsh-plugin-desktop/scripts/package-dir.mjs +++ b/dsh-plugin-desktop/scripts/package-dir.mjs @@ -3,13 +3,16 @@ import { spawnSync } from 'node:child_process' import { createRequire } from 'node:module' import { dirname, resolve } from 'node:path' +import { existsSync } from 'node:fs' import { fileURLToPath } from 'node:url' +import { electronBuilderEnvironment } from './electron-builder-environment.ts' import { withoutWindowsSigningSecrets } from './package-win.ts' import { withoutMacReleaseSecrets } from './release-preflight.ts' const require = createRequire(import.meta.url) const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))) const builderCli = require.resolve('electron-builder/cli.js') +const electronDist = resolve(dirname(require.resolve('electron/package.json')), 'dist') /** Electron Builder overrides that make the directory build unconditionally unsigned. */ export const UNSIGNED_DIRECTORY_BUILD_ARGS = Object.freeze([ @@ -28,10 +31,10 @@ export const UNSIGNED_DIRECTORY_BUILD_ARGS = Object.freeze([ * @returns {NodeJS.ProcessEnv} */ export function unsignedDirectoryBuildEnvironment(environment) { - return { + return electronBuilderEnvironment({ ...withoutWindowsSigningSecrets(withoutMacReleaseSecrets(environment)), CSC_IDENTITY_AUTO_DISCOVERY: 'false', - } + }) } /** @@ -40,6 +43,7 @@ export function unsignedDirectoryBuildEnvironment(environment) { * env?: NodeJS.ProcessEnv, * cwd?: string, * electronBuilderCli?: string, + * electronDistPath?: string, * nodeExecutable?: string, * run?: typeof spawnSync, * }} [options] @@ -48,9 +52,14 @@ export function packageDirectory(options = {}) { const run = options.run ?? spawnSync const nodeExecutable = options.nodeExecutable ?? process.execPath const electronBuilderCli = options.electronBuilderCli ?? builderCli + const configuredElectronDist = options.electronDistPath ?? electronDist const result = run( nodeExecutable, - [electronBuilderCli, ...UNSIGNED_DIRECTORY_BUILD_ARGS], + [ + electronBuilderCli, + ...UNSIGNED_DIRECTORY_BUILD_ARGS, + ...(existsSync(configuredElectronDist) ? [`--config.electronDist=${configuredElectronDist}`] : []), + ], { cwd: options.cwd ?? packageRoot, env: unsignedDirectoryBuildEnvironment(options.env ?? process.env), diff --git a/dsh-plugin-desktop/scripts/package-mac.ts b/dsh-plugin-desktop/scripts/package-mac.ts index c79429c683..234150c0ae 100644 --- a/dsh-plugin-desktop/scripts/package-mac.ts +++ b/dsh-plugin-desktop/scripts/package-mac.ts @@ -1,12 +1,13 @@ /** Build an unsigned macOS DMG smoke artifact on a native macOS host. */ import { spawnSync } from 'node:child_process' -import { rmSync } from 'node:fs' +import { existsSync, rmSync } from 'node:fs' import { createRequire } from 'node:module' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { withoutMacReleaseSecrets } from './release-preflight.ts' import { prepareInstalledMacUniversalRuntime } from './mac-universal.ts' +import { electronBuilderEnvironment } from './electron-builder-environment.ts' /** Injectable native macOS packaging boundary used by focused tests. */ export interface MacSmokePackageOptions { @@ -30,6 +31,8 @@ export interface MacSmokePackageOptions { readonly prepareRuntime: () => void /** Absolute electron-builder CLI module. */ readonly builderCli: string + /** Local Electron distribution when already installed, avoiding another download. */ + readonly electronDist?: string /** Absolute packaged-DMG verification script. */ readonly verifier: string /** Node executable used to run package-local scripts. */ @@ -63,6 +66,7 @@ function defaultOptions(): MacSmokePackageOptions { const workspaceRoot = resolve(desktopRoot, '..') const require = createRequire(import.meta.url) const outputDir = resolve(desktopRoot, 'dist', 'mac-smoke', process.env.WORKDSH_MAC_ARCH ?? process.arch) + const electronDist = resolve(dirname(require.resolve('electron/package.json')), 'dist') return { env: process.env, platform: process.platform, @@ -74,6 +78,7 @@ function defaultOptions(): MacSmokePackageOptions { resetOutput: () => rmSync(outputDir, { recursive: true, force: true }), prepareRuntime: () => prepareInstalledMacUniversalRuntime(desktopRoot), builderCli: require.resolve('electron-builder/cli.js'), + ...(existsSync(resolve(electronDist, 'Electron.app')) ? { electronDist } : {}), verifier: fileURLToPath(new URL('./verify-mac-smoke.ts', import.meta.url)), nodeExecutable: process.execPath, run, @@ -135,12 +140,13 @@ export function packageMacSmoke(options: MacSmokePackageOptions = defaultOptions '--config.mac.notarize=false', '--config.npmRebuild=false', `--config.directories.output=${options.outputDir}`, + ...(options.electronDist === undefined ? [] : [`--config.electronDist=${options.electronDist}`]), ], options.desktopRoot, - { + electronBuilderEnvironment({ ...cleanEnvironment, CSC_IDENTITY_AUTO_DISCOVERY: 'false', - }, + }), ) options.run( options.nodeExecutable, diff --git a/dsh-plugin-desktop/scripts/prepare-workdsh-primary-runtime.mjs b/dsh-plugin-desktop/scripts/prepare-workdsh-primary-runtime.mjs new file mode 100644 index 0000000000..67d5a39312 --- /dev/null +++ b/dsh-plugin-desktop/scripts/prepare-workdsh-primary-runtime.mjs @@ -0,0 +1,58 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process' +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const desktopRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const upstreamRoot = resolve(desktopRoot, '..', 'deepseek-harness') +const target = process.platform === 'win32' ? 'win-x64' + : process.platform === 'darwin' ? `mac-${process.env.WORKDSH_MAC_ARCH ?? process.arch}` + : undefined +if (target === undefined || !['win-x64', 'mac-arm64', 'mac-x64'].includes(target)) { + throw new Error(`Unsupported WorkDSH primary runtime target: ${target ?? process.platform}`) +} +const output = join(desktopRoot, 'build', 'workdsh-runtime') +const cache = join(desktopRoot, 'build', '.workdsh-primary-runtime-cache') +const corepack = process.platform === 'win32' ? 'corepack.cmd' : 'corepack' +const result = spawnSync(corepack, [ + 'pnpm', 'run', 'prepare:primary-runtime', '--target', target, '--output', output, '--cache', cache, +], { + cwd: upstreamRoot, + env: { ...process.env, CI: 'true' }, + stdio: 'inherit', + shell: process.platform === 'win32', +}) +if (result.error) throw result.error +if (result.status !== 0) throw new Error(`Official primary runtime preparation failed: ${result.status}`) +const manifestPath = join(output, 'primary-runtime', 'runtime.json') +if (!existsSync(manifestPath) || !existsSync(join(output, 'office-skills'))) { + throw new Error('Official primary runtime payload is incomplete') +} +const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) +if (manifest.desktopVersion !== '0.1.7-rc.2' || manifest.platform !== process.platform + || manifest.arch !== target.slice(4)) { + throw new Error(`Official primary runtime metadata does not match ${target} / rc.2`) +} +const profile = join(output, 'profiles', 'workdsh') +const packagePath = join(profile, 'package.json') +if (!existsSync(packagePath)) throw new Error('WorkDSH release profile must be prepared first') +const profilePackage = JSON.parse(readFileSync(packagePath, 'utf8')) +const packages = [ + '@deepseek-ai/dsh-tool-workspace-dependencies', + '@deepseek-ai/dsh-skill-office', +] +if (packages.some(name => profilePackage.dependencies?.[name] !== '0.1.7-rc.2' + || !existsSync(join(profile, 'node_modules', name, 'package.json')))) { + const pnpm = process.platform === 'win32' ? 'npx.cmd' : 'npx' + const install = spawnSync(pnpm, [ + '--yes', 'pnpm@11.8.0', '--dir', profile, 'add', '--save-exact', + ...packages.map(name => `${name}@0.1.7-rc.2`), + ], { env: process.env, stdio: 'inherit', shell: process.platform === 'win32' }) + if (install.error) throw install.error + if (install.status !== 0) throw new Error(`Official Office plugins failed to install: ${install.status}`) +} +const patch = `# Official DeepSeek Harness Desktop workspace dependencies and Office skills.\n- insert:\n - id: workspace-dependencies\n name: '@deepseek-ai/dsh-tool-workspace-dependencies'\n config:\n source: !!js "process.env.DSH_BUNDLED_PRIMARY_RUNTIME"\n root: !!js "process.getBuiltinModule('node:path').join(process.env.DSH_HOME, 'dsh-runtimes', 'dsh-primary-runtime')"\n - id: skill-office\n name: '@deepseek-ai/dsh-skill-office'\n config:\n assetRoot: !!js "process.getBuiltinModule('node:path').join(process.env.DSH_BUNDLED_PRIMARY_RUNTIME, '..', 'office-skills')"\n node: !!js "process.getBuiltinModule('node:path').join(process.env.DSH_BUNDLED_PRIMARY_RUNTIME, 'dependencies', 'node', 'bin', process.platform === 'win32' ? 'node.exe' : 'node')"\n` +writeFileSync(join(profile, 'cordis.patch.yml'), patch) +console.log(`Prepared official DeepSeek Harness ${target} primary runtime at ${output}`) diff --git a/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs b/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs index 0fbd95512e..2f93d75d2f 100644 --- a/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs +++ b/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs @@ -31,9 +31,10 @@ async function installReleasedProfile() { 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 rawInstallerPath = join(releaseDir, 'install-workdsh.original.mjs') const installerPath = join(releaseDir, 'install-workdsh.mjs') - await download(`${base}/install-workdsh.mjs`, installerPath) - let installer = readFileSync(installerPath, 'utf8') + await download(`${base}/install-workdsh.mjs`, rawInstallerPath) + let installer = readFileSync(rawInstallerPath, 'utf8') installer = installer.replace( 'profilePackage.packageManager = manifest.packageManager;', "profilePackage.packageManager = manifest.packageManager;\n profilePackage.devEngines = { ...profilePackage.devEngines, packageManager: { name: 'pnpm', version: manifest.packageManager.slice('pnpm@'.length), onFail: 'ignore' } };", @@ -56,9 +57,24 @@ async function installReleasedProfile() { } } writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n') + const installSpawn = process.platform === 'win32' ? 'portableSpawn' : 'spawnSync' installer = readFileSync(installerPath, 'utf8').replace( " execute(['plugin', '--profile', profile, 'add', join(directory, item.filename)]);", - " execute(['plugin', '--profile', profile, 'allow-version', `${name}@${item.version}`, '--dsh-version', expectedHarness, '--accept-risk']);\n execute(['plugin', '--profile', profile, 'add', join(directory, item.filename)]);", + ` execute(['plugin', '--profile', profile, 'allow-version', \`\${name}@\${item.version}\`, '--dsh-version', expectedHarness, '--accept-risk']); + // The DSH plugin-manager wrapper can retain an idle pnpm process after a + // completed local tarball add. Use the same pinned pnpm directly; the + // release tarball hash and exact compatibility approval were checked above. + const profileDir = join(dshHome, 'profiles', profile); + const added = ${installSpawn}(corepack, ['--dir', profileDir, 'add', '--save-exact', join(directory, item.filename)], { stdio: 'inherit' }); + if (added.error) throw added.error; + if (added.status !== 0) process.exit(added.status ?? 1); + if (name === 'workdsh-bundle') { + const profilePackage = JSON.parse(readFileSync(profileManifest, 'utf8')); + const bundles = profilePackage.dsh?.profile?.bundles ?? []; + if (!bundles.includes(name)) bundles.push(name); + profilePackage.dsh = { ...profilePackage.dsh, profile: { ...profilePackage.dsh?.profile, bundles } }; + writeFileSync(profileManifest, JSON.stringify(profilePackage, null, 2) + '\\n'); + }`, ) writeFileSync(installerPath, installer) for (const item of manifest.packages) { @@ -99,8 +115,8 @@ async function prepareNodeExecutable(path) { if (process.platform !== 'win32') chmodSync(path, 0o755) return } - const targetArch = process.env.WORKDSH_MAC_ARCH - if (targetArch !== undefined && targetArch !== 'x64' && targetArch !== 'arm64') { + const targetArch = process.env.WORKDSH_MAC_ARCH ?? process.arch + if (targetArch !== 'x64' && targetArch !== 'arm64') { throw new Error(`unsupported macOS target architecture: ${targetArch}`) } if (targetArch === process.arch) { @@ -108,15 +124,14 @@ async function prepareNodeExecutable(path) { chmodSync(path, 0o755) return } - const downloadArch = targetArch ?? (process.arch === 'arm64' ? 'x64' : 'arm64') + const downloadArch = targetArch const cache = join(desktopRoot, 'build', '.workdsh-node') const archive = join(cache, `node-v${process.versions.node}-darwin-${downloadArch}.tar.gz`) const extracted = join(cache, `node-v${process.versions.node}-darwin-${downloadArch}`, 'bin', 'node') mkdirSync(cache, { recursive: true }) await download(`https://nodejs.org/dist/v${process.versions.node}/node-v${process.versions.node}-darwin-${downloadArch}.tar.gz`, archive) if (!existsSync(extracted)) run('/usr/bin/tar', ['-xzf', archive, '-C', cache]) - if (targetArch === undefined) run('/usr/bin/lipo', ['-create', process.execPath, extracted, '-output', path]) - else cpSync(extracted, path) + cpSync(extracted, path) chmodSync(path, 0o755) } @@ -132,7 +147,23 @@ const installedDshVersion = candidate => { return undefined } } -const source = candidates.find(candidate => installedDshVersion(candidate) === DSH_VERSION) +const releasePackages = [ + 'workdsh-provider-identity-local', 'workdsh-plugin-audit', 'workdsh-plugin-access', + 'workdsh-plugin-skills', 'workdsh-plugin-experts', 'workdsh-plugin-connectors', + 'workdsh-plugin-activity', 'workdsh-plugin-office', 'workdsh-plugin-library', + 'workdsh-plugin-projects', 'workdsh-bundle', +] +const isPreparedProfile = candidate => { + if (installedDshVersion(candidate) !== DSH_VERSION) return false + if (!releasePackages.every(name => existsSync(join(candidate, 'node_modules', name, 'package.json')))) return false + try { + const profile = JSON.parse(readFileSync(join(candidate, 'package.json'), 'utf8')) + return profile.dsh?.profile?.bundles?.includes('workdsh-bundle') === true + } catch { + return false + } +} +const source = candidates.find(isPreparedProfile) if (source) { rmSync(output, { recursive: true, force: true }) @@ -141,7 +172,7 @@ if (source) { // 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 (installedDshVersion(destination) !== DSH_VERSION || !existsSync(cli)) { +} else if (!isPreparedProfile(destination) || !existsSync(cli)) { rmSync(output, { recursive: true, force: true }) await installReleasedProfile() } diff --git a/dsh-plugin-desktop/src/workdsh-main.ts b/dsh-plugin-desktop/src/workdsh-main.ts index 86cdbac141..78f4b83f5b 100644 --- a/dsh-plugin-desktop/src/workdsh-main.ts +++ b/dsh-plugin-desktop/src/workdsh-main.ts @@ -17,7 +17,7 @@ import { join } from 'node:path' import { fileURLToPath } from 'node:url' const PROFILE_NAME = 'workdsh' -const RUNTIME_VERSION = '0.1.0-alpha.8' +const RUNTIME_VERSION = '0.1.0-alpha.9+dsh-0.1.7-rc.2' const READY_PATTERN = /dsh web:\s+(http:\/\/127\.0\.0\.1:\d+\/?\?token=[^\s]+)/u let runtime: ChildProcess | undefined @@ -47,6 +47,10 @@ function bundledNodeExecutable(): string { ) } +function bundledPrimaryRuntime(): string { + return join(process.resourcesPath, 'workdsh-runtime', 'primary-runtime') +} + function materializeRuntimeProfile(home: string): string { const source = bundledProfileDirectory() const target = join(home, 'profiles', PROFILE_NAME) @@ -126,6 +130,7 @@ function startRuntime(home: string, profileDir: string): void { env: { ...process.env, DSH_HOME: home, + DSH_BUNDLED_PRIMARY_RUNTIME: bundledPrimaryRuntime(), ELECTRON_RUN_AS_NODE: undefined, }, stdio: ['ignore', 'pipe', 'pipe'], diff --git a/dsh-plugin-desktop/tests/package-mac.spec.ts b/dsh-plugin-desktop/tests/package-mac.spec.ts index 07055a4071..ab1f303b9d 100644 --- a/dsh-plugin-desktop/tests/package-mac.spec.ts +++ b/dsh-plugin-desktop/tests/package-mac.spec.ts @@ -78,6 +78,7 @@ describe('macOS DMG smoke packaging', () => { PATH: '/usr/bin:/bin', SAFE_VALUE: 'kept', CSC_IDENTITY_AUTO_DISCOVERY: 'false', + DSH_ELECTRON_BUILDER_TRAVERSAL_ONLY: '1', }, }) expect(calls[2]).toEqual({ diff --git a/dsh-plugin-desktop/tests/package.spec.ts b/dsh-plugin-desktop/tests/package.spec.ts index ac89eed801..6e77e315c2 100644 --- a/dsh-plugin-desktop/tests/package.spec.ts +++ b/dsh-plugin-desktop/tests/package.spec.ts @@ -832,16 +832,16 @@ 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 && yarn run prepare:electron-native && yarn run prepare:workdsh-runtime && node scripts/package-dir.mjs') + .toBe('yarn run build && yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-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('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?.['dist:mac']).toBe('yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/release-mac.ts') + expect(manifest.scripts?.['dist:mac-smoke']).toBe('yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/package-mac.ts') + expect(manifest.scripts?.['dist:win']).toBe('yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-runtime && node scripts/package-win.ts') + expect(manifest.scripts?.['dist:win-portable']).toBe('yarn run prepare:workdsh-runtime && yarn run prepare:workdsh-primary-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') diff --git a/upstream.json b/upstream.json index 14f55d7e3b..e39cfdaff9 100644 --- a/upstream.json +++ b/upstream.json @@ -10,8 +10,8 @@ "package": "dsh-plugin-desktop" }, "beta": { - "commit": "82a5fd61a7cf5c293cec4bdff68f455398d685e9", - "sourceVersion": "0.1.3-alpha.2", + "commit": "477b4f420553e8a52c2fbccc464d7561b239c443", + "sourceVersion": "0.1.7-rc.2", "runtimePackageVersion": "0.1.3-alpha.2", "runtimeSource": "vendor/dsh-runtime/0.1.3-alpha.2/manifest.json", "package": "dsh-plugin-desktop-beta"