coding-tool-x 3.5.5 → 3.5.6

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 (32) hide show
  1. package/README.md +8 -4
  2. package/dist/web/assets/{Analytics-gvYu5sCM.js → Analytics-CRNCHeui.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-CPlH8Ehd.js → ConfigTemplates-C0erJdo2.js} +1 -1
  4. package/dist/web/assets/{Home-B-qbu3uk.js → Home-CL5z6Q4d.js} +1 -1
  5. package/dist/web/assets/{PluginManager-B2tQ_YUq.js → PluginManager-hDx0XMO_.js} +1 -1
  6. package/dist/web/assets/{ProjectList-kDadoXXs.js → ProjectList-BNsz96av.js} +1 -1
  7. package/dist/web/assets/{SessionList-eLgITwTV.js → SessionList-CG1UhFo3.js} +1 -1
  8. package/dist/web/assets/{SkillManager-B7zEB5Op.js → SkillManager-D6Vwpajh.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-C-RzB3ud.js → WorkspaceManager-C3TjeOPy.js} +1 -1
  10. package/dist/web/assets/{icons-DlxD2wZJ.js → icons-CQuif85v.js} +1 -1
  11. package/dist/web/assets/index-GuER-BmS.js +2 -0
  12. package/dist/web/assets/{index-BHeh2z0i.css → index-VGAxnLqi.css} +1 -1
  13. package/dist/web/index.html +3 -3
  14. package/package.json +1 -1
  15. package/src/commands/stats.js +41 -4
  16. package/src/index.js +1 -0
  17. package/src/server/api/codex-sessions.js +6 -3
  18. package/src/server/api/dashboard.js +25 -1
  19. package/src/server/api/gemini-sessions.js +6 -3
  20. package/src/server/api/hooks.js +17 -1
  21. package/src/server/api/opencode-sessions.js +6 -3
  22. package/src/server/api/plugins.js +24 -33
  23. package/src/server/api/sessions.js +6 -3
  24. package/src/server/index.js +6 -4
  25. package/src/server/services/codex-sessions.js +107 -9
  26. package/src/server/services/network-access.js +14 -0
  27. package/src/server/services/notification-hooks.js +175 -16
  28. package/src/server/services/plugins-service.js +502 -44
  29. package/src/server/services/session-launch-command.js +81 -0
  30. package/src/server/services/sessions.js +103 -33
  31. package/src/server/websocket-server.js +25 -1
  32. package/dist/web/assets/index-DG00t-zy.js +0 -2
@@ -208,6 +208,123 @@ function stripJsonComments(input = '') {
208
208
  return result;
209
209
  }
210
210
 
