itismyskillmarket 1.3.20 → 1.3.22

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/index.js CHANGED
@@ -2081,6 +2081,8 @@ API_ROUTES.GET["/api/skills"] = async (_req, res, url) => {
2081
2081
  const page = Math.max(1, parseInt(url.searchParams.get("page") || "1"));
2082
2082
  const limit = Math.min(100, Math.max(1, parseInt(url.searchParams.get("limit") || "20")));
2083
2083
  const search = url.searchParams.get("search") || "";
2084
+ const sort = url.searchParams.get("sort") || "name";
2085
+ const platform = url.searchParams.get("platform") || "";
2084
2086
  const cacheKey = `search:${search}:limit:${limit}`;
2085
2087
  let searchResult = getCached(cacheKey);
2086
2088
  if (!searchResult) {
@@ -2118,16 +2120,38 @@ API_ROUTES.GET["/api/skills"] = async (_req, res, url) => {
2118
2120
  platforms: meta?.platforms || [],
2119
2121
  author: info.author?.name || pkg?.author?.name || "",
2120
2122
  homepage: pkg?.homepage || "",
2121
- repository: pkg?.repository?.url || ""
2123
+ repository: pkg?.repository?.url || "",
2124
+ updated: info.time?.[latestVersion] || info.time?.modified || ""
2122
2125
  };
2123
2126
  } catch {
2124
2127
  fetchErrors++;
2125
2128
  return null;
2126
2129
  }
2127
2130
  }, 3);
2128
- const skills = skillDetails.filter(Boolean);
2129
- const totalPages = Math.ceil(total / limit) || 1;
2130
- jsonResponse(res, 200, { skills, page, totalPages, total, fetchErrors });
2131
+ let skills = skillDetails.filter(Boolean);
2132
+ if (platform) {
2133
+ skills = skills.filter(
2134
+ (s) => Array.isArray(s.platforms) && s.platforms.includes(platform)
2135
+ );
2136
+ }
2137
+ skills.sort((a, b) => {
2138
+ const nameA = (a.displayName || a.id || "").toLowerCase();
2139
+ const nameB = (b.displayName || b.id || "").toLowerCase();
2140
+ switch (sort) {
2141
+ case "-name":
2142
+ return nameB.localeCompare(nameA);
2143
+ case "updated":
2144
+ return (a.updated || "").localeCompare(b.updated || "");
2145
+ case "-updated":
2146
+ return (b.updated || "").localeCompare(a.updated || "");
2147
+ case "name":
2148
+ default:
2149
+ return nameA.localeCompare(nameB);
2150
+ }
2151
+ });
2152
+ const filteredTotal = skills.length;
2153
+ const totalPages = Math.ceil(filteredTotal / limit) || 1;
2154
+ jsonResponse(res, 200, { skills, page, totalPages, total: filteredTotal, fetchErrors });
2131
2155
  } catch (err) {
2132
2156
  jsonResponse(res, 500, {
2133
2157
  error: String(err),
@@ -2164,7 +2188,8 @@ API_ROUTES.GET["/api/platforms"] = async (_req, res, _url) => {
2164
2188
  id: adapter.id,
2165
2189
  name: adapter.name,
2166
2190
  available: !!isAvailable,
2167
- installedCount: Array.isArray(installed) ? installed.length : 0
2191
+ installedCount: Array.isArray(installed) ? installed.length : 0,
2192
+ installedSkills: Array.isArray(installed) ? installed : []
2168
2193
  };
2169
2194
  })
2170
2195
  );
@@ -2173,6 +2198,34 @@ API_ROUTES.GET["/api/platforms"] = async (_req, res, _url) => {
2173
2198
  jsonResponse(res, 500, { error: String(err) });
2174
2199
  }
2175
2200
  };
