desktop-pet-app 0.3.3 → 0.3.4

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.
Files changed (3) hide show
  1. package/bin/cli.js +162 -21
  2. package/out/main/index.js +32 -76
  3. package/package.json +11 -3
package/bin/cli.js CHANGED
@@ -1,32 +1,173 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawn } = require('node:child_process')
3
+ const { execFileSync, spawn } = require('node:child_process')
4
4
  const fs = require('node:fs')
5
+ const os = require('node:os')
5
6
  const path = require('node:path')
6
7
 
7
- let electron
8
- try {
9
- electron = require('electron')
10
- } catch (error) {
11
- console.error('未找到 electron 包,请重新安装 desktop-pet-app。')
12
- console.error('若在国内网络安装失败,可先设置镜像再重装:')
13
- console.error(' ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm install --global <包路径或包名>')
14
- console.error(error instanceof Error ? error.message : String(error))
15
- process.exit(1)
8
+ const manifest = require('../package.json')
9
+
10
+ const MAC_APP_PATH = '/Applications/Desktop Pet.app'
11
+ const MAC_BUNDLE_ID = 'com.xinshu.desktoppet'
12
+ const TAKEOVER_TIMEOUT_MS = 5_000
13
+
14
+ function parseVersion(value) {
15
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(value)
16
+ if (!match) return undefined
17
+ return {
18
+ major: Number(match[1]),
19
+ minor: Number(match[2]),
20
+ patch: Number(match[3]),
21
+ prerelease: match[4]
22
+ }
23
+ }
24
+
25
+ function compareVersions(a, b) {
26
+ const left = parseVersion(a)
27
+ const right = parseVersion(b)
28
+ if (!left || !right) throw new Error(`无效版本号:${!left ? a : b}`)
29
+ for (const key of ['major', 'minor', 'patch']) {
30
+ if (left[key] !== right[key]) return left[key] > right[key] ? 1 : -1
31
+ }
32
+ if (!left.prerelease && right.prerelease) return 1
33
+ if (left.prerelease && !right.prerelease) return -1
34
+ if (!left.prerelease || !right.prerelease) return 0
35
+ return left.prerelease.localeCompare(right.prerelease, undefined, { numeric: true })
16
36
  }
17
37
 
18
- if (typeof electron !== 'string' || !fs.existsSync(electron)) {
19
- console.error('electron 已安装但二进制缺失,请用镜像重装:')
20
- console.error(' ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm install --global --force <包路径或包名>')
21
- process.exit(1)
38
+ function selectLaunchTarget(npmVersion, nativeApp, forceNpm = false) {
39
+ if (!forceNpm && nativeApp && compareVersions(nativeApp.version, npmVersion) >= 0) {
40
+ return { kind: 'native', version: nativeApp.version, appPath: nativeApp.appPath }
41
+ }
42
+ return { kind: 'npm', version: npmVersion }
22
43
  }
23
44
 
24
- const appPath = path.join(__dirname, '..')
45
+ function readPlistValue(plistPath, key) {
46
+ return execFileSync('/usr/libexec/PlistBuddy', ['-c', `Print :${key}`, plistPath], {
47
+ encoding: 'utf8',
48
+ stdio: ['ignore', 'pipe', 'ignore']
49
+ }).trim()
50
+ }
51
+
52
+ function installedMacApp() {
53
+ const plistPath = path.join(MAC_APP_PATH, 'Contents', 'Info.plist')
54
+ const executablePath = path.join(MAC_APP_PATH, 'Contents', 'MacOS', 'Desktop Pet')
55
+ try {
56
+ if (!fs.existsSync(executablePath)) return undefined
57
+ if (readPlistValue(plistPath, 'CFBundleIdentifier') !== MAC_BUNDLE_ID) return undefined
58
+ const version = readPlistValue(plistPath, 'CFBundleShortVersionString')
59
+ if (!parseVersion(version)) return undefined
60
+ return { version, appPath: MAC_APP_PATH }
61
+ } catch {
62
+ return undefined
63
+ }
64
+ }
65
+
66
+ function runningDesktopPet() {
67
+ const endpointPath = process.env.DESKTOP_PET_PORT_FILE?.trim() || path.join(os.homedir(), '.desktop-pet-port')
68
+ try {
69
+ const endpoint = JSON.parse(fs.readFileSync(endpointPath, 'utf8'))
70
+ if (!Number.isSafeInteger(endpoint.pid) || endpoint.pid < 1) return undefined
71
+ process.kill(endpoint.pid, 0)
72
+ return {
73
+ pid: endpoint.pid,
74
+ version: typeof endpoint.appVersion === 'string' && parseVersion(endpoint.appVersion)
75
+ ? endpoint.appVersion
76
+ : undefined
77
+ }
78
+ } catch {
79
+ return undefined
80
+ }
81
+ }
82
+
83
+ function shouldReplaceRunningVersion(targetVersion, runningVersion) {
84
+ return runningVersion === undefined || compareVersions(targetVersion, runningVersion) > 0
85
+ }
86
+
87
+ function processIsDesktopPet(pid) {
88
+ if (process.platform === 'win32') return false
89
+ try {
90
+ const command = execFileSync('/bin/ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8' })
91
+ return /desktop[- ]pet/i.test(command)
92
+ } catch {
93
+ return false
94
+ }
95
+ }
96
+
97
+ function processIsRunning(pid) {
98
+ try {
99
+ process.kill(pid, 0)
100
+ return true
101
+ } catch {
102
+ return false
103
+ }
104
+ }
105
+
106
+ function stopOlderRuntime(targetVersion) {
107
+ const running = runningDesktopPet()
108
+ if (!running || !shouldReplaceRunningVersion(targetVersion, running.version) || !processIsDesktopPet(running.pid)) return
109
+
110
+ try {
111
+ process.kill(running.pid, 'SIGTERM')
112
+ } catch {
113
+ return
114
+ }
115
+
116
+ const deadline = Date.now() + TAKEOVER_TIMEOUT_MS
117
+ while (Date.now() < deadline && processIsRunning(running.pid)) {
118
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100)
119
+ }
120
+ if (processIsRunning(running.pid)) {
121
+ try {
122
+ process.kill(running.pid, 'SIGKILL')
123
+ } catch {}
124
+ }
125
+ }
126
+
127
+ function npmElectronPath() {
128
+ try {
129
+ return require('electron')
130
+ } catch (error) {
131
+ console.error('未找到 electron 包,请重新安装 desktop-pet-app。')
132
+ console.error('若在国内网络安装失败,可先设置镜像再重装:')
133
+ console.error(' ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm install --global <包路径或包名>')
134
+ console.error(error instanceof Error ? error.message : String(error))
135
+ process.exit(1)
136
+ }
137
+ }
138
+
139
+ function launch() {
140
+ const appPath = path.join(__dirname, '..')
141
+ const forceNpm = Boolean(process.env.DESKTOP_PET_PROFILE?.trim()) || process.platform !== 'darwin'
142
+ const target = selectLaunchTarget(
143
+ manifest.version,
144
+ process.platform === 'darwin' ? installedMacApp() : undefined,
145
+ forceNpm
146
+ )
147
+ stopOlderRuntime(target.version)
148
+
149
+ let command
150
+ let args
151
+ let env = process.env
152
+ if (target.kind === 'native') {
153
+ command = '/usr/bin/open'
154
+ args = ['-n', target.appPath, '--args', ...process.argv.slice(2)]
155
+ } else {
156
+ command = npmElectronPath()
157
+ if (typeof command !== 'string' || !fs.existsSync(command)) {
158
+ console.error('electron 已安装但二进制缺失,请用镜像重装:')
159
+ console.error(' ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm install --global --force <包路径或包名>')
160
+ process.exit(1)
161
+ }
162
+ args = [appPath, ...process.argv.slice(2)]
163
+ env = { ...process.env, DESKTOP_PET_NPM_DISTRIBUTION: '1' }
164
+ }
165
+
166
+ const child = spawn(command, args, { stdio: 'ignore', detached: true, env })
167
+ child.on('error', (error) => console.error(`Desktop Pet 启动失败:${error.message}`))
168
+ child.unref()
169
+ }
25
170
 
