coding-tool-x 3.7.5 → 3.7.7
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/dist/web/assets/{Analytics-DnnufrQi.js → Analytics-B6FzPvdJ.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BTVhMSfe.js → ConfigTemplates-DXVnqR5c.js} +1 -1
- package/dist/web/assets/{Home-D03PEREx.js → Home-FHJB7hYI.js} +1 -1
- package/dist/web/assets/{PluginManager-BiMqEiyQ.js → PluginManager-D8RToURX.js} +1 -1
- package/dist/web/assets/ProjectList-Cdz4jnOg.js +1 -0
- package/dist/web/assets/ProjectList-VeMNXfgO.css +1 -0
- package/dist/web/assets/SessionList-C4chCPxK.css +1 -0
- package/dist/web/assets/SessionList-XYydFlQg.js +1 -0
- package/dist/web/assets/{SkillManager-DAy8k5CL.js → SkillManager-Bu9p7mMb.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Bs2D6R9j.js → WorkspaceManager-Bx0Gj3Mk.js} +1 -1
- package/dist/web/assets/index-DxkAvn4g.js +2 -0
- package/dist/web/assets/index-S8wrU2Nf.css +1 -0
- package/dist/web/index.html +2 -2
- 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/loader.js +76 -5
- package/src/index.js +11 -0
- package/src/server/api/config.js +0 -3
- package/src/server/api/omp-channels.js +50 -7
- package/src/server/api/settings.js +0 -2
- package/src/server/api/workspaces.js +39 -1
- package/src/server/services/dashboard-snapshot-worker.js +2 -1
- package/src/server/services/https-cert.js +3 -2
- package/src/server/services/native-oauth-adapters.js +2 -1
- package/src/server/services/omp-auth-provider-worker.js +29 -0
- package/src/server/services/omp-auth-providers.js +138 -11
- package/src/server/services/omp-config.js +7 -4
- package/src/server/services/omp-sessions.js +18 -14
- package/src/server/services/plugins-service.js +2 -1
- package/src/server/services/project-snapshots.js +8 -1
- package/src/server/services/session-snapshots.js +10 -1
- package/src/server/services/sessions.js +35 -19
- package/src/server/services/snapshot-cache.js +47 -3
- package/src/server/services/web-build.js +2 -1
- package/src/server/services/workspace-service.js +204 -98
- package/src/utils/cli-flags.js +7 -0
- package/src/utils/session.js +10 -4
- package/dist/web/assets/ProjectList-C6BA3V49.js +0 -1
- package/dist/web/assets/ProjectList-DQ5ZCJO2.css +0 -1
- package/dist/web/assets/SessionList--9Bl6EhV.css +0 -1
- package/dist/web/assets/SessionList-DChG04jz.js +0 -1
- package/dist/web/assets/index-DnDBHGCC.js +0 -2
- package/dist/web/assets/index-U3TOywtB.css +0 -1
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
|
@@ -17,6 +17,7 @@ const { ensureStorageDirMigrated } = require('./config/paths');
|
|
|
17
17
|
const { normalizePlatformKey } = require('./shared/platforms');
|
|
18
18
|
const PluginManager = require('./plugins/plugin-manager');
|
|
19
19
|
const eventBus = require('./plugins/event-bus');
|
|
20
|
+
const { hasHostFlag } = require('./utils/cli-flags');
|
|
20
21
|
const chalk = require('chalk');
|
|
21
22
|
const path = require('path');
|
|
22
23
|
const fs = require('fs');
|
|
@@ -54,9 +55,12 @@ function showHelp() {
|
|
|
54
55
|
console.log(' ctx ui 前台启动 Web UI(仅本地访问)');
|
|
55
56
|
console.log(' ctx ui --https 前台启动 Web UI(本地 HTTPS)');
|
|
56
57
|
console.log(' ctx ui --host 前台启动 Web UI(允许 LAN 访问,默认允许远程写操作)');
|
|
58
|
+
console.log(' ctx ui --hosts 前台启动 Web UI(同 --host)');
|
|
59
|
+
console.log(' ctx --hosts 前台启动 Web UI(同 ctx ui --host)');
|
|
57
60
|
console.log(' ctx ui start 后台启动 Web UI');
|
|
58
61
|
console.log(' ctx ui start --https 后台启动 Web UI(本地 HTTPS)');
|
|
59
62
|
console.log(' ctx ui start --host 后台启动 Web UI(允许 LAN 访问,默认允许远程写操作)');
|
|
63
|
+
console.log(' ctx ui start --hosts 后台启动 Web UI(同 --host)');
|
|
60
64
|
console.log(' ctx ui stop 停止 Web UI');
|
|
61
65
|
console.log(' ctx ui restart 重启 Web UI\n');
|
|
62
66
|
console.log(chalk.gray(' 提示: 如需禁止 LAN 远程写操作,可设置 CC_TOOL_ALLOW_REMOTE_WRITE=false\n'));
|
|
@@ -149,6 +153,13 @@ async function main() {
|
|
|
149
153
|
// 处理命令行参数
|
|
150
154
|
const args = process.argv.slice(2);
|
|
151
155
|
|
|
156
|
+
// LAN 快捷入口,等同于 ctx ui --host/--hosts
|
|
157
|
+
if (hasHostFlag(process.argv) && !args.some((arg) => arg && !arg.startsWith('-'))) {
|
|
158
|
+
const { handleUI } = require('./commands/ui');
|
|
159
|
+
await handleUI();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
152
163
|
// --version 或 -v - 显示版本号
|
|
153
164
|
if (args[0] === '--version' || args[0] === '-v') {
|
|
154
165
|
console.log(getVersion());
|
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
|
|
|
@@ -490,7 +488,6 @@ router.post('/advanced', (req, res) => {
|
|
|
490
488
|
// 更新配置
|
|
491
489
|
const newConfig = {
|
|
492
490
|
...config,
|
|
493
|
-
projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
|
|
494
491
|
ports: normalizedPorts,
|
|
495
492
|
maxLogs: maxLogs !== undefined ? parseInt(maxLogs) : config.maxLogs,
|
|
496
493
|
statsInterval: statsInterval !== undefined ? parseInt(statsInterval) : config.statsInterval,
|
|
@@ -23,9 +23,13 @@ const {
|
|
|
23
23
|
} = require('../services/model-detector');
|
|
24
24
|
const {
|
|
25
25
|
findAuthProviderForKey,
|
|
26
|
-
|
|
26
|
+
getCachedOmpAuthProviderSnapshot,
|
|
27
|
+
getOmpAuthProviderCacheMeta,
|
|
28
|
+
warmOmpAuthProviderSnapshot
|
|
27
29
|
} = require('../services/omp-auth-providers');
|
|
28
30
|
|
|
31
|
+
const CHANNEL_LIST_AUTH_OPTIONS = { accountCheck: false, includeStatus: false };
|
|
32
|
+
|
|
29
33
|
function uniqueModels(models = []) {
|
|
30
34
|
const seen = new Set();
|
|
31
35
|
const result = [];
|
|
@@ -136,19 +140,53 @@ function attachAuthProvider(channel, snapshot) {
|
|
|
136
140
|
};
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
function getAuthSnapshotForChannelList() {
|
|
144
|
+
const snapshot = getCachedOmpAuthProviderSnapshot(CHANNEL_LIST_AUTH_OPTIONS);
|
|
145
|
+
const shouldWarm = !snapshot || snapshot.stale;
|
|
146
|
+
if (shouldWarm) {
|
|
147
|
+
warmOmpAuthProviderSnapshot(CHANNEL_LIST_AUTH_OPTIONS);
|
|
148
|
+
}
|
|
149
|
+
const meta = getOmpAuthProviderCacheMeta(CHANNEL_LIST_AUTH_OPTIONS);
|
|
150
|
+
return {
|
|
151
|
+
snapshot,
|
|
152
|
+
meta: {
|
|
153
|
+
...meta,
|
|
154
|
+
stale: !snapshot || Boolean(snapshot.stale),
|
|
155
|
+
refreshing: shouldWarm || meta.refreshing,
|
|
156
|
+
fallback: !snapshot
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildUnavailableAuthProviderMeta(error) {
|
|
162
|
+
return {
|
|
163
|
+
cached: false,
|
|
164
|
+
stale: false,
|
|
165
|
+
refreshing: false,
|
|
166
|
+
fallback: true,
|
|
167
|
+
checkedAt: new Date().toISOString(),
|
|
168
|
+
error: error || 'omp-not-available'
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
139
172
|
module.exports = () => {
|
|
140
173
|
router.get('/', (req, res) => {
|
|
141
174
|
try {
|
|
142
175
|
if (!isOmpInstalled()) {
|
|
143
|
-
return res.json({
|
|
176
|
+
return res.json({
|
|
177
|
+
channels: [],
|
|
178
|
+
installed: false,
|
|
179
|
+
error: 'OMP CLI not installed',
|
|
180
|
+
authProviderMeta: buildUnavailableAuthProviderMeta('OMP CLI not installed')
|
|
181
|
+
});
|
|
144
182
|
}
|
|
145
183
|
const data = getChannels();
|
|
146
|
-
const
|
|
184
|
+
const { snapshot: authSnapshot, meta: authProviderMeta } = getAuthSnapshotForChannelList();
|
|
147
185
|
const channels = (data.channels || []).map(ch => ({
|
|
148
186
|
...attachAuthProvider(ch, authSnapshot),
|
|
149
187
|
health: getChannelHealthStatus(ch.id, 'omp')
|
|
150
188
|
}));
|
|
151
|
-
res.json({ channels, installed: true });
|
|
189
|
+
res.json({ channels, installed: true, authProviderMeta });
|
|
152
190
|
} catch (err) {
|
|
153
191
|
console.error('[OMP Channels API] Failed to get channels:', err);
|
|
154
192
|
res.status(500).json({ error: err.message });
|
|
@@ -158,14 +196,19 @@ module.exports = () => {
|
|
|
158
196
|
router.get('/enabled', (req, res) => {
|
|
159
197
|
try {
|
|
160
198
|
if (!isOmpInstalled()) {
|
|
161
|
-
return res.json({
|
|
199
|
+
return res.json({
|
|
200
|
+
channels: [],
|
|
201
|
+
installed: false,
|
|
202
|
+
error: 'OMP CLI not installed',
|
|
203
|
+
authProviderMeta: buildUnavailableAuthProviderMeta('OMP CLI not installed')
|
|
204
|
+
});
|
|
162
205
|
}
|
|
163
206
|
const data = getChannels();
|
|
164
|
-
const
|
|
207
|
+
const { snapshot: authSnapshot, meta: authProviderMeta } = getAuthSnapshotForChannelList();
|
|
165
208
|
const channels = (data.channels || [])
|
|
166
209
|
.filter(ch => ch.enabled !== false)
|
|
167
210
|
.map(ch => ({ ...attachAuthProvider(ch, authSnapshot), health: getChannelHealthStatus(ch.id, 'omp') }));
|
|
168
|
-
res.json({ channels, installed: true });
|
|
211
|
+
res.json({ channels, installed: true, authProviderMeta });
|
|
169
212
|
} catch (err) {
|
|
170
213
|
console.error('[OMP Channels API] Failed to get enabled channels:', err);
|
|
171
214
|
res.status(500).json({ error: err.message });
|
|
@@ -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
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { fork } = require('child_process');
|
|
4
4
|
|
|
5
|
-
const DASHBOARD_SNAPSHOT_WORKER_TIMEOUT_MS =
|
|
5
|
+
const DASHBOARD_SNAPSHOT_WORKER_TIMEOUT_MS = 180 * 1000;
|
|
6
6
|
|
|
7
7
|
function sortClaudeProjects(projects, order = []) {
|
|
8
8
|
if (!Array.isArray(order) || order.length === 0) {
|
|
@@ -130,6 +130,7 @@ function runDashboardSnapshotWorker(kind, source, config = {}, options = {}) {
|
|
|
130
130
|
return new Promise((resolve, reject) => {
|
|
131
131
|
const child = fork(__filename, [], {
|
|
132
132
|
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
|
133
|
+
windowsHide: true,
|
|
133
134
|
env: {
|
|
134
135
|
...process.env,
|
|
135
136
|
CC_TOOL_DASHBOARD_SNAPSHOT_WORKER: '1'
|
|
@@ -42,7 +42,7 @@ function resolveOpenSslCommand() {
|
|
|
42
42
|
|
|
43
43
|
for (const command of commands) {
|
|
44
44
|
try {
|
|
45
|
-
execFileSync(command, ['version'], { stdio: 'ignore' });
|
|
45
|
+
execFileSync(command, ['version'], { stdio: 'ignore', windowsHide: true });
|
|
46
46
|
return command;
|
|
47
47
|
} catch {
|
|
48
48
|
// Try next candidate.
|
|
@@ -96,7 +96,8 @@ function generatePemCertificateWithOpenSsl(command) {
|
|
|
96
96
|
'-config',
|
|
97
97
|
configPath
|
|
98
98
|
], {
|
|
99
|
-
stdio: 'ignore'
|
|
99
|
+
stdio: 'ignore',
|
|
100
|
+
windowsHide: true
|
|
100
101
|
});
|
|
101
102
|
|
|
102
103
|
writeMeta({
|
|
@@ -1029,7 +1029,8 @@ function runOmpCommand(args = [], options = {}) {
|
|
|
1029
1029
|
...process.env,
|
|
1030
1030
|
...(options.env || {})
|
|
1031
1031
|
},
|
|
1032
|
-
timeout: options.timeout || 10000
|
|
1032
|
+
timeout: options.timeout || 10000,
|
|
1033
|
+
windowsHide: true
|
|
1033
1034
|
});
|
|
1034
1035
|
const status = result?.status === undefined || result?.status === null ? 0 : result.status;
|
|
1035
1036
|
return {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getOmpAuthProviderSnapshot } = require('./omp-auth-providers');
|
|
4
|
+
|
|
5
|
+
function attachWorkerHandler() {
|
|
6
|
+
process.on('message', (options = {}) => {
|
|
7
|
+
try {
|
|
8
|
+
const value = getOmpAuthProviderSnapshot(options);
|
|
9
|
+
if (process.send) {
|
|
10
|
+
process.send({ ok: true, value }, () => process.exit(0));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (process.send) {
|
|
15
|
+
process.send({ ok: false, error: error?.message || String(error) }, () => process.exit(1));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (process.env.CC_TOOL_OMP_AUTH_PROVIDER_WORKER === '1' || require.main === module) {
|
|
24
|
+
attachWorkerHandler();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
attachWorkerHandler
|
|
29
|
+
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
const
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { fork, spawnSync } = require('child_process');
|
|
2
3
|
const ompConfig = require('./omp-config');
|
|
3
4
|
|
|
4
5
|
const COMMAND_TIMEOUT_MS = 5000;
|
|
5
6
|
const TOKEN_LIST_TIMEOUT_MS = 3000;
|
|
6
7
|
const CACHE_TTL_MS = 15000;
|
|
8
|
+
const WARMUP_WORKER_TIMEOUT_MS = COMMAND_TIMEOUT_MS + 1000;
|
|
7
9
|
|
|
8
10
|
const LOGIN_PROVIDER_IDS = new Set([
|
|
9
11
|
'openai-codex',
|
|
@@ -50,6 +52,20 @@ const PROVIDER_ALIASES = {
|
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
let snapshotCache = {};
|
|
55
|
+
const warmupTimers = new Map();
|
|
56
|
+
const warmupErrors = new Map();
|
|
57
|
+
|
|
58
|
+
function getSnapshotCacheKey(options = {}) {
|
|
59
|
+
return options.accountCheck === false
|
|
60
|
+
? (options.includeStatus === false ? 'metadata' : 'metadata-status')
|
|
61
|
+
: 'full';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeSnapshotCache(cacheKey, loadedAt, value) {
|
|
65
|
+
snapshotCache[cacheKey] = { loadedAt, value };
|
|
66
|
+
warmupErrors.delete(cacheKey);
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
53
69
|
|
|
54
70
|
function normalizeProviderId(value = '') {
|
|
55
71
|
return String(value || '')
|
|
@@ -95,7 +111,8 @@ function runCommand(command, args = [], options = {}) {
|
|
|
95
111
|
...process.env,
|
|
96
112
|
...(options.env || {})
|
|
97
113
|
},
|
|
98
|
-
timeout: options.timeout || COMMAND_TIMEOUT_MS
|
|
114
|
+
timeout: options.timeout || COMMAND_TIMEOUT_MS,
|
|
115
|
+
windowsHide: true
|
|
99
116
|
});
|
|
100
117
|
if (typeof result === 'string' || Buffer.isBuffer(result)) {
|
|
101
118
|
return { status: 0, stdout: normalizeCommandOutput(result), stderr: '' };
|
|
@@ -205,9 +222,7 @@ function buildEmptySnapshot(runtime, reason) {
|
|
|
205
222
|
|
|
206
223
|
function getOmpAuthProviderSnapshot(options = {}) {
|
|
207
224
|
const now = Date.now();
|
|
208
|
-
const cacheKey = options
|
|
209
|
-
? (options.includeStatus === false ? 'metadata' : 'metadata-status')
|
|
210
|
-
: 'full';
|
|
225
|
+
const cacheKey = getSnapshotCacheKey(options);
|
|
211
226
|
const cached = snapshotCache[cacheKey];
|
|
212
227
|
if (!options.forceRefresh && cached && now - cached.loadedAt < CACHE_TTL_MS) {
|
|
213
228
|
return cached.value;
|
|
@@ -217,16 +232,14 @@ function getOmpAuthProviderSnapshot(options = {}) {
|
|
|
217
232
|
const runtime = options.runtime || ompConfig.resolveOmpRuntime(env, options.runtimeOptions || {});
|
|
218
233
|
if (!runtime || runtime.runtime !== 'omp' || !runtime.installed) {
|
|
219
234
|
const value = buildEmptySnapshot(runtime || null, 'omp-not-available');
|
|
220
|
-
|
|
221
|
-
return value;
|
|
235
|
+
return writeSnapshotCache(cacheKey, now, value);
|
|
222
236
|
}
|
|
223
237
|
|
|
224
238
|
const command = runtime.command;
|
|
225
239
|
const listResult = parseJsonCommand(command, ['auth-broker', 'list', '--json'], options);
|
|
226
240
|
if (!listResult.ok || !Array.isArray(listResult.value)) {
|
|
227
241
|
const value = buildEmptySnapshot(runtime, listResult.error || listResult.stderr || 'auth-provider-list-failed');
|
|
228
|
-
|
|
229
|
-
return value;
|
|
242
|
+
return writeSnapshotCache(cacheKey, now, value);
|
|
230
243
|
}
|
|
231
244
|
|
|
232
245
|
const supportedProviders = listResult.value
|
|
@@ -268,8 +281,113 @@ function getOmpAuthProviderSnapshot(options = {}) {
|
|
|
268
281
|
aliases: { ...PROVIDER_ALIASES },
|
|
269
282
|
checkedAt: new Date().toISOString()
|
|
270
283
|
};
|
|
271
|
-
|
|
272
|
-
|
|
284
|
+
return writeSnapshotCache(cacheKey, now, value);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function getCachedOmpAuthProviderSnapshot(options = {}) {
|
|
288
|
+
const cacheKey = getSnapshotCacheKey(options);
|
|
289
|
+
const cached = snapshotCache[cacheKey];
|
|
290
|
+
if (!cached) return null;
|
|
291
|
+
return {
|
|
292
|
+
...cached.value,
|
|
293
|
+
stale: Date.now() - cached.loadedAt >= CACHE_TTL_MS
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getOmpAuthProviderCacheMeta(options = {}) {
|
|
298
|
+
const cacheKey = getSnapshotCacheKey(options);
|
|
299
|
+
const cached = snapshotCache[cacheKey];
|
|
300
|
+
const stale = !cached || Date.now() - cached.loadedAt >= CACHE_TTL_MS;
|
|
301
|
+
const cachedReason = cached?.value?.available === false ? cached.value.reason || null : null;
|
|
302
|
+
return {
|
|
303
|
+
cached: Boolean(cached),
|
|
304
|
+
stale,
|
|
305
|
+
refreshing: warmupTimers.has(cacheKey),
|
|
306
|
+
fallback: !cached,
|
|
307
|
+
checkedAt: cached?.value?.checkedAt || null,
|
|
308
|
+
error: warmupErrors.get(cacheKey) || cachedReason || null
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function warmOmpAuthProviderSnapshot(options = {}) {
|
|
313
|
+
const cacheKey = getSnapshotCacheKey(options);
|
|
314
|
+
if (warmupTimers.has(cacheKey)) return;
|
|
315
|
+
|
|
316
|
+
const finishWarmup = (task, error = null, value = null) => {
|
|
317
|
+
if (!warmupTimers.has(cacheKey)) return;
|
|
318
|
+
if (task.timeout) clearTimeout(task.timeout);
|
|
319
|
+
if (task.child) {
|
|
320
|
+
task.child.removeAllListeners();
|
|
321
|
+
if (!task.child.killed) task.child.kill();
|
|
322
|
+
}
|
|
323
|
+
warmupTimers.delete(cacheKey);
|
|
324
|
+
if (error) {
|
|
325
|
+
warmupErrors.set(cacheKey, sanitizeIdentity(error?.message || String(error || 'auth-provider-warmup-failed')));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (value) {
|
|
329
|
+
writeSnapshotCache(cacheKey, Date.now(), value);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Tests and callers that supply a function-based command runner cannot cross
|
|
334
|
+
// the process boundary. Production channel-list warmups use the worker path.
|
|
335
|
+
if (process.env.NODE_ENV === 'test' || options.commandRunner) {
|
|
336
|
+
const task = { timeout: null, child: null };
|
|
337
|
+
const timer = setTimeout(() => {
|
|
338
|
+
try {
|
|
339
|
+
finishWarmup(task, null, getOmpAuthProviderSnapshot(options));
|
|
340
|
+
} catch (error) {
|
|
341
|
+
finishWarmup(task, error);
|
|
342
|
+
}
|
|
343
|
+
}, 0);
|
|
344
|
+
task.timeout = timer;
|
|
345
|
+
warmupTimers.set(cacheKey, task);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const workerPath = path.join(__dirname, 'omp-auth-provider-worker.js');
|
|
350
|
+
const task = { timeout: null, child: null };
|
|
351
|
+
try {
|
|
352
|
+
const child = fork(workerPath, [], {
|
|
353
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
354
|
+
windowsHide: true,
|
|
355
|
+
env: {
|
|
356
|
+
...process.env,
|
|
357
|
+
CC_TOOL_OMP_AUTH_PROVIDER_WORKER: '1'
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
task.child = child;
|
|
361
|
+
task.timeout = setTimeout(() => {
|
|
362
|
+
finishWarmup(task, new Error('auth-provider-warmup-timed-out'));
|
|
363
|
+
}, WARMUP_WORKER_TIMEOUT_MS);
|
|
364
|
+
warmupTimers.set(cacheKey, task);
|
|
365
|
+
|
|
366
|
+
child.on('message', (message) => {
|
|
367
|
+
if (message?.ok) {
|
|
368
|
+
finishWarmup(task, null, message.value);
|
|
369
|
+
} else {
|
|
370
|
+
finishWarmup(task, new Error(message?.error || 'auth-provider-warmup-failed'));
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
child.on('error', (error) => finishWarmup(task, error));
|
|
374
|
+
child.on('exit', (code, signal) => {
|
|
375
|
+
if (warmupTimers.get(cacheKey) === task) {
|
|
376
|
+
finishWarmup(task, new Error(`auth-provider-warmup-worker-exited-${code || signal || 'unknown'}`));
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
child.send({
|
|
380
|
+
accountCheck: options.accountCheck,
|
|
381
|
+
includeStatus: options.includeStatus,
|
|
382
|
+
forceRefresh: options.forceRefresh === true,
|
|
383
|
+
runtime: options.runtime,
|
|
384
|
+
runtimeOptions: options.runtimeOptions
|
|
385
|
+
});
|
|
386
|
+
} catch (error) {
|
|
387
|
+
// `fork` itself can fail before the task is registered. Preserve the
|
|
388
|
+
// failure for the non-blocking status endpoint instead of dropping it.
|
|
389
|
+
warmupErrors.set(cacheKey, sanitizeIdentity(error?.message || String(error || 'auth-provider-warmup-failed')));
|
|
390
|
+
}
|
|
273
391
|
}
|
|
274
392
|
|
|
275
393
|
function findAuthProviderForKey(providerKey, snapshot) {
|
|
@@ -280,6 +398,12 @@ function findAuthProviderForKey(providerKey, snapshot) {
|
|
|
280
398
|
|
|
281
399
|
function clearOmpAuthProviderCache() {
|
|
282
400
|
snapshotCache = {};
|
|
401
|
+
for (const task of warmupTimers.values()) {
|
|
402
|
+
if (task.timeout) clearTimeout(task.timeout);
|
|
403
|
+
if (task.child && !task.child.killed) task.child.kill();
|
|
404
|
+
}
|
|
405
|
+
warmupTimers.clear();
|
|
406
|
+
warmupErrors.clear();
|
|
283
407
|
}
|
|
284
408
|
|
|
285
409
|
module.exports = {
|
|
@@ -287,7 +411,10 @@ module.exports = {
|
|
|
287
411
|
ACCOUNT_CHECK_PROVIDER_IDS,
|
|
288
412
|
clearOmpAuthProviderCache,
|
|
289
413
|
findAuthProviderForKey,
|
|
414
|
+
getCachedOmpAuthProviderSnapshot,
|
|
415
|
+
getOmpAuthProviderCacheMeta,
|
|
290
416
|
getOmpAuthProviderSnapshot,
|
|
417
|
+
warmOmpAuthProviderSnapshot,
|
|
291
418
|
normalizeProviderId,
|
|
292
419
|
resolveProviderAlias,
|
|
293
420
|
sanitizeIdentity
|