2201
+ API_ROUTES.GET["/api/platform-info"] = async (_req, res, url) => {
2202
+ try {
2203
+ const platformId = url.searchParams.get("id") || "";
2204
+ if (!platformId) {
2205
+ jsonResponse(res, 400, { error: 'Missing "id" query parameter' });
2206
+ return;
2207
+ }
2208
+ const allAdapters = getAllAdapters();
2209
+ const adapter = allAdapters.find((a) => a.id === platformId);
2210
+ if (!adapter) {
2211
+ jsonResponse(res, 404, { error: `Platform "${platformId}" not found` });
2212
+ return;
2213
+ }
2214
+ const available = await detectPlatforms();
2215
+ const isAvailable = available.find((a) => a.id === adapter.id);
2216
+ const installed = await adapter.listInstalled();
2217
+ const installedSkills = Array.isArray(installed) ? installed : [];
2218
+ jsonResponse(res, 200, {
2219
+ id: adapter.id,
2220
+ name: adapter.name,
2221
+ available: !!isAvailable,
2222
+ installedCount: installedSkills.length,
2223
+ installedSkills
2224
+ });
2225
+ } catch (err) {
2226
+ jsonResponse(res, 500, { error: String(err) });
2227
+ }
2228
+ };
2176
2229
  API_ROUTES.GET["/api/skill-info"] = async (_req, res, url) => {
2177
2230
  try {
2178
2231
  const skillName = url.searchParams.get("skill") || "";
@@ -2213,6 +2266,15 @@ API_ROUTES.GET["/api/skill-info"] = async (_req, res, url) => {
2213
2266
  jsonResponse(res, 500, { error: String(err) });
2214
2267
  }
2215
2268
  };
2269
+ API_ROUTES.GET["/api/version"] = async (_req, res, _url) => {
2270
+ try {
2271
+ const pkgPath = join4(__dirname2, "..", "package.json");
2272
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
2273
+ jsonResponse(res, 200, { version: pkg.version || "1.0.0" });
2274
+ } catch {
2275
+ jsonResponse(res, 200, { version: "1.0.0" });
2276
+ }
2277
+ };
2216
2278
  API_ROUTES.GET["/api/config"] = async (_req, res, _url) => {
2217
2279
  jsonResponse(res, 200, {
2218
2280
  npmScope: NPM_SCOPE,
package/gui/app.js CHANGED
@@ -11,6 +11,8 @@ const state = {
11
11
  currentPage: 1,
12
12
  pageSize: 20,
13
13
  searchQuery: '',
14
+ sortBy: 'name',
15
+ platformFilter: '',
14
16
  totalPages: 1,
15
17
  previousView: 'skills',
16
18
  };
@@ -470,6 +472,23 @@ function applyI18nToStaticElements() {
470
472
  if (backBtn) backBtn.innerHTML = `← ${t('nav.back')}`;
471
473
  }
472
474
 
475
+ // -----------------------------------------------------------------------------
476
+ // 版本号加载
477
+ // -----------------------------------------------------------------------------
478
+
479
+ async function loadVersion() {
480
+ try {
481
+ const response = await fetch('/api/version');
482
+ const data = await response.json();
483
+ const el = document.getElementById('gui-version');
484
+ if (el && data.version) {
485
+ el.textContent = `v${data.version}`;
486
+ }
487
+ } catch {
488
+ // 静默失败,保留 HTML 中的占位符
489
+ }
490
+ }
491
+
473
492
  // 重新渲染当前视图
474
493
  function reRenderCurrentView() {
475
494
  // 更新语言选项
@@ -508,6 +527,7 @@ document.addEventListener('DOMContentLoaded', () => {
508
527
  initializeNavigation();
509
528
  initializeControls();
510
529
  initializeCollapsibleSections();
530
+ loadVersion();
511
531
  loadSkills();
512
532
  });
513
533
 
@@ -561,8 +581,8 @@ function switchView(view) {
561
581
  targetView.classList.add('active');
562
582
  }
563
583
 
564
- // 加载对应数据 (skill-detail 视图的数据由 showSkillDetail 加载)
565
- if (view !== 'skill-detail') {
584
+ // 加载对应数据 (skill-detail / platform-detail 视图的数据由各自函数加载)
585
+ if (view !== 'skill-detail' && view !== 'platform-detail') {
566
586
  switch(view) {
567
587
  case 'skills':
568
588
  loadSkills();
@@ -608,6 +628,26 @@ function initializeControls() {
608
628
  loadSkills();
609
629
  });
610
630
 
631
+ // 排序
632
+ const sortSelect = document.getElementById('sort-select');
633
+ if (sortSelect) {
634
+ sortSelect.addEventListener('change', () => {
635
+ state.sortBy = sortSelect.value;
636
+ state.currentPage = 1;
637
+ loadSkills();
638
+ });
639
+ }
640
+
641
+ // 平台分类过滤
642
+ const platformFilter = document.getElementById('platform-filter');
643
+ if (platformFilter) {
644
+ platformFilter.addEventListener('change', () => {
645
+ state.platformFilter = platformFilter.value;
646
+ state.currentPage = 1;
647
+ loadSkills();
648
+ });
649
+ }
650
+
611
651
  // 刷新按钮
612
652
  document.getElementById('refresh-skills').addEventListener('click', () => loadSkills());
613
653
  document.getElementById('update-all').addEventListener('click', updateAllSkills);
@@ -651,11 +691,15 @@ async function loadSkills() {
651
691
  const params = new URLSearchParams({
652
692
  page: state.currentPage.toString(),
653
693
  limit: state.pageSize.toString(),
694
+ sort: state.sortBy,
654
695
  });
655
696
 
656
697
  if (state.searchQuery) {
657
698
  params.append('search', state.searchQuery);
658
699
  }
700
+ if (state.platformFilter) {
701
+ params.append('platform', state.platformFilter);
702
+ }
659
703
 
660
704
  const response = await fetch(`/api/skills?${params}`);
661
705
  const data = await response.json();
@@ -668,6 +712,7 @@ async function loadSkills() {
668
712
  renderSkills(data.skills || data, container);
669
713
  renderPagination(data.page, data.totalPages || 1);
670
714
  renderFetchWarning(data.fetchErrors);
715
+ updatePlatformFilterOptions(data.skills || []);
671
716
  } catch (err) {
672
717
  container.innerHTML = `<div class="loading">${t('error.generic')}: ${err.message}</div>`;
673
718
  }
@@ -745,6 +790,34 @@ function renderFetchWarning(fetchErrors) {
745
790
  // 分页
746
791
  // -----------------------------------------------------------------------------
747
792
 
793
+ /** 收集所有可用平台,更新分类过滤下拉框 */
794
+ function updatePlatformFilterOptions(skills) {
795
+ const select = document.getElementById('platform-filter');
796
+ if (!select) return;
797
+
798
+ // 收集所有唯一的平台名
799
+ const allPlatforms = new Set();
800
+ (skills || []).forEach(s => {
801
+ if (Array.isArray(s.platforms)) {
802
+ s.platforms.forEach(p => allPlatforms.add(p));
803
+ }
804
+ });
805
+
806
+ const currentVal = select.value;
807
+ const sortedPlatforms = [...allPlatforms].sort();
808
+
809
+ // 只有当平台列表有变化时才重新渲染
810
+ const currentOptions = Array.from(select.options).slice(1).map(o => o.value).sort().join(',');
811
+ const newOptions = sortedPlatforms.join(',');
812
+ if (currentOptions === newOptions) return;
813
+
814
+ select.innerHTML = `
815
+ <option value="">All Platforms</option>
816
+ ${sortedPlatforms.map(p => `<option value="${p}"${currentVal === p ? ' selected' : ''}>${p}</option>`).join('')}
817
+ `;
818
+ select.value = currentVal && allPlatforms.has(currentVal) ? currentVal : '';
819
+ }
820
+
748
821
  function renderPagination(currentPage, totalPages) {
749
822
  state.currentPage = currentPage;
750
823
  state.totalPages = totalPages;
@@ -801,7 +874,7 @@ function renderPlatforms(platforms, container) {
801
874
  }
802
875
 
803
876
  container.innerHTML = platforms.map(platform => `
804
- <div class="platform-card">
877
+ <div class="platform-card" onclick="showPlatformDetail('${platform.id}')" style="cursor: pointer;">
805
878
  <div>
806
879
  <h3>${platform.name}</h3>
807
880
  <div class="status ${platform.available ? 'status-available' : 'status-unavailable'}">
@@ -809,12 +882,86 @@ function renderPlatforms(platforms, container) {
809
882
  </div>
810
883
  </div>
811
884
  <div>
812
- ${platform.installedCount ? `<span>${t('status.skillsInstalled', { count: platform.installedCount })}</span>` : ''}
885
+ ${platform.installedCount ? `<span>${t('status.skillsInstalled', { count: platform.installedCount })}</span>` : '<span style="color: var(--text-muted); font-size: 0.85rem;">0 installed</span>'}
813
886
  </div>
814
887
  </div>
815
888
  `).join('');
816
889
  }
817
890
 
891
+ // -----------------------------------------------------------------------------
892
+ // Platform 详情视图
893
+ // -----------------------------------------------------------------------------
894
+
895
+ async function showPlatformDetail(platformId) {
896
+ const content = document.getElementById('platform-detail-content');
897
+ content.innerHTML = `<div class="loading">${t('loading.generic')}</div>`;
898
+
899
+ state.previousView = state.currentView;
900
+ state.currentView = 'platform-detail';
901
+
902
+ document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
903
+ const targetView = document.getElementById('view-platform-detail');
904
+ if (targetView) targetView.classList.add('active');
905
+
906
+ try {
907
+ const response = await fetch(`/api/platform-info?id=${encodeURIComponent(platformId)}`);
908
+ const platform = await response.json();
909
+
910
+ if (platform.error) {
911
+ content.innerHTML = `<div class="loading">${t('error.generic')}: ${platform.error}</div>`;
912
+ return;
913
+ }
914
+
915
+ const skills = platform.installedSkills || [];
916
+
917
+ content.innerHTML = `
918
+ <div class="platform-detail">
919
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
920
+ <div>
921
+ <h2 style="color: var(--accent); font-size: 1.5rem; margin: 0;">${platform.name}</h2>
922
+ <span class="status ${platform.available ? 'status-available' : 'status-unavailable'}">
923
+ ${platform.available ? t('status.available') : t('status.unavailable')}
924
+ </span>
925
+ <span style="color: var(--text-muted); font-size: 0.85rem; margin-left: 12px;">
926
+ ${t('status.skillsInstalled', { count: platform.installedCount })}
927
+ </span>
928
+ </div>
929
+ </div>
930
+ <div class="admin-skill-row" style="background: var(--bg-card); padding: 10px 16px; margin-bottom: 12px;">
931
+ <span style="color: var(--text-muted); font-size: 0.85rem;">Platform ID</span>
932
+ <span style="color: var(--text-secondary); font-size: 0.9rem; font-family: monospace;">${platform.id}</span>
933
+ </div>
934
+ <h3 style="color: var(--text-secondary); margin-bottom: 10px; font-size: 1rem;">
935
+ Installed Skills (${skills.length})
936
+ </h3>
937
+ ${skills.length === 0 ? '<div class="loading" style="padding: 20px;">No skills installed on this platform.</div>' : `
938
+ <div style="display: flex; flex-direction: column; gap: 6px;">
939
+ ${skills.map((skill, i) => `
940
+ <div class="admin-skill-row" style="cursor: pointer;" onclick="showSkillDetail('${skill.id}')">
941
+ <div class="admin-skill-info">
942
+ <h4>${skill.displayName || skill.id}</h4>
943
+ <div class="admin-skill-meta">${skill.id}@${skill.version || 'latest'}</div>
944
+ </div>
945
+ <button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); showSkillDetail('${skill.id}')">${t('btn.info')}</button>
946
+ </div>
947
+ `).join('')}
948
+ </div>
949
+ `}
950
+ </div>
951
+ `;
952
+ } catch (err) {
953
+ content.innerHTML = `<div class="loading">${t('error.generic')}: ${err.message}</div>`;
954
+ }
955
+ }
956
+
957
+ function goBackFromPlatformDetail() {
958
+ const target = state.previousView || 'platforms';
959
+ document.querySelectorAll('.nav-btn').forEach(b => {
960
+ b.classList.toggle('active', b.dataset.view === target);
961
+ });
962
+ switchView(target);
963
+ }
964
+
818
965
  // -----------------------------------------------------------------------------
819
966
  // Help 视图
820
967
  // -----------------------------------------------------------------------------
package/gui/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <header class="topbar">
13
13
  <div class="topbar-left">
14
14
  <span class="topbar-logo">📦 SkillMarket</span>
15
- <span class="topbar-version" id="gui-version">v1.3.16</span>
15
+ <span class="topbar-version" id="gui-version">...</span>
16
16
  </div>
17
17
  <nav class="topbar-nav">
18
18
  <button class="nav-btn active" data-view="skills">📋 Skills</button>
@@ -37,6 +37,15 @@
37
37
  <h2>Available Skills</h2>
38
38
  <div class="controls">
39
39
  <input type="text" id="search-input" placeholder="🔍 Search skills...">
40
+ <select id="sort-select">
41
+ <option value="name">Name A-Z</option>
42
+ <option value="-name">Name Z-A</option>
43
+ <option value="-updated">Recently Updated</option>
44
+ <option value="updated">Least Recently Updated</option>
45
+ </select>
46
+ <select id="platform-filter">
47
+ <option value="">All Platforms</option>
48
+ </select>
40
49
  <select id="page-size">
41
50
  <option value="10">10 per page</option>
42
51
  <option value="20" selected>20 per page</option>
@@ -66,6 +75,14 @@
66
75
  <div id="platforms-list" class="platforms-list"></div>
67
76
  </div>
68
77
 
78
+ <!-- Platform 详情视图 -->
79
+ <div id="view-platform-detail" class="view">
80
+ <div class="view-header">
81
+ <button class="btn btn-secondary" onclick="goBackFromPlatformDetail()">← Back to Platforms</button>
82
+ </div>
83
+ <div id="platform-detail-content"></div>
84
+ </div>
85
+
69
86
  <!-- Help 视图 -->
70
87
  <div id="view-help" class="view">
71
88
  <div class="view-header">
package/gui/style.css CHANGED
@@ -40,6 +40,13 @@ body {
40
40
  overflow: hidden;
41
41
  }
42
42
 
43
+ #app {
44
+ display: flex;
45
+ flex-direction: column;
46
+ flex: 1;
47
+ min-height: 0;
48
+ }
49
+
43
50
  /* -----------------------------------------------------------------------------
44
51
  顶部导航栏
45
52
  ----------------------------------------------------------------------------- */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "itismyskillmarket",
3
- "version": "1.3.20",
3
+ "version": "1.3.22",
4
4
  "description": "Cross-platform skill manager for AI coding tools",
5
5
  "type": "module",
6
6
  "bin": {