211
+ function isPlainObject(value) {
212
+ return !!value && typeof value === 'object' && !Array.isArray(value);
213
+ }
214
+
215
+ function getNestedValue(source, segments = []) {
216
+ return segments.reduce((acc, segment) => (acc && acc[segment] !== undefined ? acc[segment] : undefined), source);
217
+ }
218
+
219
+ function normalizeContentText(value = '') {
220
+ return String(value || '').trim();
221
+ }
222
+
223
+ function normalizeContentEntry(raw, fallback = {}) {
224
+ if (typeof raw === 'string') {
225
+ const name = normalizeContentText(raw);
226
+ if (!name) return null;
227
+ return {
228
+ name,
229
+ path: normalizeRepoPath(fallback.path || ''),
230
+ description: normalizeContentText(fallback.description || '')
231
+ };
232
+ }
233
+
234
+ if (!isPlainObject(raw)) {
235
+ return null;
236
+ }
237
+
238
+ const candidateName = [
239
+ raw.name,
240
+ raw.displayName,
241
+ raw.id,
242
+ raw.title,
243
+ raw.fileName,
244
+ raw.directory,
245
+ raw.path,
246
+ fallback.name
247
+ ].map(normalizeContentText).find(Boolean) || '';
248
+ const itemPath = normalizeRepoPath(raw.path || raw.file || raw.fileName || raw.directory || fallback.path || '');
249
+ const description = normalizeContentText(raw.description || raw.prompt || raw.usage || fallback.description || '');
250
+ const derivedName = candidateName || (itemPath ? path.posix.basename(itemPath, path.posix.extname(itemPath)) : '');
251
+
252
+ if (!derivedName && !itemPath) {
253
+ return null;
254
+ }
255
+
256
+ return {
257
+ name: derivedName,
258
+ path: itemPath,
259
+ description
260
+ };
261
+ }
262
+
263
+ function normalizeContentCollection(value, fallback = {}) {
264
+ if (Array.isArray(value)) {
265
+ return value.map(item => normalizeContentEntry(item, fallback)).filter(Boolean);
266
+ }
267
+
268
+ if (isPlainObject(value)) {
269
+ return Object.entries(value)
270
+ .map(([key, item]) => {
271
+ if (isPlainObject(item)) {
272
+ return normalizeContentEntry({
273
+ ...item,
274
+ name: item.name || key
275
+ }, fallback);
276
+ }
277
+
278
+ if (typeof item === 'string') {
279
+ return normalizeContentEntry({
280
+ name: key,
281
+ description: item
282
+ }, fallback);
283
+ }
284
+
285
+ return normalizeContentEntry({ name: key }, fallback);
286
+ })
287
+ .filter(Boolean);
288
+ }
289
+
290
+ return [];
291
+ }
292
+
293
+ function mergeContentCollections(...collections) {
294
+ const merged = [];
295
+ const seen = new Set();
296
+
297
+ for (const collection of collections) {
298
+ for (const rawEntry of collection || []) {
299
+ const entry = normalizeContentEntry(rawEntry);
300
+ if (!entry) continue;
301
+
302
+ const normalizedPath = normalizeRepoPath(entry.path || '');
303
+ const normalizedName = normalizeContentText(entry.name || '');
304
+ const key = normalizedPath
305
+ ? `path:${normalizedPath.toLowerCase()}`
306
+ : (normalizedName ? `name:${normalizedName.toLowerCase()}` : '');
307
+
308
+ if (!key || seen.has(key)) {
309
+ continue;
310
+ }
311
+
312
+ seen.add(key);
313
+ merged.push({
314
+ name: normalizedName || path.posix.basename(normalizedPath, path.posix.extname(normalizedPath)),
315
+ path: normalizedPath,
316
+ description: normalizeContentText(entry.description || '')
317
+ });
318
+ }
319
+ }
320
+
321
+ return merged.sort((a, b) => {
322
+ const left = a.name || a.path;
323
+ const right = b.name || b.path;
324
+ return left.localeCompare(right);
325
+ });
326
+ }
327
+
211
328
  class PluginsService {
212
329
  constructor(platform = 'claude') {
213
330
  this.platform = ['claude', 'opencode'].includes(platform) ? platform : 'claude';
@@ -221,6 +338,202 @@ class PluginsService {
221
338
  this._marketCache = null;
222
339
  }
223
340
 
341
+ _createEmptyContentSummary() {
342
+ return {
343
+ skills: [],
344
+ agents: [],
345
+ commands: [],
346
+ hooks: [],
347
+ files: [],
348
+ totalFiles: 0,
349
+ additionalFileCount: 0
350
+ };
351
+ }
352
+
353
+ _getManifestCollection(manifest = null, paths = []) {
354
+ if (!isPlainObject(manifest)) {
355
+ return [];
356
+ }
357
+
358
+ return mergeContentCollections(
359
+ ...paths.map(segments => normalizeContentCollection(getNestedValue(manifest, segments)))
360
+ );
361
+ }
362
+
363
+ _summarizeManifestContent(manifest = null) {
364
+ const emptySummary = this._createEmptyContentSummary();
365
+ if (!isPlainObject(manifest)) {
366
+ return emptySummary;
367
+ }
368
+
369
+ return {
370
+ ...emptySummary,
371
+ skills: this._getManifestCollection(manifest, [
372
+ ['skills'],
373
+ ['ctx', 'skills'],
374
+ ['contributes', 'skills']
375
+ ]),
376
+ agents: this._getManifestCollection(manifest, [
377
+ ['agents'],
378
+ ['ctx', 'agents'],
379
+ ['contributes', 'agents']
380
+ ]),
381
+ commands: this._getManifestCollection(manifest, [
382
+ ['commands'],
383
+ ['ctx', 'commands'],
384
+ ['contributes', 'commands']
385
+ ]),
386
+ hooks: this._getManifestCollection(manifest, [
387
+ ['hooks'],
388
+ ['ctx', 'hooks'],
389
+ ['contributes', 'hooks']
390
+ ])
391
+ };
392
+ }
393
+
394
+ _summarizeContentFromFiles(files = [], directory = '') {
395
+ const emptySummary = this._createEmptyContentSummary();
396
+ const normalizedDirectory = normalizeRepoPath(directory);
397
+ const directoryLooksLikeFile = /\.[a-z0-9]+$/i.test(path.posix.basename(normalizedDirectory || ''));
398
+ const allFiles = (Array.isArray(files) ? files : [])
399
+ .filter(item => item && item.type === 'blob' && item.path)
400
+ .map(item => ({
401
+ ...item,
402
+ path: normalizeRepoPath(item.path)
403
+ }))
404
+ .sort((a, b) => a.path.localeCompare(b.path));
405
+
406
+ let scopedFiles = allFiles;
407
+ if (normalizedDirectory) {
408
+ if (directoryLooksLikeFile) {
409
+ scopedFiles = allFiles.filter(item => item.path === normalizedDirectory);
410
+ } else {
411
+ const directoryPrefix = `${normalizedDirectory}/`;
412
+ scopedFiles = allFiles.filter(item => item.path.startsWith(directoryPrefix));
413
+ }
414
+ }
415
+
416
+ const toScopedPath = (filePath = '') => {
417
+ if (!normalizedDirectory || directoryLooksLikeFile) {
418
+ return normalizeRepoPath(filePath);
419
+ }
420
+ const prefix = `${normalizedDirectory}/`;
421
+ return filePath.startsWith(prefix) ? filePath.slice(prefix.length) : filePath;
422
+ };
423
+
424
+ const skillEntries = [];
425
+ const agentEntries = [];
426
+ const commandEntries = [];
427
+
428
+ for (const file of scopedFiles) {
429
+ const scopedPath = toScopedPath(file.path);
430
+ const skillMatch = scopedPath.match(/^skills\/([^/]+)\/SKILL\.md$/i);
431
+ if (skillMatch) {
432
+ skillEntries.push({ name: skillMatch[1], path: scopedPath });
433
+ }
434
+
435
+ const agentMatch = scopedPath.match(/^agents\/(.+)\.md$/i);
436
+ if (agentMatch) {
437
+ agentEntries.push({
438
+ name: agentMatch[1].split('/').pop(),
439
+ path: scopedPath
440
+ });
441
+ }
442
+
443
+ const commandMatch = scopedPath.match(/^(?:commands|prompts)\/(.+)\.md$/i);
444
+ if (commandMatch) {
445
+ commandEntries.push({
446
+ name: commandMatch[1].split('/').pop(),
447
+ path: scopedPath
448
+ });
449
+ }
450
+ }
451
+
452
+ const filePreview = scopedFiles.slice(0, 8).map(file => {
453
+ const scopedPath = toScopedPath(file.path);
454
+ return {
455
+ name: path.posix.basename(scopedPath),
456
+ path: scopedPath
457
+ };
458
+ });
459
+
460
+ return {
461
+ ...emptySummary,
462
+ skills: mergeContentCollections(skillEntries),
463
+ agents: mergeContentCollections(agentEntries),
464
+ commands: mergeContentCollections(commandEntries),
465
+ files: filePreview,
466
+ totalFiles: scopedFiles.length,
467
+ additionalFileCount: Math.max(0, scopedFiles.length - filePreview.length)
468
+ };
469
+ }
470
+
471
+ _mergeContentSummary(...summaries) {
472
+ const emptySummary = this._createEmptyContentSummary();
473
+ const validSummaries = summaries.filter(Boolean);
474
+ const fileSummary = validSummaries.find(summary => Array.isArray(summary.files) && summary.files.length > 0) || emptySummary;
475
+
476
+ return {
477
+ ...emptySummary,
478
+ skills: mergeContentCollections(...validSummaries.map(summary => summary.skills)),
479
+ agents: mergeContentCollections(...validSummaries.map(summary => summary.agents)),
480
+ commands: mergeContentCollections(...validSummaries.map(summary => summary.commands)),
481
+ hooks: mergeContentCollections(...validSummaries.map(summary => summary.hooks)),
482
+ files: fileSummary.files || [],
483
+ totalFiles: fileSummary.totalFiles || 0,
484
+ additionalFileCount: fileSummary.additionalFileCount || 0
485
+ };
486
+ }
487
+
488
+ _scanInstallPath(installPath = '') {
489
+ const resolvedPath = String(installPath || '').trim();
490
+ if (!resolvedPath || !fs.existsSync(resolvedPath)) {
491
+ return [];
492
+ }
493
+
494
+ try {
495
+ const stat = fs.statSync(resolvedPath);
496
+ if (stat.isFile()) {
497
+ const fileName = path.basename(resolvedPath);
498
+ return [{ path: fileName, type: 'blob', name: fileName }];
499
+ }
500
+
501
+ const tree = [];
502
+ this.scanLocalRepoTree(resolvedPath, resolvedPath, tree);
503
+ return tree;
504
+ } catch {
505
+ return [];
506
+ }
507
+ }
508
+
509
+ _readPluginManifestFromInstallPath(installPath = '') {
510
+ const resolvedPath = String(installPath || '').trim();
511
+ if (!resolvedPath || !fs.existsSync(resolvedPath)) {
512
+ return null;
513
+ }
514
+
515
+ try {
516
+ const stat = fs.statSync(resolvedPath);
517
+ if (stat.isFile()) {
518
+ if (!resolvedPath.endsWith('.json')) {
519
+ return null;
520
+ }
521
+ return JSON.parse(stripJsonComments(fs.readFileSync(resolvedPath, 'utf8')));
522
+ }
523
+
524
+ const candidates = ['plugin.json', 'package.json'];
525
+ for (const candidate of candidates) {
526
+ const manifestPath = path.join(resolvedPath, candidate);
527
+ if (!fs.existsSync(manifestPath)) continue;
528
+ return JSON.parse(stripJsonComments(fs.readFileSync(manifestPath, 'utf8')));
529
+ }
530
+ } catch {
531
+ return null;
532
+ }
533
+
534
+ return null;
535
+ }
536
+
224
537
  normalizeRepoConfig(repo = {}) {
225
538
  const provider = SUPPORTED_REPO_PROVIDERS.includes(repo.provider)
226
539
  ? repo.provider
@@ -719,7 +1032,173 @@ class PluginsService {
719
1032
  description: manifest?.description || '',
720
1033
  author: manifest?.author || '',
721
1034
  commands: manifest?.commands || [],
722
- hooks: manifest?.hooks || [],
1035
+ hooks: manifest?.hooks || manifest?.ctx?.hooks || [],
1036
+ manifest
1037
+ };
1038
+ }
1039
+
1040
+ resolvePluginRepo(plugin = {}) {
1041
+ let repo = null;
1042
+
1043
+ if (plugin.repoProvider || plugin.repoLocalPath || plugin.repoProjectPath || plugin.repoOwner) {
1044
+ try {
1045
+ repo = this.normalizeRepoConfig({
1046
+ id: plugin.repoId,
1047
+ provider: plugin.repoProvider,
1048
+ host: plugin.repoHost,
1049
+ owner: plugin.repoOwner,
1050
+ name: plugin.repoName,
1051
+ branch: plugin.repoBranch || 'main',
1052
+ projectPath: plugin.repoProjectPath,
1053
+ localPath: plugin.repoLocalPath,
1054
+ repoUrl: plugin.repoUrl
1055
+ });
1056
+ } catch {
1057
+ repo = null;
1058
+ }
1059
+ } else if (plugin.source) {
1060
+ repo = this.parseRepoTreeSource(plugin.source);
1061
+ }
1062
+
1063
+ if (!repo && plugin.repoUrl) {
1064
+ const parsedTreeSource = this.parseRepoTreeSource(plugin.repoUrl);
1065
+ if (parsedTreeSource) {
1066
+ repo = parsedTreeSource;
1067
+ } else if (plugin.repoUrl.includes('github.com')) {
1068
+ const match = plugin.repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
1069
+ if (match) {
1070
+ try {
1071
+ repo = this.normalizeRepoConfig({
1072
+ provider: 'github',
1073
+ owner: match[1],
1074
+ name: match[2],
1075
+ branch: plugin.repoBranch || 'main'
1076
+ });
1077
+ } catch {
1078
+ repo = null;
1079
+ }
1080
+ }
1081
+ }
1082
+ }
1083
+
1084
+ return repo;
1085
+ }
1086
+
1087
+ async _readPluginManifestFromRepo(plugin = {}, repo = null) {
1088
+ const resolvedRepo = repo || this.resolvePluginRepo(plugin);
1089
+ if (!resolvedRepo) {
1090
+ return null;
1091
+ }
1092
+
1093
+ const normalizedDirectory = normalizeRepoPath(plugin.directory || '');
1094
+ const candidates = [];
1095
+
1096
+ if (normalizedDirectory) {
1097
+ if (/\.[a-z0-9]+$/i.test(path.posix.basename(normalizedDirectory))) {
1098
+ candidates.push(normalizedDirectory);
1099
+ } else {
1100
+ candidates.push(`${normalizedDirectory}/plugin.json`, `${normalizedDirectory}/package.json`);
1101
+ }
1102
+ }
1103
+
1104
+ candidates.push('plugin.json', 'package.json');
1105
+
1106
+ for (const candidate of Array.from(new Set(candidates))) {
1107
+ try {
1108
+ return await this.fetchRepoJson(resolvedRepo, candidate);
1109
+ } catch {
1110
+ // try next candidate
1111
+ }
1112
+ }
1113
+
1114
+ return null;
1115
+ }
1116
+
1117
+ async getPluginDetail(pluginInput = {}) {
1118
+ const requestedPlugin = typeof pluginInput === 'string'
1119
+ ? { name: pluginInput }
1120
+ : { ...(pluginInput || {}) };
1121
+ const requestedName = String(requestedPlugin.name || '').trim();
1122
+
1123
+ if (!requestedName && !requestedPlugin.directory) {
1124
+ return null;
1125
+ }
1126
+
1127
+ const installedPlugin = this.listPlugins().plugins.find(plugin => {
1128
+ if (requestedName && (plugin.name === requestedName || plugin.directory === requestedName)) {
1129
+ return true;
1130
+ }
1131
+ if (requestedPlugin.installPath && plugin.installPath === requestedPlugin.installPath) {
1132
+ return true;
1133
+ }
1134
+ return Boolean(
1135
+ requestedPlugin.repoId &&
1136
+ plugin.repoId === requestedPlugin.repoId &&
1137
+ plugin.directory === requestedPlugin.directory
1138
+ );
1139
+ }) || null;
1140
+ const legacyPlugin = (!this._isOpenCode() && requestedName) ? getPlugin(requestedName) : null;
1141
+
1142
+ const mergedPlugin = {
1143
+ ...requestedPlugin,
1144
+ ...(installedPlugin || {}),
1145
+ ...(legacyPlugin || {})
1146
+ };
1147
+ mergedPlugin.name = requestedName || mergedPlugin.name || mergedPlugin.directory || '';
1148
+
1149
+ if (!mergedPlugin.name) {
1150
+ return null;
1151
+ }
1152
+
1153
+ const fallbackInstallPath = (!mergedPlugin.installPath && !this._isOpenCode())
1154
+ ? path.join(INSTALLED_DIR, mergedPlugin.name)
1155
+ : '';
1156
+ const installPath = mergedPlugin.installPath || (fallbackInstallPath && fs.existsSync(fallbackInstallPath) ? fallbackInstallPath : '');
1157
+
1158
+ let manifest = this._readPluginManifestFromInstallPath(installPath);
1159
+ const repo = this.resolvePluginRepo(mergedPlugin);
1160
+ if (!manifest) {
1161
+ manifest = await this._readPluginManifestFromRepo(mergedPlugin, repo);
1162
+ }
1163
+
1164
+ let fileSummary = this._createEmptyContentSummary();
1165
+ if (installPath && fs.existsSync(installPath)) {
1166
+ fileSummary = this._summarizeContentFromFiles(this._scanInstallPath(installPath));
1167
+ } else if (repo) {
1168
+ try {
1169
+ const repoTree = await this.fetchRepoTree(repo);
1170
+ fileSummary = this._summarizeContentFromFiles(repoTree, mergedPlugin.directory || '');
1171
+ } catch {
1172
+ fileSummary = this._createEmptyContentSummary();
1173
+ }
1174
+ }
1175
+
1176
+ const manifestSummary = this._summarizeManifestContent(manifest);
1177
+ const contentSummary = this._mergeContentSummary(manifestSummary, fileSummary);
1178
+ const commands = mergeContentCollections(
1179
+ contentSummary.commands,
1180
+ normalizeContentCollection(mergedPlugin.commands)
1181
+ );
1182
+ const hooks = mergeContentCollections(
1183
+ contentSummary.hooks,
1184
+ normalizeContentCollection(mergedPlugin.hooks)
1185
+ );
1186
+ const installed = mergedPlugin.installed !== undefined
1187
+ ? !!mergedPlugin.installed
1188
+ : Boolean(mergedPlugin.isInstalled || installPath || installedPlugin);
1189
+
1190
+ return {
1191
+ ...mergedPlugin,
1192
+ installPath,
1193
+ installed,
1194
+ description: mergedPlugin.description || manifest?.description || '',
1195
+ author: mergedPlugin.author || manifest?.author || '',
1196
+ version: mergedPlugin.version || manifest?.version || (this._isOpenCode() ? 'latest' : '1.0.0'),
1197
+ commands,
1198
+ hooks,
1199
+ skills: contentSummary.skills,
1200
+ agents: contentSummary.agents,
1201
+ contentSummary,
723
1202
  manifest
724
1203
  };
725
1204
  }
@@ -1892,44 +2371,7 @@ class PluginsService {
1892
2371
  }
1893
2372
  }
1894
2373
 
1895
- let repo = null;
1896
- if (plugin.repoProvider || plugin.repoLocalPath || plugin.repoProjectPath || plugin.repoOwner) {
1897
- try {
1898
- repo = this.normalizeRepoConfig({
1899
- id: plugin.repoId,
1900
- provider: plugin.repoProvider,
1901
- host: plugin.repoHost,
1902
- owner: plugin.repoOwner,
1903
- name: plugin.repoName,
1904
- branch: plugin.repoBranch || 'main',
1905
- projectPath: plugin.repoProjectPath,
1906
- localPath: plugin.repoLocalPath,
1907
- repoUrl: plugin.repoUrl
1908
- });
1909
- } catch {
1910
- repo = null;
1911
- }
1912
- } else if (plugin.source) {
1913
- repo = this.parseRepoTreeSource(plugin.source);
1914
- }
1915
-
1916
- if (!repo && plugin.repoUrl) {
1917
- const parsedTreeSource = this.parseRepoTreeSource(plugin.repoUrl);
1918
- if (parsedTreeSource) {
1919
- repo = parsedTreeSource;
1920
- } else if (plugin.repoUrl.includes('github.com')) {
1921
- const match = plugin.repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
1922
- if (match) {
1923
- repo = this.normalizeRepoConfig({
1924
- provider: 'github',
1925
- owner: match[1],
1926
- name: match[2],
1927
- branch: plugin.repoBranch || 'main'
1928
- });
1929
- }
1930
- }
1931
- }
1932
-
2374
+ const repo = this.resolvePluginRepo(plugin);
1933
2375
  if (!repo) return '';
1934
2376
 
1935
2377
  for (const candidate of readmeCandidates) {
@@ -1980,6 +2422,7 @@ class PluginsService {
1980
2422
  lspServers: data.lspServers || null,
1981
2423
  commands: data.commands || [],
1982
2424
  hooks: data.hooks || [],
2425
+ contentSummary: data.contentSummary || this._createEmptyContentSummary(),
1983
2426
  isInstalled: false
1984
2427
  };
1985
2428
  }
@@ -2019,7 +2462,11 @@ class PluginsService {
2019
2462
  directory: file.path,
2020
2463
  installSource: githubRepo ? '' : installSource,
2021
2464
  marketplaceFormat: 'opencode-plugin-json',
2022
- repoUrl
2465
+ repoUrl,
2466
+ contentSummary: this._mergeContentSummary(
2467
+ this._summarizeManifestContent(manifest),
2468
+ this._summarizeContentFromFiles(tree, file.path)
2469
+ )
2023
2470
  });