26
- const child = spawn(electron, [appPath, ...process.argv.slice(2)], {
27
- stdio: 'ignore',
28
- detached: true,
29
- env: { ...process.env, DESKTOP_PET_NPM_DISTRIBUTION: '1' }
30
- })
171
+ if (require.main === module) launch()
31
172
 
32
- child.unref()
173
+ module.exports = { compareVersions, selectLaunchTarget, shouldReplaceRunningVersion }
package/out/main/index.js CHANGED
@@ -22,8 +22,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
  const electron = require("electron");
25
- const node_path = require("node:path");
26
25
  const node_fs = require("node:fs");
26
+ const node_path = require("node:path");
27
27
  const path = require("path");
28
28
  const os = require("os");
29
29
  const node_crypto = require("node:crypto");
@@ -1680,6 +1680,7 @@ const REQUEST_TIMEOUT_MS = 6e3;
1680
1680
  const DOWNLOAD_TIMEOUT_MS = 10 * 6e4;
1681
1681
  const LEGACY_OFFICIAL_HTTP_HOST = "47.94.20.104";
1682
1682
  const MAC_BUNDLE_ID = "com.xinshu.desktoppet";
1683
+ const MAC_INSTALLED_APP = "/Applications/Desktop Pet.app";
1683
1684
  let status = { kind: "idle" };
