coding-tool-x 3.5.8 → 3.5.10
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/CHANGELOG.md +11 -0
- package/README.md +4 -25
- package/bin/ctx.js +1 -6
- package/dist/web/assets/{Analytics-BzoNzfbi.js → Analytics-DPb1zErn.js} +2 -2
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-O4ikBt1o.js → ConfigTemplates-Bki2k4Lo.js} +1 -1
- package/dist/web/assets/{Home-BQjsnblU.js → Home-BKYHKSax.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/{PluginManager-DS_DJnVc.js → PluginManager-jgeQOE24.js} +1 -1
- package/dist/web/assets/ProjectList-CF67RzP_.js +1 -0
- package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
- package/dist/web/assets/SessionList-DhHsY5q9.js +1 -0
- package/dist/web/assets/SessionList-lz23U6ZJ.css +1 -0
- package/dist/web/assets/{SkillManager-DpNE02r0.js → SkillManager-CtEz5gGp.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DMY7_SHh.js → WorkspaceManager-D3B0Y5XE.js} +1 -1
- package/dist/web/assets/icons-lKCTySl7.js +1 -0
- package/dist/web/assets/index-BS7aplYp.css +1 -0
- package/dist/web/assets/index-BnyNm74G.js +2 -0
- package/dist/web/assets/{markdown-DyTJGI4N.js → markdown-BNJfEgCA.js} +1 -1
- package/dist/web/assets/{naive-ui-Cg4_ZeoT.js → naive-ui-DM897mIP.js} +1 -1
- package/dist/web/assets/vendors-DQA4Hw9v.js +7 -0
- package/dist/web/assets/vue-vendor-B5RqjEjQ.js +45 -0
- package/dist/web/index.html +8 -8
- package/package.json +1 -2
- package/src/commands/daemon.js +5 -33
- package/src/commands/export-config.js +6 -6
- package/src/commands/stats.js +4 -41
- package/src/commands/ui.js +3 -12
- package/src/config/channel-preset-websites.js +114 -0
- package/src/config/default.js +9 -5
- package/src/config/loader.js +2 -2
- package/src/config/model-metadata.json +16 -20
- package/src/config/paths.js +33 -166
- package/src/index.js +34 -125
- package/src/server/api/agents.js +2 -52
- package/src/server/api/codex-proxy.js +4 -2
- package/src/server/api/codex-sessions.js +88 -9
- package/src/server/api/commands.js +2 -38
- package/src/server/api/dashboard.js +1 -25
- package/src/server/api/gemini-proxy.js +4 -2
- package/src/server/api/gemini-sessions.js +86 -9
- package/src/server/api/hooks.js +1 -17
- package/src/server/api/opencode-proxy.js +7 -2
- package/src/server/api/opencode-sessions.js +82 -7
- package/src/server/api/plugins.js +34 -128
- package/src/server/api/proxy.js +5 -3
- package/src/server/api/sessions.js +180 -24
- package/src/server/api/workspaces.js +0 -14
- package/src/server/index.js +9 -31
- package/src/server/services/agents-service.js +62 -269
- package/src/server/services/base/base-channel-service.js +3 -1
- package/src/server/services/channels.js +5 -0
- package/src/server/services/codex-channels.js +2 -0
- package/src/server/services/codex-sessions.js +78 -108
- package/src/server/services/commands-service.js +81 -281
- package/src/server/services/config-export-service.js +7 -7
- package/src/server/services/config-registry-service.js +9 -8
- package/src/server/services/config-sync-manager.js +41 -61
- package/src/server/services/config-sync-service.js +3 -3
- package/src/server/services/gemini-channels.js +17 -6
- package/src/server/services/gemini-config.js +4 -3
- package/src/server/services/gemini-sessions.js +51 -26
- package/src/server/services/gemini-settings-manager.js +3 -2
- package/src/server/services/mcp-service.js +14 -9
- package/src/server/services/native-oauth-adapters.js +3 -3
- package/src/server/services/network-access.js +2 -61
- package/src/server/services/notification-hooks.js +19 -184
- package/src/server/services/opencode-channels.js +9 -0
- package/src/server/services/opencode-sessions.js +4 -4
- package/src/server/services/opencode-settings-manager.js +3 -3
- package/src/server/services/plugins-service.js +62 -996
- package/src/server/services/prompts-service.js +9 -5
- package/src/server/services/sessions.js +128 -99
- package/src/server/services/skill-service.js +18 -155
- package/src/server/websocket-server.js +4 -35
- package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
- package/dist/web/assets/Home-qzk118Of.css +0 -1
- package/dist/web/assets/ProjectList-CqYDtsHx.js +0 -1
- package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
- package/dist/web/assets/SessionList-CfPtcq6Y.css +0 -1
- package/dist/web/assets/SessionList-DMlLtMCz.js +0 -1
- package/dist/web/assets/icons-CEq2hYB-.js +0 -1
- package/dist/web/assets/index-Clf0l3wc.js +0 -2
- package/dist/web/assets/index-Dih_bOsv.css +0 -1
- package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
- package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
- package/docs/Caddyfile.example +0 -19
- package/docs/reverse-proxy-https.md +0 -57
- package/src/server/services/https-cert.js +0 -171
- package/src/server/services/session-launch-command.js +0 -81
- package/src/server/services/web-ui-runtime.js +0 -54
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { RepoScannerBase } = require('./repo-scanner-base');
|
|
11
|
-
const { NATIVE_PATHS
|
|
11
|
+
const { NATIVE_PATHS } = require('../../config/paths');
|
|
12
12
|
const {
|
|
13
13
|
parseCommandContent,
|
|
14
14
|
parseFrontmatter
|
|
@@ -17,21 +17,18 @@ const {
|
|
|
17
17
|
// 默认仓库源
|
|
18
18
|
const DEFAULT_REPOS = [];
|
|
19
19
|
const SUPPORTED_PLATFORMS = ['claude', 'opencode'];
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const OPENCODE_LEGACY_COMMANDS_DIR = NATIVE_PATHS.opencode.commandsLegacy;
|
|
20
|
+
const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
21
|
+
const CLAUDE_COMMANDS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'commands');
|
|
23
22
|
|
|
24
23
|
const PLATFORM_CONFIG = {
|
|
25
24
|
claude: {
|
|
26
25
|
userCommandsDir: CLAUDE_COMMANDS_DIR,
|
|
27
|
-
storageDir: PATHS.localCommands.claude,
|
|
28
26
|
projectCommandsDir: (projectPath) => path.join(projectPath, '.claude', 'commands'),
|
|
29
27
|
repoType: 'commands'
|
|
30
28
|
},
|
|
31
29
|
opencode: {
|
|
32
|
-
userCommandsDir:
|
|
33
|
-
|
|
34
|
-
legacyUserCommandsDir: OPENCODE_LEGACY_COMMANDS_DIR,
|
|
30
|
+
userCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'commands'),
|
|
31
|
+
legacyUserCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'command'),
|
|
35
32
|
projectCommandsDir: (projectPath) => {
|
|
36
33
|
const modern = path.join(projectPath, '.opencode', 'commands');
|
|
37
34
|
const legacy = path.join(projectPath, '.opencode', 'command');
|
|
@@ -57,25 +54,6 @@ function ensureDir(dirPath) {
|
|
|
57
54
|
}
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
function normalizeCommandRelativePath(relativePath, errorLabel = '命令路径') {
|
|
61
|
-
const raw = String(relativePath || '').replace(/\\/g, '/').trim();
|
|
62
|
-
if (!raw || raw.includes('\0')) {
|
|
63
|
-
throw new Error(`${errorLabel}不合法`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const normalized = path.posix.normalize(raw).replace(/^(\.\/)+/, '');
|
|
67
|
-
if (!normalized ||
|
|
68
|
-
normalized === '.' ||
|
|
69
|
-
normalized === '..' ||
|
|
70
|
-
normalized.startsWith('../') ||
|
|
71
|
-
normalized.includes('/../') ||
|
|
72
|
-
path.posix.isAbsolute(normalized)) {
|
|
73
|
-
throw new Error(`${errorLabel}不合法`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return normalized;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
57
|
/**
|
|
80
58
|
* 生成 frontmatter 字符串(用于命令创建/更新)
|
|
81
59
|
*/
|
|
@@ -111,7 +89,7 @@ function generateCommandFrontmatter(data) {
|
|
|
111
89
|
/**
|
|
112
90
|
* 递归扫描目录获取命令文件
|
|
113
91
|
*/
|
|
114
|
-
function scanCommandsDir(dir, basePath, scope
|
|
92
|
+
function scanCommandsDir(dir, basePath, scope) {
|
|
115
93
|
const commands = [];
|
|
116
94
|
|
|
117
95
|
if (!fs.existsSync(dir)) {
|
|
@@ -126,7 +104,7 @@ function scanCommandsDir(dir, basePath, scope, options = {}) {
|
|
|
126
104
|
|
|
127
105
|
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
128
106
|
// 递归扫描子目录
|
|
129
|
-
const subCommands = scanCommandsDir(fullPath, basePath, scope
|
|
107
|
+
const subCommands = scanCommandsDir(fullPath, basePath, scope);
|
|
130
108
|
commands.push(...subCommands);
|
|
131
109
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
132
110
|
// 解析命令文件
|
|
@@ -135,10 +113,11 @@ function scanCommandsDir(dir, basePath, scope, options = {}) {
|
|
|
135
113
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
136
114
|
|
|
137
115
|
// 计算相对路径和命令名
|
|
138
|
-
const relativePath =
|
|
116
|
+
const relativePath = path.relative(basePath, fullPath);
|
|
139
117
|
const commandName = entry.name.replace(/\.md$/, '');
|
|
140
118
|
const namespace = path.dirname(relativePath);
|
|
141
|
-
|
|
119
|
+
|
|
120
|
+
commands.push({
|
|
142
121
|
name: commandName,
|
|
143
122
|
namespace: namespace === '.' ? null : namespace,
|
|
144
123
|
scope,
|
|
@@ -153,15 +132,7 @@ function scanCommandsDir(dir, basePath, scope, options = {}) {
|
|
|
153
132
|
body,
|
|
154
133
|
fullContent: content,
|
|
155
134
|
updatedAt: fs.statSync(fullPath).mtime.getTime()
|
|
156
|
-
};
|
|
157
|
-
const decoratedCommand = typeof options.decorate === 'function'
|
|
158
|
-
? {
|
|
159
|
-
...command,
|
|
160
|
-
...options.decorate(command)
|
|
161
|
-
}
|
|
162
|
-
: command;
|
|
163
|
-
|
|
164
|
-
commands.push(decoratedCommand);
|
|
135
|
+
});
|
|
165
136
|
} catch (err) {
|
|
166
137
|
console.warn(`[CommandsService] Failed to parse ${fullPath}:`, err.message);
|
|
167
138
|
}
|
|
@@ -269,7 +240,6 @@ class CommandsService {
|
|
|
269
240
|
const config = PLATFORM_CONFIG[this.platform];
|
|
270
241
|
|
|
271
242
|
this.userCommandsDir = config.userCommandsDir;
|
|
272
|
-
this.storageDir = config.storageDir;
|
|
273
243
|
if (this.platform === 'opencode') {
|
|
274
244
|
const legacyUserDir = config.legacyUserCommandsDir;
|
|
275
245
|
if (legacyUserDir && fs.existsSync(legacyUserDir) && !fs.existsSync(this.userCommandsDir)) {
|
|
@@ -280,7 +250,6 @@ class CommandsService {
|
|
|
280
250
|
this.projectCommandsDir = config.projectCommandsDir;
|
|
281
251
|
this.repoScanner = new CommandsRepoScanner(this.platform, this.userCommandsDir);
|
|
282
252
|
ensureDir(this.userCommandsDir);
|
|
283
|
-
ensureDir(this.storageDir);
|
|
284
253
|
}
|
|
285
254
|
|
|
286
255
|
getProjectCommandsDir(projectPath) {
|
|
@@ -292,165 +261,28 @@ class CommandsService {
|
|
|
292
261
|
* 获取所有命令列表
|
|
293
262
|
* @param {string} projectPath - 项目路径(可选,用于获取项目级命令)
|
|
294
263
|
*/
|
|
295
|
-
|
|
296
|
-
return path.join(this.storageDir, normalizeCommandRelativePath(relativePath));
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
isInstalledCommand(relativePath) {
|
|
300
|
-
return fs.existsSync(path.join(this.userCommandsDir, normalizeCommandRelativePath(relativePath)));
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
buildCommandContent({ description, allowedTools, argumentHint, agent, model, subtask, body }) {
|
|
304
|
-
const frontmatterData = {};
|
|
305
|
-
if (description) frontmatterData.description = description;
|
|
306
|
-
if (this.platform !== 'opencode') {
|
|
307
|
-
if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
|
|
308
|
-
if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
|
|
309
|
-
}
|
|
310
|
-
if (agent) frontmatterData.agent = agent;
|
|
311
|
-
if (model) frontmatterData.model = model;
|
|
312
|
-
if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
|
|
313
|
-
|
|
314
|
-
let content = '';
|
|
315
|
-
if (Object.keys(frontmatterData).length > 0) {
|
|
316
|
-
content = generateCommandFrontmatter(frontmatterData) + '\n\n';
|
|
317
|
-
}
|
|
318
|
-
content += body || '';
|
|
319
|
-
return content;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
copyFileRecursive(sourcePath, targetPath) {
|
|
323
|
-
ensureDir(path.dirname(targetPath));
|
|
324
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
ensureManagedCommandCopy(relativePath, sourcePath, options = {}) {
|
|
328
|
-
const overwrite = options.overwrite === true;
|
|
329
|
-
const normalizedPath = normalizeCommandRelativePath(relativePath);
|
|
330
|
-
if (!sourcePath || !fs.existsSync(sourcePath)) {
|
|
331
|
-
return false;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const managedPath = this.getManagedCommandPath(normalizedPath);
|
|
335
|
-
if (fs.existsSync(managedPath) && !overwrite) {
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
this.copyFileRecursive(sourcePath, managedPath);
|
|
340
|
-
return true;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
removeEmptyCommandDirs(baseDir, relativePath) {
|
|
344
|
-
const normalizedPath = normalizeCommandRelativePath(relativePath);
|
|
345
|
-
let currentDir = path.dirname(path.join(baseDir, normalizedPath));
|
|
346
|
-
const resolvedBaseDir = path.resolve(baseDir);
|
|
347
|
-
|
|
348
|
-
while (currentDir && currentDir !== resolvedBaseDir && currentDir.startsWith(`${resolvedBaseDir}${path.sep}`)) {
|
|
349
|
-
try {
|
|
350
|
-
if (fs.existsSync(currentDir) && fs.readdirSync(currentDir).length === 0) {
|
|
351
|
-
fs.rmdirSync(currentDir);
|
|
352
|
-
currentDir = path.dirname(currentDir);
|
|
353
|
-
continue;
|
|
354
|
-
}
|
|
355
|
-
} catch {
|
|
356
|
-
// ignore cleanup failures
|
|
357
|
-
}
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
mergeInstalledUserCommands(commands, options = {}) {
|
|
363
|
-
const syncManagedLocalCommands = options.syncManagedLocalCommands === true;
|
|
364
|
-
const installedCommands = scanCommandsDir(this.userCommandsDir, this.userCommandsDir, 'user', {
|
|
365
|
-
decorate: () => ({
|
|
366
|
-
installed: true,
|
|
367
|
-
isManagedLocal: false,
|
|
368
|
-
source: 'native-installed'
|
|
369
|
-
})
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
for (const installedCommand of installedCommands) {
|
|
373
|
-
const existing = commands.find(command =>
|
|
374
|
-
command.scope === 'user' &&
|
|
375
|
-
command.path.toLowerCase() === installedCommand.path.toLowerCase()
|
|
376
|
-
);
|
|
377
|
-
const managedPath = this.getManagedCommandPath(installedCommand.path);
|
|
378
|
-
|
|
379
|
-
if (existing) {
|
|
380
|
-
if (syncManagedLocalCommands && fs.existsSync(managedPath)) {
|
|
381
|
-
this.ensureManagedCommandCopy(installedCommand.path, installedCommand.fullPath, { overwrite: true });
|
|
382
|
-
}
|
|
383
|
-
existing.installed = true;
|
|
384
|
-
if (!existing.description && installedCommand.description) existing.description = installedCommand.description;
|
|
385
|
-
if (!existing.body && installedCommand.body) existing.body = installedCommand.body;
|
|
386
|
-
if (!existing.fullContent && installedCommand.fullContent) existing.fullContent = installedCommand.fullContent;
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
this.ensureManagedCommandCopy(installedCommand.path, installedCommand.fullPath, {
|
|
391
|
-
overwrite: syncManagedLocalCommands
|
|
392
|
-
});
|
|
393
|
-
commands.push(installedCommand);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
mergeLocalUserCommands(commands) {
|
|
398
|
-
if (!fs.existsSync(this.storageDir)) return;
|
|
399
|
-
|
|
400
|
-
const localCommands = scanCommandsDir(this.storageDir, this.storageDir, 'user', {
|
|
401
|
-
decorate: (command) => ({
|
|
402
|
-
installed: this.isInstalledCommand(command.path),
|
|
403
|
-
isManagedLocal: true,
|
|
404
|
-
source: 'local'
|
|
405
|
-
})
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
for (const localCommand of localCommands) {
|
|
409
|
-
const existing = commands.find(command =>
|
|
410
|
-
command.scope === 'user' &&
|
|
411
|
-
command.path.toLowerCase() === localCommand.path.toLowerCase()
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
if (existing) {
|
|
415
|
-
existing.isManagedLocal = true;
|
|
416
|
-
existing.installed = this.isInstalledCommand(existing.path);
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
commands.push(localCommand);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
listCommands(projectPath = null, options = {}) {
|
|
264
|
+
listCommands(projectPath = null) {
|
|
425
265
|
const commands = [];
|
|
426
|
-
const syncManagedLocalCommands = options.syncManagedLocalCommands === true;
|
|
427
266
|
|
|
428
|
-
|
|
429
|
-
this.
|
|
267
|
+
// 获取用户级命令
|
|
268
|
+
const userCommands = scanCommandsDir(this.userCommandsDir, this.userCommandsDir, 'user');
|
|
269
|
+
commands.push(...userCommands);
|
|
430
270
|
|
|
431
271
|
// 获取项目级命令(如果提供了项目路径)
|
|
432
272
|
if (projectPath) {
|
|
433
273
|
const projectCommandsDir = this.getProjectCommandsDir(projectPath);
|
|
434
|
-
const projectCommands = scanCommandsDir(projectCommandsDir, projectCommandsDir, 'project'
|
|
435
|
-
decorate: () => ({
|
|
436
|
-
installed: true,
|
|
437
|
-
isManagedLocal: false,
|
|
438
|
-
source: 'native-installed'
|
|
439
|
-
})
|
|
440
|
-
});
|
|
274
|
+
const projectCommands = scanCommandsDir(projectCommandsDir, projectCommandsDir, 'project');
|
|
441
275
|
commands.push(...projectCommands);
|
|
442
276
|
}
|
|
443
277
|
|
|
444
278
|
// 按名称排序
|
|
445
279
|
commands.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
446
|
-
const userCount = commands.filter(command => command.scope === 'user').length;
|
|
447
|
-
const projectCount = commands.length - userCount;
|
|
448
280
|
|
|
449
281
|
return {
|
|
450
282
|
commands,
|
|
451
283
|
total: commands.length,
|
|
452
|
-
userCount,
|
|
453
|
-
projectCount
|
|
284
|
+
userCount: userCommands.length,
|
|
285
|
+
projectCount: commands.length - userCommands.length
|
|
454
286
|
};
|
|
455
287
|
}
|
|
456
288
|
|
|
@@ -460,9 +292,7 @@ class CommandsService {
|
|
|
460
292
|
*/
|
|
461
293
|
async listAllCommands(projectPath = null, forceRefresh = false) {
|
|
462
294
|
// 获取本地命令
|
|
463
|
-
const { commands: localCommands, userCount, projectCount } = this.listCommands(projectPath
|
|
464
|
-
syncManagedLocalCommands: forceRefresh
|
|
465
|
-
});
|
|
295
|
+
const { commands: localCommands, userCount, projectCount } = this.listCommands(projectPath);
|
|
466
296
|
|
|
467
297
|
// 获取远程命令
|
|
468
298
|
let remoteCommands = [];
|
|
@@ -510,30 +340,25 @@ class CommandsService {
|
|
|
510
340
|
? this.userCommandsDir
|
|
511
341
|
: this.getProjectCommandsDir(projectPath);
|
|
512
342
|
|
|
513
|
-
const relativePath =
|
|
343
|
+
const relativePath = namespace
|
|
514
344
|
? path.join(namespace, `${name}.md`)
|
|
515
|
-
: `${name}.md
|
|
345
|
+
: `${name}.md`;
|
|
346
|
+
|
|
516
347
|
const fullPath = path.join(baseDir, relativePath);
|
|
517
|
-
const managedPath = scope === 'user' ? this.getManagedCommandPath(relativePath) : '';
|
|
518
|
-
const activePath = fs.existsSync(fullPath)
|
|
519
|
-
? fullPath
|
|
520
|
-
: (managedPath && fs.existsSync(managedPath) ? managedPath : '');
|
|
521
348
|
|
|
522
|
-
if (!
|
|
349
|
+
if (!fs.existsSync(fullPath)) {
|
|
523
350
|
return null;
|
|
524
351
|
}
|
|
525
352
|
|
|
526
|
-
const content = fs.readFileSync(
|
|
353
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
527
354
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
528
|
-
const installed = fs.existsSync(fullPath);
|
|
529
|
-
const isManagedLocal = scope === 'user' && !!managedPath && fs.existsSync(managedPath);
|
|
530
355
|
|
|
531
356
|
return {
|
|
532
357
|
name,
|
|
533
358
|
namespace,
|
|
534
359
|
scope,
|
|
535
360
|
path: relativePath,
|
|
536
|
-
fullPath
|
|
361
|
+
fullPath,
|
|
537
362
|
description: frontmatter.description || '',
|
|
538
363
|
allowedTools: frontmatter['allowed-tools'] || '',
|
|
539
364
|
argumentHint: frontmatter['argument-hint'] || '',
|
|
@@ -542,10 +367,7 @@ class CommandsService {
|
|
|
542
367
|
subtask: frontmatter.subtask || '',
|
|
543
368
|
body,
|
|
544
369
|
fullContent: content,
|
|
545
|
-
|
|
546
|
-
isManagedLocal,
|
|
547
|
-
source: installed ? 'native-installed' : 'local',
|
|
548
|
-
updatedAt: fs.statSync(activePath).mtime.getTime()
|
|
370
|
+
updatedAt: fs.statSync(fullPath).mtime.getTime()
|
|
549
371
|
};
|
|
550
372
|
}
|
|
551
373
|
|
|
@@ -570,28 +392,30 @@ class CommandsService {
|
|
|
570
392
|
ensureDir(targetDir);
|
|
571
393
|
|
|
572
394
|
const filePath = path.join(targetDir, `${name}.md`);
|
|
573
|
-
const relativePath = normalizeCommandRelativePath(namespace ? path.join(namespace, `${name}.md`) : `${name}.md`);
|
|
574
|
-
const managedPath = scope === 'user' ? this.getManagedCommandPath(relativePath) : '';
|
|
575
395
|
|
|
576
396
|
// 检查是否已存在
|
|
577
|
-
if (fs.existsSync(filePath)
|
|
397
|
+
if (fs.existsSync(filePath)) {
|
|
578
398
|
throw new Error(`命令 "${name}" 已存在`);
|
|
579
399
|
}
|
|
580
400
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
401
|
+
// 生成文件内容
|
|
402
|
+
const frontmatterData = {};
|
|
403
|
+
if (description) frontmatterData.description = description;
|
|
404
|
+
if (this.platform !== 'opencode') {
|
|
405
|
+
if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
|
|
406
|
+
if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
|
|
407
|
+
}
|
|
408
|
+
if (agent) frontmatterData.agent = agent;
|
|
409
|
+
if (model) frontmatterData.model = model;
|
|
410
|
+
if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
|
|
590
411
|
|
|
591
|
-
|
|
592
|
-
if (
|
|
593
|
-
|
|
412
|
+
let content = '';
|
|
413
|
+
if (Object.keys(frontmatterData).length > 0) {
|
|
414
|
+
content = generateCommandFrontmatter(frontmatterData) + '\n\n';
|
|
594
415
|
}
|
|
416
|
+
content += body || '';
|
|
417
|
+
|
|
418
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
595
419
|
|
|
596
420
|
return this.getCommand(name, scope, projectPath, namespace);
|
|
597
421
|
}
|
|
@@ -604,34 +428,34 @@ class CommandsService {
|
|
|
604
428
|
? this.userCommandsDir
|
|
605
429
|
: this.getProjectCommandsDir(projectPath);
|
|
606
430
|
|
|
607
|
-
const relativePath =
|
|
431
|
+
const relativePath = namespace
|
|
608
432
|
? path.join(namespace, `${name}.md`)
|
|
609
|
-
: `${name}.md
|
|
433
|
+
: `${name}.md`;
|
|
434
|
+
|
|
610
435
|
const filePath = path.join(baseDir, relativePath);
|
|
611
|
-
const managedPath = scope === 'user' ? this.getManagedCommandPath(relativePath) : '';
|
|
612
|
-
const hasManagedCopy = managedPath && fs.existsSync(managedPath);
|
|
613
436
|
|
|
614
|
-
if (!fs.existsSync(filePath)
|
|
437
|
+
if (!fs.existsSync(filePath)) {
|
|
615
438
|
throw new Error(`命令 "${name}" 不存在`);
|
|
616
439
|
}
|
|
617
440
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
subtask,
|
|
625
|
-
body
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
if (fs.existsSync(filePath)) {
|
|
629
|
-
fs.writeFileSync(filePath, content, 'utf-8');
|
|
441
|
+
// 生成文件内容
|
|
442
|
+
const frontmatterData = {};
|
|
443
|
+
if (description) frontmatterData.description = description;
|
|
444
|
+
if (this.platform !== 'opencode') {
|
|
445
|
+
if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
|
|
446
|
+
if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
|
|
630
447
|
}
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
|
|
448
|
+
if (agent) frontmatterData.agent = agent;
|
|
449
|
+
if (model) frontmatterData.model = model;
|
|
450
|
+
if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
|
|
451
|
+
|
|
452
|
+
let content = '';
|
|
453
|
+
if (Object.keys(frontmatterData).length > 0) {
|
|
454
|
+
content = generateCommandFrontmatter(frontmatterData) + '\n\n';
|
|
634
455
|
}
|
|
456
|
+
content += body || '';
|
|
457
|
+
|
|
458
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
635
459
|
|
|
636
460
|
return this.getCommand(name, scope, projectPath, namespace);
|
|
637
461
|
}
|
|
@@ -644,27 +468,29 @@ class CommandsService {
|
|
|
644
468
|
? this.userCommandsDir
|
|
645
469
|
: this.getProjectCommandsDir(projectPath);
|
|
646
470
|
|
|
647
|
-
const relativePath =
|
|
471
|
+
const relativePath = namespace
|
|
648
472
|
? path.join(namespace, `${name}.md`)
|
|
649
|
-
: `${name}.md
|
|
473
|
+
: `${name}.md`;
|
|
474
|
+
|
|
650
475
|
const filePath = path.join(baseDir, relativePath);
|
|
651
|
-
const managedPath = scope === 'user' ? this.getManagedCommandPath(relativePath) : '';
|
|
652
|
-
let removed = false;
|
|
653
476
|
|
|
654
|
-
if (fs.existsSync(filePath)) {
|
|
655
|
-
|
|
656
|
-
this.removeEmptyCommandDirs(baseDir, relativePath);
|
|
657
|
-
removed = true;
|
|
477
|
+
if (!fs.existsSync(filePath)) {
|
|
478
|
+
return { success: false, message: '命令不存在' };
|
|
658
479
|
}
|
|
659
480
|
|
|
660
|
-
|
|
661
|
-
fs.unlinkSync(managedPath);
|
|
662
|
-
this.removeEmptyCommandDirs(this.storageDir, relativePath);
|
|
663
|
-
removed = true;
|
|
664
|
-
}
|
|
481
|
+
fs.unlinkSync(filePath);
|
|
665
482
|
|
|
666
|
-
|
|
667
|
-
|
|
483
|
+
// 如果目录为空,删除目录
|
|
484
|
+
if (namespace) {
|
|
485
|
+
const namespaceDir = path.join(baseDir, namespace);
|
|
486
|
+
try {
|
|
487
|
+
const remaining = fs.readdirSync(namespaceDir);
|
|
488
|
+
if (remaining.length === 0) {
|
|
489
|
+
fs.rmdirSync(namespaceDir);
|
|
490
|
+
}
|
|
491
|
+
} catch (err) {
|
|
492
|
+
// 忽略删除目录错误
|
|
493
|
+
}
|
|
668
494
|
}
|
|
669
495
|
|
|
670
496
|
return { success: true, message: '命令已删除' };
|
|
@@ -731,37 +557,11 @@ class CommandsService {
|
|
|
731
557
|
return this.repoScanner.installCommand(command);
|
|
732
558
|
}
|
|
733
559
|
|
|
734
|
-
installLocalCommand(relativePath) {
|
|
735
|
-
const normalizedPath = normalizeCommandRelativePath(relativePath);
|
|
736
|
-
const managedPath = this.getManagedCommandPath(normalizedPath);
|
|
737
|
-
const targetPath = path.join(this.userCommandsDir, normalizedPath);
|
|
738
|
-
|
|
739
|
-
if (!fs.existsSync(managedPath)) {
|
|
740
|
-
throw new Error(`本地命令 "${normalizedPath}" 不存在`);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
if (fs.existsSync(targetPath)) {
|
|
744
|
-
return { success: true, message: 'Already installed' };
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
this.copyFileRecursive(managedPath, targetPath);
|
|
748
|
-
return { success: true, message: 'Installed successfully' };
|
|
749
|
-
}
|
|
750
|
-
|
|
751
560
|
/**
|
|
752
561
|
* 卸载命令
|
|
753
562
|
*/
|
|
754
563
|
uninstallCommand(relativePath) {
|
|
755
|
-
|
|
756
|
-
const targetPath = path.join(this.userCommandsDir, normalizedPath);
|
|
757
|
-
|
|
758
|
-
if (fs.existsSync(targetPath)) {
|
|
759
|
-
fs.unlinkSync(targetPath);
|
|
760
|
-
this.removeEmptyCommandDirs(this.userCommandsDir, normalizedPath);
|
|
761
|
-
return { success: true, message: 'Uninstalled successfully' };
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
return { success: true, message: 'Not installed' };
|
|
564
|
+
return this.repoScanner.uninstall(relativePath);
|
|
765
565
|
}
|
|
766
566
|
|
|
767
567
|
// ==================== 格式转换 ====================
|
|
@@ -14,7 +14,7 @@ const opencodeChannelsService = require('./opencode-channels');
|
|
|
14
14
|
const { AgentsService } = require('./agents-service');
|
|
15
15
|
const { CommandsService } = require('./commands-service');
|
|
16
16
|
const { SkillService } = require('./skill-service');
|
|
17
|
-
const { PATHS, NATIVE_PATHS
|
|
17
|
+
const { PATHS, NATIVE_PATHS } = require('../../config/paths');
|
|
18
18
|
|
|
19
19
|
const CONFIG_VERSION = '1.4.0';
|
|
20
20
|
const SKILL_FILE_ENCODING = 'base64';
|
|
@@ -25,8 +25,8 @@ const LEGACY_CC_TOOL_DIR = PATHS.base;
|
|
|
25
25
|
const CLAUDE_SETTINGS_PATH = NATIVE_PATHS.claude.settings;
|
|
26
26
|
const LEGACY_PLUGINS_DIR = path.join(LEGACY_CC_TOOL_DIR, 'plugins', 'installed');
|
|
27
27
|
const LEGACY_PLUGINS_REGISTRY = path.join(LEGACY_CC_TOOL_DIR, 'plugins', 'registry.json');
|
|
28
|
-
const CLAUDE_PLUGINS_DIR = NATIVE_PATHS.claude.plugins;
|
|
29
|
-
const NATIVE_PLUGINS_REGISTRY =
|
|
28
|
+
const CLAUDE_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
|
|
29
|
+
const NATIVE_PLUGINS_REGISTRY = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
|
|
30
30
|
const PLUGIN_IGNORE_DIRS = new Set(['.git', 'node_modules', '.DS_Store']);
|
|
31
31
|
const PLUGIN_IGNORE_FILES = new Set(['.DS_Store']);
|
|
32
32
|
const PLUGIN_SENSITIVE_PATTERNS = [
|
|
@@ -44,7 +44,7 @@ const CC_PROMPTS_PATH = PATHS.prompts;
|
|
|
44
44
|
const CC_SECURITY_PATH = PATHS.security;
|
|
45
45
|
const LEGACY_UI_CONFIG_PATH = PATHS.uiConfig;
|
|
46
46
|
const LEGACY_NOTIFY_HOOK_PATH = PATHS.notifyHook;
|
|
47
|
-
const GEMINI_SETTINGS_PATH = NATIVE_PATHS.gemini.settings;
|
|
47
|
+
const GEMINI_SETTINGS_PATH = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'settings.json');
|
|
48
48
|
const AGENT_PLATFORMS = ['claude', 'codex', 'opencode'];
|
|
49
49
|
const COMMAND_PLATFORMS = ['claude', 'opencode'];
|
|
50
50
|
const SKILL_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
|
|
@@ -156,7 +156,7 @@ function writeJsonFileAbsolute(filePath, data, overwrite, options = {}) {
|
|
|
156
156
|
return 'failed';
|
|
157
157
|
}
|
|
158
158
|
if (fs.existsSync(filePath) && !overwrite) return 'skipped';
|
|
159
|
-
ensureDir(
|
|
159
|
+
ensureDir(path.dirname(filePath));
|
|
160
160
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
161
161
|
if (options.mode) {
|
|
162
162
|
try {
|
|
@@ -173,7 +173,7 @@ function writeTextFileAbsolute(filePath, content, overwrite, options = {}) {
|
|
|
173
173
|
return 'failed';
|
|
174
174
|
}
|
|
175
175
|
if (fs.existsSync(filePath) && !overwrite) return 'skipped';
|
|
176
|
-
ensureDir(
|
|
176
|
+
ensureDir(path.dirname(filePath));
|
|
177
177
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
178
178
|
if (options.mode) {
|
|
179
179
|
try {
|
|
@@ -507,7 +507,7 @@ function syncImportedChannelsToNativeConfigs(importChannelsByType, nativeConfigs
|
|
|
507
507
|
]
|
|
508
508
|
);
|
|
509
509
|
if (persistedClaudeChannel?.id) {
|
|
510
|
-
ensureDir(NATIVE_PATHS.claude.
|
|
510
|
+
ensureDir(path.dirname(NATIVE_PATHS.claude.settings));
|
|
511
511
|
channelsService.applyChannelToSettings(persistedClaudeChannel.id);
|
|
512
512
|
}
|
|
513
513
|
}
|
|
@@ -17,11 +17,12 @@ const REGISTRY_FILE = PATHS.configRegistry;
|
|
|
17
17
|
const CONFIGS_DIR = PATHS.configs;
|
|
18
18
|
|
|
19
19
|
// Claude Code native directories
|
|
20
|
+
const CLAUDE_HOME_DIR = path.dirname(NATIVE_PATHS.claude.settings);
|
|
20
21
|
const CLAUDE_DIRS = {
|
|
21
|
-
skills:
|
|
22
|
-
commands:
|
|
23
|
-
agents:
|
|
24
|
-
plugins:
|
|
22
|
+
skills: path.join(CLAUDE_HOME_DIR, 'skills'),
|
|
23
|
+
commands: path.join(CLAUDE_HOME_DIR, 'commands'),
|
|
24
|
+
agents: path.join(CLAUDE_HOME_DIR, 'agents'),
|
|
25
|
+
plugins: path.join(CLAUDE_HOME_DIR, 'plugins')
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
// Valid config types
|
|
@@ -511,7 +512,7 @@ class ConfigRegistryService {
|
|
|
511
512
|
this._importFileBasedConfigs(type, sourceDir, destDir, entryRelativePath, registry, result);
|
|
512
513
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
513
514
|
// This is a config file
|
|
514
|
-
const name = entryRelativePath;
|
|
515
|
+
const name = normalizeRelativeConfigName(entryRelativePath);
|
|
515
516
|
|
|
516
517
|
// Skip if already in registry
|
|
517
518
|
if (registry[type][name]) {
|
|
@@ -520,8 +521,8 @@ class ConfigRegistryService {
|
|
|
520
521
|
}
|
|
521
522
|
|
|
522
523
|
// Copy file to cc-tool configs
|
|
523
|
-
const sourcePath = path.join(sourceDir,
|
|
524
|
-
const destPath = path.join(destDir,
|
|
524
|
+
const sourcePath = path.join(sourceDir, name);
|
|
525
|
+
const destPath = path.join(destDir, name);
|
|
525
526
|
|
|
526
527
|
try {
|
|
527
528
|
// Ensure destination directory exists
|
|
@@ -800,7 +801,7 @@ class ConfigRegistryService {
|
|
|
800
801
|
if (entry.isDirectory()) {
|
|
801
802
|
this._syncFileBasedRegistry(type, baseDir, entryRelativePath, registry, result);
|
|
802
803
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
803
|
-
const name = entryRelativePath;
|
|
804
|
+
const name = normalizeRelativeConfigName(entryRelativePath);
|
|
804
805
|
|
|
805
806
|
if (!registry[type][name]) {
|
|
806
807
|
registry[type][name] = {
|