zcode-pro 0.0.0-stage → 0.17.0

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 (39) hide show
  1. package/LICENSE +201 -0
  2. package/README.en.md +164 -0
  3. package/README.md +163 -2
  4. package/bin/zcode-pro.mjs +37 -0
  5. package/cli.mjs +9 -0
  6. package/dist/inject.js +5842 -0
  7. package/package.json +43 -4
  8. package/src/host/cdp.mjs +126 -0
  9. package/src/host/helper.mjs +2134 -0
  10. package/src/host/main.mjs +281 -0
  11. package/src/host/paths.mjs +117 -0
  12. package/src/host/pickFolder.mjs +152 -0
  13. package/src/host/settings.mjs +61 -0
  14. package/src/host/setup.mjs +514 -0
  15. package/src/host/taskIndex.mjs +255 -0
  16. package/src/host/taskOrder.mjs +117 -0
  17. package/src/host/webdav.mjs +105 -0
  18. package/src/inject/core.js +772 -0
  19. package/src/inject/features/alias.js +211 -0
  20. package/src/inject/features/copy-path.js +78 -0
  21. package/src/inject/features/file-menu.js +147 -0
  22. package/src/inject/features/idle-reclaim.js +179 -0
  23. package/src/inject/features/image-menu.js +215 -0
  24. package/src/inject/features/open-folder.js +43 -0
  25. package/src/inject/features/pinned-collapse.js +132 -0
  26. package/src/inject/features/pinned-expand.js +142 -0
  27. package/src/inject/features/plugin-updates.js +28 -0
  28. package/src/inject/features/project-menu.js +58 -0
  29. package/src/inject/features/relocate-dialog.js +156 -0
  30. package/src/inject/features/session-switch.js +244 -0
  31. package/src/inject/features/settings-dialog.js +1503 -0
  32. package/src/inject/features/settings-entry.js +21 -0
  33. package/src/inject/features/styles.js +311 -0
  34. package/src/inject/features/task-order.js +78 -0
  35. package/src/inject/features/title-lock.js +242 -0
  36. package/src/inject/features/toolbar-icons.js +243 -0
  37. package/src/inject/features/ws-running.js +472 -0
  38. package/src/inject/index.js +60 -0
  39. package/src/inject/ui.js +734 -0
