dsh-clean-desktop-shell 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/runtime.js ADDED
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Electron runtime provisioning for the plugin-market (branch 2) shell.
3
+ *
4
+ * The electron runtime is NOT an npm dependency (electron-builder forbids
5
+ * electron in "dependencies", and pnpm's allowBuilds would block its
6
+ * postinstall anyway). This module provisions it under
7
+ * $DSH_HOME/desktop-shell-runtime/electron-v<ver>/:
8
+ *
9
+ * 1. resolve the version (package.json → desktopShell.electronVersion)
10
+ * 2. if that version dir exists → reuse it
11
+ * 3. otherwise reuse a local electron package (DSH_SHELL_ELECTRON_DIR),
12
+ * else download the official zip from the fastest reachable source
13
+ * 4. drop stale electron-v* dirs (no unbounded disk growth)
14
+ */
15
+ import { spawn } from 'node:child_process'
16
+ import {
17
+ cpSync,
18
+ existsSync,
19
+ mkdirSync,
20
+ readFileSync,
21
+ readdirSync,
22
+ renameSync,
23
+ rmSync,
24
+ symlinkSync,
25
+ } from 'node:fs'
26
+ import { join } from 'node:path'
27
+ import { PKG_ROOT, EXE_NAME, ARCH, PLATFORM, runtimeRoot } from './common.js'
28
+
29
+ function electronVersion() {
30
+ try {
31
+ const meta = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'))
32
+ return meta.desktopShell?.electronVersion || null
33
+ } catch {
34
+ return null
35
+ }
36
+ }
37
+
38
+ function versionDir(root, version) {
39
+ return join(root, `electron-v${version}`)
40
+ }
41
+
42
+ function zipName(version) {
43
+ return `electron-v${version}-${PLATFORM}-${ARCH}.zip`
44
+ }
45
+
46
+ /** Resolve (or provision) the runtime and return the electron exe path. */
47
+ export async function ensureRuntime(ctx) {
48
+ const version = electronVersion()
49
+ if (!version) throw new Error('desktopShell.electronVersion missing in package.json')
50
+ const root = runtimeRoot()
51
+ const dir = versionDir(root, version)
52
+ const exe = join(dir, EXE_NAME)
53
+
54
+ // 1) Already provisioned for this version?
55
+ if (existsSync(exe)) {
56
+ cleanupOldVersions(root, dir)
57
+ return exe
58
+ }
59
+
60
+ // 2) Local reuse: DSH_SHELL_ELECTRON_DIR → link/copy its dist/ (fast).
61
+ const localSrc = process.env.DSH_SHELL_ELECTRON_DIR
62
+ if (localSrc) {
63
+ const srcExe = join(localSrc, 'dist', EXE_NAME)
64
+ if (existsSync(srcExe) && provisionLocalDist(localSrc, dir)) {
65
+ ctx.logger.info(`[clean-desktop-shell] reused electron runtime from ${localSrc}`)
66
+ }
67
+ }
68
+
69
+ // 3) Download + extract the official zip from a network-appropriate source.
70
+ if (!existsSync(exe)) {
71
+ mkdirSync(root, { recursive: true })
72
+ await downloadRuntime(ctx, version, root, dir)
73
+ }
74
+
75
+ if (!existsSync(exe)) {
76
+ throw new Error(
77
+ 'electron runtime provisioning failed — check network, or point DSH_SHELL_ELECTRON_DIR at an electron package',
78
+ )
79
+ }
80
+ cleanupOldVersions(root, dir)
81
+ ctx.logger.info(`[clean-desktop-shell] electron runtime ${version} ready at ${dir}`)
82
+ return exe
83
+ }
84
+
85
+ async function downloadRuntime(ctx, version, root, dir) {
86
+ const tmpZip = join(root, `.electron-${version}.zip.tmp`)
87
+ rmSync(tmpZip, { force: true })
88
+ const urls = await runtimeUrls(version)
89
+ for (const url of urls) {
90
+ ctx.logger.info(`[clean-desktop-shell] downloading electron ${version} from ${url}`)
91
+ if (await fetchFile(url, tmpZip)) {
92
+ // Zip extracts to an inner dir named like the zip basename.
93
+ const inner = join(root, zipName(version).replace(/\.zip$/, ''))
94
+ try {
95
+ await extractZip(tmpZip, root)
96
+ if (existsSync(join(inner, EXE_NAME)) && inner !== dir) {
97
+ rmSync(dir, { recursive: true, force: true })
98
+ renameSync(inner, dir)
99
+ }
100
+ rmSync(tmpZip, { force: true })
101
+ return
102
+ } catch (err) {
103
+ ctx.logger.warn(`[clean-desktop-shell] extract failed: ${err?.message ?? err}`)
104
+ rmSync(inner, { recursive: true, force: true })
105
+ }
106
+ }
107
+ // Failed download — drop the partial file so a later run starts clean.
108
+ rmSync(tmpZip, { force: true })
109
+ }
110
+ throw new Error('electron download failed from all sources')
111
+ }
112
+
113
+ /**
114
+ * Pick the download source by racing a HEAD probe against each candidate
115
+ * (direct connection, 3s each). The fastest reachable source goes first —
116
+ * this naturally prefers the domestic npmmirror mirror on CN networks,
117
+ * the official GitHub source on international/well-proxied networks, and
118
+ * never wastes a full download on a dead source.
119
+ */
120
+ async function runtimeUrls(version) {
121
+ const candidates = [
122
+ { name: 'github', url: `https://github.com/electron/electron/releases/download/v${version}/${zipName(version)}` },
123
+ { name: 'npmmirror', url: `https://npmmirror.com/mirrors/electron/${version}/${zipName(version)}` },
124
+ ]
125
+ const results = await Promise.all(
126
+ candidates.map(async (c) => {
127
+ const t0 = Date.now()
128
+ try {
129
+ const ctrl = new AbortController()
130
+ const timer = setTimeout(() => ctrl.abort(), 3000)
131
+ const res = await fetch(c.url, { signal: ctrl.signal, method: 'HEAD' })
132
+ clearTimeout(timer)
133
+ if (res.status < 500) return { ...c, ms: Date.now() - t0 }
134
+ } catch {
135
+ // unreachable — drop
136
+ }
137
+ return null
138
+ }),
139
+ )
140
+ const ok = results.filter(Boolean).sort((a, b) => a.ms - b.ms)
141
+ if (ok.length === 0) {
142
+ // Probes all failed (offline?) — still try both, mirror first (cheap).
143
+ return [candidates[1].url, candidates[0].url]
144
+ }
145
+ const rest = candidates.map((c) => c.url).filter((u) => u !== ok[0].url)
146
+ return [ok[0].url, ...rest]
147
+ }
148
+
149
+ function fetchFile(url, dest) {
150
+ return new Promise((resolve) => {
151
+ // curl is available on Windows 10+; streams to disk, honors proxy env.
152
+ // --max-time keeps a stalled download from hanging forever (a proxy
153
+ // stall previously left a half-written .zip.tmp and blocked the shell
154
+ // launch); --retry 2 rides out transient failures.
155
+ const child = spawn(
156
+ 'curl',
157
+ ['-L', '--fail', '--silent', '--show-error', '--retry', '2', '--max-time', '600', '-o', dest, url],
158
+ { windowsHide: true, stdio: 'ignore' },
159
+ )
160
+ child.on('error', () => resolve(false))
161
+ child.on('exit', (code) => resolve(code === 0))
162
+ })
163
+ }
164
+
165
+ function extractZip(zipPath, dest) {
166
+ // Windows ships bsdtar (tar.exe) which reads zip; fall back to
167
+ // PowerShell Expand-Archive if needed.
168
+ const child = spawn('tar', ['-xf', zipPath, '-C', dest], { windowsHide: true, stdio: 'ignore' })
169
+ return new Promise((resolve, reject) => {
170
+ child.on('error', reject)
171
+ child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`tar exit ${code}`))))
172
+ })
173
+ }
174
+
175
+ /** Remove version dirs older than the current one (dead weight). */
176
+ function cleanupOldVersions(root, currentDir) {
177
+ try {
178
+ for (const entry of readdirSync(root)) {
179
+ if (!entry.startsWith('electron-v')) continue
180
+ const full = join(root, entry)
181
+ if (full === currentDir) continue
182
+ rmSync(full, { recursive: true, force: true })
183
+ }
184
+ } catch {
185
+ // best-effort
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Provision a local electron package's dist/ as the version dir itself,
191
+ * so the layout matches a downloaded runtime: <dir>/electron(.exe) at the
192
+ * version-dir root. Windows: junction (zero-copy, instant) — a 269MB
193
+ * recursive cpSync can be killed by sandbox/AV on large trees, so only
194
+ * fall back to a copy.
195
+ */
196
+ function provisionLocalDist(srcPkg, destDir) {
197
+ if (process.platform === 'win32') {
198
+ try {
199
+ rmSync(destDir, { recursive: true, force: true })
200
+ symlinkSync(join(srcPkg, 'dist'), destDir, 'junction')
201
+ return true
202
+ } catch {
203
+ // fall through to a real copy
204
+ }
205
+ }
206
+ try {
207
+ rmSync(destDir, { recursive: true, force: true })
208
+ cpSync(join(srcPkg, 'dist'), destDir, { recursive: true })
209
+ return true
210
+ } catch {
211
+ return false
212
+ }
213
+ }
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "dsh-clean-desktop-shell",
3
- "version": "0.1.4",
4
- "description": "Clean desktop shell for DeepSeek Harness (DSH) as a DSH plugin — reuses your web profile, tray/single-instance/auto-launch, zero visual changes. DSH 插件形态的纯净桌面壳:复用现有 web profile,托盘管理后端,零视觉改造。",
3
+ "version": "0.1.6",
4
+ "description": "Clean desktop shell for DeepSeek Harness (DSH) as a DSH plugin — wraps your web profile in a native window, tray-managed backend, offline auto-reconnect, zero visual changes. DSH 插件形态的纯净桌面壳:复用现有 web profile,托盘管理后端,零视觉改造。",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Icather/dsh-clean-desktop-shell.git"
8
+ },
9
+ "homepage": "https://github.com/Icather/dsh-clean-desktop-shell#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/Icather/dsh-clean-desktop-shell/issues"
12
+ },
5
13
  "type": "module",
