zcode-pro 0.16.0__py3-none-any.whl
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.
- zcode_pro/__init__.py +3 -0
- zcode_pro/__main__.py +4 -0
- zcode_pro/app/cli.mjs +9 -0
- zcode_pro/app/dist/inject.js +5821 -0
- zcode_pro/app/src/host/cdp.mjs +126 -0
- zcode_pro/app/src/host/helper.mjs +2134 -0
- zcode_pro/app/src/host/main.mjs +262 -0
- zcode_pro/app/src/host/paths.mjs +73 -0
- zcode_pro/app/src/host/pickFolder.mjs +152 -0
- zcode_pro/app/src/host/settings.mjs +61 -0
- zcode_pro/app/src/host/taskIndex.mjs +255 -0
- zcode_pro/app/src/host/taskOrder.mjs +117 -0
- zcode_pro/app/src/host/webdav.mjs +105 -0
- zcode_pro/app/src/inject/core.js +751 -0
- zcode_pro/app/src/inject/features/alias.js +211 -0
- zcode_pro/app/src/inject/features/copy-path.js +78 -0
- zcode_pro/app/src/inject/features/file-menu.js +147 -0
- zcode_pro/app/src/inject/features/idle-reclaim.js +179 -0
- zcode_pro/app/src/inject/features/image-menu.js +215 -0
- zcode_pro/app/src/inject/features/open-folder.js +43 -0
- zcode_pro/app/src/inject/features/pinned-collapse.js +132 -0
- zcode_pro/app/src/inject/features/pinned-expand.js +142 -0
- zcode_pro/app/src/inject/features/plugin-updates.js +28 -0
- zcode_pro/app/src/inject/features/project-menu.js +58 -0
- zcode_pro/app/src/inject/features/relocate-dialog.js +156 -0
- zcode_pro/app/src/inject/features/session-switch.js +244 -0
- zcode_pro/app/src/inject/features/settings-dialog.js +1503 -0
- zcode_pro/app/src/inject/features/settings-entry.js +21 -0
- zcode_pro/app/src/inject/features/styles.js +311 -0
- zcode_pro/app/src/inject/features/task-order.js +78 -0
- zcode_pro/app/src/inject/features/title-lock.js +242 -0
- zcode_pro/app/src/inject/features/toolbar-icons.js +243 -0
- zcode_pro/app/src/inject/features/ws-running.js +472 -0
- zcode_pro/app/src/inject/index.js +55 -0
- zcode_pro/app/src/inject/ui.js +734 -0
- zcode_pro/cli.py +71 -0
- zcode_pro-0.16.0.dist-info/METADATA +45 -0
- zcode_pro-0.16.0.dist-info/RECORD +41 -0
- zcode_pro-0.16.0.dist-info/WHEEL +4 -0
- zcode_pro-0.16.0.dist-info/entry_points.txt +2 -0
- zcode_pro-0.16.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// ZCode Pro 主流程:
|
|
2
|
+
// 1. 定位 ZCode 可执行文件
|
|
3
|
+
// 2. 若 CDP 端口不可达且 ZCode 未在运行 → 以 --remote-debugging-port 启动 ZCode
|
|
4
|
+
// (若 ZCode 已在运行但没有调试端口,绝不主动杀进程,只提示用户手动重启)
|
|
5
|
+
// 3. 连接 CDP,向所有页面注入增强脚本(新文档自动注入 + 已加载页面立即注入)
|
|
6
|
+
// 4. 启动本地辅助 HTTP 服务并常驻,CDP 断开时自动重连
|
|
7
|
+
import { spawn, execSync, spawnSync } from 'node:child_process';
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { dirname, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { randomBytes } from 'node:crypto';
|
|
12
|
+
import { resolveZcodeExecutable, resolveDataRootDir } from './paths.mjs';
|
|
13
|
+
import { CdpConnection, fetchBrowserWsUrl } from './cdp.mjs';
|
|
14
|
+
import { startHelper } from './helper.mjs';
|
|
15
|
+
|
|
16
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const INJECT_BUNDLE = resolve(here, '..', '..', 'dist', 'inject.js');
|
|
18
|
+
|
|
19
|
+
function usage() {
|
|
20
|
+
return `ZCode Pro — ZCode 桌面版界面增强启动器
|
|
21
|
+
|
|
22
|
+
用法: zcode-pro [选项]
|
|
23
|
+
|
|
24
|
+
选项:
|
|
25
|
+
--cdp-port <端口> CDP 调试端口 (默认 9333)
|
|
26
|
+
--helper-port <端口> 本地辅助服务端口 (默认 47889)
|
|
27
|
+
--zcode-path <路径> ZCode 可执行文件路径
|
|
28
|
+
--inject-only 只注入已运行的实例(要求其带调试端口),不启动新实例
|
|
29
|
+
--verbose 输出详细日志
|
|
30
|
+
-h, --help 显示帮助
|
|
31
|
+
|
|
32
|
+
环境变量:
|
|
33
|
+
ZCODEPRO_ZCODE_PATH ZCode 可执行文件路径(同 --zcode-path)
|
|
34
|
+
ZCODEPRO_CDP_PORT CDP 端口
|
|
35
|
+
ZCODEPRO_HELPER_PORT 辅助服务端口
|
|
36
|
+
ZCODE_DATA_BASE_DIR ZCode 数据根目录(与 ZCode 自身语义一致)
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const out = { cdpPort: 0, helperPort: 0, zcodePath: '', injectOnly: false, verbose: false, help: false };
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const a = argv[i];
|
|
44
|
+
if (a === '--cdp-port') out.cdpPort = Number(argv[++i]);
|
|
45
|
+
else if (a === '--helper-port') out.helperPort = Number(argv[++i]);
|
|
46
|
+
else if (a === '--zcode-path') out.zcodePath = argv[++i];
|
|
47
|
+
else if (a === '--inject-only' || a === '--no-launch') out.injectOnly = true;
|
|
48
|
+
else if (a === '--verbose' || a === '-v') out.verbose = true;
|
|
49
|
+
else if (a === '-h' || a === '--help') out.help = true;
|
|
50
|
+
else { console.error('未知参数: ' + a); out.help = true; }
|
|
51
|
+
}
|
|
52
|
+
out.cdpPort = out.cdpPort || Number(process.env.ZCODEPRO_CDP_PORT) || 9333;
|
|
53
|
+
out.helperPort = out.helperPort || Number(process.env.ZCODEPRO_HELPER_PORT) || 47889;
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
58
|
+
|
|
59
|
+
// 桌面快捷方式启动(Terminal=false)时 stderr 不可见,致命错误用桌面通知兜底;
|
|
60
|
+
// 终端运行或有通知服务缺失时静默退回纯 stderr。
|
|
61
|
+
function guiNotify(title, body) {
|
|
62
|
+
if (process.platform !== 'linux' || process.stderr.isTTY) return;
|
|
63
|
+
try {
|
|
64
|
+
spawnSync('notify-send', ['-a', 'ZCode Pro', '-i', 'zcode', title, body], { stdio: 'ignore', timeout: 4000 });
|
|
65
|
+
} catch { /* ignore */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function cdpAlive(port) {
|
|
69
|
+
try { await fetchBrowserWsUrl(port, 1200); return true; } catch { return false; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 只检测 ZCode 应用进程是否存在,绝不干预。
|
|
73
|
+
// 注意:zcodepro 自身可能以 ELECTRON_RUN_AS_NODE 方式复用 zcode 二进制运行,
|
|
74
|
+
// 进程名同为 zcode,因此要排除自己的 pid。
|
|
75
|
+
function zcodeAppProcessExists() {
|
|
76
|
+
try {
|
|
77
|
+
if (process.platform === 'win32') {
|
|
78
|
+
execSync('tasklist /FI "IMAGENAME eq ZCode.exe" /NH | find /I "ZCode.exe"', { stdio: 'ignore' });
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const out = execSync('pgrep -x zcode 2>/dev/null || true', { encoding: 'utf8' });
|
|
82
|
+
const pids = out.split('\n').map((s) => s.trim()).filter((s) => /^\d+$/.test(s));
|
|
83
|
+
// 排除自身(zcodepro 以 ELECTRON_RUN_AS_NODE 运行时进程名也是 zcode)
|
|
84
|
+
return pids.some((pid) => Number(pid) !== process.pid);
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function launchZcode(exe, cdpPort, verbose) {
|
|
91
|
+
// 启动器自身可能以 ELECTRON_RUN_AS_NODE=1 复用 zcode 二进制运行(无系统 node 时的回退),
|
|
92
|
+
// 该变量绝不能带给拉起的 ZCode——否则新实例同样以纯 Node 模式启动,窗口永远不出现。
|
|
93
|
+
const env = { ...process.env };
|
|
94
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
95
|
+
const child = spawn(exe, [`--remote-debugging-port=${cdpPort}`], {
|
|
96
|
+
detached: true,
|
|
97
|
+
stdio: 'ignore',
|
|
98
|
+
env,
|
|
99
|
+
});
|
|
100
|
+
child.unref();
|
|
101
|
+
if (verbose) console.log(`[zcodepro] 已启动 ZCode (pid=${child.pid}),调试端口 ${cdpPort}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function waitForCdp(port, timeoutMs) {
|
|
105
|
+
const start = Date.now();
|
|
106
|
+
while (Date.now() - start < timeoutMs) {
|
|
107
|
+
if (await cdpAlive(port)) return true;
|
|
108
|
+
await sleep(400);
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function buildInjectSource(token, helperPort, features) {
|
|
114
|
+
if (!existsSync(INJECT_BUNDLE)) {
|
|
115
|
+
throw new Error('缺少注入脚本 dist/inject.js,请先运行 npm run build');
|
|
116
|
+
}
|
|
117
|
+
const bundle = readFileSync(INJECT_BUNDLE, 'utf8');
|
|
118
|
+
const bootstrap = `window.__ZCODEPRO__={helperUrl:'http://127.0.0.1:${helperPort}',token:${JSON.stringify(token)},features:${JSON.stringify(features)}};`;
|
|
119
|
+
return bootstrap + '\n' + bundle;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function run(argv) {
|
|
123
|
+
const args = parseArgs(argv);
|
|
124
|
+
if (args.help) { console.log(usage()); return; }
|
|
125
|
+
const log = (...a) => { if (args.verbose) console.log('[zcodepro]', ...a); };
|
|
126
|
+
|
|
127
|
+
let exe;
|
|
128
|
+
try {
|
|
129
|
+
exe = resolveZcodeExecutable(args.zcodePath);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (err.code === 'ZCODE_NOT_FOUND') guiNotify('ZCode Pro 启动失败', '未找到 ZCode 可执行文件,详见终端输出。');
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
const dataRoot = resolveDataRootDir();
|
|
135
|
+
const token = randomBytes(16).toString('hex');
|
|
136
|
+
const state = { injectedPages: 0 };
|
|
137
|
+
|
|
138
|
+
// 1. 确保 ZCode 带着 CDP 端口在运行
|
|
139
|
+
if (!(await cdpAlive(args.cdpPort))) {
|
|
140
|
+
if (args.injectOnly) {
|
|
141
|
+
console.error(`[zcodepro] --inject-only 模式:端口 ${args.cdpPort} 上没有可注入的 ZCode 实例。`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
// 直接尝试启动;若单实例锁冲突(已有实例未开调试端口),端口不会起来
|
|
145
|
+
launchZcode(exe, args.cdpPort, args.verbose);
|
|
146
|
+
const ok = await waitForCdp(args.cdpPort, 30000);
|
|
147
|
+
if (!ok) {
|
|
148
|
+
console.error(`[zcodepro] 等待 ZCode 调试端口 ${args.cdpPort} 超时。`);
|
|
149
|
+
if (zcodeAppProcessExists()) {
|
|
150
|
+
console.error('[zcodepro] 检测到 ZCode 已在运行,但未开启调试端口,无法注入。');
|
|
151
|
+
console.error('[zcodepro] 请手动退出当前 ZCode(zcodepro 不会替你关闭正在运行的实例),然后重新运行 zcodepro。');
|
|
152
|
+
guiNotify('ZCode Pro 暂无法注入', 'ZCode 已在运行但未开启调试端口,请退出 ZCode 后重新打开 ZCode Pro。');
|
|
153
|
+
} else {
|
|
154
|
+
guiNotify('ZCode Pro 启动失败', `等待 ZCode 调试端口 ${args.cdpPort} 超时。`);
|
|
155
|
+
}
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
log(`复用已在监听的 CDP 端口 ${args.cdpPort}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 2. 启动辅助服务
|
|
163
|
+
const server = await startHelper({ port: args.helperPort, token, dataRoot, state });
|
|
164
|
+
console.log(`[zcodepro] 辅助服务已就绪: http://127.0.0.1:${args.helperPort} (数据目录 ${dataRoot})`);
|
|
165
|
+
|
|
166
|
+
// 3. 连接 CDP 并注入(断线自动重连)
|
|
167
|
+
const injectedTargets = new Map(); // targetId -> sessionId
|
|
168
|
+
let stopping = false;
|
|
169
|
+
|
|
170
|
+
let healthRes;
|
|
171
|
+
try {
|
|
172
|
+
healthRes = await fetch(`http://127.0.0.1:${args.helperPort}/health`, { headers: { 'X-ZCodePro-Token': token } }).then((r) => r.json());
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error(`[zcodepro] 辅助服务健康检查失败: ${err.message}`);
|
|
175
|
+
server.close();
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
const source = await buildInjectSource(token, args.helperPort, healthRes.features || {});
|
|
179
|
+
|
|
180
|
+
async function attachConnection() {
|
|
181
|
+
const conn = new CdpConnection(await fetchBrowserWsUrl(args.cdpPort));
|
|
182
|
+
await conn.connect();
|
|
183
|
+
injectedTargets.clear();
|
|
184
|
+
|
|
185
|
+
const handleAttach = async (sessionId, targetInfo) => {
|
|
186
|
+
const targetId = targetInfo?.targetId;
|
|
187
|
+
if (!targetId || targetInfo?.type !== 'page') return;
|
|
188
|
+
// 安全护栏:引导脚本内含 helper 鉴权 token,只能注入 ZCode 应用自身的页面。
|
|
189
|
+
// 外部网站若以内嵌浏览器/网页形式成为 page target,拿到 token 即可调用本地
|
|
190
|
+
// helper(改写增强配置/别名表等)。规则:放行非 http(s)(file://、应用自有协议)与
|
|
191
|
+
// 本机回环地址,拦截其余外部网页。代价是 http(s) 非 localhost 的自有页面不注入。
|
|
192
|
+
const pageUrl = String(targetInfo?.url || '');
|
|
193
|
+
const isExternalWeb = /^https?:\/\//i.test(pageUrl) && !/^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?/i.test(pageUrl);
|
|
194
|
+
if (isExternalWeb) {
|
|
195
|
+
log('跳过外部页面注入(防止 token 泄露): ' + pageUrl.slice(0, 120));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
await conn.send('Page.enable', {}, sessionId);
|
|
200
|
+
await conn.send('Page.addScriptToEvaluateOnNewDocument', { source, runImmediately: true }, sessionId);
|
|
201
|
+
const res = await conn.send('Runtime.evaluate', { expression: source, silent: true }, sessionId);
|
|
202
|
+
if (res?.exceptionDetails) log('注入执行异常: ' + String(res.exceptionDetails.exception?.description || '').slice(0, 200));
|
|
203
|
+
injectedTargets.set(targetId, sessionId);
|
|
204
|
+
state.injectedPages = injectedTargets.size;
|
|
205
|
+
conn.onSession(sessionId, (m) => {
|
|
206
|
+
if (m.type === 'detached') {
|
|
207
|
+
injectedTargets.delete(targetId);
|
|
208
|
+
state.injectedPages = injectedTargets.size;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
log(`已注入页面 target=${targetId}`);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
log('注入会话失败: ' + err.message);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
conn.onSession('', (msg) => {
|
|
218
|
+
if (msg.type === 'attached') {
|
|
219
|
+
handleAttach(msg.sessionId, msg.targetInfo).catch((e) => log('attach 处理失败: ' + e.message));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
await conn.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
|
|
223
|
+
return conn;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let conn = null;
|
|
227
|
+
for (let attempt = 0; attempt < 5 && !conn; attempt++) {
|
|
228
|
+
try {
|
|
229
|
+
conn = await attachConnection();
|
|
230
|
+
} catch (err) {
|
|
231
|
+
log('CDP 连接失败: ' + err.message + ',重试…');
|
|
232
|
+
await sleep(1000);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!conn) {
|
|
236
|
+
console.error('[zcodepro] 无法连接 CDP,退出。');
|
|
237
|
+
server.close();
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
console.log('[zcodepro] 已连接 CDP 并开始注入。保持本进程运行以维持增强;Ctrl+C 退出(不影响 ZCode)。');
|
|
241
|
+
|
|
242
|
+
process.on('SIGINT', () => { stopping = true; try { conn.close(); } catch {} server.close(); process.exit(0); });
|
|
243
|
+
process.on('SIGTERM', () => { stopping = true; try { conn.close(); } catch {} server.close(); process.exit(0); });
|
|
244
|
+
|
|
245
|
+
while (!stopping) {
|
|
246
|
+
await sleep(3000);
|
|
247
|
+
if (stopping) break;
|
|
248
|
+
if (!conn.closed) continue;
|
|
249
|
+
// 连接断开:ZCode 退出则一并退出,否则重连
|
|
250
|
+
if (!(await cdpAlive(args.cdpPort))) {
|
|
251
|
+
console.log('[zcodepro] ZCode 已退出,zcodepro 一并退出。');
|
|
252
|
+
server.close();
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
conn = await attachConnection();
|
|
257
|
+
log('CDP 已重连');
|
|
258
|
+
} catch (err) {
|
|
259
|
+
log('CDP 重连失败: ' + err.message);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// 跨平台定位 ZCode 桌面版可执行文件。
|
|
2
|
+
// 优先级:--zcode-path 参数 > ZCODEPRO_ZCODE_PATH 环境变量 > 平台默认候选路径 > PATH 上的 zcode。
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { arch, homedir, platform } from 'node:os';
|
|
6
|
+
|
|
7
|
+
function candidates() {
|
|
8
|
+
const home = homedir();
|
|
9
|
+
if (platform() === 'win32') {
|
|
10
|
+
const localAppData = process.env.LOCALAPPDATA || join(home, 'AppData', 'Local');
|
|
11
|
+
const programFiles = process.env.PROGRAMFILES || 'C:\\Program Files';
|
|
12
|
+
return [
|
|
13
|
+
join(localAppData, 'Programs', 'ZCode', 'ZCode.exe'),
|
|
14
|
+
join(localAppData, 'Programs', 'zcode', 'zcode.exe'),
|
|
15
|
+
join(programFiles, 'ZCode', 'ZCode.exe'),
|
|
16
|
+
join(programFiles, 'zcode', 'zcode.exe'),
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
if (platform() === 'darwin') {
|
|
20
|
+
return [
|
|
21
|
+
'/Applications/ZCode.app/Contents/MacOS/ZCode',
|
|
22
|
+
join(home, 'Applications', 'ZCode.app', 'Contents', 'MacOS', 'ZCode'),
|
|
23
|
+
'/Applications/ZCode Preview.app/Contents/MacOS/ZCode Preview',
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
// linux(含 AUR 包,AUR 版安装到 /opt/ZCode)
|
|
27
|
+
const list = [
|
|
28
|
+
'/opt/ZCode/zcode',
|
|
29
|
+
'/usr/lib/zcode/zcode',
|
|
30
|
+
'/usr/share/zcode/zcode',
|
|
31
|
+
'/usr/local/ZCode/zcode',
|
|
32
|
+
join(home, '.local', 'share', 'zcode', 'zcode'),
|
|
33
|
+
];
|
|
34
|
+
if (arch() === 'arm64') list.unshift('/opt/ZCode/zcode'); // AUR aarch64 同路径
|
|
35
|
+
return list;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveZcodeExecutable(explicit) {
|
|
39
|
+
const tried = [];
|
|
40
|
+
const check = (p) => {
|
|
41
|
+
if (!p) return null;
|
|
42
|
+
tried.push(p);
|
|
43
|
+
try {
|
|
44
|
+
if (existsSync(p)) return p;
|
|
45
|
+
} catch { /* ignore */ }
|
|
46
|
+
return null;
|
|
47
|
+
};
|
|
48
|
+
const fromFlag = check(explicit);
|
|
49
|
+
if (fromFlag) return fromFlag;
|
|
50
|
+
const fromEnv = check(process.env.ZCODEPRO_ZCODE_PATH);
|
|
51
|
+
if (fromEnv) return fromEnv;
|
|
52
|
+
for (const p of candidates()) {
|
|
53
|
+
const hit = check(p);
|
|
54
|
+
if (hit) return hit;
|
|
55
|
+
}
|
|
56
|
+
// 最后退回 PATH(仅类 Unix;Windows 上 PATH 查找由 shell 负责)
|
|
57
|
+
if (platform() !== 'win32') {
|
|
58
|
+
const hit = check('/usr/sbin/zcode') || check('/usr/bin/zcode');
|
|
59
|
+
if (hit) return hit;
|
|
60
|
+
}
|
|
61
|
+
const err = new Error(
|
|
62
|
+
'未找到 ZCode 可执行文件。请通过 --zcode-path <路径> 或环境变量 ZCODEPRO_ZCODE_PATH 指定。\n已尝试:\n' +
|
|
63
|
+
tried.map((p) => ' - ' + p).join('\n')
|
|
64
|
+
);
|
|
65
|
+
err.code = 'ZCODE_NOT_FOUND';
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ZCode 数据根目录,与主进程逻辑保持一致:ZCODE_DATA_BASE_DIR || $HOME,再拼 .zcode
|
|
70
|
+
export function resolveDataRootDir() {
|
|
71
|
+
const base = (process.env.ZCODE_DATA_BASE_DIR || '').trim() || homedir();
|
|
72
|
+
return join(base, '.zcode');
|
|
73
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// 系统原生目录选择器(Linux:xdg-desktop-portal FileChooser,经 gdbus 调用,
|
|
2
|
+
// KDE/GNOME 弹出各自的原生对话框),支持指定起始目录。
|
|
3
|
+
// 返回 { path: string | null }(用户取消为 null)或 { unavailable: true }(环境无可用选择器)。
|
|
4
|
+
|
|
5
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
6
|
+
import { existsSync, statSync } from 'node:fs';
|
|
7
|
+
import { dirname } from 'node:path';
|
|
8
|
+
|
|
9
|
+
// kdialog(KDE 原生):起始目录参数可靠生效。
|
|
10
|
+
// xdg-desktop-portal-kde 不读 current_folder 选项(二进制中无该字符串),
|
|
11
|
+
// KFileWidget 转而用“应用上次访问目录”记忆,导致起始目录失效;
|
|
12
|
+
// GTK 后端支持 current_folder,无 kdialog 时回退门户。
|
|
13
|
+
function kdialogPickFolder(startDir, title) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const child = spawn('kdialog', ['--getexistingdirectory', startDir, '--title', title], {
|
|
16
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
17
|
+
});
|
|
18
|
+
let out = '';
|
|
19
|
+
child.stdout.on('data', (d) => { out += d.toString(); });
|
|
20
|
+
child.on('error', () => resolve(undefined)); // 无 kdialog 或无法启动 → 回退门户
|
|
21
|
+
child.on('close', (code) => {
|
|
22
|
+
if (code === 0) {
|
|
23
|
+
const p = out.trim();
|
|
24
|
+
resolve(p || null);
|
|
25
|
+
} else {
|
|
26
|
+
resolve(null); // 用户取消(Esc/关闭)
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function kdialogAvailable() {
|
|
33
|
+
const r = spawnSync('kdialog', ['--version'], { encoding: 'utf8' });
|
|
34
|
+
return !r.error && r.status === 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// zenity(GTK 原生):--filename 指定起始目录(带尾部 / 让对话框定位到该目录)
|
|
38
|
+
function zenityPickFolder(startDir) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const withSlash = startDir.endsWith('/') ? startDir : startDir + '/';
|
|
41
|
+
const child = spawn('zenity', ['--file-selection', '--directory', '--filename=' + withSlash], {
|
|
42
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
43
|
+
});
|
|
44
|
+
let out = '';
|
|
45
|
+
child.stdout.on('data', (d) => { out += d.toString(); });
|
|
46
|
+
child.on('error', () => resolve(undefined)); // 无 zenity 或无法启动 → 回退门户
|
|
47
|
+
child.on('close', (code) => {
|
|
48
|
+
if (code === 0) {
|
|
49
|
+
const p = out.trim();
|
|
50
|
+
resolve(p || null);
|
|
51
|
+
} else {
|
|
52
|
+
resolve(null); // 用户取消(Esc/关闭)
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function zenityAvailable() {
|
|
59
|
+
const r = spawnSync('zenity', ['--version'], { encoding: 'utf8' });
|
|
60
|
+
return !r.error && r.status === 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function gdbusAvailable() {
|
|
64
|
+
const r = spawnSync('gdbus', ['--version'], { encoding: 'utf8' });
|
|
65
|
+
return !r.error && r.status === 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 起始目录不存在时(切换文件夹的典型场景:原路径已被移走/删除),
|
|
69
|
+
// 逐级向上找到第一个真实存在的目录,避免 portal 回退打开 home
|
|
70
|
+
function firstExistingDir(p) {
|
|
71
|
+
let cur = p;
|
|
72
|
+
for (let i = 0; cur && i < 128; i++) {
|
|
73
|
+
try {
|
|
74
|
+
if (existsSync(cur) && statSync(cur).isDirectory()) return cur;
|
|
75
|
+
} catch { /* ignore */ }
|
|
76
|
+
const parent = dirname(cur);
|
|
77
|
+
if (parent === cur) break;
|
|
78
|
+
cur = parent;
|
|
79
|
+
}
|
|
80
|
+
return '/';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function portalPickFolder(startDir, title, timeoutMs) {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
let monitor;
|
|
86
|
+
let timer = null;
|
|
87
|
+
let buf = '';
|
|
88
|
+
let done = false;
|
|
89
|
+
const finish = (v) => {
|
|
90
|
+
if (done) return;
|
|
91
|
+
done = true;
|
|
92
|
+
if (timer) clearTimeout(timer);
|
|
93
|
+
try { monitor && monitor.kill(); } catch { /* ignore */ }
|
|
94
|
+
resolve(v);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
monitor = spawn('gdbus', ['monitor', '--session', '--dest', 'org.freedesktop.portal.Desktop'], {
|
|
98
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
99
|
+
});
|
|
100
|
+
monitor.on('error', () => finish(undefined));
|
|
101
|
+
monitor.stdout.on('data', (d) => {
|
|
102
|
+
buf += d.toString();
|
|
103
|
+
if (done) return;
|
|
104
|
+
// 只认带我们 handle_token 的 Response 块(portal 的请求路径含调用方指定的 token)
|
|
105
|
+
const idx = buf.lastIndexOf('zcodepro_tok');
|
|
106
|
+
if (idx < 0) return;
|
|
107
|
+
const block = buf.slice(idx);
|
|
108
|
+
if (!block.includes('Response')) return;
|
|
109
|
+
const uri = /uris[^']*'([^']+)'/.exec(block) || /uris[^"]*"([^"]+)"/.exec(block);
|
|
110
|
+
if (uri) {
|
|
111
|
+
let p = uri[1];
|
|
112
|
+
if (p.startsWith('file://')) {
|
|
113
|
+
try { p = decodeURIComponent(p.slice(7)); } catch { p = p.slice(7); }
|
|
114
|
+
}
|
|
115
|
+
finish(p);
|
|
116
|
+
} else {
|
|
117
|
+
finish(null); // 用户取消或其他非成功响应
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// 等 monitor 完成订阅(gdbus 就绪即输出首行)再发起对话框调用
|
|
122
|
+
monitor.stdout.once('data', () => {
|
|
123
|
+
if (done) return;
|
|
124
|
+
const bytes = Array.from(Buffer.from(startDir + '\0', 'utf8'));
|
|
125
|
+
const opts = `{"handle_token": <"zcodepro_tok_${Date.now().toString(36)}">, "modal": <true>, "directory": <true>, "multiple": <false>, "current_folder": <[${bytes.join(',')}]>}`;
|
|
126
|
+
const call = spawnSync('gdbus', ['call', '--session', '--dest', 'org.freedesktop.portal.Desktop',
|
|
127
|
+
'--object-path', '/org/freedesktop/portal/desktop',
|
|
128
|
+
'--method', 'org.freedesktop.portal.FileChooser.OpenFile', '', title, opts], { encoding: 'utf8' });
|
|
129
|
+
if (call.status !== 0 || !/object path/i.test((call.stdout || '') + (call.stderr || ''))) finish(undefined);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
timer = setTimeout(() => finish(null), timeoutMs);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function pickFolderSystem(startDir, title, timeoutMs = 180000) {
|
|
137
|
+
if (process.platform !== 'linux') return { unavailable: true };
|
|
138
|
+
const start = firstExistingDir(String(startDir || '/'));
|
|
139
|
+
const dialogTitle = title || '选择文件夹';
|
|
140
|
+
if (kdialogAvailable()) {
|
|
141
|
+
const r = await kdialogPickFolder(start, dialogTitle);
|
|
142
|
+
if (r !== undefined) return { path: r };
|
|
143
|
+
}
|
|
144
|
+
if (zenityAvailable()) {
|
|
145
|
+
const r = await zenityPickFolder(start);
|
|
146
|
+
if (r !== undefined) return { path: r };
|
|
147
|
+
}
|
|
148
|
+
if (!gdbusAvailable()) return { unavailable: true };
|
|
149
|
+
const r = await portalPickFolder(start, dialogTitle, timeoutMs);
|
|
150
|
+
if (r === undefined) return { unavailable: true };
|
|
151
|
+
return { path: r };
|
|
152
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// 读写 ZCode 的 setting.json(~/.zcode/v2/setting.json)。
|
|
2
|
+
// 读用于 helper 的 /projects 端点;写仅用于“切换文件夹”后的路径引用同步,
|
|
3
|
+
// 采用原子写并保留备份。
|
|
4
|
+
|
|
5
|
+
import { copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
export function readSettings(settingsFile) {
|
|
9
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
10
|
+
try {
|
|
11
|
+
if (!existsSync(settingsFile)) return {};
|
|
12
|
+
return JSON.parse(readFileSync(settingsFile, 'utf8'));
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (attempt === 4) throw new Error('读取 setting.json 失败: ' + err.message);
|
|
15
|
+
// 可能正被原子重命名,稍候重试
|
|
16
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 40);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function writeSettingsAtomic(settingsFile, next) {
|
|
22
|
+
const dir = join(settingsFile, '..');
|
|
23
|
+
const backup = join(dir, 'setting.json.zcodepro-backup');
|
|
24
|
+
if (existsSync(settingsFile)) {
|
|
25
|
+
try { copyFileSync(settingsFile, backup); } catch { /* 备份失败不阻塞 */ }
|
|
26
|
+
}
|
|
27
|
+
const tmp = settingsFile + '.zcodepro-tmp';
|
|
28
|
+
writeFileSync(tmp, JSON.stringify(next, null, 2), 'utf8');
|
|
29
|
+
renameSync(tmp, settingsFile);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 将 setting.json 中所有引用旧路径的字段映射到新路径,返回新 settings 对象。
|
|
33
|
+
// 替换时保留原值的尾分隔符风格(ZCode 自身多写带 / 的路径),减少与宿主写入的差异。
|
|
34
|
+
export function remapSettingsPaths(settings, oldPath, newPath) {
|
|
35
|
+
const next = JSON.parse(JSON.stringify(settings));
|
|
36
|
+
const norm = (p) => (typeof p === 'string' ? p.replace(/[\\/]+$/, '') : '');
|
|
37
|
+
const swap = (p) => {
|
|
38
|
+
if (typeof p !== 'string' || norm(p) !== norm(oldPath)) return p;
|
|
39
|
+
return newPath + p.slice(norm(oldPath).length); // 保留原尾分隔符(可为空)
|
|
40
|
+
};
|
|
41
|
+
if (Array.isArray(next.recentProjects)) {
|
|
42
|
+
next.recentProjects = next.recentProjects.map(swap);
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(next.lastWorkspaceSession)) {
|
|
45
|
+
next.lastWorkspaceSession = next.lastWorkspaceSession.map((entry) => {
|
|
46
|
+
if (entry) entry.workspacePath = swap(entry.workspacePath);
|
|
47
|
+
return entry;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (next.webRemoteControlLastEnabledContext) {
|
|
51
|
+
next.webRemoteControlLastEnabledContext.workspacePath = swap(next.webRemoteControlLastEnabledContext.workspacePath);
|
|
52
|
+
}
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isProjectOpenInTabs(settings, projectPath) {
|
|
57
|
+
const norm = (p) => (typeof p === 'string' ? p.replace(/[\\/]+$/, '') : '');
|
|
58
|
+
const target = norm(projectPath);
|
|
59
|
+
const tabs = Array.isArray(settings.lastWorkspaceSession) ? settings.lastWorkspaceSession : [];
|
|
60
|
+
return tabs.some((entry) => entry && norm(entry.workspacePath) === target);
|
|
61
|
+
}
|