@@ -0,0 +1,514 @@
1
+ // zcode-pro setup:本机集成设置入口(npm / pip 等包管理器安装不写系统目录,
2
+ // 需要菜单入口等集成时显式运行本子命令)。按「设置项」组织(id / 标题 /
3
+ // install / uninstall),以后新增设置项往 items 里加即可。
4
+ // setup 不依赖 ZCode 运行,在 main.run 里先于 ZCode 定位处理。
5
+ //
6
+ // 快捷方式各平台形态(与 install.sh / install.ps1 的条目内容保持一致):
7
+ // - Linux:应用菜单 .desktop(Exec 指向 PATH 中的 zcode-pro,Icon=zcode 复用 ZCode 图标)
8
+ // - Windows:开始菜单 .lnk → wscript 运行生成的 vbs(静默启动、无控制台窗口),
9
+ // 图标取 ZCode.exe;包管理器安装不带 vbs,这里按 PATH 命令现生成
10
+ // - macOS:~/Applications 下的“ZCode Pro.app”包(图标复制 ZCode 的 app.icns,无则省略)
11
+ //
12
+ // Windows 中文编码注意(vbs / PowerShell 均受代码页影响):
13
+ // - vbs 由 wscript 按 ANSI 代码页解析,不支持 UTF-8——生成内容只用 ASCII;
14
+ // - PowerShell 脚本一律经 -EncodedCommand 传 Base64(UTF-16LE),中文不经命令行与
15
+ // 文件编码,避开代码页问题;
16
+ // - 仓库内 PowerShell 脚本(install.ps1 等)必须保存为带 BOM 的 UTF-8。
17
+ import { execFileSync } from 'node:child_process';
18
+ import { accessSync, chmodSync, constants as fsConstants, copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
19
+ import { basename, dirname, join, sep } from 'node:path';
20
+ import { homedir, platform } from 'node:os';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { resolveZcodeExecutable } from './paths.mjs';
23
+
24
+ const here = dirname(fileURLToPath(import.meta.url)); // …/src/host(随包安装后为包内对应目录)
25
+ const pkgRoot = dirname(dirname(here)); // 安装根目录(含 cli.mjs 与 bin/)
26
+
27
+ const SUPPORTED = ['linux', 'win32', 'darwin'];
28
+
29
+ // —— PATH 中的 zcode-pro 命令 ——
30
+
31
+ function commandInPath(plat) {
32
+ const pathSep = plat === 'win32' ? ';' : ':';
33
+ // Windows 上 npm 会生成 zcode-pro.cmd / zcode-pro.ps1,pip 生成 zcode-pro.exe,
34
+ // 只有 .cmd / .exe 能被 wscript 的 sh.Run 直接运行
35
+ const names = plat === 'win32' ? ['zcode-pro.cmd', 'zcode-pro.exe'] : ['zcode-pro'];
36
+ for (const dir of (process.env.PATH || '').split(pathSep)) {
37
+ if (!dir) continue;
38
+ for (const name of names) {
39
+ const p = join(dir, name);
40
+ try {
41
+ if (plat === 'win32') {
42
+ if (existsSync(p)) return p;
43
+ } else {
44
+ accessSync(p, fsConstants.X_OK); // 可执行
45
+ return p;
46
+ }
47
+ } catch { /* 继续找 */ }
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+
53
+ // PATH 命令是否属于当前运行的这份安装:
54
+ // - POSIX npm / AUR / install.sh:命令(经符号链接)最终指向本包目录内的启动器;
55
+ // - Windows npm:全局命令是普通 .cmd(非符号链接),按 npm 目录布局认定——
56
+ // 命令旁的 node_modules\zcode-pro 是否即当前包;
57
+ // - pip:命令是 Python 控制台脚本,位于解释器目录而非包目录——POSIX 为文本脚本,
58
+ // 校验其 shebang 解释器所属环境是否包含当前包;Windows 为 exe 启动器,读不出
59
+ // 内容,只能按文件名认定(同机多 Python 环境时以 PATH 靠前者为准,属已知局限)。
60
+ function sameInstall(cmdPath, plat) {
61
+ let real = cmdPath;
62
+ try { real = realpathSync(cmdPath); } catch { /* 保持原路径 */ }
63
+ if (real === pkgRoot || real.startsWith(pkgRoot + sep)) return true;
64
+ if (plat === 'win32') {
65
+ try {
66
+ if (realpathSync(join(dirname(real), 'node_modules', 'zcode-pro')) === pkgRoot) return true;
67
+ } catch { /* 布局不符则继续 */ }
68
+ return pkgRoot.split(/[\\/]/).includes('zcode_pro')
69
+ && basename(real).toLowerCase().startsWith('zcode-pro');
70
+ }
71
+ if (pkgRoot.split(sep).includes('zcode_pro')) {
72
+ try {
73
+ const shebang = readFileSync(real, 'utf-8').match(/^#!\s*(\S+)/);
74
+ if (shebang && pkgRoot.startsWith(dirname(dirname(shebang[1])) + sep)) return true;
75
+ } catch { /* 读不到内容则不认定 */ }
76
+ }
77
+ return false;
78
+ }
79
+
80
+ // —— Windows 快捷方式(路径 / 脚本生成) ——
81
+
82
+ // 本功能创建的 .lnk 归属标记(PowerShell -like 通配):vbs 方案的 Arguments 指向
83
+ // 我们的 vbs;WSH 异常降级方案的 start 窗口标题用固定名。卸载据此只删自己的
84
+ const OWN_LNK_MARKERS = ['*zcode-pro-setup.vbs*', '*ZCodePro-Setup*'];
85
+
86
+ // 归属判断的 PowerShell 表达式:每个 -like 都必须带完整变量前缀(写成共享的
87
+ // “-like a -or -like b” 是语法错误)
88
+ const ownsExpr = (v) => OWN_LNK_MARKERS.map((m) => `${v}.Arguments -like '${m}'`).join(' -or ');
89
+
90
+ function windowsPaths() {
91
+ const appData = process.env.APPDATA || join(homedir(), 'AppData', 'Roaming');
92
+ const localAppData = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local');
93
+ return {
94
+ appData,
95
+ localAppData,
96
+ lnkPath: join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'ZCode Pro.lnk'),
97
+ vbsDir: join(localAppData, 'ZCodePro', 'setup'),
98
+ vbsPath: join(localAppData, 'ZCodePro', 'setup', 'zcode-pro-setup.vbs'),
99
+ };
100
+ }
101
+
102
+ // 经 Base64(UTF-16LE) 的 -EncodedCommand 执行 PowerShell:中文不经过命令行与
103
+ // 文件编码,任何系统代码页下都安全。返回 stdout;非零退出码抛错
104
+ function runPowerShell(script) {
105
+ const b64 = Buffer.from(script, 'utf16le').toString('base64');
106
+ return execFileSync('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', b64],
107
+ { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
108
+ }
109
+
110
+ // WSH 是否可用:复现 vbs 的启动调用(WScript.Shell.Run 隐藏窗口执行)。
111
+ // 部分机器(精简系统/安全软件/组件禁用)上该调用报 0x800A01AD,需降级 cmd 启动。
112
+ // ZCODEPRO_FORCE_NO_WSH=1 供测试强制走降级路径。
113
+ let wshOkCache;
114
+
115
+ function windowsWshOk() {
116
+ if (process.env.ZCODEPRO_FORCE_NO_WSH === '1') return false;
117
+ if (wshOkCache !== undefined) return wshOkCache;
118
+ try {
119
+ runPowerShell([
120
+ "$ErrorActionPreference = 'Stop'",
121
+ '$sh = New-Object -ComObject WScript.Shell',
122
+ "$sh.Run('cmd /c exit 0', 0, $true) | Out-Null",
123
+ ].join('\r\n'));
124
+ wshOkCache = true;
125
+ } catch {
126
+ wshOkCache = false;
127
+ }
128
+ return wshOkCache;
129
+ }
130
+
131
+ // .lnk 是否本功能创建(按 Arguments 里的归属标记判断;读不到按外来处理,不覆盖)
132
+ function lnkIsOurs(lnkPath) {
133
+ try {
134
+ const out = runPowerShell([
135
+ "$ErrorActionPreference = 'Stop'",
136
+ `$l = (New-Object -ComObject WScript.Shell).CreateShortcut('${psQuote(lnkPath)}')`,
137
+ `Write-Output (${OWN_LNK_MARKERS.map((m) => `$l.Arguments -like '${m}'`).join(' -or ')})`,
138
+ ].join('\r\n'));
139
+ return /True/.test(out);
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ const psQuote = (s) => String(s).replace(/'/g, "''"); // PowerShell 单引号字面量转义
146
+
147
+ // 快捷方式计划(导出仅供测试审查,不执行)。desktop 为真时同时创建桌面快捷方式,
148
+ // 桌面目录由 PowerShell 的 GetFolderPath 解析(正确跟随 OneDrive 重定向等)。
149
+ // wshOk 为假(WSH 异常的机器)时降级:.lnk 直接经 cmd 最小化启动,不再依赖
150
+ // wscript+vbs,代价是启动瞬间任务栏短暂闪一个最小化控制台
151
+ export function windowsShortcutPlan(cmd, desktop = false, wshOk = true) {
152
+ const paths = windowsPaths();
153
+ // vbs 由 wscript 按 ANSI 代码页解析(不支持 UTF-8),内容只用 ASCII
154
+ const vbsContent = [
155
+ `' Generated by zcode-pro setup: run zcode-pro silently (no console window); forwards all arguments`,
156
+ 'Set sh = CreateObject("WScript.Shell")',
157
+ `cmd = """${cmd.replace(/"/g, '""')}"""`,
158
+ 'If WScript.Arguments.Count > 0 Then',
159
+ ' Dim parts()',
160
+ ' ReDim parts(WScript.Arguments.Count - 1)',
161
+ ' For i = 0 To WScript.Arguments.Count - 1',
162
+ ' parts(i) = """" & WScript.Arguments(i) & """"',
163
+ ' Next',
164
+ ' cmd = cmd & " " & Join(parts, " ")',
165
+ 'End If',
166
+ 'sh.Run cmd, 0, False',
167
+ '',
168
+ ].join('\r\n');
169
+ // 图标复用已安装的 ZCode:探测走 paths.mjs 同一条优先级(含注册表,覆盖自定义
170
+ // 安装目录);未安装 ZCode 时留空,快捷方式回落系统默认图标
171
+ let icon = '';
172
+ try { icon = resolveZcodeExecutable(''); } catch { /* 未安装 ZCode,省略图标 */ }
173
+ const iconLine = icon ? ` $l.IconLocation = '${psQuote(`${icon},0`)}'` : '';
174
+ const setProps = wshOk ? [
175
+ 'function setProps($l) {',
176
+ ' $l.TargetPath = "$env:SystemRoot\\System32\\wscript.exe"',
177
+ ` $l.Arguments = '${psQuote(`"${paths.vbsPath}"`)}'`,
178
+ ` $l.WorkingDirectory = '${psQuote(paths.vbsDir)}'`,
179
+ " $l.Description = 'ZCode 桌面版增强启动器(自定义别名等,不修改客户端文件)'",
180
+ iconLine,
181
+ '}',
182
+ ] : [
183
+ 'function setProps($l) {',
184
+ ' $l.TargetPath = "$env:SystemRoot\\System32\\cmd.exe"',
185
+ ` $l.Arguments = '/c start "ZCodePro-Setup" /min "${psQuote(cmd)}"'`,
186
+ ` $l.WorkingDirectory = '${psQuote(paths.vbsDir)}'`,
187
+ " $l.Description = 'ZCode 桌面版增强启动器(自定义别名等,不修改客户端文件)'",
188
+ iconLine,
189
+ ' $l.WindowStyle = 7',
190
+ '}',
191
+ ];
192
+ const psScript = [
193
+ "$ErrorActionPreference = 'Stop'",
194
+ '$wsh = New-Object -ComObject WScript.Shell',
195
+ ...setProps,
196
+ `$lnk = $wsh.CreateShortcut('${psQuote(paths.lnkPath)}')`,
197
+ 'setProps $lnk',
198
+ '$lnk.Save()',
199
+ ...(desktop ? [
200
+ "$desk = Join-Path ([Environment]::GetFolderPath('Desktop')) 'ZCode Pro.lnk'",
201
+ '$dl = $wsh.CreateShortcut($desk)',
202
+ '# 已存在且非本功能创建(Arguments 无归属标记)时不覆盖',
203
+ `if (-not (Test-Path $desk) -or (${ownsExpr('$dl')})) {`,
204
+ ' setProps $dl',
205
+ ' $dl.Save()',
206
+ '}',
207
+ ] : []),
208
+ ].filter(Boolean).join('\r\n');
209
+ return { ...paths, vbsContent, psScript, icon, wshOk };
210
+ }
211
+
212
+ // —— 快捷方式设置项 ——
213
+
214
+ const DESKTOP_FILE = 'zcode-pro.desktop';
215
+ // setup 创建的条目带标记:卸载只动自己的,不误删 install.sh / AUR / install.ps1 的同名条目
216
+ const SETUP_MARK = 'X-ZCodePro-Setup=true';
217
+
218
+ function desktopEntry(execCmd) {
219
+ const exec = /\s/.test(execCmd) ? `"${execCmd}"` : execCmd;
220
+ return [
221
+ '[Desktop Entry]',
222
+ 'Type=Application',
223
+ 'Name=ZCode Pro',
224
+ 'Comment=ZCode 桌面版增强启动器(自定义别名等,不修改客户端文件)',
225
+ `Exec=${exec}`,
226
+ 'Icon=zcode',
227
+ 'Terminal=false',
228
+ 'Categories=Development;IDE;',
229
+ 'Keywords=zcode;pro;enhance;',
230
+ SETUP_MARK,
231
+ '',
232
+ ].join('\n');
233
+ }
234
+
235
+ function applicationsDir() {
236
+ return join(process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'applications');
237
+ }
238
+
239
+ // Linux 桌面目录:优先 xdg-user-dir(正确跟随本地化目录名,如 ~/桌面)
240
+ function desktopDir() {
241
+ try {
242
+ const dir = execFileSync('xdg-user-dir', ['DESKTOP'], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
243
+ if (dir) return dir;
244
+ } catch { /* 无该命令时按常见值回退 */ }
245
+ return existsSync(join(homedir(), 'Desktop')) ? join(homedir(), 'Desktop') : join(homedir(), '桌面');
246
+ }
247
+
248
+ // macOS 应用包目录、桌面入口(指向应用包的符号链接)与图标来源
249
+ function macAppBundle() {
250
+ return join(homedir(), 'Applications', 'ZCode Pro.app');
251
+ }
252
+
253
+ function macDesktopLink() {
254
+ return join(homedir(), 'Desktop', 'ZCode Pro.app');
255
+ }
256
+
257
+ function plistContent(hasIcon) {
258
+ const pairs = [
259
+ ['CFBundleName', 'ZCode Pro'],
260
+ ['CFBundleDisplayName', 'ZCode Pro'],
261
+ ['CFBundleIdentifier', 'com.duanluan.zcodepro'],
262
+ ['CFBundleVersion', '1'],
263
+ ['CFBundleShortVersionString', '1'],
264
+ ['CFBundlePackageType', 'APPL'],
265
+ ['CFBundleExecutable', 'zcode-pro'],
266
+ ];
267
+ if (hasIcon) pairs.push(['CFBundleIconFile', 'app']);
268
+ const body = pairs.map(([k, v]) => ` <key>${k}</key>\n <string>${v}</string>`).join('\n');
269
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n${body}\n</dict>\n</plist>\n`;
270
+ }
271
+
272
+ const shortcutItem = {
273
+ id: 'desktop-shortcut',
274
+ title: '快捷方式“ZCode Pro”(应用菜单/开始菜单,图标复用 ZCode)',
275
+ supported: (plat) => SUPPORTED.includes(plat),
276
+
277
+ install(plat, opts = {}) {
278
+ const cmd = commandInPath(plat);
279
+ if (!cmd) {
280
+ throw new Error('未在 PATH 中找到 zcode-pro 命令,无法创建快捷方式;请确认安装方式后重试。');
281
+ }
282
+ if (!sameInstall(cmd, plat)) {
283
+ throw new Error(`PATH 中的 zcode-pro(${cmd})不属于当前运行的这份安装,` +
284
+ '为避免快捷方式指向旧版本已跳过;请用那份安装自己的方式管理快捷方式。');
285
+ }
286
+ if (plat === 'linux') return this.installLinux(cmd, opts);
287
+ if (plat === 'win32') return this.installWindows(cmd, opts);
288
+ return this.installMac(cmd, opts);
289
+ },
290
+
291
+ installLinux(cmd, opts = {}) {
292
+ // 系统级(AUR 等)已有同名条目时不再建用户级:desktop 文件 id 相同时用户级
293
+ // 会遮蔽系统级(项目既有约定),残留后表现为菜单一直运行另一份
294
+ if (existsSync('/usr/share/applications/zcode-pro.desktop')) {
295
+ return { skipped: '检测到系统级 ZCode Pro 快捷方式(AUR 等已安装),用户级条目会遮蔽它,已跳过' };
296
+ }
297
+ const appsDir = applicationsDir();
298
+ const target = join(appsDir, DESKTOP_FILE);
299
+ const parts = [];
300
+ if (existsSync(target)) {
301
+ const cur = readFileSync(target, 'utf-8');
302
+ if (!cur.includes(SETUP_MARK)) {
303
+ return { skipped: `${target} 已存在且非 setup 创建(如 install.sh),未覆盖` };
304
+ }
305
+ const exec = (cur.match(/^Exec=(.+)$/m) || [])[1] || '';
306
+ if (exec.includes(cmd)) {
307
+ parts.push('应用菜单条目已存在且指向当前命令,无需重复创建');
308
+ } else {
309
+ writeFileSync(target, desktopEntry(cmd));
310
+ parts.push(`已更新 ${target}(Exec=${cmd})`);
311
+ }
312
+ } else {
313
+ mkdirSync(appsDir, { recursive: true });
314
+ writeFileSync(target, desktopEntry(cmd));
315
+ parts.push(`已写入 ${target}(Exec=${cmd})`);
316
+ }
317
+ try { execFileSync('update-desktop-database', [appsDir], { stdio: 'ignore' }); } catch { /* 无该命令时忽略 */ }
318
+ // 桌面条目独立判断(不因菜单条目幂等而短路)
319
+ if (opts.desktop) {
320
+ const dDir = desktopDir();
321
+ const dTarget = join(dDir, DESKTOP_FILE);
322
+ if (!existsSync(dTarget) || readFileSync(dTarget, 'utf-8').includes(SETUP_MARK)) {
323
+ writeFileSync(dTarget, desktopEntry(cmd));
324
+ parts.push(`桌面 ${dTarget}`);
325
+ } else {
326
+ parts.push('桌面已存在非 setup 创建的条目,未覆盖');
327
+ }
328
+ }
329
+ if (parts.length === 1 && parts[0].startsWith('应用菜单条目已存在')) return { skipped: parts[0] };
330
+ return { detail: parts.join(';') };
331
+ },
332
+
333
+ installWindows(cmd, opts = {}) {
334
+ const wshOk = windowsWshOk();
335
+ const plan = windowsShortcutPlan(cmd, !!opts.desktop, wshOk);
336
+ // .lnk 已存在且不属于本功能(Arguments 无归属标记)= install.ps1 等其他方式创建,不覆盖
337
+ if (existsSync(plan.lnkPath) && !lnkIsOurs(plan.lnkPath)) {
338
+ return { skipped: `已存在其他方式创建的 ${plan.lnkPath},未覆盖` };
339
+ }
340
+ if (wshOk) {
341
+ mkdirSync(plan.vbsDir, { recursive: true });
342
+ writeFileSync(plan.vbsPath, plan.vbsContent);
343
+ }
344
+ runPowerShell(plan.psScript);
345
+ const how = wshOk
346
+ ? `经 ${plan.vbsPath} 静默启动`
347
+ : '本机 WSH 异常,经 cmd 最小化启动(启动时会短暂闪烁控制台窗口)';
348
+ return { detail: `已创建 ${plan.lnkPath}(${how}${opts.desktop ? ',含桌面' : ''})` };
349
+ },
350
+
351
+ installMac(cmd, opts = {}) {
352
+ const app = macAppBundle();
353
+ const contents = join(app, 'Contents');
354
+ const marker = join(contents, 'zcodepro-setup-marker');
355
+ if (existsSync(app) && !existsSync(marker)) {
356
+ return { skipped: `${app} 已存在且非 setup 创建,未覆盖` };
357
+ }
358
+ let iconCopied = false;
359
+ for (const src of [join('/', 'Applications', 'ZCode.app', 'Contents', 'Resources', 'app.icns'),
360
+ join(homedir(), 'Applications', 'ZCode.app', 'Contents', 'Resources', 'app.icns')]) {
361
+ if (!existsSync(src)) continue;
362
+ try {
363
+ mkdirSync(join(contents, 'Resources'), { recursive: true });
364
+ copyFileSync(src, join(contents, 'Resources', 'app.icns'));
365
+ iconCopied = true;
366
+ break;
367
+ } catch { /* 复制失败则省略图标 */ }
368
+ }
369
+ mkdirSync(join(contents, 'MacOS'), { recursive: true });
370
+ const launcher = join(contents, 'MacOS', 'zcode-pro');
371
+ writeFileSync(launcher, `#!/bin/sh\nexec "${cmd}" "$@"\n`);
372
+ chmodSync(launcher, 0o755);
373
+ writeFileSync(join(contents, 'Info.plist'), plistContent(iconCopied));
374
+ writeFileSync(marker, '');
375
+ let detail = `已创建 ${app}(启动器指向 ${cmd}${iconCopied ? ',图标已复制' : ''})`;
376
+ if (opts.desktop) {
377
+ const link = macDesktopLink();
378
+ if (existsSync(link)) {
379
+ detail += ';桌面已存在同名入口,未覆盖';
380
+ } else {
381
+ try {
382
+ symlinkSync(app, link);
383
+ detail += `;桌面 ${link}`;
384
+ } catch { /* 建链接失败仅省略桌面入口 */ }
385
+ }
386
+ }
387
+ return { detail };
388
+ },
389
+
390
+ uninstall(plat) {
391
+ if (plat === 'linux') return this.uninstallLinux();
392
+ if (plat === 'win32') return this.uninstallWindows();
393
+ return this.uninstallMac();
394
+ },
395
+
396
+ uninstallLinux() {
397
+ const target = join(applicationsDir(), DESKTOP_FILE);
398
+ const dTarget = join(desktopDir(), DESKTOP_FILE);
399
+ const removed = [];
400
+ if (existsSync(target) && readFileSync(target, 'utf-8').includes(SETUP_MARK)) {
401
+ rmSync(target);
402
+ removed.push(target);
403
+ }
404
+ // setup --desktop 建的桌面条目一并清理(同样只动带标记的)
405
+ if (existsSync(dTarget) && readFileSync(dTarget, 'utf-8').includes(SETUP_MARK)) {
406
+ rmSync(dTarget);
407
+ removed.push(dTarget);
408
+ }
409
+ if (!removed.length) return { skipped: '未发现 setup 创建的快捷方式' };
410
+ try { execFileSync('update-desktop-database', [applicationsDir()], { stdio: 'ignore' }); } catch { /* 无该命令时忽略 */ }
411
+ return { detail: `已移除 ${removed.join('、')}` };
412
+ },
413
+
414
+ uninstallWindows() {
415
+ const paths = windowsPaths();
416
+ const hadVbs = existsSync(paths.vbsPath);
417
+ const hadLnk = existsSync(paths.lnkPath);
418
+ if (!hadVbs && !hadLnk) return { skipped: '未发现 setup 创建的快捷方式' };
419
+ // .lnk 可能已被 install.ps1 等重建并指向它自己:只有 Arguments 带本功能归属
420
+ // 标记才删;校验失败也保留 .lnk(宁可漏删不误删)
421
+ let lnkRemoved = false;
422
+ let verified = false;
423
+ try {
424
+ runPowerShell([
425
+ "$ErrorActionPreference = 'Stop'",
426
+ '$wsh = New-Object -ComObject WScript.Shell',
427
+ `$lnk = $wsh.CreateShortcut('${psQuote(paths.lnkPath)}')`,
428
+ `if (${ownsExpr('$lnk')}) { Remove-Item -LiteralPath '${psQuote(paths.lnkPath)}' -Force }`,
429
+ // setup --desktop 建的桌面 .lnk 一并清理(归属校验同开始菜单)
430
+ "$desk = Join-Path ([Environment]::GetFolderPath('Desktop')) 'ZCode Pro.lnk'",
431
+ 'if (Test-Path $desk) {',
432
+ ' $dl = $wsh.CreateShortcut($desk)',
433
+ ` if (${ownsExpr('$dl')}) { Remove-Item -LiteralPath $desk -Force }`,
434
+ '}',
435
+ ].join('\r\n'));
436
+ verified = true;
437
+ lnkRemoved = !existsSync(paths.lnkPath);
438
+ } catch { /* 校验失败则不动 .lnk */ }
439
+ let vbsCleaned = !hadVbs;
440
+ if (hadVbs) {
441
+ try {
442
+ rmSyncWithRetry(paths.vbsDir);
443
+ vbsCleaned = true;
444
+ } catch (err) {
445
+ // 目录被杀软/索引短暂锁定导致 EPERM:先试原生 rmdir,仍失败但里面的文件
446
+ // 已删干净时按成功处理(残留空目录无害,下次运行会覆盖)
447
+ try {
448
+ execFileSync('cmd', ['/c', 'rmdir', '/s', '/q', paths.vbsDir], { stdio: 'ignore' });
449
+ vbsCleaned = true;
450
+ } catch {
451
+ vbsCleaned = !existsSync(paths.vbsPath);
452
+ if (!vbsCleaned) console.error(` (${paths.vbsDir} 删除失败,可能被占用,可稍后手动删除)`);
453
+ }
454
+ }
455
+ }
456
+ const parts = [];
457
+ if (hadLnk) {
458
+ parts.push(lnkRemoved ? '开始菜单/桌面快捷方式' : verified ? '快捷方式已非本功能创建,未删除' : '快捷方式归属校验失败,未删除 .lnk');
459
+ }
460
+ if (hadVbs) parts.push(vbsCleaned ? '生成的启动脚本' : '启动脚本(删除失败)');
461
+ return { detail: `已移除:${parts.join(';')}` };
462
+ },
463
+
464
+ uninstallMac() {
465
+ const app = macAppBundle();
466
+ const marker = join(app, 'Contents', 'zcodepro-setup-marker');
467
+ if (!existsSync(marker)) return { skipped: '未发现 setup 创建的应用包' };
468
+ // setup --desktop 建的桌面入口一并清理(只删指向本应用包的链接)
469
+ const link = macDesktopLink();
470
+ try {
471
+ if (existsSync(link) && realpathSync(link) === realpathSync(app)) rmSync(link);
472
+ } catch { /* 链接失效则忽略 */ }
473
+ rmSync(app, { recursive: true, force: true });
474
+ return { detail: `已移除 ${app}` };
475
+ },
476
+ };
477
+
478
+ // Windows 上刚写过的目录可能被杀软/索引短暂占用,立即删除偶发 EPERM:短暂等待后重试
479
+ function rmSyncWithRetry(target, tries = 4) {
480
+ for (let i = 0; ; i++) {
481
+ try {
482
+ rmSync(target, { recursive: true, force: true });
483
+ return;
484
+ } catch (err) {
485
+ if (i >= tries - 1) throw err;
486
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
487
+ }
488
+ }
489
+ }
490
+
491
+ // —— 设置项注册处:以后新增设置项在这里加 ——
492
+
493
+ const items = [shortcutItem];
494
+
495
+ export async function runSetup({ uninstall = false, platform: plat = platform(), desktop = false } = {}) {
496
+ console.log(`[zcodepro] setup ${uninstall ? '卸载' : '安装'}本机集成${desktop && !uninstall ? '(含桌面)' : ''}:`);
497
+ let failed = 0;
498
+ for (const item of items) {
499
+ if (!item.supported(plat)) {
500
+ console.log(` · ${item.title}:当前平台(${plat})暂不支持,跳过`);
501
+ continue;
502
+ }
503
+ try {
504
+ const res = uninstall ? item.uninstall(plat) : item.install(plat, { desktop });
505
+ if (res && res.skipped) console.log(` · ${item.title}:${res.skipped}`);
506
+ else console.log(` ✓ ${item.title}:${res && res.detail ? res.detail : '完成'}`);
507
+ } catch (err) {
508
+ failed++;
509
+ console.error(` ✗ ${item.title}:${err?.message || err}`);
510
+ }
511
+ }
512
+ console.log(uninstall ? '' : ' 撤销:zcode-pro setup --uninstall');
513
+ if (failed) process.exitCode = 1;
514
+ }