dsh-clean-desktop-shell 0.1.11 → 0.1.13

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/package.json CHANGED
@@ -1,127 +1,140 @@
1
- {
2
- "name": "dsh-clean-desktop-shell",
3
- "version": "0.1.11",
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
- },
13
- "type": "module",
14
- "main": "lib/index.js",
15
- "files": [
16
- "lib/",
17
- "electron/",
18
- "src/",
19
- "scripts/",
20
- "build/icon.png",
21
- "build/icon.ico",
22
- "cordis.patch.yml",
23
- "README.md",
24
- "README.en.md",
25
- "CONTRIBUTORS.md",
26
- "LICENSE",
27
- "version.txt"
28
- ],
29
- "exports": {
30
- ".": "./lib/index.js",
31
- "./client": "./lib/client.js",
32
- "./package.json": "./package.json"
33
- },
34
- "scripts": {
35
- "build": "node scripts/build.mjs",
36
- "check": "node scripts/check-syntax.mjs && node scripts/selftest-runtime.mjs",
37
- "dev": "electron electron/main.js",
38
- "icons": "node scripts/gen-icons.mjs",
39
- "pack": "electron-builder --win nsis",
40
- "pack:mac": "electron-builder --mac dmg"
41
- },
42
- "dsh": {
43
- "bundle": {
44
- "patch": "./cordis.patch.yml"
45
- },
46
- "client": {
47
- "inject": [
48
- "@deepseek-ai/dsh-client-runtime",
49
- "@deepseek-ai/dsh-client-locale",
50
- "@deepseek-ai/dsh-client-ui-settings",
51
- "@deepseek-ai/dsh-client-ui-theme"
52
- ],
53
- "platform": "web"
54
- }
55
- },
56
- "peerDependencies": {
57
- "@deepseek-ai/dsh": "*"
58
- },
59
- "peerDependenciesMeta": {
60
- "@deepseek-ai/dsh": {
61
- "optional": true
62
- }
63
- },
64
- "devDependencies": {
65
- "electron": "^33.0.0",
66
- "electron-builder": "^25.0.0",
67
- "png-to-ico": "^3.0.2",
68
- "sharp": "^0.35.3"
69
- },
70
- "build": {
71
- "appId": "com.icather.dsh-clean-desktop-shell",
72
- "productName": "DSH Clean Desktop Shell",
73
- "asar": true,
74
- "files": [
75
- "electron/**/*",
76
- "lib/**/*",
77
- "src/**/*",
78
- "build/icon.png",
79
- "package.json",
80
- "cordis.patch.yml"
81
- ],
82
- "extraMetadata": {
83
- "main": "electron/main.js"
84
- },
85
- "win": {
86
- "target": [
87
- {
88
- "target": "nsis",
89
- "arch": [
90
- "x64"
91
- ]
92
- }
93
- ],
94
- "icon": "build/icon.ico",
95
- "signAndEditExecutable": false
96
- },
97
- "mac": {
98
- "target": [
99
- "dmg"
100
- ],
101
- "category": "public.app-category.developer-tools",
102
- "icon": "build/icon.png"
103
- },
104
- "nsis": {
105
- "oneClick": true,
106
- "perMachine": false,
107
- "createDesktopShortcut": true,
108
- "createStartMenuShortcut": true
109
- },
110
- "publish": {
111
- "provider": "github",
112
- "owner": "Icather",
113
- "repo": "dsh-clean-desktop-shell"
114
- }
115
- },
116
- "license": "MIT",
117
- "allowScripts": {
118
- "electron@33.4.11": true
119
- },
120
- "dependencies": {
121
- "electron-updater": "^6.8.9",
122
- "semver": "^7.7.2"
123
- },
124
- "desktopShell": {
125
- "electronVersion": "33.4.11"
126
- }
127
- }
1
+ {
2
+ "name": "dsh-clean-desktop-shell",
3
+ "version": "0.1.13",
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
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "engines": {
16
+ "node": ">=20.0.0"
17
+ },
18
+ "files": [
19
+ "lib/",
20
+ "electron/",
21
+ "src/",
22
+ "scripts/",
23
+ "build/icon.png",
24
+ "build/icon.ico",
25
+ "cordis.patch.yml",
26
+ "README.md",
27
+ "README.en.md",
28
+ "CONTRIBUTORS.md",
29
+ "LICENSE",
30
+ "version.txt"
31
+ ],
32
+ "exports": {
33
+ ".": "./lib/index.js",
34
+ "./client": "./lib/client.js",
35
+ "./package.json": "./package.json"
36
+ },
37
+ "scripts": {
38
+ "build": "node scripts/build.mjs",
39
+ "check": "node scripts/check-syntax.mjs && node scripts/selftest-runtime.mjs && node scripts/selftest-recovery.mjs",
40
+ "dev": "electron electron/main.js",
41
+ "icons": "node scripts/gen-icons.mjs",
42
+ "pack": "electron-builder --win nsis",
43
+ "pack:mac": "electron-builder --mac dmg"
44
+ },
45
+ "dsh": {
46
+ "bundle": {
47
+ "patch": "./cordis.patch.yml"
48
+ },
49
+ "compatibility": {
50
+ "dsh": ">=0.1.1 <0.2.0",
51
+ "dshReleases": {
52
+ "0.1.1-rc.2": "unknown",
53
+ "0.1.5-rc.1": "compatible",
54
+ "0.1.5-rc.2": "unknown",
55
+ "0.1.5-alpha.2": "unknown"
56
+ }
57
+ },
58
+ "client": {
59
+ "inject": [
60
+ "@deepseek-ai/dsh-client-connection",
61
+ "@deepseek-ai/dsh-client-runtime",
62
+ "@deepseek-ai/dsh-client-locale",
63
+ "@deepseek-ai/dsh-client-ui-settings",
64
+ "@deepseek-ai/dsh-client-ui-theme"
65
+ ],
66
+ "platform": "web"
67
+ }
68
+ },
69
+ "peerDependencies": {
70
+ "@deepseek-ai/dsh": "*"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "@deepseek-ai/dsh": {
74
+ "optional": true
75
+ }
76
+ },
77
+ "devDependencies": {
78
+ "electron": "^33.0.0",
79
+ "electron-builder": "^25.0.0",
80
+ "png-to-ico": "^3.0.2",
81
+ "sharp": "^0.35.3"
82
+ },
83
+ "build": {
84
+ "appId": "com.icather.dsh-clean-desktop-shell",
85
+ "productName": "DSH Clean Desktop Shell",
86
+ "asar": true,
87
+ "files": [
88
+ "electron/**/*",
89
+ "lib/**/*",
90
+ "src/**/*",
91
+ "build/icon.png",
92
+ "package.json",
93
+ "cordis.patch.yml"
94
+ ],
95
+ "extraMetadata": {
96
+ "main": "electron/main.js"
97
+ },
98
+ "win": {
99
+ "target": [
100
+ {
101
+ "target": "nsis",
102
+ "arch": [
103
+ "x64"
104
+ ]
105
+ }
106
+ ],
107
+ "icon": "build/icon.ico",
108
+ "signAndEditExecutable": false
109
+ },
110
+ "mac": {
111
+ "target": [
112
+ "dmg"
113
+ ],
114
+ "category": "public.app-category.developer-tools",
115
+ "icon": "build/icon.png"
116
+ },
117
+ "nsis": {
118
+ "oneClick": true,
119
+ "perMachine": false,
120
+ "createDesktopShortcut": true,
121
+ "createStartMenuShortcut": true
122
+ },
123
+ "publish": {
124
+ "provider": "github",
125
+ "owner": "Icather",
126
+ "repo": "dsh-clean-desktop-shell"
127
+ }
128
+ },
129
+ "license": "MIT",
130
+ "allowScripts": {
131
+ "electron@33.4.11": true
132
+ },
133
+ "dependencies": {
134
+ "electron-updater": "^6.8.9",
135
+ "semver": "^7.7.2"
136
+ },
137
+ "desktopShell": {
138
+ "electronVersion": "33.4.11"
139
+ }
140
+ }
@@ -1,54 +1,54 @@
1
- /**
2
- * Syntax gate: `node --check` every JS file that ships in the package
3
- * (electron/ main-process modules + lib/ host/client bundle). Previously
4
- * only lib/index.js was checked — a syntax slip in any other file shipped
5
- * unnoticed.
6
- *
7
- * The two sandboxed preloads are CommonJS (require/module.exports) while
8
- * package.json declares "type": "module", so plain --check on them fails
9
- * spuriously; they are validated through a temporary .cjs copy instead.
10
- */
11
- import { spawnSync } from 'node:child_process'
12
- import { readdirSync, readFileSync, statSync, rmSync, writeFileSync, mkdtempSync } from 'node:fs'
13
- import { dirname, join } from 'node:path'
14
- import { tmpdir } from 'node:os'
15
- import { fileURLToPath } from 'node:url'
16
-
17
- const root = dirname(dirname(fileURLToPath(import.meta.url)))
18
- // CJS files relative to electron/ — parsed as CommonJS despite the ESM package.
19
- const CJS_FILES = new Set(['preload.js', 'progress-preload.js'])
20
-
21
- function listJs(dir) {
22
- return readdirSync(join(root, dir)).filter((f) => f.endsWith('.js'))
23
- }
24
-
25
- const scratch = mkdtempSync(join(tmpdir(), 'dsh-shell-check-'))
26
- let bad = 0
27
- let checked = 0
28
-
29
- try {
30
- for (const dir of ['electron', 'lib']) {
31
- for (const f of listJs(dir)) {
32
- const full = join(root, dir, f)
33
- if (!statSync(full).isFile()) continue
34
- const cjs = CJS_FILES.has(f)
35
- const target = cjs ? join(scratch, `${dir}-${f}.cjs`) : full
36
- if (cjs) writeFileSync(target, readFileSync(full))
37
- const r = spawnSync(process.execPath, ['--check', target], { stdio: 'pipe' })
38
- const ok = r.status === 0
39
- checked++
40
- if (!ok) {
41
- bad++
42
- console.log(` FAIL ${dir}/${f}${cjs ? ' (checked as CJS)' : ''}`)
43
- if (r.stderr) console.error(r.stderr.toString().trim())
44
- } else {
45
- console.log(` ok ${dir}/${f}${cjs ? ' (checked as CJS)' : ''}`)
46
- }
47
- }
48
- }
49
- } finally {
50
- rmSync(scratch, { recursive: true, force: true })
51
- }
52
-
53
- console.log(bad === 0 ? `PASS (${checked} files)` : `FAIL (${bad}/${checked} file(s))`)
54
- process.exit(bad === 0 ? 0 : 1)
1
+ /**
2
+ * Syntax gate: `node --check` every JS file that ships in the package
3
+ * (electron/ main-process modules + lib/ host/client bundle). Previously
4
+ * only lib/index.js was checked — a syntax slip in any other file shipped
5
+ * unnoticed.
6
+ *
7
+ * The two sandboxed preloads are CommonJS (require/module.exports) while
8
+ * package.json declares "type": "module", so plain --check on them fails
9
+ * spuriously; they are validated through a temporary .cjs copy instead.
10
+ */
11
+ import { spawnSync } from 'node:child_process'
12
+ import { readdirSync, readFileSync, statSync, rmSync, writeFileSync, mkdtempSync } from 'node:fs'
13
+ import { dirname, join } from 'node:path'
14
+ import { tmpdir } from 'node:os'
15
+ import { fileURLToPath } from 'node:url'
16
+
17
+ const root = dirname(dirname(fileURLToPath(import.meta.url)))
18
+ // CJS files relative to electron/ — parsed as CommonJS despite the ESM package.
19
+ const CJS_FILES = new Set(['preload.js', 'progress-preload.js'])
20
+
21
+ function listJs(dir) {
22
+ return readdirSync(join(root, dir)).filter((f) => f.endsWith('.js'))
23
+ }
24
+
25
+ const scratch = mkdtempSync(join(tmpdir(), 'dsh-shell-check-'))
26
+ let bad = 0
27
+ let checked = 0
28
+
29
+ try {
30
+ for (const dir of ['electron', 'lib']) {
31
+ for (const f of listJs(dir)) {
32
+ const full = join(root, dir, f)
33
+ if (!statSync(full).isFile()) continue
34
+ const cjs = CJS_FILES.has(f)
35
+ const target = cjs ? join(scratch, `${dir}-${f}.cjs`) : full
36
+ if (cjs) writeFileSync(target, readFileSync(full))
37
+ const r = spawnSync(process.execPath, ['--check', target], { stdio: 'pipe' })
38
+ const ok = r.status === 0
39
+ checked++
40
+ if (!ok) {
41
+ bad++
42
+ console.log(` FAIL ${dir}/${f}${cjs ? ' (checked as CJS)' : ''}`)
43
+ if (r.stderr) console.error(r.stderr.toString().trim())
44
+ } else {
45
+ console.log(` ok ${dir}/${f}${cjs ? ' (checked as CJS)' : ''}`)
46
+ }
47
+ }
48
+ }
49
+ } finally {
50
+ rmSync(scratch, { recursive: true, force: true })
51
+ }
52
+
53
+ console.log(bad === 0 ? `PASS (${checked} files)` : `FAIL (${bad}/${checked} file(s))`)
54
+ process.exit(bad === 0 ? 0 : 1)
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Self-check for the backend-outage run (electron/outage.js).
3
+ *
4
+ * Defends the phase-1 recovery transitions, not threshold constants alone:
5
+ * - consecutive probe failures escalate the in-page notice from transient
6
+ * to persistent/unavailable only after the agreed boundary, and a single
7
+ * success resets the run (a one-off slow response can never escalate a
8
+ * loaded session);
9
+ * - the navigation token discards probe results that finish after the
10
+ * window navigated or was disposed — a stale result (success or failure)
11
+ * must never mutate the run nor produce a notice payload.
12
+ *
13
+ * Run: node scripts/selftest-recovery.mjs
14
+ * Electron additionally checks that a real failed spawn remains retryable.
15
+ */
16
+ import { OutageRun, PERSISTENT_AFTER_FAILURES } from '../electron/outage.js'
17
+
18
+ let failures = 0
19
+
20
+ function check(label, cond) {
21
+ console.log(` ${cond ? 'ok ' : 'FAIL'} ${label}`)
22
+ if (!cond) failures++
23
+ }
24
+
25
+ function same(a, b) {
26
+ return JSON.stringify(a) === JSON.stringify(b)
27
+ }
28
+
29
+ console.log('[escalation: transient → persistent → recovery]')
30
+ const run = new OutageRun()
31
+ check('a healthy probe yields the ok payload', same(run.apply(run.checkpoint(), true), { state: 'ok' }))
32
+ for (let i = 1; i < PERSISTENT_AFTER_FAILURES; i++) {
33
+ const out = run.apply(run.checkpoint(), false)
34
+ check(
35
+ `failure ${i} stays transient (failures=${run.failures})`,
36
+ same(out, { state: 'degraded', persistent: false }) && run.failures === i,
37
+ )
38
+ }
39
+ const escalated = run.apply(run.checkpoint(), false)
40
+ check(
41
+ `failure ${PERSISTENT_AFTER_FAILURES} escalates to persistent`,
42
+ same(escalated, { state: 'degraded', persistent: true }) &&
43
+ run.failures === PERSISTENT_AFTER_FAILURES,
44
+ )
45
+ check(
46
+ 'further failures stay persistent',
47
+ same(run.apply(run.checkpoint(), false), { state: 'degraded', persistent: true }),
48
+ )
49
+ const recovered = run.apply(run.checkpoint(), true)
50
+ check('a success recovers and resets the run', same(recovered, { state: 'ok' }) && run.failures === 0)
51
+ check(
52
+ 'a failure after recovery is transient again',
53
+ same(run.apply(run.checkpoint(), false), { state: 'degraded', persistent: false }) &&
54
+ run.failures === 1,
55
+ )
56
+
57
+ console.log('[stale results after navigation are discarded]')
58
+ const run2 = new OutageRun()
59
+ const t1 = run2.checkpoint()
60
+ const t2 = run2.checkpoint()
61
+ check('checkpoints stay valid while no navigation happens', run2.isCurrent(t1) && run2.isCurrent(t2))
62
+ run2.apply(t1, false)
63
+ run2.reset() // e.g. showOffline / reload / fresh page load
64
+ check('reset invalidates outstanding checkpoints', !run2.isCurrent(t1) && !run2.isCurrent(t2))
65
+ check('a stale failure result is discarded (no payload)', run2.apply(t1, false) === null)
66
+ check('a stale success result is discarded too', run2.apply(t2, true) === null)
67
+ check('discards did not mutate the fresh run', run2.failures === 0)
68
+ check(
69
+ 'a fresh probe after the reset starts over at failure 1',
70
+ same(run2.apply(run2.checkpoint(), false), { state: 'degraded', persistent: false }) &&
71
+ run2.failures === 1,
72
+ )
73
+
74
+ if (process.versions.electron) {
75
+ const { default: assert } = await import('node:assert/strict')
76
+ const { mkdtempSync, writeFileSync, unlinkSync, rmdirSync } = await import('node:fs')
77
+ const { tmpdir } = await import('node:os')
78
+ const { join } = await import('node:path')
79
+ const service = await import('../electron/service.js')
80
+ const folder = mkdtempSync(join(tmpdir(), 'dsh-failed-spawn-'))
81
+ const command = join(folder, 'dsh')
82
+ const fetch = globalThis.fetch
83
+ // Keep the probe isolated from any real backend; process spawning stays real.
84
+ globalThis.fetch = async () => { throw new Error('isolated unreachable backend') }
85
+ try {
86
+ writeFileSync(command, 'not an executable', { mode: 0o600 })
87
+ await assert.rejects(service.start({ backendPath: folder }))
88
+ await assert.rejects(service.start({ backendPath: folder }))
89
+ check('a failed spawn rejects again instead of reporting a live backend', true)
90
+ } catch (error) {
91
+ console.error(error)
92
+ check('a failed spawn rejects again instead of reporting a live backend', false)
93
+ } finally {
94
+ globalThis.fetch = fetch
95
+ unlinkSync(command)
96
+ rmdirSync(folder)
97
+ }
98
+
99
+ const { app, ipcMain } = await import('electron')
100
+ const { createServer } = await import('node:http')
101
+ const { once } = await import('node:events')
102
+ const { createMainWindow } = await import('../electron/window.js')
103
+ const server = createServer((_request, response) => response.end('<!doctype html><title>Recovery check</title>'))
104
+ app.whenReady().then(async () => {
105
+ let window
106
+ try {
107
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
108
+ window = createMainWindow({ target: `http://127.0.0.1:${server.address().port}/?dsh-desktop-mode=advanced` })
109
+ window.hide()
110
+ await once(window.webContents, 'did-finish-load')
111
+ await window.webContents.executeJavaScript(`
112
+ window.attempts = 0
113
+ window.shellAPI.onReconnectRequest(() => {
114
+ if (++window.attempts > 5) return
115
+ window.shellAPI.connectionReport('connecting')
116
+ window.shellAPI.connectionReport('disconnected')
117
+ })
118
+ window.shellAPI.connectionReport('disconnected')
119
+ `)
120
+ for (let i = 0; i < 5; i++) ipcMain.emit('shell:retry-connection', { sender: window.webContents })
121
+ await new Promise(resolve => setTimeout(resolve, 500))
122
+ assert.equal(await window.webContents.executeJavaScript('window.attempts'), 1)
123
+ check('burst retries share one attempt and immediate failure does not feed back', true)
124
+ } catch (error) {
125
+ console.error(error)
126
+ check('burst retries share one attempt and immediate failure does not feed back', false)
127
+ } finally {
128
+ window?.destroy()
129
+ server.close()
130
+ }
131
+ finish()
132
+ })
133
+ }
134
+
135
+ function finish() {
136
+ console.log(`\n${failures === 0 ? 'PASS' : `FAIL (${failures} check(s))`}`)
137
+ process.exit(failures === 0 ? 0 : 1)
138
+ }
139
+
140
+ if (!process.versions.electron) finish()