coding-tool-x 3.5.6 → 3.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +17 -0
  2. package/bin/ctx.js +6 -1
  3. package/dist/web/assets/{Analytics-CRNCHeui.js → Analytics-BzoNzfbi.js} +2 -2
  4. package/dist/web/assets/Analytics-vQS5IWvs.css +1 -0
  5. package/dist/web/assets/{ConfigTemplates-C0erJdo2.js → ConfigTemplates-O4ikBt1o.js} +1 -1
  6. package/dist/web/assets/{Home-CL5z6Q4d.js → Home-BQjsnblU.js} +1 -1
  7. package/dist/web/assets/Home-qzk118Of.css +1 -0
  8. package/dist/web/assets/{PluginManager-hDx0XMO_.js → PluginManager-DS_DJnVc.js} +1 -1
  9. package/dist/web/assets/ProjectList-CqYDtsHx.js +1 -0
  10. package/dist/web/assets/ProjectList-GCC2QOmq.css +1 -0
  11. package/dist/web/assets/SessionList-CfPtcq6Y.css +1 -0
  12. package/dist/web/assets/SessionList-DMlLtMCz.js +1 -0
  13. package/dist/web/assets/{SkillManager-D6Vwpajh.js → SkillManager-DpNE02r0.js} +1 -1
  14. package/dist/web/assets/{WorkspaceManager-C3TjeOPy.js → WorkspaceManager-DMY7_SHh.js} +1 -1
  15. package/dist/web/assets/icons-CEq2hYB-.js +1 -0
  16. package/dist/web/assets/index-Clf0l3wc.js +2 -0
  17. package/dist/web/assets/index-Dih_bOsv.css +1 -0
  18. package/dist/web/assets/{naive-ui-BaTCPPL5.js → naive-ui-Cg4_ZeoT.js} +1 -1
  19. package/dist/web/assets/{vendors-Fza9uSYn.js → vendors-Bsp-dq2d.js} +1 -1
  20. package/dist/web/assets/vue-vendor-BxIT0uQq.js +45 -0
  21. package/dist/web/index.html +7 -7
  22. package/docs/Caddyfile.example +19 -0
  23. package/docs/reverse-proxy-https.md +57 -0
  24. package/package.json +2 -1
  25. package/src/commands/daemon.js +33 -5
  26. package/src/commands/export-config.js +6 -6
  27. package/src/commands/ui.js +12 -3
  28. package/src/config/default.js +2 -6
  29. package/src/config/loader.js +2 -2
  30. package/src/config/paths.js +166 -33
  31. package/src/index.js +124 -34
  32. package/src/server/api/agents.js +52 -2
  33. package/src/server/api/commands.js +38 -2
  34. package/src/server/api/plugins.js +104 -1
  35. package/src/server/api/sessions.js +5 -5
  36. package/src/server/index.js +25 -5
  37. package/src/server/services/agents-service.js +269 -62
  38. package/src/server/services/commands-service.js +281 -81
  39. package/src/server/services/config-export-service.js +7 -7
  40. package/src/server/services/config-registry-service.js +4 -5
  41. package/src/server/services/config-sync-manager.js +61 -41
  42. package/src/server/services/config-sync-service.js +3 -3
  43. package/src/server/services/gemini-channels.js +5 -5
  44. package/src/server/services/gemini-config.js +3 -4
  45. package/src/server/services/gemini-sessions.js +23 -20
  46. package/src/server/services/gemini-settings-manager.js +2 -3
  47. package/src/server/services/https-cert.js +171 -0
  48. package/src/server/services/mcp-service.js +9 -14
  49. package/src/server/services/native-oauth-adapters.js +3 -3
  50. package/src/server/services/network-access.js +47 -2
  51. package/src/server/services/notification-hooks.js +11 -5
  52. package/src/server/services/opencode-sessions.js +4 -4
  53. package/src/server/services/opencode-settings-manager.js +3 -3
  54. package/src/server/services/plugins-service.js +499 -23
  55. package/src/server/services/prompts-service.js +5 -9
  56. package/src/server/services/sessions.js +2 -2
  57. package/src/server/services/skill-service.js +155 -18
  58. package/src/server/services/web-ui-runtime.js +54 -0
  59. package/src/server/websocket-server.js +11 -4
  60. package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
  61. package/dist/web/assets/Home-BQxQ1LhR.css +0 -1
  62. package/dist/web/assets/ProjectList-BNsz96av.js +0 -1
  63. package/dist/web/assets/ProjectList-DL4JK6ci.css +0 -1
  64. package/dist/web/assets/SessionList-B8dXVXfi.css +0 -1
  65. package/dist/web/assets/SessionList-CG1UhFo3.js +0 -1
  66. package/dist/web/assets/icons-CQuif85v.js +0 -1
  67. package/dist/web/assets/index-GuER-BmS.js +0 -2
  68. package/dist/web/assets/index-VGAxnLqi.css +0 -1
  69. package/dist/web/assets/vue-vendor-aWwwFAao.js +0 -45
