zen-gitsync 2.12.2 → 2.12.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 (47) hide show
  1. package/LICENSE +190 -21
  2. package/index.js +25 -11
  3. package/package.json +18 -5
  4. package/scripts/convert-colors-to-vars.cjs +286 -272
  5. package/scripts/convert-fontsize-to-vars.cjs +221 -207
  6. package/scripts/convert-spacing-to-vars.cjs +256 -242
  7. package/scripts/convert-to-standard-vars.cjs +282 -268
  8. package/scripts/release.js +599 -585
  9. package/src/config.js +350 -336
  10. package/src/gitCommit.js +455 -440
  11. package/src/ui/public/assets/{EditorView-bnJmBq-i.js → EditorView-BZaOzahT.js} +2 -2
  12. package/src/ui/public/assets/EditorView-CbqSI9nw.css +1 -0
  13. package/src/ui/public/assets/{SourceMapView-Rz5SD0A0.js → SourceMapView-D_8mnVt2.js} +3 -3
  14. package/src/ui/public/assets/SourceMapView-DyMK80hS.css +1 -0
  15. package/src/ui/public/assets/{index-Bo3tntQh.js → index-BgFwmXzV.js} +11 -11
  16. package/src/ui/public/assets/{index-bOs5P8fz.css → index-Dsi6tg7k.css} +1 -1
  17. package/src/ui/public/index.html +2 -2
  18. package/src/ui/server/index.js +410 -396
  19. package/src/ui/server/middleware/requestLogger.js +51 -37
  20. package/src/ui/server/routes/branchStatus.js +101 -87
  21. package/src/ui/server/routes/code.js +110 -96
  22. package/src/ui/server/routes/codeAnalysis.js +995 -981
  23. package/src/ui/server/routes/config.js +1190 -1158
  24. package/src/ui/server/routes/exec.js +272 -258
  25. package/src/ui/server/routes/fileOpen.js +279 -265
  26. package/src/ui/server/routes/fs.js +701 -687
  27. package/src/ui/server/routes/git/diff.js +352 -338
  28. package/src/ui/server/routes/git/diffUtils.js +128 -114
  29. package/src/ui/server/routes/git/stash.js +552 -538
  30. package/src/ui/server/routes/git/tags.js +172 -158
  31. package/src/ui/server/routes/git.js +190 -176
  32. package/src/ui/server/routes/gitOps.js +1179 -1165
  33. package/src/ui/server/routes/instances.js +14 -0
  34. package/src/ui/server/routes/npm.js +1023 -1009
  35. package/src/ui/server/routes/process.js +82 -68
  36. package/src/ui/server/routes/status.js +67 -53
  37. package/src/ui/server/routes/terminal.js +319 -305
  38. package/src/ui/server/socket/registerUiSocketHandlers.js +226 -212
  39. package/src/ui/server/utils/createSavePortToFile.js +46 -32
  40. package/src/ui/server/utils/instanceRegistry.js +14 -0
  41. package/src/ui/server/utils/pathGuard.js +14 -0
  42. package/src/ui/server/utils/pathGuard.test.js +14 -0
  43. package/src/ui/server/utils/randomStartPort.js +14 -0
  44. package/src/ui/server/utils/startServerOnAvailablePort.js +101 -87
  45. package/src/utils/index.js +14 -0
  46. package/src/ui/public/assets/EditorView-CHBjgiZc.css +0 -1
  47. package/src/ui/public/assets/SourceMapView-DhQX0K7t.css +0 -1
