coding-tool-x 3.6.11 → 3.6.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/README.md +6 -8
- package/dist/web/assets/{Analytics-BuBlsbcI.js → Analytics-CnA75GxO.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-DQczxuOX.js → ConfigTemplates-D8XEscQY.js} +1 -1
- package/dist/web/assets/{Home-BvAzzWnB.js → Home-BiaKZMah.js} +1 -1
- package/dist/web/assets/{PluginManager-C8gAlaps.js → PluginManager-BzKZ8TYu.js} +1 -1
- package/dist/web/assets/{ProjectList-nqgi6_cR.js → ProjectList-BPfG_92X.js} +1 -1
- package/dist/web/assets/{SessionList-D8AxMYy6.js → SessionList-BIVtw00v.js} +1 -1
- package/dist/web/assets/{SkillManager-BDbNbNkK.js → SkillManager-fxOvFTYD.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DGGaGW2c.js → WorkspaceManager-Ch2mM8jm.js} +1 -1
- package/dist/web/assets/{index-y-eEayxR.js → index-BpBFF3b2.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/src/commands/daemon.js +3 -1
- package/src/commands/port-config.js +0 -2
- package/src/commands/switch.js +1 -5
- package/src/commands/ui.js +4 -3
- package/src/commands/workspace.js +35 -3
- package/src/config/default.js +0 -6
- package/src/config/loader.js +76 -5
- package/src/index.js +11 -0
- package/src/server/api/config.js +0 -3
- package/src/server/api/settings.js +0 -2
- package/src/server/api/workspaces.js +39 -1
- package/src/server/index.js +0 -15
- package/src/server/services/sessions.js +35 -19
- package/src/server/services/workspace-service.js +220 -98
- package/src/utils/cli-flags.js +7 -0
- package/src/utils/session.js +10 -4
package/dist/web/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" href="/favicon.ico">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>coding-tool-x · AI 编程工作台</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BpBFF3b2.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/markdown-BNJfEgCA.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-B5RqjEjQ.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/naive-ui-kcWDeJwl.js">
|
package/package.json
CHANGED
package/src/commands/daemon.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
getPortToolIssue,
|
|
12
12
|
formatPortToolIssue
|
|
13
13
|
} = require('../utils/port-helper');
|
|
14
|
+
const { hasHostFlag } = require('../utils/cli-flags');
|
|
14
15
|
|
|
15
16
|
const PM2_APP_NAME = 'cc-tool';
|
|
16
17
|
const STARTUP_LOG_FILE = 'cc-tool-out.log';
|
|
@@ -499,7 +500,7 @@ async function handleStart() {
|
|
|
499
500
|
try {
|
|
500
501
|
const config = loadConfig();
|
|
501
502
|
const port = config.ports?.webUI || 19999;
|
|
502
|
-
const enableHost =
|
|
503
|
+
const enableHost = hasHostFlag();
|
|
503
504
|
const enableHttps = process.argv.includes('--https');
|
|
504
505
|
fs.mkdirSync(PATHS.logs, { recursive: true });
|
|
505
506
|
|
|
@@ -733,6 +734,7 @@ module.exports = {
|
|
|
733
734
|
_test: {
|
|
734
735
|
detectStalePm2RuntimeIssue,
|
|
735
736
|
extractMissingPm2ForkScriptPath,
|
|
737
|
+
buildStartOptions,
|
|
736
738
|
shouldTreatPortOwnershipAsReady,
|
|
737
739
|
getManagedPorts,
|
|
738
740
|
shouldStopPM2Process
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
const chalk = require('chalk');
|
|
3
3
|
const inquirer = require('inquirer');
|
|
4
4
|
const { loadConfig, saveConfig } = require('../config/loader');
|
|
5
|
-
const { HOME_DIR } = require('../config/paths');
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* 配置端口
|
|
@@ -112,7 +111,6 @@ async function handlePortConfig() {
|
|
|
112
111
|
// 保存配置(保留其余字段)
|
|
113
112
|
saveConfig({
|
|
114
113
|
...config,
|
|
115
|
-
projectsDir: config.projectsDir.replace(HOME_DIR, '~'),
|
|
116
114
|
ports: config.ports,
|
|
117
115
|
});
|
|
118
116
|
|
package/src/commands/switch.js
CHANGED
|
@@ -3,7 +3,6 @@ const chalk = require('chalk');
|
|
|
3
3
|
const { getAvailableProjects } = require('../utils/session');
|
|
4
4
|
const { promptSelectProject } = require('../ui/prompts');
|
|
5
5
|
const { saveConfig } = require('../config/loader');
|
|
6
|
-
const { HOME_DIR } = require('../config/paths');
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* 切换项目
|
|
@@ -29,10 +28,7 @@ async function switchProject(config) {
|
|
|
29
28
|
config.defaultProject = selectedProject;
|
|
30
29
|
|
|
31
30
|
// 保存到配置文件(保留其余字段)
|
|
32
|
-
saveConfig({
|
|
33
|
-
...config,
|
|
34
|
-
projectsDir: config.projectsDir.replace(HOME_DIR, '~')
|
|
35
|
-
});
|
|
31
|
+
saveConfig({ ...config });
|
|
36
32
|
|
|
37
33
|
// 使用解析后的名称显示
|
|
38
34
|
const { parseRealProjectPath } = require('../server/services/sessions');
|
package/src/commands/ui.js
CHANGED
|
@@ -3,13 +3,14 @@ const { startServer } = require('../server');
|
|
|
3
3
|
const open = require('open');
|
|
4
4
|
const { getProxyStatus } = require('../server/proxy-server');
|
|
5
5
|
const { loadConfig } = require('../config/loader');
|
|
6
|
+
const { hasHostFlag } = require('../utils/cli-flags');
|
|
6
7
|
|
|
7
8
|
async function handleUI() {
|
|
8
9
|
// 检查是否为 daemon 模式(PM2 启动)
|
|
9
10
|
const isDaemon = process.argv.includes('--daemon');
|
|
10
11
|
|
|
11
|
-
// 检查是否启用 LAN 访问 (--host 标志)
|
|
12
|
-
const enableHost =
|
|
12
|
+
// 检查是否启用 LAN 访问 (--host/--hosts 标志)
|
|
13
|
+
const enableHost = hasHostFlag();
|
|
13
14
|
const enableHttps = process.argv.includes('--https');
|
|
14
15
|
const host = enableHost ? '0.0.0.0' : '127.0.0.1';
|
|
15
16
|
const protocol = enableHttps ? 'https' : 'http';
|
|
@@ -18,7 +19,7 @@ async function handleUI() {
|
|
|
18
19
|
console.clear();
|
|
19
20
|
console.log(chalk.cyan.bold('\n[NET] 启动 Coding-Tool Web UI...\n'));
|
|
20
21
|
if (enableHost) {
|
|
21
|
-
console.log(chalk.yellow('[WARN] LAN 访问已启用 (--host)\n'));
|
|
22
|
+
console.log(chalk.yellow('[WARN] LAN 访问已启用 (--host/--hosts)\n'));
|
|
22
23
|
}
|
|
23
24
|
if (enableHttps) {
|
|
24
25
|
console.log(chalk.yellow('[LOCK] 已启用本地 HTTPS 模式 (--https)\n'));
|
|
@@ -188,6 +188,8 @@ async function createWorkspace() {
|
|
|
188
188
|
|
|
189
189
|
let createWorktree = false;
|
|
190
190
|
let branch = '';
|
|
191
|
+
let branchMode = 'existing';
|
|
192
|
+
let baseBranch = '';
|
|
191
193
|
|
|
192
194
|
if (isGit) {
|
|
193
195
|
const { shouldCreateWorktree } = await inquirer.prompt([
|
|
@@ -200,11 +202,24 @@ async function createWorkspace() {
|
|
|
200
202
|
]);
|
|
201
203
|
|
|
202
204
|
if (shouldCreateWorktree) {
|
|
205
|
+
const { selectedBranchMode } = await inquirer.prompt([
|
|
206
|
+
{
|
|
207
|
+
type: 'list',
|
|
208
|
+
name: 'selectedBranchMode',
|
|
209
|
+
message: '选择 worktree 分支模式:',
|
|
210
|
+
choices: [
|
|
211
|
+
{ name: '使用已有分支', value: 'existing' },
|
|
212
|
+
{ name: '新建分支', value: 'new' }
|
|
213
|
+
],
|
|
214
|
+
default: 'existing'
|
|
215
|
+
}
|
|
216
|
+
]);
|
|
217
|
+
|
|
203
218
|
const { branchName } = await inquirer.prompt([
|
|
204
219
|
{
|
|
205
220
|
type: 'input',
|
|
206
221
|
name: 'branchName',
|
|
207
|
-
message: '
|
|
222
|
+
message: selectedBranchMode === 'new' ? '输入新分支名:' : '输入已有分支名:',
|
|
208
223
|
validate: input => {
|
|
209
224
|
if (!input || !input.trim()) {
|
|
210
225
|
return '分支名不能为空';
|
|
@@ -214,7 +229,20 @@ async function createWorkspace() {
|
|
|
214
229
|
}
|
|
215
230
|
]);
|
|
216
231
|
|
|
232
|
+
if (selectedBranchMode === 'new') {
|
|
233
|
+
const { baseBranchName } = await inquirer.prompt([
|
|
234
|
+
{
|
|
235
|
+
type: 'input',
|
|
236
|
+
name: 'baseBranchName',
|
|
237
|
+
message: '输入基础分支(可选,如 main):',
|
|
238
|
+
default: ''
|
|
239
|
+
}
|
|
240
|
+
]);
|
|
241
|
+
baseBranch = baseBranchName;
|
|
242
|
+
}
|
|
243
|
+
|
|
217
244
|
createWorktree = true;
|
|
245
|
+
branchMode = selectedBranchMode;
|
|
218
246
|
branch = branchName;
|
|
219
247
|
}
|
|
220
248
|
}
|
|
@@ -223,7 +251,9 @@ async function createWorkspace() {
|
|
|
223
251
|
sourcePath,
|
|
224
252
|
name: linkName,
|
|
225
253
|
createWorktree,
|
|
226
|
-
branch
|
|
254
|
+
branch,
|
|
255
|
+
branchMode,
|
|
256
|
+
baseBranch
|
|
227
257
|
});
|
|
228
258
|
|
|
229
259
|
console.log(chalk.green(`\n[v] 已添加: ${linkName}\n`));
|
|
@@ -245,7 +275,9 @@ async function createWorkspace() {
|
|
|
245
275
|
projects.forEach((proj, index) => {
|
|
246
276
|
console.log(chalk.gray(` ${index + 1}. ${proj.name} → ${proj.sourcePath}`));
|
|
247
277
|
if (proj.createWorktree) {
|
|
248
|
-
|
|
278
|
+
const modeLabel = proj.branchMode === 'new' ? '新建分支' : '已有分支';
|
|
279
|
+
const baseLabel = proj.branchMode === 'new' && proj.baseBranch ? `,基于 ${proj.baseBranch}` : '';
|
|
280
|
+
console.log(chalk.gray(` (创建 worktree: ${modeLabel} ${proj.branch}${baseLabel})`));
|
|
249
281
|
}
|
|
250
282
|
});
|
|
251
283
|
console.log('');
|
package/src/config/default.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
// 默认配置
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const { resolvePreferredHomeDir } = require('../utils/home-dir');
|
|
5
2
|
const modelMetadataConfig = require('./model-metadata.json');
|
|
6
3
|
|
|
7
|
-
const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
|
|
8
|
-
|
|
9
4
|
const DEFAULT_CONFIG = {
|
|
10
|
-
projectsDir: path.join(HOME_DIR, '.claude', 'projects'),
|
|
11
5
|
defaultProject: null,
|
|
12
6
|
maxDisplaySessions: 100,
|
|
13
7
|
pageSize: 15,
|
package/src/config/loader.js
CHANGED
|
@@ -3,7 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const DEFAULT_CONFIG = require('./default');
|
|
6
|
-
const { PATHS, ensureStorageDirMigrated } = require('./paths');
|
|
6
|
+
const { PATHS, NATIVE_PATHS, ensureStorageDirMigrated } = require('./paths');
|
|
7
7
|
const { resolvePreferredHomeDir } = require('../utils/home-dir');
|
|
8
8
|
const eventBus = require('../plugins/event-bus');
|
|
9
9
|
|
|
@@ -23,12 +23,79 @@ function getConfigFilePath() {
|
|
|
23
23
|
* 展开 ~ 为用户主目录
|
|
24
24
|
*/
|
|
25
25
|
function expandHome(filepath) {
|
|
26
|
+
if (typeof filepath !== 'string') {
|
|
27
|
+
return filepath;
|
|
28
|
+
}
|
|
26
29
|
if (filepath.startsWith('~')) {
|
|
27
30
|
return path.join(HOME_DIR, filepath.slice(1));
|
|
28
31
|
}
|
|
29
32
|
return filepath;
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
function collapseHome(filepath) {
|
|
36
|
+
if (typeof filepath !== 'string') {
|
|
37
|
+
return filepath;
|
|
38
|
+
}
|
|
39
|
+
if (filepath === HOME_DIR) {
|
|
40
|
+
return '~';
|
|
41
|
+
}
|
|
42
|
+
const prefix = `${HOME_DIR}${path.sep}`;
|
|
43
|
+
if (filepath.startsWith(prefix)) {
|
|
44
|
+
return path.join('~', filepath.slice(prefix.length));
|
|
45
|
+
}
|
|
46
|
+
return filepath;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getNativeClaudeProjectsDir() {
|
|
50
|
+
return NATIVE_PATHS?.claude?.projects || path.join(HOME_DIR, '.claude', 'projects');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function hasExplicitProjectsDir(config = {}) {
|
|
54
|
+
return Object.prototype.hasOwnProperty.call(config, 'projectsDir') &&
|
|
55
|
+
typeof config.projectsDir === 'string' &&
|
|
56
|
+
config.projectsDir.trim() !== '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveClaudeProjectsDir(config = {}) {
|
|
60
|
+
if (!hasExplicitProjectsDir(config) || isDefaultProjectsDir(config.projectsDir)) {
|
|
61
|
+
return getNativeClaudeProjectsDir();
|
|
62
|
+
}
|
|
63
|
+
return expandHome(config.projectsDir);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeComparablePath(filepath) {
|
|
67
|
+
if (typeof filepath !== 'string' || !filepath.trim()) {
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
return path.normalize(expandHome(filepath.trim()));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isDefaultProjectsDir(projectsDir) {
|
|
74
|
+
const normalized = normalizeComparablePath(projectsDir);
|
|
75
|
+
if (!normalized) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
const nativeDefault = normalizeComparablePath(getNativeClaudeProjectsDir());
|
|
79
|
+
const legacyHomeDefault = normalizeComparablePath(path.join(HOME_DIR, '.claude', 'projects'));
|
|
80
|
+
if (normalized === nativeDefault || normalized === legacyHomeDefault) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
const normalizedParts = normalized.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
84
|
+
const last = normalizedParts[normalizedParts.length - 1];
|
|
85
|
+
const previous = normalizedParts[normalizedParts.length - 2];
|
|
86
|
+
return previous === '.claude' && last === 'projects' && !fs.existsSync(normalized);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeConfigForSave(config = {}) {
|
|
90
|
+
const normalized = { ...config };
|
|
91
|
+
if (isDefaultProjectsDir(normalized.projectsDir)) {
|
|
92
|
+
delete normalized.projectsDir;
|
|
93
|
+
} else if (typeof normalized.projectsDir === 'string') {
|
|
94
|
+
normalized.projectsDir = collapseHome(normalized.projectsDir);
|
|
95
|
+
}
|
|
96
|
+
return normalized;
|
|
97
|
+
}
|
|
98
|
+
|
|
32
99
|
function mergePricing(defaultPricing, overrides = {}) {
|
|
33
100
|
const merged = {};
|
|
34
101
|
Object.keys(defaultPricing).forEach((key) => {
|
|
@@ -96,7 +163,7 @@ function loadConfig() {
|
|
|
96
163
|
if (fs.existsSync(configFilePath)) {
|
|
97
164
|
const userConfig = readJsonFile(configFilePath);
|
|
98
165
|
const config = { ...DEFAULT_CONFIG, ...userConfig };
|
|
99
|
-
config.projectsDir =
|
|
166
|
+
config.projectsDir = resolveClaudeProjectsDir(userConfig);
|
|
100
167
|
|
|
101
168
|
// 合并 ports 配置
|
|
102
169
|
config.ports = { ...DEFAULT_CONFIG.ports, ...userConfig.ports };
|
|
@@ -120,7 +187,7 @@ function loadConfig() {
|
|
|
120
187
|
}
|
|
121
188
|
const defaultConfig = {
|
|
122
189
|
...DEFAULT_CONFIG,
|
|
123
|
-
projectsDir:
|
|
190
|
+
projectsDir: resolveClaudeProjectsDir(),
|
|
124
191
|
currentProject: DEFAULT_CONFIG.defaultProject
|
|
125
192
|
};
|
|
126
193
|
eventBus.emitSync('config:loaded', { config: defaultConfig });
|
|
@@ -137,8 +204,9 @@ function saveConfig(config) {
|
|
|
137
204
|
if (!fs.existsSync(dir)) {
|
|
138
205
|
fs.mkdirSync(dir, { recursive: true });
|
|
139
206
|
}
|
|
140
|
-
|
|
141
|
-
|
|
207
|
+
const normalizedConfig = normalizeConfigForSave(config);
|
|
208
|
+
fs.writeFileSync(configFilePath, JSON.stringify(normalizedConfig, null, 2), 'utf8');
|
|
209
|
+
eventBus.emitSync('config:saved', { config: normalizedConfig });
|
|
142
210
|
} catch (error) {
|
|
143
211
|
console.error(`保存配置失败 (${configFilePath}):`, error.message);
|
|
144
212
|
}
|
|
@@ -148,5 +216,8 @@ module.exports = {
|
|
|
148
216
|
loadConfig,
|
|
149
217
|
saveConfig,
|
|
150
218
|
expandHome,
|
|
219
|
+
resolveClaudeProjectsDir,
|
|
220
|
+
normalizeConfigForSave,
|
|
221
|
+
isDefaultProjectsDir,
|
|
151
222
|
getConfigFilePath
|
|
152
223
|
};
|
package/src/index.js
CHANGED
|
@@ -16,6 +16,7 @@ const { handleUpdate } = require('./commands/update');
|
|
|
16
16
|
const { ensureStorageDirMigrated } = require('./config/paths');
|
|
17
17
|
const PluginManager = require('./plugins/plugin-manager');
|
|
18
18
|
const eventBus = require('./plugins/event-bus');
|
|
19
|
+
const { hasHostFlag } = require('./utils/cli-flags');
|
|
19
20
|
const chalk = require('chalk');
|
|
20
21
|
const path = require('path');
|
|
21
22
|
const fs = require('fs');
|
|
@@ -53,9 +54,12 @@ function showHelp() {
|
|
|
53
54
|
console.log(' ctx ui 前台启动 Web UI(仅本地访问)');
|
|
54
55
|
console.log(' ctx ui --https 前台启动 Web UI(本地 HTTPS)');
|
|
55
56
|
console.log(' ctx ui --host 前台启动 Web UI(允许 LAN 访问)');
|
|
57
|
+
console.log(' ctx ui --hosts 前台启动 Web UI(同 --host)');
|
|
58
|
+
console.log(' ctx --hosts 前台启动 Web UI(同 ctx ui --host)');
|
|
56
59
|
console.log(' ctx ui start 后台启动 Web UI');
|
|
57
60
|
console.log(' ctx ui start --https 后台启动 Web UI(本地 HTTPS)');
|
|
58
61
|
console.log(' ctx ui start --host 后台启动 Web UI(允许 LAN 访问)');
|
|
62
|
+
console.log(' ctx ui start --hosts 后台启动 Web UI(同 --host)');
|
|
59
63
|
console.log(' ctx ui stop 停止 Web UI');
|
|
60
64
|
console.log(' ctx ui restart 重启 Web UI\n');
|
|
61
65
|
|
|
@@ -156,6 +160,13 @@ async function main() {
|
|
|
156
160
|
return;
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
// LAN 快捷入口,等同于 ctx ui --host/--hosts
|
|
164
|
+
if (hasHostFlag(process.argv) && !args.some((arg) => arg && !arg.startsWith('-'))) {
|
|
165
|
+
const { handleUI } = require('./commands/ui');
|
|
166
|
+
await handleUI();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
159
170
|
// daemon 命令兼容(保持与 README 中旧命令一致)
|
|
160
171
|
if (args[0] === 'daemon') {
|
|
161
172
|
const subCommand = args[1] || 'status';
|
package/src/server/api/config.js
CHANGED
|
@@ -330,7 +330,6 @@ router.post('/default-models', (req, res) => {
|
|
|
330
330
|
// Save config
|
|
331
331
|
const newConfig = {
|
|
332
332
|
...config,
|
|
333
|
-
projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
|
|
334
333
|
defaultModels: newDefaultModels
|
|
335
334
|
};
|
|
336
335
|
|
|
@@ -378,7 +377,6 @@ router.post('/default-models/reset', (req, res) => {
|
|
|
378
377
|
// Save config
|
|
379
378
|
const newConfig = {
|
|
380
379
|
...config,
|
|
381
|
-
projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
|
|
382
380
|
defaultModels: newDefaultModels
|
|
383
381
|
};
|
|
384
382
|
|
|
@@ -489,7 +487,6 @@ router.post('/advanced', (req, res) => {
|
|
|
489
487
|
// 更新配置
|
|
490
488
|
const newConfig = {
|
|
491
489
|
...config,
|
|
492
|
-
projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
|
|
493
490
|
ports: normalizedPorts,
|
|
494
491
|
maxLogs: maxLogs !== undefined ? parseInt(maxLogs) : config.maxLogs,
|
|
495
492
|
statsInterval: statsInterval !== undefined ? parseInt(statsInterval) : config.statsInterval,
|
|
@@ -90,7 +90,6 @@ function handleSaveModelSettings(req, res) {
|
|
|
90
90
|
const config = loadConfig();
|
|
91
91
|
const newConfig = {
|
|
92
92
|
...config,
|
|
93
|
-
projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
|
|
94
93
|
modelMetadataOverrides: overrides && typeof overrides === 'object'
|
|
95
94
|
? overrides
|
|
96
95
|
: (config.modelMetadataOverrides || {})
|
|
@@ -124,7 +123,6 @@ function handleDeleteModelOverride(req, res) {
|
|
|
124
123
|
|
|
125
124
|
const newConfig = {
|
|
126
125
|
...config,
|
|
127
|
-
projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
|
|
128
126
|
modelMetadataOverrides: overrides
|
|
129
127
|
};
|
|
130
128
|
saveConfig(newConfig);
|
|
@@ -33,6 +33,24 @@ function validateBranchName(branchName) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function normalizeBranchMode(branchMode) {
|
|
37
|
+
if (typeof branchMode !== 'string' || !branchMode.trim()) {
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
return branchMode.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateBranchMode(branchMode) {
|
|
44
|
+
const normalized = normalizeBranchMode(branchMode);
|
|
45
|
+
if (!normalized) {
|
|
46
|
+
return { valid: true, normalized: '' };
|
|
47
|
+
}
|
|
48
|
+
if (normalized !== 'existing' && normalized !== 'new') {
|
|
49
|
+
return { valid: false, normalized, message: `分支模式不合法: ${normalized}` };
|
|
50
|
+
}
|
|
51
|
+
return { valid: true, normalized };
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
/**
|
|
37
55
|
* GET /api/workspaces
|
|
38
56
|
* 获取所有工作区列表
|
|
@@ -226,6 +244,17 @@ router.post('/', (req, res) => {
|
|
|
226
244
|
});
|
|
227
245
|
}
|
|
228
246
|
|
|
247
|
+
const branchModeValidation = validateBranchMode(proj.branchMode);
|
|
248
|
+
if (!branchModeValidation.valid) {
|
|
249
|
+
return res.status(400).json({
|
|
250
|
+
success: false,
|
|
251
|
+
message: branchModeValidation.message
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (branchModeValidation.normalized) {
|
|
255
|
+
proj.branchMode = branchModeValidation.normalized;
|
|
256
|
+
}
|
|
257
|
+
|
|
229
258
|
const normalizedBranch = normalizeBranchName(proj.branch);
|
|
230
259
|
if (normalizedBranch) {
|
|
231
260
|
const branchValidation = validateBranchName(normalizedBranch);
|
|
@@ -329,9 +358,10 @@ router.put('/:id/last-used', (req, res) => {
|
|
|
329
358
|
router.post('/:id/projects', (req, res) => {
|
|
330
359
|
try {
|
|
331
360
|
const { id } = req.params;
|
|
332
|
-
const { sourcePath, name, createWorktree, branch, baseBranch } = req.body;
|
|
361
|
+
const { sourcePath, name, createWorktree, branch, baseBranch, branchMode } = req.body;
|
|
333
362
|
const normalizedBranch = normalizeBranchName(branch);
|
|
334
363
|
const normalizedBaseBranch = normalizeBranchName(baseBranch);
|
|
364
|
+
const branchModeValidation = validateBranchMode(branchMode);
|
|
335
365
|
|
|
336
366
|
if (!sourcePath || !sourcePath.trim()) {
|
|
337
367
|
return res.status(400).json({
|
|
@@ -340,6 +370,13 @@ router.post('/:id/projects', (req, res) => {
|
|
|
340
370
|
});
|
|
341
371
|
}
|
|
342
372
|
|
|
373
|
+
if (!branchModeValidation.valid) {
|
|
374
|
+
return res.status(400).json({
|
|
375
|
+
success: false,
|
|
376
|
+
message: branchModeValidation.message
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
343
380
|
if (normalizedBranch) {
|
|
344
381
|
const branchValidation = validateBranchName(normalizedBranch);
|
|
345
382
|
if (!branchValidation.valid) {
|
|
@@ -364,6 +401,7 @@ router.post('/:id/projects', (req, res) => {
|
|
|
364
401
|
sourcePath,
|
|
365
402
|
name,
|
|
366
403
|
createWorktree,
|
|
404
|
+
branchMode: branchModeValidation.normalized || branchMode,
|
|
367
405
|
branch: normalizedBranch || branch,
|
|
368
406
|
baseBranch: normalizedBaseBranch || baseBranch
|
|
369
407
|
});
|
package/src/server/index.js
CHANGED
|
@@ -23,7 +23,6 @@ const { startProxyServer } = require('./proxy-server');
|
|
|
23
23
|
const { startCodexProxyServer } = require('./codex-proxy-server');
|
|
24
24
|
const { startGeminiProxyServer } = require('./gemini-proxy-server');
|
|
25
25
|
const { startOpenCodeProxyServer, collectProxyModelList } = require('./opencode-proxy-server');
|
|
26
|
-
const { createRemoteMutationGuard } = require('./services/network-access');
|
|
27
26
|
const { createApiRequestLogger } = require('./services/request-logger');
|
|
28
27
|
const { inspectWebBuildState, ensureWebDistReady } = require('./services/web-build');
|
|
29
28
|
const { ensureHttpsCredentials } = require('./services/https-cert');
|
|
@@ -149,8 +148,6 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
149
148
|
|
|
150
149
|
const app = express();
|
|
151
150
|
const useHttps = options.useHttps === true || process.argv.includes('--https');
|
|
152
|
-
const lanMode = host === '0.0.0.0';
|
|
153
|
-
const allowRemoteMutation = process.env.CC_TOOL_ALLOW_REMOTE_WRITE === 'true';
|
|
154
151
|
|
|
155
152
|
// Middleware
|
|
156
153
|
app.use(express.json({ limit: '100mb' }));
|
|
@@ -170,15 +167,6 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
170
167
|
// API 请求日志(由 CC_TOOL_LOG_API_REQUESTS=true 环境变量控制,默认关闭)
|
|
171
168
|
app.use('/api', createApiRequestLogger());
|
|
172
169
|
|
|
173
|
-
if (lanMode) {
|
|
174
|
-
app.use('/api', createRemoteMutationGuard({
|
|
175
|
-
enabled: true,
|
|
176
|
-
allowRemoteMutation,
|
|
177
|
-
message: '出于安全考虑,LAN 模式默认仅允许本机执行写操作。可设置 CC_TOOL_ALLOW_REMOTE_WRITE=true 覆盖。'
|
|
178
|
-
}));
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
170
|
// API Routes
|
|
183
171
|
app.use('/api/projects', require('./api/projects')(config));
|
|
184
172
|
app.use('/api/sessions', require('./api/sessions')(config));
|
|
@@ -316,9 +304,6 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
316
304
|
console.log(chalk.gray(' [TIP] 首次访问自签名证书时,浏览器可能会提示手动信任本地证书'));
|
|
317
305
|
}
|
|
318
306
|
|
|
319
|
-
if (host === '0.0.0.0' && !allowRemoteMutation) {
|
|
320
|
-
console.log(chalk.yellow(' [LOCK] 已启用 LAN 安全保护:远程写操作默认禁用'));
|
|
321
|
-
}
|
|
322
307
|
// 自动恢复代理状态
|
|
323
308
|
autoRestoreProxies();
|
|
324
309
|
|
|
@@ -17,6 +17,17 @@ const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
|
|
|
17
17
|
const CODEX_PROJECTS_DIR = path.join(path.dirname(NATIVE_PATHS.codex.config), 'projects');
|
|
18
18
|
const GEMINI_PROJECTS_DIR = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'projects');
|
|
19
19
|
|
|
20
|
+
function resolveProjectsDir(config = {}) {
|
|
21
|
+
return config.projectsDir || CLAUDE_PROJECTS_DIR;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function withResolvedProjectsDir(config = {}) {
|
|
25
|
+
return {
|
|
26
|
+
...config,
|
|
27
|
+
projectsDir: resolveProjectsDir(config)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
// Base directory for cc-tool data
|
|
21
32
|
function getCcToolDir() {
|
|
22
33
|
return PATHS.base;
|
|
@@ -182,7 +193,7 @@ function sliceClaudeSessionContentByUserMessage(content, afterUserMessageNumber)
|
|
|
182
193
|
|
|
183
194
|
// Get all projects with stats (async version)
|
|
184
195
|
async function getProjects(config) {
|
|
185
|
-
const projectsDir = config
|
|
196
|
+
const projectsDir = resolveProjectsDir(config);
|
|
186
197
|
|
|
187
198
|
if (!fs.existsSync(projectsDir)) {
|
|
188
199
|
return [];
|
|
@@ -383,16 +394,17 @@ function extractCwdFromSessionHeader(sessionFile) {
|
|
|
383
394
|
|
|
384
395
|
// Get projects with detailed stats (with caching) - async version
|
|
385
396
|
async function getProjectsWithStats(config, options = {}) {
|
|
397
|
+
const resolvedConfig = withResolvedProjectsDir(config);
|
|
386
398
|
if (!options.force) {
|
|
387
399
|
// Check enhanced cache first
|
|
388
|
-
const cacheKey = `${CacheKeys.PROJECTS}${
|
|
400
|
+
const cacheKey = `${CacheKeys.PROJECTS}${resolvedConfig.projectsDir}`;
|
|
389
401
|
const enhancedCached = globalCache.get(cacheKey);
|
|
390
402
|
if (enhancedCached) {
|
|
391
403
|
return enhancedCached;
|
|
392
404
|
}
|
|
393
405
|
|
|
394
406
|
// Check old cache
|
|
395
|
-
const cached = getCachedProjects(
|
|
407
|
+
const cached = getCachedProjects(resolvedConfig);
|
|
396
408
|
if (cached) {
|
|
397
409
|
globalCache.set(cacheKey, cached, 300000); // 5分钟
|
|
398
410
|
return cached;
|
|
@@ -400,22 +412,22 @@ async function getProjectsWithStats(config, options = {}) {
|
|
|
400
412
|
}
|
|
401
413
|
|
|
402
414
|
try {
|
|
403
|
-
const data = await buildProjectsWithStats(
|
|
415
|
+
const data = await buildProjectsWithStats(resolvedConfig);
|
|
404
416
|
if (!Array.isArray(data)) {
|
|
405
|
-
console.warn(`[getProjectsWithStats] Unexpected non-array result for ${
|
|
417
|
+
console.warn(`[getProjectsWithStats] Unexpected non-array result for ${resolvedConfig.projectsDir}, returning empty array.`);
|
|
406
418
|
return [];
|
|
407
419
|
}
|
|
408
|
-
setCachedProjects(
|
|
409
|
-
globalCache.set(`${CacheKeys.PROJECTS}${
|
|
420
|
+
setCachedProjects(resolvedConfig, data);
|
|
421
|
+
globalCache.set(`${CacheKeys.PROJECTS}${resolvedConfig.projectsDir}`, data, 300000);
|
|
410
422
|
return data;
|
|
411
423
|
} catch (err) {
|
|
412
|
-
console.error(`[getProjectsWithStats] Failed to build projects for ${
|
|
424
|
+
console.error(`[getProjectsWithStats] Failed to build projects for ${resolvedConfig.projectsDir}:`, err);
|
|
413
425
|
return [];
|
|
414
426
|
}
|
|
415
427
|
}
|
|
416
428
|
|
|
417
429
|
async function buildProjectsWithStats(config) {
|
|
418
|
-
const projectsDir = config
|
|
430
|
+
const projectsDir = resolveProjectsDir(config);
|
|
419
431
|
|
|
420
432
|
if (!fs.existsSync(projectsDir)) {
|
|
421
433
|
return [];
|
|
@@ -482,7 +494,7 @@ async function buildProjectsWithStats(config) {
|
|
|
482
494
|
|
|
483
495
|
// 获取 Claude 项目/会话数量(轻量统计)
|
|
484
496
|
function getProjectAndSessionCounts(config) {
|
|
485
|
-
const projectsDir = config
|
|
497
|
+
const projectsDir = resolveProjectsDir(config);
|
|
486
498
|
if (!fs.existsSync(projectsDir)) {
|
|
487
499
|
return { projectCount: 0, sessionCount: 0 };
|
|
488
500
|
}
|
|
@@ -670,7 +682,8 @@ async function getSessionsForProject(config, projectName) {
|
|
|
670
682
|
|
|
671
683
|
// Delete a session
|
|
672
684
|
function deleteSession(config, projectName, sessionId) {
|
|
673
|
-
const
|
|
685
|
+
const resolvedConfig = withResolvedProjectsDir(config);
|
|
686
|
+
const projectDir = path.join(resolvedConfig.projectsDir, projectName);
|
|
674
687
|
const sessionFile = path.join(projectDir, sessionId + '.jsonl');
|
|
675
688
|
|
|
676
689
|
if (!fs.existsSync(sessionFile)) {
|
|
@@ -678,14 +691,15 @@ function deleteSession(config, projectName, sessionId) {
|
|
|
678
691
|
}
|
|
679
692
|
|
|
680
693
|
fs.unlinkSync(sessionFile);
|
|
681
|
-
invalidateProjectsCache(
|
|
694
|
+
invalidateProjectsCache(resolvedConfig);
|
|
682
695
|
invalidateSessionResultCache(projectName);
|
|
683
696
|
return { success: true };
|
|
684
697
|
}
|
|
685
698
|
|
|
686
699
|
// Fork a session
|
|
687
700
|
function forkSession(config, projectName, sessionId, options = {}) {
|
|
688
|
-
const
|
|
701
|
+
const resolvedConfig = withResolvedProjectsDir(config);
|
|
702
|
+
const projectDir = path.join(resolvedConfig.projectsDir, projectName);
|
|
689
703
|
const sessionFile = path.join(projectDir, sessionId + '.jsonl');
|
|
690
704
|
|
|
691
705
|
if (!fs.existsSync(sessionFile)) {
|
|
@@ -714,7 +728,7 @@ function forkSession(config, projectName, sessionId, options = {}) {
|
|
|
714
728
|
const { setAlias } = require('./alias');
|
|
715
729
|
setAlias(newSessionId, options.alias);
|
|
716
730
|
}
|
|
717
|
-
invalidateProjectsCache(
|
|
731
|
+
invalidateProjectsCache(resolvedConfig);
|
|
718
732
|
invalidateSessionResultCache(projectName);
|
|
719
733
|
|
|
720
734
|
return {
|
|
@@ -766,7 +780,8 @@ function saveSessionOrder(projectName, order) {
|
|
|
766
780
|
|
|
767
781
|
// Delete a project (remove the entire project directory)
|
|
768
782
|
function deleteProject(config, projectName) {
|
|
769
|
-
const
|
|
783
|
+
const resolvedConfig = withResolvedProjectsDir(config);
|
|
784
|
+
const projectDir = path.join(resolvedConfig.projectsDir, projectName);
|
|
770
785
|
|
|
771
786
|
if (!fs.existsSync(projectDir)) {
|
|
772
787
|
throw new Error('Project not found');
|
|
@@ -776,19 +791,20 @@ function deleteProject(config, projectName) {
|
|
|
776
791
|
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
777
792
|
|
|
778
793
|
// Remove from order file if exists
|
|
779
|
-
const order = getProjectOrder(
|
|
794
|
+
const order = getProjectOrder(resolvedConfig);
|
|
780
795
|
const newOrder = order.filter(name => name !== projectName);
|
|
781
796
|
if (newOrder.length !== order.length) {
|
|
782
|
-
saveProjectOrder(
|
|
797
|
+
saveProjectOrder(resolvedConfig, newOrder);
|
|
783
798
|
}
|
|
784
799
|
|
|
785
|
-
invalidateProjectsCache(
|
|
800
|
+
invalidateProjectsCache(resolvedConfig);
|
|
786
801
|
return { success: true };
|
|
787
802
|
}
|
|
788
803
|
|
|
789
804
|
// Search sessions for keyword
|
|
790
805
|
function searchSessions(config, projectName, keyword, contextLength = 15) {
|
|
791
|
-
const
|
|
806
|
+
const resolvedConfig = withResolvedProjectsDir(config);
|
|
807
|
+
const projectDir = path.join(resolvedConfig.projectsDir, projectName);
|
|
792
808
|
|
|
793
809
|
if (!fs.existsSync(projectDir)) {
|
|
794
810
|
return [];
|