@@ -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 } = require('../../config/paths');
11
+ const { NATIVE_PATHS, PATHS } = require('../../config/paths');
12
12
  const {
13
13
  parseCommandContent,
14
14
  parseFrontmatter
@@ -17,18 +17,21 @@ const {
17
17
  // 默认仓库源
18
18
  const DEFAULT_REPOS = [];
19
19
  const SUPPORTED_PLATFORMS = ['claude', 'opencode'];
20
- const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
21
- const CLAUDE_COMMANDS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'commands');
20
+ const CLAUDE_COMMANDS_DIR = NATIVE_PATHS.claude.commands;
21
+ const OPENCODE_COMMANDS_DIR = NATIVE_PATHS.opencode.commands;
22
+ const OPENCODE_LEGACY_COMMANDS_DIR = NATIVE_PATHS.opencode.commandsLegacy;
22
23
 
23
24
  const PLATFORM_CONFIG = {
24
25
  claude: {
25
26
  userCommandsDir: CLAUDE_COMMANDS_DIR,
27
+ storageDir: PATHS.localCommands.claude,
26
28
  projectCommandsDir: (projectPath) => path.join(projectPath, '.claude', 'commands'),
27
29
  repoType: 'commands'
28
30
  },
29
31
  opencode: {
30
- userCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'commands'),
31
- legacyUserCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'command'),
32
+ userCommandsDir: OPENCODE_COMMANDS_DIR,
33
+ storageDir: PATHS.localCommands.opencode,
34
+ legacyUserCommandsDir: OPENCODE_LEGACY_COMMANDS_DIR,
32
35
  projectCommandsDir: (projectPath) => {
33
36
  const modern = path.join(projectPath, '.opencode', 'commands');
34
37
  const legacy = path.join(projectPath, '.opencode', 'command');
@@ -54,6 +57,25 @@ function ensureDir(dirPath) {
54
57
  }
55
58
  }
56
59
 
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
+
57
79
  /**
58
80
  * 生成 frontmatter 字符串(用于命令创建/更新)
59
81
  */
@@ -89,7 +111,7 @@ function generateCommandFrontmatter(data) {
89
111
  /**
90
112
  * 递归扫描目录获取命令文件
91
113
  */
92
- function scanCommandsDir(dir, basePath, scope) {
114
+ function scanCommandsDir(dir, basePath, scope, options = {}) {
93
115
  const commands = [];
94
116
 
95
117
  if (!fs.existsSync(dir)) {
@@ -104,7 +126,7 @@ function scanCommandsDir(dir, basePath, scope) {
104
126
 
105
127
  if (entry.isDirectory() && !entry.name.startsWith('.')) {
106
128
  // 递归扫描子目录
107
- const subCommands = scanCommandsDir(fullPath, basePath, scope);
129
+ const subCommands = scanCommandsDir(fullPath, basePath, scope, options);
108
130
  commands.push(...subCommands);
109
131
  } else if (entry.isFile() && entry.name.endsWith('.md')) {
110
132
  // 解析命令文件
@@ -113,11 +135,10 @@ function scanCommandsDir(dir, basePath, scope) {
113
135
  const { frontmatter, body } = parseFrontmatter(content);
114
136
 
115
137
  // 计算相对路径和命令名
116
- const relativePath = path.relative(basePath, fullPath);
138
+ const relativePath = normalizeCommandRelativePath(path.relative(basePath, fullPath));
117
139
  const commandName = entry.name.replace(/\.md$/, '');
118
140
  const namespace = path.dirname(relativePath);
119
-
120
- commands.push({
141
+ const command = {
121
142
  name: commandName,
122
143
  namespace: namespace === '.' ? null : namespace,
123
144
  scope,
@@ -132,7 +153,15 @@ function scanCommandsDir(dir, basePath, scope) {
132
153
  body,
133
154
  fullContent: content,
134
155
  updatedAt: fs.statSync(fullPath).mtime.getTime()
135
- });
156
+ };
157
+ const decoratedCommand = typeof options.decorate === 'function'
158
+ ? {
159
+ ...command,
160
+ ...options.decorate(command)
161
+ }
162
+ : command;
163
+
164
+ commands.push(decoratedCommand);
136
165
  } catch (err) {
137
166
  console.warn(`[CommandsService] Failed to parse ${fullPath}:`, err.message);
138
167
  }
@@ -240,6 +269,7 @@ class CommandsService {
240
269
  const config = PLATFORM_CONFIG[this.platform];
241
270
 
242
271
  this.userCommandsDir = config.userCommandsDir;
272
+ this.storageDir = config.storageDir;
243
273
  if (this.platform === 'opencode') {
244
274
  const legacyUserDir = config.legacyUserCommandsDir;
245
275
  if (legacyUserDir && fs.existsSync(legacyUserDir) && !fs.existsSync(this.userCommandsDir)) {
@@ -250,6 +280,7 @@ class CommandsService {
250
280
  this.projectCommandsDir = config.projectCommandsDir;
251
281
  this.repoScanner = new CommandsRepoScanner(this.platform, this.userCommandsDir);
252
282
  ensureDir(this.userCommandsDir);
283
+ ensureDir(this.storageDir);
253
284
  }
254
285
 
255
286
  getProjectCommandsDir(projectPath) {
@@ -261,28 +292,165 @@ class CommandsService {
261
292
  * 获取所有命令列表
262
293
  * @param {string} projectPath - 项目路径(可选,用于获取项目级命令)
263
294
  */
264
- listCommands(projectPath = null) {
295
+ getManagedCommandPath(relativePath) {
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 = {}) {
265
425
  const commands = [];
426
+ const syncManagedLocalCommands = options.syncManagedLocalCommands === true;
266
427
 
267
- // 获取用户级命令
268
- const userCommands = scanCommandsDir(this.userCommandsDir, this.userCommandsDir, 'user');
269
- commands.push(...userCommands);
428
+ this.mergeInstalledUserCommands(commands, { syncManagedLocalCommands });
429
+ this.mergeLocalUserCommands(commands);
270
430
 
271
431
  // 获取项目级命令(如果提供了项目路径)
272
432
  if (projectPath) {
273
433
  const projectCommandsDir = this.getProjectCommandsDir(projectPath);
274
- const projectCommands = scanCommandsDir(projectCommandsDir, projectCommandsDir, 'project');
434
+ const projectCommands = scanCommandsDir(projectCommandsDir, projectCommandsDir, 'project', {
435
+ decorate: () => ({
436
+ installed: true,
437
+ isManagedLocal: false,
438
+ source: 'native-installed'
439
+ })
440
+ });
275
441
  commands.push(...projectCommands);
276
442
  }
277
443
 
278
444
  // 按名称排序
279
445
  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;
280
448
 
281
449
  return {
282
450
  commands,
283
451
  total: commands.length,
284
- userCount: userCommands.length,
285
- projectCount: commands.length - userCommands.length
452
+ userCount,
453
+ projectCount
286
454
  };
287
455
  }
288
456
 
@@ -292,7 +460,9 @@ class CommandsService {
292
460
  */
293
461
  async listAllCommands(projectPath = null, forceRefresh = false) {
294
462
  // 获取本地命令
295
- const { commands: localCommands, userCount, projectCount } = this.listCommands(projectPath);
463
+ const { commands: localCommands, userCount, projectCount } = this.listCommands(projectPath, {
464
+ syncManagedLocalCommands: forceRefresh
465
+ });
296
466
 
297
467
  // 获取远程命令
298
468
  let remoteCommands = [];
@@ -340,25 +510,30 @@ class CommandsService {
340
510
  ? this.userCommandsDir
341
511
  : this.getProjectCommandsDir(projectPath);
342
512
 
343
- const relativePath = namespace
513
+ const relativePath = normalizeCommandRelativePath(namespace
344
514
  ? path.join(namespace, `${name}.md`)
345
- : `${name}.md`;
346
-
515
+ : `${name}.md`);
347
516
  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 : '');
348
521
 
349
- if (!fs.existsSync(fullPath)) {
522
+ if (!activePath) {
350
523
  return null;
351
524
  }
352
525
 
353
- const content = fs.readFileSync(fullPath, 'utf-8');
526
+ const content = fs.readFileSync(activePath, 'utf-8');
354
527
  const { frontmatter, body } = parseFrontmatter(content);
528
+ const installed = fs.existsSync(fullPath);
529
+ const isManagedLocal = scope === 'user' && !!managedPath && fs.existsSync(managedPath);
355
530
 
356
531
  return {
357
532
  name,
358
533
  namespace,
359
534
  scope,
360
535
  path: relativePath,
361
- fullPath,
536
+ fullPath: activePath,
362
537
  description: frontmatter.description || '',
363
538
  allowedTools: frontmatter['allowed-tools'] || '',
364
539
  argumentHint: frontmatter['argument-hint'] || '',
@@ -367,7 +542,10 @@ class CommandsService {
367
542
  subtask: frontmatter.subtask || '',
368
543
  body,
369
544
  fullContent: content,
370
- updatedAt: fs.statSync(fullPath).mtime.getTime()
545
+ installed,
546
+ isManagedLocal,
547
+ source: installed ? 'native-installed' : 'local',
548
+ updatedAt: fs.statSync(activePath).mtime.getTime()
371
549
  };
372
550
  }
373
551
 
@@ -392,30 +570,28 @@ class CommandsService {
392
570
  ensureDir(targetDir);
393
571
 
394
572
  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) : '';
395
575
 
396
576
  // 检查是否已存在
397
- if (fs.existsSync(filePath)) {
577
+ if (fs.existsSync(filePath) || (managedPath && fs.existsSync(managedPath))) {
398
578
  throw new Error(`命令 "${name}" 已存在`);
399
579
  }
400
580
 
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;
411
-
412
- let content = '';
413
- if (Object.keys(frontmatterData).length > 0) {
414
- content = generateCommandFrontmatter(frontmatterData) + '\n\n';
415
- }
416
- content += body || '';
581
+ const content = this.buildCommandContent({
582
+ description,
583
+ allowedTools,
584
+ argumentHint,
585
+ agent,
586
+ model,
587
+ subtask,
588
+ body
589
+ });
417
590
 
418
591
  fs.writeFileSync(filePath, content, 'utf-8');
592
+ if (managedPath) {
593
+ this.copyFileRecursive(filePath, managedPath);
594
+ }
419
595
 
420
596
  return this.getCommand(name, scope, projectPath, namespace);
421
597
  }
@@ -428,34 +604,34 @@ class CommandsService {
428
604
  ? this.userCommandsDir
429
605
  : this.getProjectCommandsDir(projectPath);
430
606
 
431
- const relativePath = namespace
607
+ const relativePath = normalizeCommandRelativePath(namespace
432
608
  ? path.join(namespace, `${name}.md`)
433
- : `${name}.md`;
434
-
609
+ : `${name}.md`);
435
610
  const filePath = path.join(baseDir, relativePath);
611
+ const managedPath = scope === 'user' ? this.getManagedCommandPath(relativePath) : '';
612
+ const hasManagedCopy = managedPath && fs.existsSync(managedPath);
436
613
 
437
- if (!fs.existsSync(filePath)) {
614
+ if (!fs.existsSync(filePath) && !hasManagedCopy) {
438
615
  throw new Error(`命令 "${name}" 不存在`);
439
616
  }
440
617
 
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;
447
- }
448
- if (agent) frontmatterData.agent = agent;
449
- if (model) frontmatterData.model = model;
450
- if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
618
+ const content = this.buildCommandContent({
619
+ description,
620
+ allowedTools,
621
+ argumentHint,
622
+ agent,
623
+ model,
624
+ subtask,
625
+ body
626
+ });
451
627
 
452
- let content = '';
453
- if (Object.keys(frontmatterData).length > 0) {
454
- content = generateCommandFrontmatter(frontmatterData) + '\n\n';
628
+ if (fs.existsSync(filePath)) {
629
+ fs.writeFileSync(filePath, content, 'utf-8');
630
+ }
631
+ if (managedPath) {
632
+ ensureDir(path.dirname(managedPath));
633
+ fs.writeFileSync(managedPath, content, 'utf-8');
455
634
  }
456
- content += body || '';
457
-
458
- fs.writeFileSync(filePath, content, 'utf-8');
459
635
 
460
636
  return this.getCommand(name, scope, projectPath, namespace);
461
637
  }
@@ -468,29 +644,27 @@ class CommandsService {
468
644
  ? this.userCommandsDir
469
645
  : this.getProjectCommandsDir(projectPath);
470
646
 
471
- const relativePath = namespace
647
+ const relativePath = normalizeCommandRelativePath(namespace
472
648
  ? path.join(namespace, `${name}.md`)
473
- : `${name}.md`;
474
-
649
+ : `${name}.md`);
475
650
  const filePath = path.join(baseDir, relativePath);
651
+ const managedPath = scope === 'user' ? this.getManagedCommandPath(relativePath) : '';
652
+ let removed = false;
476
653
 
477
- if (!fs.existsSync(filePath)) {
478
- return { success: false, message: '命令不存在' };
654
+ if (fs.existsSync(filePath)) {
655
+ fs.unlinkSync(filePath);
656
+ this.removeEmptyCommandDirs(baseDir, relativePath);
657
+ removed = true;
479
658
  }
480
659
 
481
- fs.unlinkSync(filePath);
660
+ if (managedPath && fs.existsSync(managedPath)) {
661
+ fs.unlinkSync(managedPath);
662
+ this.removeEmptyCommandDirs(this.storageDir, relativePath);
663
+ removed = true;
664
+ }
482
665
 
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
- }
666
+ if (!removed) {
667
+ return { success: false, message: '命令不存在' };
494
668
  }
495
669
 
496
670
  return { success: true, message: '命令已删除' };
@@ -557,11 +731,37 @@ class CommandsService {
557
731
  return this.repoScanner.installCommand(command);
558
732
  }
559
733
 
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
+
560
751
  /**
561
752
  * 卸载命令
562
753
  */
563
754
  uninstallCommand(relativePath) {
564
- return this.repoScanner.uninstall(relativePath);
755
+ const normalizedPath = normalizeCommandRelativePath(relativePath);
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' };
565
765
  }
566
766
 
567
767
  // ==================== 格式转换 ====================
@@ -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 } = require('../../config/paths');
17
+ const { PATHS, NATIVE_PATHS, getNativePathDir } = 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 = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
29
- const NATIVE_PLUGINS_REGISTRY = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
28
+ const CLAUDE_PLUGINS_DIR = NATIVE_PATHS.claude.plugins;
29
+ const NATIVE_PLUGINS_REGISTRY = NATIVE_PATHS.claude.installedPlugins;
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 = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'settings.json');
47
+ const GEMINI_SETTINGS_PATH = NATIVE_PATHS.gemini.settings;
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(path.dirname(filePath));
159
+ ensureDir(getNativePathDir(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(path.dirname(filePath));
176
+ ensureDir(getNativePathDir(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(path.dirname(NATIVE_PATHS.claude.settings));
510
+ ensureDir(NATIVE_PATHS.claude.dir);
511
511
  channelsService.applyChannelToSettings(persistedClaudeChannel.id);
512
512
  }
513
513
  }
@@ -17,12 +17,11 @@ 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);
21
20
  const CLAUDE_DIRS = {
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')
21
+ skills: NATIVE_PATHS.claude.skills,
22
+ commands: NATIVE_PATHS.claude.commands,
23
+ agents: NATIVE_PATHS.claude.agents,
24
+ plugins: NATIVE_PATHS.claude.plugins
26
25
  };
27
26
 
28
27
  // Valid config types