@@ -1,265 +1,279 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import open from 'open';
4
- import { spawn } from 'child_process';
5
-
6
- function spawnDetached(command, args, options = {}) {
7
- return new Promise((resolve, reject) => {
8
- const child = spawn(command, args, {
9
- detached: true,
10
- stdio: 'ignore',
11
- ...options
12
- });
13
-
14
- child.on('error', reject);
15
- child.on('spawn', () => {
16
- child.unref();
17
- resolve('success');
18
- });
19
- });
20
- }
21
-
22
- async function launchClaudeCode(dirPath, { permissionMode } = {}) {
23
- // 透传可选的权限模式参数到 claude CLI(如 acceptEdits)
24
- // 注意:permissionMode 必须是一个 token 字符串,避免 shell 注入
25
- const SAFE_MODE = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
26
- const cliArgs = [];
27
- if (permissionMode && typeof permissionMode === 'string' && SAFE_MODE.test(permissionMode)) {
28
- cliArgs.push('--permission-mode', permissionMode);
29
- }
30
-
31
- if (process.platform === 'win32') {
32
- return spawnDetached('cmd.exe', ['/c', 'start', '""', 'claude', ...cliArgs], {
33
- cwd: dirPath
34
- });
35
- }
36
-
37
- return spawnDetached('claude', cliArgs, {
38
- cwd: dirPath
39
- });
40
- }
41
-
42
- export function registerFileOpenRoutes({
43
- app
44
- }) {
45
- // 打开文件
46
- app.post('/api/open-file', async (req, res) => {
47
- try {
48
- const { filePath, context } = req.body;
49
-
50
- if (!filePath) {
51
- return res.status(400).json({
52
- success: false,
53
- error: '文件路径不能为空'
54
- });
55
- }
56
-
57
- let targetFilePath = filePath;
58
-
59
- // 根据上下文处理不同的文件打开方式
60
- switch (context) {
61
- case 'git-status':
62
- // Git状态:直接打开当前工作目录中的文件
63
- targetFilePath = path.resolve(process.cwd(), filePath);
64
- break;
65
-
66
- case 'commit-detail':
67
- // 提交详情:这里可以考虑创建临时文件显示该提交时的文件内容
68
- // 暂时先打开当前版本的文件
69
- targetFilePath = path.resolve(process.cwd(), filePath);
70
- break;
71
-
72
- case 'stash-detail':
73
- // Stash详情:同样暂时打开当前版本的文件
74
- targetFilePath = path.resolve(process.cwd(), filePath);
75
- break;
76
-
77
- default:
78
- targetFilePath = path.resolve(process.cwd(), filePath);
79
- }
80
-
81
- try {
82
- // 检查文件是否存在
83
- await fs.access(targetFilePath);
84
-
85
- // 使用系统默认程序打开文件
86
- await open(targetFilePath, { wait: false });
87
-
88
- res.json({
89
- success: true,
90
- message: `已打开文件: ${path.basename(targetFilePath)}`
91
- });
92
- } catch (error) {
93
- // 如果文件不存在,尝试在编辑器中创建新文件
94
- if (error.code === 'ENOENT') {
95
- try {
96
- await open(targetFilePath, { wait: false });
97
- res.json({
98
- success: true,
99
- message: `已在编辑器中打开文件: ${path.basename(targetFilePath)}`
100
- });
101
- } catch (openError) {
102
- res.status(400).json({
103
- success: false,
104
- error: `无法打开文件 "${path.basename(targetFilePath)}": ${openError.message}`
105
- });
106
- }
107
- } else {
108
- res.status(400).json({
109
- success: false,
110
- error: `无法访问文件 "${path.basename(targetFilePath)}": ${error.message}`
111
- });
112
- }
113
- }
114
- } catch (error) {
115
- res.status(500).json({
116
- success: false,
117
- error: error.message
118
- });
119
- }
120
- });
121
-
122
- // 用VSCode打开文件
123
- app.post('/api/open-with-vscode', async (req, res) => {
124
- try {
125
- const { filePath, context } = req.body;
126
-
127
- if (!filePath) {
128
- return res.status(400).json({
129
- success: false,
130
- error: '文件路径不能为空'
131
- });
132
- }
133
-
134
- let targetFilePath = filePath;
135
-
136
- // 根据上下文处理不同的文件打开方式
137
- switch (context) {
138
- case 'git-status':
139
- case 'commit-detail':
140
- case 'stash-detail':
141
- targetFilePath = path.resolve(process.cwd(), filePath);
142
- break;
143
- default:
144
- targetFilePath = path.resolve(process.cwd(), filePath);
145
- }
146
-
147
- try {
148
- // 使用VSCode打开文件
149
- // 尝试使用 'code' 命令打开文件
150
- // 使用已导入的 spawn
151
-
152
- // 创建一个Promise来处理spawn的异步结果
153
- const spawnPromise = new Promise((resolve, reject) => {
154
- const vscodeProcess = spawn('code', [targetFilePath], {
155
- detached: true,
156
- stdio: 'ignore'
157
- });
158
-
159
- // 监听错误事件
160
- vscodeProcess.on('error', (err) => {
161
- reject(err);
162
- });
163
-
164
- // 监听spawn事件,表示进程成功启动
165
- vscodeProcess.on('spawn', () => {
166
- resolve('success');
167
- });
168
-
169
- vscodeProcess.unref();
170
- });
171
-
172
- await spawnPromise;
173
-
174
- res.json({
175
- success: true,
176
- message: `已用VSCode打开文件: ${path.basename(targetFilePath)}`
177
- });
178
- } catch (error) {
179
- // 如果VSCode命令不可用,尝试直接用open打开
180
- try {
181
- await open(targetFilePath, { app: { name: 'code' } });
182
- res.json({
183
- success: true,
184
- message: `已用VSCode打开文件: ${path.basename(targetFilePath)}`
185
- });
186
- } catch (openError) {
187
- // 最后的备用方案:尝试用系统默认编辑器打开
188
- try {
189
- await open(targetFilePath);
190
- res.json({
191
- success: true,
192
- message: `VSCode不可用,已用系统默认程序打开文件: ${path.basename(targetFilePath)}`
193
- });
194
- } catch (finalError) {
195
- res.status(400).json({
196
- success: false,
197
- error: `无法打开文件 "${path.basename(targetFilePath)}": VSCode可能未安装或未添加到PATH,且系统默认程序也无法打开该文件`
198
- });
199
- }
200
- }
201
- }
202
- } catch (error) {
203
- res.status(500).json({
204
- success: false,
205
- error: error.message
206
- });
207
- }
208
- });
209
-
210
- // 用 VSCode 打开目录
211
- app.post('/api/open-directory-with-vscode', async (req, res) => {
212
- try {
213
- const { path: dirPath } = req.body;
214
- if (!dirPath) {
215
- return res.status(400).json({ success: false, error: '目录路径不能为空' });
216
- }
217
-
218
- try {
219
- await spawnDetached('code', [dirPath]);
220
- res.json({ success: true, message: '已用 VSCode 打开目录' });
221
- } catch {
222
- // fallback:通过 open 模块指定 code 应用
223
- try {
224
- await open(dirPath, { app: { name: 'code' } });
225
- res.json({ success: true, message: '已用 VSCode 打开目录' });
226
- } catch (openError) {
227
- res.status(400).json({ success: false, error: 'VSCode 可能未安装或未添加到 PATH' });
228
- }
229
- }
230
- } catch (error) {
231
- res.status(500).json({ success: false, error: error.message });
232
- }
233
- });
234
-
235
- // Claude Code 打开目录
236
- app.post('/api/open-directory-with-claude-code', async (req, res) => {
237
- try {
238
- const { path: dirPath, permissionMode } = req.body || {};
239
- if (!dirPath) {
240
- return res.status(400).json({ success: false, error: '目录路径不能为空' });
241
- }
242
-
243
- try {
244
- await fs.access(dirPath);
245
- } catch (error) {
246
- return res.status(400).json({ success: false, error: `目录不存在或不可访问: ${dirPath}` });
247
- }
248
-
249
- try {
250
- await launchClaudeCode(dirPath, { permissionMode });
251
- const message = permissionMode
252
- ? `已用 Claude Code 打开目录(permission-mode=${permissionMode})`
253
- : '已用 Claude Code 打开目录';
254
- res.json({ success: true, message });
255
- } catch (error) {
256
- res.status(400).json({
257
- success: false,
258
- error: '未检测到 Claude Code,请先安装并确保可以在终端中直接运行 claude'
259
- });
260
- }
261
- } catch (error) {
262
- res.status(500).json({ success: false, error: error.message });
263
- }
264
- });
265
- }
1
+ // Copyright 2026 xz333221
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+ import fs from 'fs/promises';
16
+ import path from 'path';
17
+ import open from 'open';
18
+ import { spawn } from 'child_process';
19
+
20
+ function spawnDetached(command, args, options = {}) {
21
+ return new Promise((resolve, reject) => {
22
+ const child = spawn(command, args, {
23
+ detached: true,
24
+ stdio: 'ignore',
25
+ ...options
26
+ });
27
+
28
+ child.on('error', reject);
29
+ child.on('spawn', () => {
30
+ child.unref();
31
+ resolve('success');
32
+ });
33
+ });
34
+ }
35
+
36
+ async function launchClaudeCode(dirPath, { permissionMode } = {}) {
37
+ // 透传可选的权限模式参数到 claude CLI(如 acceptEdits)
38
+ // 注意:permissionMode 必须是一个 token 字符串,避免 shell 注入
39
+ const SAFE_MODE = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
40
+ const cliArgs = [];
41
+ if (permissionMode && typeof permissionMode === 'string' && SAFE_MODE.test(permissionMode)) {
42
+ cliArgs.push('--permission-mode', permissionMode);
43
+ }
44
+
45
+ if (process.platform === 'win32') {
46
+ return spawnDetached('cmd.exe', ['/c', 'start', '""', 'claude', ...cliArgs], {
47
+ cwd: dirPath
48
+ });
49
+ }
50
+
51
+ return spawnDetached('claude', cliArgs, {
52
+ cwd: dirPath
53
+ });
54
+ }
55
+
56
+ export function registerFileOpenRoutes({
57
+ app
58
+ }) {
59
+ // 打开文件
60
+ app.post('/api/open-file', async (req, res) => {
61
+ try {
62
+ const { filePath, context } = req.body;
63
+
64
+ if (!filePath) {
65
+ return res.status(400).json({
66
+ success: false,
67
+ error: '文件路径不能为空'
68
+ });
69
+ }
70
+
71
+ let targetFilePath = filePath;
72
+
73
+ // 根据上下文处理不同的文件打开方式
74
+ switch (context) {
75
+ case 'git-status':
76
+ // Git状态:直接打开当前工作目录中的文件
77
+ targetFilePath = path.resolve(process.cwd(), filePath);
78
+ break;
79
+
80
+ case 'commit-detail':
81
+ // 提交详情:这里可以考虑创建临时文件显示该提交时的文件内容
82
+ // 暂时先打开当前版本的文件
83
+ targetFilePath = path.resolve(process.cwd(), filePath);
84
+ break;
85
+
86
+ case 'stash-detail':
87
+ // Stash详情:同样暂时打开当前版本的文件
88
+ targetFilePath = path.resolve(process.cwd(), filePath);
89
+ break;
90
+
91
+ default:
92
+ targetFilePath = path.resolve(process.cwd(), filePath);
93
+ }
94
+
95
+ try {
96
+ // 检查文件是否存在
97
+ await fs.access(targetFilePath);
98
+
99
+ // 使用系统默认程序打开文件
100
+ await open(targetFilePath, { wait: false });
101
+
102
+ res.json({
103
+ success: true,
104
+ message: `已打开文件: ${path.basename(targetFilePath)}`
105
+ });
106
+ } catch (error) {
107
+ // 如果文件不存在,尝试在编辑器中创建新文件
108
+ if (error.code === 'ENOENT') {
109
+ try {
110
+ await open(targetFilePath, { wait: false });
111
+ res.json({
112
+ success: true,
113
+ message: `已在编辑器中打开文件: ${path.basename(targetFilePath)}`
114
+ });
115
+ } catch (openError) {
116
+ res.status(400).json({
117
+ success: false,
118
+ error: `无法打开文件 "${path.basename(targetFilePath)}": ${openError.message}`
119
+ });
120
+ }
121
+ } else {
122
+ res.status(400).json({
123
+ success: false,
124
+ error: `无法访问文件 "${path.basename(targetFilePath)}": ${error.message}`
125
+ });
126
+ }
127
+ }
128
+ } catch (error) {
129
+ res.status(500).json({
130
+ success: false,
131
+ error: error.message
132
+ });
133
+ }
134
+ });
135
+
136
+ // 用VSCode打开文件
137
+ app.post('/api/open-with-vscode', async (req, res) => {
138
+ try {
139
+ const { filePath, context } = req.body;
140
+
141
+ if (!filePath) {
142
+ return res.status(400).json({
143
+ success: false,
144
+ error: '文件路径不能为空'
145
+ });
146
+ }
147
+
148
+ let targetFilePath = filePath;
149
+
150
+ // 根据上下文处理不同的文件打开方式
151
+ switch (context) {
152
+ case 'git-status':
153
+ case 'commit-detail':
154
+ case 'stash-detail':
155
+ targetFilePath = path.resolve(process.cwd(), filePath);
156
+ break;
157
+ default:
158
+ targetFilePath = path.resolve(process.cwd(), filePath);
159
+ }
160
+
161
+ try {
162
+ // 使用VSCode打开文件
163
+ // 尝试使用 'code' 命令打开文件
164
+ // 使用已导入的 spawn
165
+
166
+ // 创建一个Promise来处理spawn的异步结果
167
+ const spawnPromise = new Promise((resolve, reject) => {
168
+ const vscodeProcess = spawn('code', [targetFilePath], {
169
+ detached: true,
170
+ stdio: 'ignore'
171
+ });
172
+
173
+ // 监听错误事件
174
+ vscodeProcess.on('error', (err) => {
175
+ reject(err);
176
+ });
177
+
178
+ // 监听spawn事件,表示进程成功启动
179
+ vscodeProcess.on('spawn', () => {
180
+ resolve('success');
181
+ });
182
+
183
+ vscodeProcess.unref();
184
+ });
185
+
186
+ await spawnPromise;
187
+
188
+ res.json({
189
+ success: true,
190
+ message: `已用VSCode打开文件: ${path.basename(targetFilePath)}`
191
+ });
192
+ } catch (error) {
193
+ // 如果VSCode命令不可用,尝试直接用open打开
194
+ try {
195
+ await open(targetFilePath, { app: { name: 'code' } });
196
+ res.json({
197
+ success: true,
198
+ message: `已用VSCode打开文件: ${path.basename(targetFilePath)}`
199
+ });
200
+ } catch (openError) {
201
+ // 最后的备用方案:尝试用系统默认编辑器打开
202
+ try {
203
+ await open(targetFilePath);
204
+ res.json({
205
+ success: true,
206
+ message: `VSCode不可用,已用系统默认程序打开文件: ${path.basename(targetFilePath)}`
207
+ });
208
+ } catch (finalError) {
209
+ res.status(400).json({
210
+ success: false,
211
+ error: `无法打开文件 "${path.basename(targetFilePath)}": VSCode可能未安装或未添加到PATH,且系统默认程序也无法打开该文件`
212
+ });
213
+ }
214
+ }
215
+ }
216
+ } catch (error) {
217
+ res.status(500).json({
218
+ success: false,
219
+ error: error.message
220
+ });
221
+ }
222
+ });
223
+
224
+ // VSCode 打开目录
225
+ app.post('/api/open-directory-with-vscode', async (req, res) => {
226
+ try {
227
+ const { path: dirPath } = req.body;
228
+ if (!dirPath) {
229
+ return res.status(400).json({ success: false, error: '目录路径不能为空' });
230
+ }
231
+
232
+ try {
233
+ await spawnDetached('code', [dirPath]);
234
+ res.json({ success: true, message: '已用 VSCode 打开目录' });
235
+ } catch {
236
+ // fallback:通过 open 模块指定 code 应用
237
+ try {
238
+ await open(dirPath, { app: { name: 'code' } });
239
+ res.json({ success: true, message: '已用 VSCode 打开目录' });
240
+ } catch (openError) {
241
+ res.status(400).json({ success: false, error: 'VSCode 可能未安装或未添加到 PATH' });
242
+ }
243
+ }
244
+ } catch (error) {
245
+ res.status(500).json({ success: false, error: error.message });
246
+ }
247
+ });
248
+
249
+ // 用 Claude Code 打开目录
250
+ app.post('/api/open-directory-with-claude-code', async (req, res) => {
251
+ try {
252
+ const { path: dirPath, permissionMode } = req.body || {};
253
+ if (!dirPath) {
254
+ return res.status(400).json({ success: false, error: '目录路径不能为空' });
255
+ }
256
+
257
+ try {
258
+ await fs.access(dirPath);
259
+ } catch (error) {
260
+ return res.status(400).json({ success: false, error: `目录不存在或不可访问: ${dirPath}` });
261
+ }
262
+
263
+ try {
264
+ await launchClaudeCode(dirPath, { permissionMode });
265
+ const message = permissionMode
266
+ ? `已用 Claude Code 打开目录(permission-mode=${permissionMode})`
267
+ : '已用 Claude Code 打开目录';
268
+ res.json({ success: true, message });
269
+ } catch (error) {
270
+ res.status(400).json({
271
+ success: false,
272
+ error: '未检测到 Claude Code,请先安装并确保可以在终端中直接运行 claude'
273
+ });
274
+ }
275
+ } catch (error) {
276
+ res.status(500).json({ success: false, error: error.message });
277
+ }
278
+ });
279
+ }