1684
1685
  function updateCheckURL() {
1685
1686
  const override = process.env.DESKTOP_PET_UPDATE_API?.trim();
@@ -1915,22 +1916,17 @@ const restartFallback = () => {
1915
1916
  child.unref()
1916
1917
  }
1917
1918
  const findInstalledMacApp = async () => {
1918
- const query = 'kMDItemCFBundleIdentifier == "${MAC_BUNDLE_ID}"'
1919
- const { stdout } = await run('/usr/bin/mdfind', [query])
1920
- const candidates = stdout.split('\n').map((value) => value.trim()).filter((value) => value.endsWith('.app'))
1921
- for (const candidate of candidates) {
1922
- try {
1923
- const plist = join(candidate, 'Contents', 'Info.plist')
1924
- const result = await run('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleShortVersionString', plist])
1925
- if (result.stdout.trim() === targetVersion) return candidate
1926
- } catch {}
1919
+ const plist = join('${MAC_INSTALLED_APP}', 'Contents', 'Info.plist')
1920
+ const bundle = await run('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleIdentifier', plist])
1921
+ const version = await run('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleShortVersionString', plist])
1922
+ if (bundle.stdout.trim() !== '${MAC_BUNDLE_ID}' || version.stdout.trim() !== targetVersion) {
1923
+ throw new Error('installed app identity or version does not match the verified update')
1927
1924
  }
1928
- throw new Error('installed app for target version was not found')
1925
+ return '${MAC_INSTALLED_APP}'
1929
1926
  }
1930
1927
  const restartInstalledApp = async () => {
1931
1928
  if (platform === 'darwin') {
1932
- // 安装器可能把包重定位到已有开发构建,且机器上可能存在多个同 bundle ID 的应用。
1933
- // 找到版本完全匹配的具体路径再启动,避免按 bundle ID 命中旧副本后静默失败。
1929
+ // PKG 禁止重定位;安装后再次核对固定路径的 bundle ID 与版本再启动。
1934
1930
  const installedApp = await findInstalledMacApp()
1935
1931
  await run('/usr/bin/open', ['-n', installedApp])
1936
1932
  return
@@ -2653,7 +2649,7 @@ async function readBody(req, maxBytes) {
2653
2649
  });
2654
2650
  }
2655
2651
  let eventServer;