2024
2471
  })
2025
2472
  );
@@ -2086,7 +2533,11 @@ class PluginsService {
2086
2533
  version: plugin.version || '1.0.0',
2087
2534
  category: plugin.category || 'general',
2088
2535
  directory: plugin.source?.replace(/^\.\//, '') || plugin.name,
2089
- lspServers: plugin.lspServers || null
2536
+ lspServers: plugin.lspServers || null,
2537
+ contentSummary: this._summarizeContentFromFiles(
2538
+ files,
2539
+ plugin.source?.replace(/^\.\//, '') || plugin.name
2540
+ )
2090
2541
  }));
2091
2542
  }
2092
2543
  continue; // Skip legacy format check
@@ -2112,6 +2563,8 @@ class PluginsService {
2112
2563
  for (const dir of pluginDirs) {
2113
2564
  try {
2114
2565
  const manifest = await readJson(`${dir}/plugin.json`);
2566
+ const manifestSummary = this._summarizeManifestContent(manifest);
2567
+ const fileSummary = this._summarizeContentFromFiles(files, dir);
2115
2568
 
2116
2569
  marketPlugins.push(this.buildMarketPluginItem(repo, {
2117
2570
  name: manifest.name || dir,
@@ -2119,8 +2572,9 @@ class PluginsService {
2119
2572
  author: manifest.author || repo.owner,
2120
2573
  version: manifest.version || '1.0.0',
2121
2574
  directory: dir,
2122
- commands: manifest.commands || [],
2123
- hooks: manifest.hooks || []
2575
+ commands: manifestSummary.commands,
2576
+ hooks: manifestSummary.hooks,
2577
+ contentSummary: this._mergeContentSummary(manifestSummary, fileSummary)
2124
2578
  }));
2125
2579
  } catch (e) {
2126
2580
  // OpenCode 仓库常见 package.json 格式
@@ -2132,7 +2586,11 @@ class PluginsService {
2132
2586
  description: pkg.description || '',
2133
2587
  author: pkg.author || repo.owner,
2134
2588
  version: pkg.version || '1.0.0',
2135
- directory: dir
2589
+ directory: dir,
2590
+ contentSummary: this._mergeContentSummary(
2591
+ this._summarizeManifestContent(pkg),
2592
+ this._summarizeContentFromFiles(files, dir)
2593
+ )
2136
2594
  }));
2137
2595
  } catch (pkgErr) {
2138
2596
  // neither plugin.json nor package.json
@@ -0,0 +1,81 @@
1
+ const { isWindowsLikePlatform } = require('../../utils/home-dir');
2
+
3
+ function escapeForDoubleQuotes(value) {
4
+ return String(value).replace(/"/g, '\\"');
5
+ }
6
+
7
+ function escapeForPowerShellSingleQuotes(value) {
8
+ return String(value).replace(/'/g, "''");
9
+ }
10
+
11
+ function buildDisplayCommand(executable, args = []) {
12
+ return [String(executable || ''), ...args.map(arg => String(arg))].filter(Boolean).join(' ').trim();
13
+ }
14
+
15
+ function buildWindowsCopyCommand(cwd, executable, args = []) {
16
+ const quotedCwd = `'${escapeForPowerShellSingleQuotes(cwd)}'`;
17
+ const quotedExecutable = `'${escapeForPowerShellSingleQuotes(executable)}'`;
18
+ const quotedArgs = args.map(arg => `'${escapeForPowerShellSingleQuotes(arg)}'`).join(' ');
19
+ const invokeCommand = quotedArgs
20
+ ? `& ${quotedExecutable} ${quotedArgs}`
21
+ : `& ${quotedExecutable}`;
22
+
23
+ return `powershell -NoProfile -ExecutionPolicy Bypass -Command "& { Set-Location -LiteralPath ${quotedCwd}; ${invokeCommand} }"`;
24
+ }
25
+
26
+ function buildPosixCopyCommand(cwd, command) {
27
+ const quotedCwd = `"${escapeForDoubleQuotes(cwd)}"`;
28
+ return `cd ${quotedCwd} && ${command}`;
29
+ }
30
+
31
+ function buildCopyCommand({
32
+ cwd,
33
+ command,
34
+ executable,
35
+ args = [],
36
+ runtimePlatform = process.platform,
37
+ runtimeEnv = process.env
38
+ }) {
39
+ const resolvedCommand = command || buildDisplayCommand(executable, args);
40
+ if (!cwd) {
41
+ return resolvedCommand;
42
+ }
43
+
44
+ if (isWindowsLikePlatform(runtimePlatform, runtimeEnv)) {
45
+ return buildWindowsCopyCommand(cwd, executable, args);
46
+ }
47
+
48
+ return buildPosixCopyCommand(cwd, resolvedCommand);
49
+ }
50
+
51
+ function buildLaunchCommand({
52
+ cwd,
53
+ executable,
54
+ args = [],
55
+ runtimePlatform = process.platform,
56
+ runtimeEnv = process.env
57
+ }) {
58
+ const command = buildDisplayCommand(executable, args);
59
+ return {
60
+ command,
61
+ copyCommand: buildCopyCommand({
62
+ cwd,
63
+ command,
64
+ executable,
65
+ args,
66
+ runtimePlatform,
67
+ runtimeEnv
68
+ })
69
+ };
70
+ }
71
+
72
+ module.exports = {
73
+ buildLaunchCommand,
74
+ _test: {
75
+ buildDisplayCommand,
76
+ buildCopyCommand,
77
+ buildWindowsCopyCommand,
78
+ buildPosixCopyCommand,
79
+ escapeForPowerShellSingleQuotes
80
+ }
81
+ };