6
14
  "main": "lib/index.js",
7
15
  "files": [
package/scripts/build.mjs CHANGED
@@ -13,6 +13,9 @@ mkdirSync(join(root, 'lib'), { recursive: true })
13
13
 
14
14
  const pairs = [
15
15
  ['src/host/index.js', 'lib/index.js'],
16
+ ['src/host/common.js', 'lib/common.js'],
17
+ ['src/host/runtime.js', 'lib/runtime.js'],
18
+ ['src/host/icon.js', 'lib/icon.js'],
16
19
  ['src/client/client.js', 'lib/client.js'],
17
20
  ]
18
21
  for (const [src, dest] of pairs) {
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Shared constants and helpers for the host half modules.
3
+ *
4
+ * These compiled files sit at lib/<name>.js, so two dirname hops reach the
5
+ * package root — the same layout as src/host/ before build, and the same
6
+ * location electron/ and build/ live in the published package.
7
+ */
8
+ import { homedir } from 'node:os'
9
+ import { dirname, join } from 'node:path'
10
+ import { fileURLToPath } from 'node:url'
11
+
12
+ export const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
13
+ export const MAIN_JS = join(PKG_ROOT, 'electron', 'main.js')
14
+ export const isWin = process.platform === 'win32'
15
+ export const EXE_NAME = isWin ? 'electron.exe' : 'electron'
16
+ export const ARCH = process.arch === 'arm64' ? 'arm64' : 'x64'
17
+ export const PLATFORM = isWin ? 'win32' : process.platform === 'darwin' ? 'darwin' : 'linux'
18
+
19
+ /** Where the self-provisioned runtimes live (shared with icon.js). */
20
+ export function runtimeRoot() {
21
+ return join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'desktop-shell-runtime')
22
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Patch the runtime electron.exe's icon resource so the Windows taskbar
3
+ * shows our whale icon.
4
+ *
5
+ * A bare runtime exe ships Electron's default icon and — as documented —
6
+ * no runtime API (BrowserWindow icon, setAppDetails, AUMID shortcuts) can
7
+ * change the taskbar button: it reads the exe's icon resource. rcedit
8
+ * (electron team's official tool) rewrites it in place.
9
+ *
10
+ * Best-effort: icon patching must never block the shell from launching.
11
+ * Idempotent: a marker file next to the exe records success; a re-provisioned
12
+ * (new version) exe has no marker and gets patched again.
13
+ */
14
+ import { spawn } from 'node:child_process'
15
+ import { existsSync, writeFileSync } from 'node:fs'
16
+ import { join } from 'node:path'
17
+ import { PKG_ROOT, isWin, runtimeRoot } from './common.js'
18
+
19
+ export async function patchExeIcon(ctx, exe) {
20
+ if (!isWin) return
21
+ const ico = join(PKG_ROOT, 'build', 'icon.ico')
22
+ if (!existsSync(ico)) return
23
+ const marker = `${exe}.whale-icon`
24
+ if (existsSync(marker)) return
25
+
26
+ // rcedit is a single self-contained exe, cached next to the runtimes.
27
+ const rcedit = join(runtimeRoot(), 'rcedit-x64.exe')
28
+ if (!existsSync(rcedit)) {
29
+ const url = 'https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe'
30
+ ctx.logger.info('[clean-desktop-shell] downloading rcedit for icon patching')
31
+ if (!(await fetchFile(url, rcedit))) {
32
+ ctx.logger.warn('[clean-desktop-shell] rcedit download failed — taskbar icon stays default')
33
+ return
34
+ }
35
+ }
36
+
37
+ const child = spawn(rcedit, [exe, '--set-icon', ico], {
38
+ windowsHide: true,
39
+ stdio: 'ignore',
40
+ })
41
+ const ok = await new Promise((resolve) => {
42
+ child.on('error', () => resolve(false))
43
+ child.on('exit', (code) => resolve(code === 0))
44
+ })
45
+ if (ok) {
46
+ writeFileSync(marker, String(Date.now()), 'utf8')
47
+ ctx.logger.info('[clean-desktop-shell] taskbar icon patched (rcedit)')
48
+ } else {
49
+ ctx.logger.warn('[clean-desktop-shell] rcedit patch failed — taskbar icon stays default')
50
+ }
51
+ }
52
+
53
+ function fetchFile(url, dest) {
54
+ return new Promise((resolve) => {
55
+ const child = spawn(
56
+ 'curl',
57
+ ['-L', '--fail', '--silent', '--show-error', '--retry', '2', '--max-time', '600', '-o', dest, url],
58
+ { windowsHide: true, stdio: 'ignore' },
59
+ )
60
+ child.on('error', () => resolve(false))
61
+ child.on('exit', (code) => resolve(code === 0))
62
+ })
63
+ }
package/src/host/index.js CHANGED
@@ -4,44 +4,14 @@
4
4
  * Branch 2 (plugin-market distribution): when installed through the DSH
5
5
  * plugin market, this host half brings up the Electron shell itself.
6
6
  *
7
- * The electron runtime is NOT an npm dependency (electron-builder forbids
8
- * electron in "dependencies", and pnpm's allowBuilds would block its
9
- * postinstall anyway). Instead the host half manages the runtime on its
10
- * own, under $DSH_HOME/desktop-shell-runtime/:
11
- *
12
- * 1. resolve the version to run (package.json → desktopShell.electronVersion)
13
- * 2. if that version dir exists → reuse it
14
- * 3. otherwise download the electron zip from the best source for the
15
- * network (official GitHub releases vs npmmirror mirror), extract it,
16
- * and drop any older version dirs (no unbounded disk growth)
17
- * 4. spawn the shell (runtime electron + electron/main.js) — the same
18
- * code branch 1 (installer) ships
19
- *
20
- * Window, tray, backend management etc. are identical to branch 1; only
21
- * the runtime provisioning differs.
7
+ * The shell code is shared with branch 1 (installer) — only the runtime
8
+ * provisioning and launch differ. See runtime.js (provisioning) and
9
+ * icon.js (Windows taskbar icon).
22
10
  */
23
11
  import { spawn } from 'node:child_process'
24
- import {
25
- cpSync,
26
- existsSync,
27
- mkdirSync,
28
- readFileSync,
29
- readdirSync,
30
- renameSync,
31
- rmSync,
32
- symlinkSync,
33
- writeFileSync,
34
- } from 'node:fs'
35
- import { homedir } from 'node:os'
36
- import { dirname, join } from 'node:path'
37
- import { fileURLToPath } from 'node:url'
38
-
39
- const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
40
- const MAIN_JS = join(PKG_ROOT, 'electron', 'main.js')
41
- const isWin = process.platform === 'win32'
42
- const EXE_NAME = isWin ? 'electron.exe' : 'electron'
43
- const ARCH = process.arch === 'arm64' ? 'arm64' : 'x64'
44
- const PLATFORM = isWin ? 'win32' : process.platform === 'darwin' ? 'darwin' : 'linux'
12
+ import { PKG_ROOT, MAIN_JS } from './common.js'
13
+ import { ensureRuntime } from './runtime.js'
14
+ import { patchExeIcon } from './icon.js'
45
15
 
46
16
  // cordis registers the plugin by this name — bundles without an explicit
47
17
  // `name` export are silently skipped by the dsh loader.
@@ -63,6 +33,7 @@ export function apply(ctx) {
63
33
  ;(async () => {
64
34
  try {
65
35
  const exe = await ensureRuntime(ctx)
36
+ await patchExeIcon(ctx, exe).catch(() => {})
66
37
  launchShell(exe, ctx)
67
38
  } catch (err) {
68
39
  ctx.logger.warn(`[clean-desktop-shell] shell launch failed: ${err?.message ?? err}`)
@@ -70,246 +41,6 @@ export function apply(ctx) {
70
41
  })()
71
42
  }
72
43
 
73
- // ---------- electron runtime provisioning ----------
74
-
75
- function electronVersion() {
76
- try {
77
- const meta = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'))
78
- return meta.desktopShell?.electronVersion || null
79
- } catch {
80
- return null
81
- }
82
- }
83
-
84
- function runtimeRoot() {
85
- return join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'desktop-shell-runtime')
86
- }
87
-
88
- function versionDir(root, version) {
89
- return join(root, `electron-v${version}`)
90
- }
91
-
92
- function zipName(version) {
93
- return `electron-v${version}-${PLATFORM}-${ARCH}.zip`
94
- }
95
-
96
- async function ensureRuntime(ctx) {
97
- const version = electronVersion()
98
- if (!version) throw new Error('desktopShell.electronVersion missing in package.json')
99
- const root = runtimeRoot()
100
- const dir = versionDir(root, version)
101
- const exe = join(dir, EXE_NAME)
102
-
103
- // 1) Already provisioned for this version?
104
- if (existsSync(exe)) {
105
- cleanupOldVersions(root, dir)
106
- await patchExeIcon(ctx, exe, root).catch(() => {})
107
- return exe
108
- }
109
-
110
- // 2) Local reuse: DSH_SHELL_ELECTRON_DIR → link/copy its dist/ (fast).
111
- const localSrc = process.env.DSH_SHELL_ELECTRON_DIR
112
- if (localSrc) {
113
- const srcExe = join(localSrc, 'dist', EXE_NAME)
114
- if (existsSync(srcExe) && provisionLocalDist(localSrc, dir)) {
115
- ctx.logger.info(`[clean-desktop-shell] reused electron runtime from ${localSrc}`)
116
- }
117
- }
118
-
119
- // 3) Download + extract the official zip from a network-appropriate source.
120
- if (!existsSync(exe)) {
121
- mkdirSync(root, { recursive: true })
122
- await downloadRuntime(ctx, version, root, dir)
123
- }
124
-
125
- if (!existsSync(exe)) {
126
- throw new Error(
127
- 'electron runtime provisioning failed — check network, or point DSH_SHELL_ELECTRON_DIR at an electron package',
128
- )
129
- }
130
- cleanupOldVersions(root, dir)
131
- ctx.logger.info(`[clean-desktop-shell] electron runtime ${version} ready at ${dir}`)
132
- await patchExeIcon(ctx, exe, root).catch(() => {})
133
- return exe
134
- }
135
-
136
- /**
137
- * Patch the runtime electron.exe's icon resource so the Windows taskbar
138
- * shows our whale icon. A bare runtime exe ships Electron's default icon
139
- * and — as documented — no runtime API (BrowserWindow icon, setAppDetails,
140
- * AUMID shortcuts) can change the taskbar button: it reads the exe's icon
141
- * resource. rcedit (electron team's official tool) rewrites it in place.
142
- *
143
- * Best-effort: icon patching must never block the shell from launching.
144
- * Idempotent: a marker file next to the exe records success; a re-provisioned
145
- * (new version) exe has no marker and gets patched again.
146
- */
147
- async function patchExeIcon(ctx, exe, root) {
148
- if (!isWin) return
149
- const ico = join(PKG_ROOT, 'build', 'icon.ico')
150
- if (!existsSync(ico)) return
151
- const marker = `${exe}.whale-icon`
152
- if (existsSync(marker)) return
153
-
154
- // rcedit is a single self-contained exe, cached next to the runtimes.
155
- const rcedit = join(root, 'rcedit-x64.exe')
156
- if (!existsSync(rcedit)) {
157
- const url = 'https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe'
158
- ctx.logger.info('[clean-desktop-shell] downloading rcedit for icon patching')
159
- if (!(await fetchFile(url, rcedit))) {
160
- ctx.logger.warn('[clean-desktop-shell] rcedit download failed — taskbar icon stays default')
161
- return
162
- }
163
- }
164
-
165
- const child = spawn(rcedit, [exe, '--set-icon', ico], {
166
- windowsHide: true,
167
- stdio: 'ignore',
168
- })
169
- const ok = await new Promise((resolve) => {
170
- child.on('error', () => resolve(false))
171
- child.on('exit', (code) => resolve(code === 0))
172
- })
173
- if (ok) {
174
- writeFileSync(marker, String(Date.now()), 'utf8')
175
- ctx.logger.info('[clean-desktop-shell] taskbar icon patched (rcedit)')
176
- } else {
177
- ctx.logger.warn('[clean-desktop-shell] rcedit patch failed — taskbar icon stays default')
178
- }
179
- }
180
-
181
- async function downloadRuntime(ctx, version, root, dir) {
182
- const tmpZip = join(root, `.electron-${version}.zip.tmp`)
183
- rmSync(tmpZip, { force: true })
184
- const urls = await runtimeUrls(version)
185
- for (const url of urls) {
186
- ctx.logger.info(`[clean-desktop-shell] downloading electron ${version} from ${url}`)
187
- if (await fetchFile(url, tmpZip)) {
188
- // Zip extracts to an inner dir named like the zip basename.
189
- const inner = join(root, zipName(version).replace(/\.zip$/, ''))
190
- try {
191
- await extractZip(tmpZip, root)
192
- if (existsSync(join(inner, EXE_NAME)) && inner !== dir) {
193
- rmSync(dir, { recursive: true, force: true })
194
- renameSync(inner, dir)
195
- }
196
- rmSync(tmpZip, { force: true })
197
- return
198
- } catch (err) {
199
- ctx.logger.warn(`[clean-desktop-shell] extract failed: ${err?.message ?? err}`)
200
- rmSync(inner, { recursive: true, force: true })
201
- }
202
- }
203
- // Failed download — drop the partial file so a later run starts clean.
204
- rmSync(tmpZip, { force: true })
205
- }
206
- throw new Error('electron download failed from all sources')
207
- }
208
-
209
- /**
210
- * Pick the download source by racing a HEAD probe against each candidate
211
- * (direct connection, 3s each). The fastest reachable source goes first —
212
- * this naturally prefers the domestic npmmirror mirror on CN networks,
213
- * the official GitHub source on international/well-proxied networks, and
214
- * never wastes a full download on a dead source.
215
- */
216
- async function runtimeUrls(version) {
217
- const candidates = [
218
- { name: 'github', url: `https://github.com/electron/electron/releases/download/v${version}/${zipName(version)}` },
219
- { name: 'npmmirror', url: `https://npmmirror.com/mirrors/electron/${version}/${zipName(version)}` },
220
- ]
221
- const results = await Promise.all(
222
- candidates.map(async (c) => {
223
- const t0 = Date.now()
224
- try {
225
- const ctrl = new AbortController()
226
- const timer = setTimeout(() => ctrl.abort(), 3000)
227
- const res = await fetch(c.url, { signal: ctrl.signal, method: 'HEAD' })
228
- clearTimeout(timer)
229
- if (res.status < 500) return { ...c, ms: Date.now() - t0 }
230
- } catch {
231
- // unreachable — drop
232
- }
233
- return null
234
- }),
235
- )
236
- const ok = results.filter(Boolean).sort((a, b) => a.ms - b.ms)
237
- if (ok.length === 0) {
238
- // Probes all failed (offline?) — still try both, mirror first (cheap).
239
- return [candidates[1].url, candidates[0].url]
240
- }
241
- const rest = candidates.map((c) => c.url).filter((u) => u !== ok[0].url)
242
- return [ok[0].url, ...rest]
243
- }
244
-
245
- function fetchFile(url, dest) {
246
- return new Promise((resolve) => {
247
- // curl is available on Windows 10+; streams to disk, honors proxy env.
248
- // --max-time keeps a stalled download from hanging forever (a proxy
249
- // stall previously left a half-written .zip.tmp and blocked the shell
250
- // launch); --retry 2 rides out transient failures.
251
- const child = spawn(
252
- 'curl',
253
- ['-L', '--fail', '--silent', '--show-error', '--retry', '2', '--max-time', '600', '-o', dest, url],
254
- { windowsHide: true, stdio: 'ignore' },
255
- )
256
- child.on('error', () => resolve(false))
257
- child.on('exit', (code) => resolve(code === 0))
258
- })
259
- }
260
-
261
- function extractZip(zipPath, dest) {
262
- // Windows ships bsdtar (tar.exe) which reads zip; fall back to
263
- // PowerShell Expand-Archive if needed.
264
- const child = spawn('tar', ['-xf', zipPath, '-C', dest], { windowsHide: true, stdio: 'ignore' })
265
- return new Promise((resolve, reject) => {
266
- child.on('error', reject)
267
- child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`tar exit ${code}`))))
268
- })
269
- }
270
-
271
- /** Remove version dirs older than the current one (dead weight). */
272
- function cleanupOldVersions(root, currentDir) {
273
- try {
274
- for (const entry of readdirSync(root)) {
275
- if (!entry.startsWith('electron-v')) continue
276
- const full = join(root, entry)
277
- if (full === currentDir) continue
278
- rmSync(full, { recursive: true, force: true })
279
- }
280
- } catch {
281
- // best-effort
282
- }
283
- }
284
-
285
- /**
286
- * Provision a local electron package's dist/ as the version dir itself,
287
- * so the layout matches a downloaded runtime: <dir>/electron(.exe) at the
288
- * version-dir root. Windows: junction (zero-copy, instant) — a 269MB
289
- * recursive cpSync can be killed by sandbox/AV on large trees, so only
290
- * fall back to a copy.
291
- */
292
- function provisionLocalDist(srcPkg, destDir) {
293
- if (isWin) {
294
- try {
295
- rmSync(destDir, { recursive: true, force: true })
296
- symlinkSync(join(srcPkg, 'dist'), destDir, 'junction')
297
- return true
298
- } catch {
299
- // fall through to a real copy
300
- }
301
- }
302
- try {
303
- rmSync(destDir, { recursive: true, force: true })
304
- cpSync(join(srcPkg, 'dist'), destDir, { recursive: true })
305
- return true
306
- } catch {
307
- return false
308
- }
309
- }
310
-
311
- // ---------- shell launch ----------
312
-
313
44
  function launchShell(exe, ctx) {
314
45
  if (launched) return
315
46
  const child = spawn(exe, [MAIN_JS], {