2656
- function startEventServer() {
2652
+ function startEventServer(appVersion = "0.0.0") {
2657
2653
  if (eventServer) return;
2658
2654
  const sessionToken = crypto.randomBytes(32).toString("base64url");
2659
2655
  const server = http.createServer((req, res) => {
@@ -2710,6 +2706,7 @@ function startEventServer() {
2710
2706
  if (addr && typeof addr === "object") {
2711
2707
  const endpoint = {
2712
2708
  protocolVersion: LOCAL_PET_PROTOCOL_VERSION,
2709
+ appVersion,
2713
2710
  port: addr.port,
2714
2711
  pid: process.pid,
2715
2712
  sessionToken,
@@ -3144,64 +3141,29 @@ async function dutyLoop(adapter) {
3144
3141
  }
3145
3142
  }
3146
3143
  }
3147
- const TAKEOVER_TIMEOUT_MS = 5e3;
3148
- const TAKEOVER_POLL_MS = 100;
3149
- function takeOverRunningInstance() {
3150
- if (process.platform === "win32") return false;
3151
- const pid = readLockOwnerPid();
3152
- if (!pid) return false;
3153
- if (!processIsDesktopPet(pid)) {
3154
- logger.warn("single-instance lock held by an unrelated process, refusing to take over", { pid });
3155
- return false;
3156
- }
3157
- logger.info("taking over running instance", { pid });
3158
- try {
3159
- process.kill(pid, "SIGTERM");
3160
- } catch {
3161
- return true;
3162
- }
3163
- if (waitForExit(pid, TAKEOVER_TIMEOUT_MS)) return true;
3164
- logger.warn("instance did not exit after SIGTERM, forcing", { pid });
3165
- try {
3166
- process.kill(pid, "SIGKILL");
3167
- } catch {
3168
- return true;
3169
- }
3170
- return waitForExit(pid, 1e3);
3171
- }
3172
- function readLockOwnerPid() {
3173
- let target;
3174
- try {
3175
- target = node_fs.readlinkSync(node_path.join(electron.app.getPath("userData"), "SingletonLock"));
3176
- } catch {
3177
- return void 0;
3178
- }
3179
- const hostPrefix = `${node_os.hostname()}-`;
3180
- if (!target.startsWith(hostPrefix)) return void 0;
3181
- const pid = Number(target.slice(hostPrefix.length));
3182
- return Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
3183
- }
3184
- function processIsDesktopPet(pid) {
3185
- try {
3186
- const command = node_child_process.execFileSync("/bin/ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
3187
- return /desktop[- ]pet/i.test(command);
3188
- } catch {
3189
- return false;
3144
+ const PRODUCT_DATA_DIRECTORY = "Desktop Pet";
3145
+ const LEGACY_NPM_DATA_DIRECTORY = "desktop-pet-app";
3146
+ function configureRuntimeIdentity() {
3147
+ const profileDirectory = process.env.DESKTOP_PET_PROFILE?.trim();
3148
+ if (profileDirectory) {
3149
+ electron.app.setPath("userData", node_path.resolve(profileDirectory));
3150
+ return;
3190
3151
  }
3191
- }
3192
- function waitForExit(pid, timeoutMs) {
3193
- const deadline = Date.now() + timeoutMs;
3194
- while (Date.now() < deadline) {
3195
- if (!processAlive(pid)) return true;
3196
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, TAKEOVER_POLL_MS);
3152
+ if (!electron.app.isPackaged && process.env.DESKTOP_PET_NPM_DISTRIBUTION !== "1") return;
3153
+ const appDataDirectory = electron.app.getPath("appData");
3154
+ const canonicalDirectory = node_path.join(appDataDirectory, PRODUCT_DATA_DIRECTORY);
3155
+ if (process.env.DESKTOP_PET_NPM_DISTRIBUTION === "1") {
3156
+ migrateLegacyNpmData(node_path.join(appDataDirectory, LEGACY_NPM_DATA_DIRECTORY), canonicalDirectory);
3197
3157
  }
3198
- return !processAlive(pid);
3158
+ electron.app.setPath("userData", canonicalDirectory);
3199
3159
  }
3200
- function processAlive(pid) {
3160
+ function migrateLegacyNpmData(legacyDirectory, canonicalDirectory) {
3161
+ if (!node_fs.existsSync(legacyDirectory) || node_fs.existsSync(canonicalDirectory)) return false;
3201
3162
  try {
3202
- process.kill(pid, 0);
3163
+ node_fs.renameSync(legacyDirectory, canonicalDirectory);
3203
3164
  return true;
3204
- } catch {
3165
+ } catch (error) {
3166
+ console.warn(`无法迁移 Desktop Pet 用户数据:${String(error)}`);
3205
3167
  return false;
3206
3168
  }
3207
3169
  }
@@ -3238,15 +3200,9 @@ function createPetTemplateImage() {
3238
3200
  image.setTemplateImage(true);
3239
3201
  return image;
3240
3202
  }
3241
- const profileDirectory = process.env.DESKTOP_PET_PROFILE?.trim();
3242
- if (profileDirectory) {
3243
- electron.app.setPath("userData", node_path.resolve(profileDirectory));
3244
- }
3203
+ configureRuntimeIdentity();
3245
3204
  registerAccountProtocol();
3246
- let singleInstance = electron.app.requestSingleInstanceLock();
3247
- if (!singleInstance && takeOverRunningInstance()) {
3248
- singleInstance = electron.app.requestSingleInstanceLock();
3249
- }
3205
+ const singleInstance = electron.app.requestSingleInstanceLock();
3250
3206
  if (!singleInstance) {
3251
3207
  logger.warn("another instance is running, quitting");
3252
3208
  electron.app.quit();
@@ -3267,7 +3223,7 @@ function ensurePrimaryPet() {
3267
3223
  electron.app.whenReady().then(async () => {
3268
3224
  createMenuBar();
3269
3225
  await startAccountCallbackRouter();
3270
- startEventServer();
3226
+ startEventServer(electron.app.getVersion());
3271
3227
  const activated = await activateStoredDevice().catch((err) => {
3272
3228
  logger.warn("device authentication failed", { error: String(err) });
3273
3229
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "desktop-pet-app",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "AI desktop pet with MCP support and a self-hosted relay server",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -32,7 +32,7 @@
32
32
  "e2e:codebuddy:context": "node scripts/e2e-codebuddy-context-round.mjs",
33
33
  "typecheck": "tsc --noEmit",
34
34
  "test": "vitest run",
35
- "package:mac": "npm run build && node scripts/prepare-electron-builder.mjs && electron-builder --mac pkg --publish never; node scripts/prepare-electron-builder.mjs --restore",
35
+ "package:mac": "npm run build && electron-builder --mac pkg --publish never",
36
36
  "pack:check": "npm pack --dry-run",
37
37
  "prepack": "npm run build",
38
38
  "prepublishOnly": "npm run typecheck && npm test && npm run pack:check"
@@ -48,6 +48,11 @@
48
48
  "bin/**",
49
49
  "package.json"
50
50
  ],
51
+ "pkg": {
52
+ "installLocation": "/Applications",
53
+ "isRelocatable": false,
54
+ "isVersionChecked": true
55
+ },
51
56
  "mac": {
52
57
  "category": "public.app-category.utilities",
53
58
  "target": [
@@ -58,14 +63,17 @@
58
63
  "devDependencies": {
59
64
  "@types/node": "^22.0.0",
60
65
  "@types/ws": "^8.18.1",
66
+ "electron": "^33.2.0",
61
67
  "electron-builder": "^26.15.3",
62
68
  "electron-vite": "^2.3.0",
63
69
  "typescript": "^5.6.3",
64
70
  "vite": "^5.4.8",
65
71
  "vitest": "^3.2.7"
66
72
  },
73
+ "peerDependencies": {
74
+ "electron": "^33.2.0"
75
+ },
67
76
  "dependencies": {
68
- "electron": "^33.2.0",
69
77
  "@modelcontextprotocol/sdk": "^1.30.0",
70
78
  "ws": "^8.21.3",
71
79
  "zod": "^4.5.4"