itismyskillmarket 1.3.26 → 1.3.27

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
@@ -1575,9 +1575,10 @@ async function verifySkill(skillName) {
1575
1575
 
1576
1576
  // src/commands/ui.ts
1577
1577
  import { createServer } from "http";
1578
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
1579
- import { join as join3, extname, dirname } from "path";
1578
+ import { readFileSync as readFileSync2, existsSync as existsSync3, mkdirSync, rmSync } from "fs";
1579
+ import { join as join3, extname, dirname, basename } from "path";
1580
1580
  import { fileURLToPath as fileURLToPath3 } from "url";
1581
+ import AdmZip from "adm-zip";
1581
1582
 
1582
1583
  // src/commands/admin.ts
1583
1584
  import { execSync as execSync2 } from "child_process";
@@ -2537,6 +2538,117 @@ API_ROUTES.POST["/api/update"] = async (req, res, _url) => {
2537
2538
  jsonResponse(res, 500, { error: String(err) });
2538
2539
  }
2539
2540
  };
2541
+ var PROJECT_ROOT = join3(__dirname2, "..");
2542
+ API_ROUTES.POST["/api/upload"] = async (req, res, _url) => {
2543
+ try {
2544
+ const body = await parseBody(req);
2545
+ const fileData = String(body.fileData || "");
2546
+ const fileName = String(body.fileName || "upload.zip");
2547
+ const skillNameOverride = body.skillNameOverride ? String(body.skillNameOverride).trim() : "";
2548
+ if (!fileData) {
2549
+ jsonResponse(res, 400, { error: "Missing fileData" });
2550
+ return;
2551
+ }
2552
+ const buffer = Buffer.from(fileData, "base64");
2553
+ if (buffer.length === 0) {
2554
+ jsonResponse(res, 400, { error: "Empty file data" });
2555
+ return;
2556
+ }
2557
+ const zip = new AdmZip(buffer);
2558
+ const entries = zip.getEntries();
2559
+ if (entries.length === 0) {
2560
+ jsonResponse(res, 400, { error: "ZIP archive is empty" });
2561
+ return;
2562
+ }
2563
+ const pkgEntry = entries.find((e) => e.entryName === "package.json" || e.entryName.endsWith("/package.json"));
2564
+ let skillName = "";
2565
+ let pkgInfo = {};
2566
+ if (pkgEntry) {
2567
+ try {
2568
+ pkgInfo = JSON.parse(pkgEntry.getData().toString("utf-8"));
2569
+ skillName = pkgInfo.skillmarket?.id || pkgInfo.name?.replace(/^@[^/]+\//, "") || "";
2570
+ } catch {
2571
+ }
2572
+ }
2573
+ if (skillNameOverride) {
2574
+ skillName = skillNameOverride;
2575
+ }
2576
+ if (!skillName) {
2577
+ const rootDirs = [...new Set(entries.map((e) => e.entryName.split("/")[0]))].filter(Boolean);
2578
+ skillName = rootDirs.length === 1 ? rootDirs[0] : basename(fileName, ".zip");
2579
+ }
2580
+ skillName = skillName.replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase();
2581
+ if (!skillName) skillName = "untitled-skill";
2582
+ const skillDir = join3(PROJECT_ROOT, "skills", skillName);
2583
+ if (existsSync3(skillDir)) {
2584
+ rmSync(skillDir, { recursive: true, force: true });
2585
+ }
2586
+ mkdirSync(skillDir, { recursive: true });
2587
+ zip.extractAllTo(skillDir, true);
2588
+ const skillMdExists = existsSync3(join3(skillDir, "SKILL.md")) || entries.some((e) => e.entryName.endsWith("SKILL.md"));
2589
+ const pkgJsonPath = join3(skillDir, "package.json");
2590
+ if (existsSync3(pkgJsonPath)) {
2591
+ try {
2592
+ pkgInfo = JSON.parse(readFileSync2(pkgJsonPath, "utf-8"));
2593
+ } catch {
2594
+ }
2595
+ }
2596
+ const meta = pkgInfo?.skillmarket || {};
2597
+ const result = {
2598
+ skillName,
2599
+ displayName: meta.displayName || pkgInfo.displayName || skillName,
2600
+ version: pkgInfo.version || "0.0.0",
2601
+ description: pkgInfo.description || meta.description || "",
2602
+ platforms: meta.platforms || [],
2603
+ hasPackageJson: existsSync3(pkgJsonPath),
2604
+ hasSkillMd: skillMdExists,
2605
+ fileCount: entries.length
2606
+ };
2607
+ jsonResponse(res, 200, result);
2608
+ } catch (err) {
2609
+ jsonResponse(res, 500, { error: String(err) });
2610
+ }
2611
+ };
2612
+ API_ROUTES.POST["/api/upload/action"] = async (req, res, _url) => {
2613
+ try {
2614
+ const body = await parseBody(req);
2615
+ const skillName = String(body.skillName || "");
2616
+ const action = String(body.action || "");
2617
+ if (!skillName) {
2618
+ jsonResponse(res, 400, { error: "Missing skillName" });
2619
+ return;
2620
+ }
2621
+ if (!["publish", "install", "both"].includes(action)) {
2622
+ jsonResponse(res, 400, { error: 'action must be "publish", "install", or "both"' });
2623
+ return;
2624
+ }
2625
+ const skillDir = join3(PROJECT_ROOT, "skills", skillName);
2626
+ if (!existsSync3(skillDir)) {
2627
+ jsonResponse(res, 404, { error: `Skill "${skillName}" not found in skills/ directory. Upload first.` });
2628
+ return;
2629
+ }
2630
+ const results = {};
2631
+ if (action === "publish" || action === "both") {
2632
+ try {
2633
+ await publishSkill(skillName);
2634
+ results.publish = { success: true, message: `${skillName} published to npm` };
2635
+ } catch (err) {
2636
+ results.publish = { success: false, message: String(err) };
2637
+ }
2638
+ }
2639
+ if (action === "install" || action === "both") {
2640
+ try {
2641
+ await installSkill(skillName, void 0, { force: true });
2642
+ results.install = { success: true, message: `${skillName} installed locally` };
2643
+ } catch (err) {
2644
+ results.install = { success: false, message: String(err) };
2645
+ }
2646
+ }
2647
+ jsonResponse(res, 200, { success: true, skillName, action, results });
2648
+ } catch (err) {
2649
+ jsonResponse(res, 500, { error: String(err) });
2650
+ }
2651
+ };
2540
2652
  function serveStaticFile(res, filePath) {
2541
2653
  if (!existsSync3(filePath)) {
2542
2654
  res.writeHead(404, { "Content-Type": "text/plain" });
package/gui/app.js CHANGED
@@ -27,6 +27,7 @@ const translations = {
27
27
  'nav.skills': 'Skills',
28
28
  'nav.installed': 'Installed',
29
29
  'nav.platforms': 'Platforms',
30
+ 'nav.upload': 'Upload',
30
31
  'nav.admin': 'Admin',
31
32
  'nav.help': 'Help',
32
33
  'nav.back': 'Back',
@@ -205,6 +206,42 @@ const translations = {
205
206
  'lang.en': 'EN',
206
207
  'lang.zh': '中',
207
208
 
209
+ // Upload 视图
210
+ 'upload.title': 'Upload Skill',
211
+ 'upload.dropzoneText': 'Drop a skill .zip file here, or click to select',
212
+ 'upload.dropzoneHint': 'The zip should contain SKILL.md and optionally package.json',
213
+ 'upload.chooseFile': 'Choose File',
214
+ 'upload.skillNameLabel': 'Skill Name',
215
+ 'upload.skillNamePlaceholder': 'Auto-detected from zip, or override here',
216
+ 'upload.uploadParse': '📤 Upload & Parse',
217
+ 'upload.processing': 'Processing...',
218
+ 'upload.parsing': 'Parsing skill archive...',
219
+ 'upload.previewTitle': 'Skill Preview',
220
+ 'upload.id': 'ID',
221
+ 'upload.version': 'Version',
222
+ 'upload.description': 'Description',
223
+ 'upload.platforms': 'Platforms',
224
+ 'upload.fileCount': 'Files',
225
+ 'upload.hasPackageJson': 'package.json',
226
+ 'upload.hasSkillMd': 'SKILL.md',
227
+ 'upload.yes': 'Yes',
228
+ 'upload.no': 'No',
229
+ 'upload.actionPublish': '📦 Publish to npm',
230
+ 'upload.actionInstall': '💻 Install Locally',
231
+ 'upload.actionBoth': '✅ Both',
232
+ 'upload.actionDiscard': '🗑 Discard',
233
+ 'upload.publishing': 'Publishing {skillName}...',
234
+ 'upload.installing': 'Installing {skillName}...',
235
+ 'upload.bothStarted': 'Publishing & installing {skillName}...',
236
+ 'upload.publishSuccess': '{skillName} published to npm successfully!',
237
+ 'upload.installSuccess': '{skillName} installed locally!',
238
+ 'upload.bothSuccess': '{skillName} published & installed!',
239
+ 'upload.discarded': 'Upload discarded',
240
+ 'upload.errorInvalidZip': 'Invalid or empty zip file',
241
+ 'upload.errorNoFile': 'Please select a zip file first',
242
+ 'upload.uploadError': 'Upload failed: {error}',
243
+ 'upload.actionError': 'Action failed: {error}',
244
+
208
245
  // 通用错误
209
246
  'error.generic': 'Error',
210
247
  },
@@ -214,6 +251,7 @@ const translations = {
214
251
  'nav.skills': '技能',
215
252
  'nav.installed': '已安装',
216
253
  'nav.platforms': '平台',
254
+ 'nav.upload': '上传',
217
255
  'nav.admin': '管理',
218
256
  'nav.help': '帮助',
219
257
  'nav.back': '返回',
@@ -392,6 +430,42 @@ const translations = {
392
430
  'lang.en': 'EN',
393
431
  'lang.zh': '中',
394
432
 
433
+ // Upload 视图
434
+ 'upload.title': '上传 Skill',
435
+ 'upload.dropzoneText': '将 skill 的 .zip 文件拖放到此处,或点击选择',
436
+ 'upload.dropzoneHint': 'zip 应包含 SKILL.md 和可选的 package.json',
437
+ 'upload.chooseFile': '选择文件',
438
+ 'upload.skillNameLabel': 'Skill 名称',
439
+ 'upload.skillNamePlaceholder': '自动从 zip 中检测,也可手动覆盖',
440
+ 'upload.uploadParse': '📤 上传并解析',
441
+ 'upload.processing': '处理中...',
442
+ 'upload.parsing': '正在解析 skill 压缩包...',
443
+ 'upload.previewTitle': 'Skill 预览',
444
+ 'upload.id': 'ID',
445
+ 'upload.version': '版本',
446
+ 'upload.description': '描述',
447
+ 'upload.platforms': '支持平台',
448
+ 'upload.fileCount': '文件数',
449
+ 'upload.hasPackageJson': 'package.json',
450
+ 'upload.hasSkillMd': 'SKILL.md',
451
+ 'upload.yes': '是',
452
+ 'upload.no': '否',
453
+ 'upload.actionPublish': '📦 发布到 npm',
454
+ 'upload.actionInstall': '💻 安装到本地',
455
+ 'upload.actionBoth': '✅ 两者都做',
456
+ 'upload.actionDiscard': '🗑 丢弃',
457
+ 'upload.publishing': '正在发布 {skillName}...',
458
+ 'upload.installing': '正在安装 {skillName}...',
459
+ 'upload.bothStarted': '正在发布并安装 {skillName}...',
460
+ 'upload.publishSuccess': '{skillName} 已成功发布到 npm!',
461
+ 'upload.installSuccess': '{skillName} 已安装到本地!',
462
+ 'upload.bothSuccess': '{skillName} 已发布并安装!',
463
+ 'upload.discarded': '上传已丢弃',
464
+ 'upload.errorInvalidZip': '无效或空的 zip 文件',
465
+ 'upload.errorNoFile': '请先选择一个 zip 文件',
466
+ 'upload.uploadError': '上传失败:{error}',
467
+ 'upload.actionError': '操作失败:{error}',
468
+
395
469
  // 通用错误
396
470
  'error.generic': '错误',
397
471
  }
@@ -454,12 +528,14 @@ function applyI18nToStaticElements() {
454
528
  const navSkills = document.querySelector('.nav-btn[data-view="skills"]');
455
529
  const navInstalled = document.querySelector('.nav-btn[data-view="installed"]');
456
530
  const navPlatforms = document.querySelector('.nav-btn[data-view="platforms"]');
531
+ const navUpload = document.querySelector('.nav-btn[data-view="upload"]');
457
532
  const navAdmin = document.querySelector('.nav-btn[data-view="admin"]');
458
533
  const navHelp = document.querySelector('.nav-btn[data-view="help"]');
459
534
 
460
535
  if (navSkills) navSkills.innerHTML = `📋 ${t('nav.skills')}`;
461
536
  if (navInstalled) navInstalled.innerHTML = `✅ ${t('nav.installed')}`;
462
537
  if (navPlatforms) navPlatforms.innerHTML = `💻 ${t('nav.platforms')}`;
538
+ if (navUpload) navUpload.innerHTML = `📤 ${t('nav.upload')}`;
463
539
  if (navAdmin) navAdmin.innerHTML = `⚙️ ${t('nav.admin')}`;
464
540
  if (navHelp) navHelp.innerHTML = `📖 ${t('nav.help')}`;
465
541
 
@@ -470,6 +546,7 @@ function applyI18nToStaticElements() {
470
546
  { selector: '#view-platforms .view-header h2', key: 'title.platforms' },
471
547
  { selector: '#view-help .view-header h2', key: 'title.help' },
472
548
  { selector: '#view-admin .view-header h2', key: 'title.admin' },
549
+ { selector: '#view-upload .view-header h2', key: 'upload.title' },
473
550
  ];
474
551
 
475
552
  titles.forEach(({ selector, key }) => {
@@ -564,6 +641,9 @@ function reRenderCurrentView() {
564
641
  case 'help':
565
642
  loadHelp();
566
643
  break;
644
+ case 'upload':
645
+ // Upload 视图不需要重新加载
646
+ break;
567
647
  case 'admin':
568
648
  loadAdminDashboard();
569
649
  break;
@@ -581,6 +661,7 @@ document.addEventListener('DOMContentLoaded', () => {
581
661
  initializeNavigation();
582
662
  initializeControls();
583
663
  initializeCollapsibleSections();
664
+ initializeUploadControls();
584
665
  loadVersion();
585
666
  loadSkills();
586
667
  });
@@ -650,6 +731,9 @@ function switchView(view) {
650
731
  case 'help':
651
732
  loadHelp();
652
733
  break;
734
+ case 'upload':
735
+ resetUploadView();
736
+ break;
653
737
  case 'admin':
654
738
  loadAdminDashboard();
655
739
  break;
@@ -1806,3 +1890,258 @@ async function execAdminAccess(skillId) {
1806
1890
  showToast(`${t('error.generic')}: ${err.message}`, 'error');
1807
1891
  }
1808
1892
  }
1893
+
1894
+ // -----------------------------------------------------------------------------
1895
+ // Upload 视图
1896
+ // -----------------------------------------------------------------------------
1897
+
1898
+ /** Upload 状态 */
1899
+ const uploadState = {
1900
+ skillName: '',
1901
+ file: null,
1902
+ data: null, // Parsed result from backend
1903
+ };
1904
+
1905
+ /** 初始化 Upload 控件 */
1906
+ function initializeUploadControls() {
1907
+ const dropzone = document.getElementById('upload-dropzone');
1908
+ const fileInput = document.getElementById('upload-file-input');
1909
+ const selectBtn = document.getElementById('upload-select-btn');
1910
+ const submitBtn = document.getElementById('upload-submit-btn');
1911
+ const skillNameInput = document.getElementById('upload-skill-name');
1912
+
1913
+ if (!dropzone) return;
1914
+
1915
+ // 文件选择
1916
+ selectBtn.addEventListener('click', () => fileInput.click());
1917
+ fileInput.addEventListener('change', (e) => {
1918
+ if (e.target.files.length > 0) {
1919
+ uploadState.file = e.target.files[0];
1920
+ submitBtn.disabled = false;
1921
+ }
1922
+ });
1923
+
1924
+ // 拖拽上传
1925
+ dropzone.addEventListener('click', () => fileInput.click());
1926
+
1927
+ dropzone.addEventListener('dragover', (e) => {
1928
+ e.preventDefault();
1929
+ dropzone.classList.add('drag-over');
1930
+ });
1931
+
1932
+ dropzone.addEventListener('dragleave', () => {
1933
+ dropzone.classList.remove('drag-over');
1934
+ });
1935
+
1936
+ dropzone.addEventListener('drop', (e) => {
1937
+ e.preventDefault();
1938
+ dropzone.classList.remove('drag-over');
1939
+ if (e.dataTransfer.files.length > 0) {
1940
+ uploadState.file = e.dataTransfer.files[0];
1941
+ submitBtn.disabled = false;
1942
+ }
1943
+ });
1944
+
1945
+ // Upload & Parse
1946
+ submitBtn.addEventListener('click', () => {
1947
+ if (!uploadState.file) {
1948
+ showToast(t('upload.errorNoFile'), 'error');
1949
+ return;
1950
+ }
1951
+ // Use skill name override if provided
1952
+ const override = skillNameInput.value.trim();
1953
+ handleUpload(uploadState.file, override || undefined);
1954
+ });
1955
+
1956
+ // Action buttons
1957
+ document.getElementById('upload-action-publish').addEventListener('click', () => {
1958
+ executeUploadAction('publish');
1959
+ });
1960
+ document.getElementById('upload-action-install').addEventListener('click', () => {
1961
+ executeUploadAction('install');
1962
+ });
1963
+ document.getElementById('upload-action-both').addEventListener('click', () => {
1964
+ executeUploadAction('both');
1965
+ });
1966
+ document.getElementById('upload-action-discard').addEventListener('click', () => {
1967
+ resetUploadView();
1968
+ showToast(t('upload.discarded'), 'info');
1969
+ });
1970
+ }
1971
+
1972
+ /** 重置 Upload 视图到 Phase 1 */
1973
+ function resetUploadView() {
1974
+ uploadState.file = null;
1975
+ uploadState.data = null;
1976
+ uploadState.skillName = '';
1977
+ document.getElementById('upload-phase1').classList.remove('hidden');
1978
+ document.getElementById('upload-phase2').classList.add('hidden');
1979
+ document.getElementById('upload-submit-btn').disabled = true;
1980
+ document.getElementById('upload-progress').classList.add('hidden');
1981
+ document.getElementById('upload-file-input').value = '';
1982
+ document.getElementById('upload-skill-name').value = '';
1983
+ }
1984
+
1985
+ /** 上传 zip 到后端 */
1986
+ async function handleUpload(file, skillNameOverride) {
1987
+ const submitBtn = document.getElementById('upload-submit-btn');
1988
+ const progress = document.getElementById('upload-progress');
1989
+ const progressFill = document.getElementById('upload-progress-fill');
1990
+ const progressText = document.getElementById('upload-progress-text');
1991
+
1992
+ submitBtn.disabled = true;
1993
+ progress.classList.remove('hidden');
1994
+ progressFill.style.width = '30%';
1995
+ progressText.textContent = t('upload.processing');
1996
+
1997
+ try {
1998
+ // Read file as base64
1999
+ const reader = new FileReader();
2000
+ const base64 = await new Promise((resolve, reject) => {
2001
+ reader.onload = () => {
2002
+ // Remove data URL prefix
2003
+ const result = reader.result;
2004
+ const commaIndex = result.indexOf(',');
2005
+ resolve(commaIndex >= 0 ? result.slice(commaIndex + 1) : result);
2006
+ };
2007
+ reader.onerror = reject;
2008
+ reader.readAsDataURL(file);
2009
+ });
2010
+
2011
+ progressFill.style.width = '60%';
2012
+ progressText.textContent = t('upload.parsing');
2013
+
2014
+ // Send to backend
2015
+ const response = await fetch('/api/upload', {
2016
+ method: 'POST',
2017
+ headers: { 'Content-Type': 'application/json' },
2018
+ body: JSON.stringify({
2019
+ fileData: base64,
2020
+ fileName: file.name,
2021
+ skillNameOverride: skillNameOverride || undefined,
2022
+ }),
2023
+ });
2024
+
2025
+ const result = await response.json();
2026
+
2027
+ if (result.error) {
2028
+ showToast(t('upload.uploadError', { error: result.error }), 'error');
2029
+ resetUploadView();
2030
+ return;
2031
+ }
2032
+
2033
+ progressFill.style.width = '100%';
2034
+ progressText.textContent = '✅ Done';
2035
+
2036
+ uploadState.data = result;
2037
+ uploadState.skillName = skillNameOverride || result.skillName;
2038
+
2039
+ // Switch to preview phase
2040
+ setTimeout(() => {
2041
+ document.getElementById('upload-phase1').classList.add('hidden');
2042
+ document.getElementById('upload-phase2').classList.remove('hidden');
2043
+ progress.classList.add('hidden');
2044
+ renderUploadPreview(result);
2045
+ }, 400);
2046
+ } catch (err) {
2047
+ showToast(t('upload.uploadError', { error: err.message }), 'error');
2048
+ resetUploadView();
2049
+ }
2050
+ }
2051
+
2052
+ /** 渲染 skill 预览卡片 */
2053
+ function renderUploadPreview(data) {
2054
+ const container = document.getElementById('upload-preview');
2055
+
2056
+ const platformsHtml = data.platforms && data.platforms.length
2057
+ ? data.platforms.map(p => `<span class="platform-tag">${p}</span>`).join('')
2058
+ : `<span style="color: var(--text-muted); font-size: 0.85rem;">N/A</span>`;
2059
+
2060
+ container.innerHTML = `
2061
+ <h2>${data.displayName || data.skillName}</h2>
2062
+ <div class="upload-preview-id">${data.skillName}@${data.version}</div>
2063
+
2064
+ <div class="upload-preview-section">
2065
+ <h3>${t('upload.description')}</h3>
2066
+ <p>${data.description || t('detail.noDescription')}</p>
2067
+ </div>
2068
+
2069
+ <div class="upload-preview-section">
2070
+ <h3>${t('upload.platforms')}</h3>
2071
+ <div style="display:flex;flex-wrap:wrap;gap:5px;">${platformsHtml}</div>
2072
+ </div>
2073
+
2074
+ <div class="upload-preview-section">
2075
+ <h3>Validation</h3>
2076
+ <div>
2077
+ <span class="upload-preview-badge ${data.hasPackageJson ? 'success' : 'warning'}">
2078
+ 📦 ${t('upload.hasPackageJson')}: ${data.hasPackageJson ? t('upload.yes') : t('upload.no')}
2079
+ </span>
2080
+ <span class="upload-preview-badge ${data.hasSkillMd ? 'success' : 'warning'}">
2081
+ 📄 ${t('upload.hasSkillMd')}: ${data.hasSkillMd ? t('upload.yes') : t('upload.no')}
2082
+ </span>
2083
+ </div>
2084
+ </div>
2085
+
2086
+ <div class="upload-preview-section" style="border-bottom:none;margin-bottom:0;padding-bottom:0;">
2087
+ <h3>Stats</h3>
2088
+ <div class="upload-preview-stats">
2089
+ <div class="upload-preview-stat">
2090
+ <div class="upload-preview-stat-value">${data.fileCount}</div>
2091
+ <div class="upload-preview-stat-label">${t('upload.fileCount')}</div>
2092
+ </div>
2093
+ <div class="upload-preview-stat">
2094
+ <div class="upload-preview-stat-value">${data.version}</div>
2095
+ <div class="upload-preview-stat-label">${t('upload.version')}</div>
2096
+ </div>
2097
+ </div>
2098
+ </div>
2099
+ `;
2100
+ }
2101
+
2102
+ /** 执行上传后的操作 */
2103
+ async function executeUploadAction(action) {
2104
+ if (!uploadState.data || !uploadState.skillName) {
2105
+ showToast('No skill uploaded. Please upload first.', 'error');
2106
+ return;
2107
+ }
2108
+
2109
+ const actionLabel = { publish: 'upload.publishing', install: 'upload.installing', both: 'upload.bothStarted' };
2110
+ const successMsg = { publish: 'upload.publishSuccess', install: 'upload.installSuccess', both: 'upload.bothSuccess' };
2111
+
2112
+ showToast(t(actionLabel[action], { skillName: uploadState.skillName }), 'info');
2113
+
2114
+ try {
2115
+ const response = await fetch('/api/upload/action', {
2116
+ method: 'POST',
2117
+ headers: { 'Content-Type': 'application/json' },
2118
+ body: JSON.stringify({
2119
+ skillName: uploadState.skillName,
2120
+ action: action,
2121
+ }),
2122
+ });
2123
+
2124
+ const result = await response.json();
2125
+
2126
+ if (result.error) {
2127
+ showToast(t('upload.actionError', { error: result.error }), 'error');
2128
+ return;
2129
+ }
2130
+
2131
+ // Check for individual operation results
2132
+ const hasError = Object.values(result.results || {}).some(r => !r.success);
2133
+ if (hasError) {
2134
+ const errors = Object.entries(result.results || {})
2135
+ .filter(([, r]) => !r.success)
2136
+ .map(([k, r]) => `${k}: ${r.message}`)
2137
+ .join('; ');
2138
+ showToast(`⚠️ ${errors}`, 'error');
2139
+ return;
2140
+ }
2141
+
2142
+ showToast(t(successMsg[action], { skillName: uploadState.skillName }), 'success');
2143
+ resetUploadView();
2144
+ } catch (err) {
2145
+ showToast(t('upload.actionError', { error: err.message }), 'error');
2146
+ }
2147
+ }
package/gui/index.html CHANGED
@@ -18,6 +18,7 @@
18
18
  <button class="nav-btn active" data-view="skills">📋 Skills</button>
19
19
  <button class="nav-btn" data-view="installed">✅ Installed</button>
20
20
  <button class="nav-btn" data-view="platforms">💻 Platforms</button>
21
+ <button class="nav-btn" data-view="upload">📤 Upload</button>
21
22
  <button class="nav-btn" data-view="admin">⚙️ Admin</button>
22
23
  <button class="nav-btn" data-view="help">📖 Help</button>
23
24
  </nav>
@@ -94,6 +95,45 @@
94
95
  <div id="admin-skills-list" class="admin-skills-list"></div>
95
96
  </div>
96
97
 
98
+ <!-- Upload 视图 -->
99
+ <div id="view-upload" class="view">
100
+ <div class="view-header">
101
+ <h2>Upload Skill</h2>
102
+ </div>
103
+ <!-- Phase 1: Upload -->
104
+ <div id="upload-phase1" class="upload-phase">
105
+ <div class="upload-dropzone" id="upload-dropzone">
106
+ <div class="upload-dropzone-icon">📦</div>
107
+ <p class="upload-dropzone-text">Drop a skill .zip file here, or click to select</p>
108
+ <p class="upload-dropzone-hint">The zip should contain SKILL.md and optionally package.json</p>
109
+ <input type="file" id="upload-file-input" accept=".zip" style="display:none">
110
+ <button id="upload-select-btn" class="btn btn-primary" style="margin-top:12px;">Choose File</button>
111
+ </div>
112
+ <div class="upload-skill-name-input">
113
+ <label for="upload-skill-name">Skill Name</label>
114
+ <input type="text" id="upload-skill-name" placeholder="Auto-detected from zip, or override here">
115
+ </div>
116
+ <button id="upload-submit-btn" class="btn btn-success" disabled style="align-self:center;margin-top:8px;">
117
+ 📤 Upload &amp; Parse
118
+ </button>
119
+ <div id="upload-progress" class="upload-progress hidden">
120
+ <div class="upload-progress-bar"><div class="upload-progress-fill" id="upload-progress-fill"></div></div>
121
+ <span id="upload-progress-text">Processing...</span>
122
+ </div>
123
+ </div>
124
+ <!-- Phase 2: Preview -->
125
+ <div id="upload-phase2" class="upload-phase hidden">
126
+ <div id="upload-preview" class="upload-preview"></div>
127
+ <div class="upload-actions">
128
+ <button id="upload-action-publish" class="btn btn-primary">📦 Publish to npm</button>
129
+ <button id="upload-action-install" class="btn btn-success">💻 Install Locally</button>
130
+ <button id="upload-action-both" class="btn btn-primary" style="background:var(--accent)">✅ Both</button>
131
+ <button id="upload-action-discard" class="btn btn-secondary">🗑 Discard</button>
132
+ </div>
133
+ </div>
134
+ <!-- Phase 3: Result (toast-based) -->
135
+ </div>
136
+
97
137
  <!-- Skill 详情视图 -->
98
138
  <div id="view-skill-detail" class="view">
99
139
  <div class="view-header">
package/gui/style.css CHANGED
@@ -914,6 +914,205 @@ body {
914
914
  gap: 5px;
915
915
  }
916
916
 
917
+ /* -----------------------------------------------------------------------------
918
+ Upload 视图
919
+ ----------------------------------------------------------------------------- */
920
+
921
+ .upload-phase {
922
+ display: flex;
923
+ flex-direction: column;
924
+ gap: 16px;
925
+ }
926
+
927
+ .upload-phase.hidden {
928
+ display: none;
929
+ }
930
+
931
+ .upload-dropzone {
932
+ border: 2px dashed var(--border-color);
933
+ border-radius: 12px;
934
+ padding: 40px 20px;
935
+ text-align: center;
936
+ cursor: pointer;
937
+ transition: border-color 0.2s, background 0.2s;
938
+ background: var(--bg-secondary);
939
+ }
940
+
941
+ .upload-dropzone:hover,
942
+ .upload-dropzone.drag-over {
943
+ border-color: var(--accent);
944
+ background: var(--bg-hover);
945
+ }
946
+
947
+ .upload-dropzone-icon {
948
+ font-size: 3rem;
949
+ margin-bottom: 10px;
950
+ }
951
+
952
+ .upload-dropzone-text {
953
+ font-size: 1rem;
954
+ color: var(--text-secondary);
955
+ margin-bottom: 6px;
956
+ }
957
+
958
+ .upload-dropzone-hint {
959
+ font-size: 0.82rem;
960
+ color: var(--text-muted);
961
+ }
962
+
963
+ .upload-skill-name-input {
964
+ display: flex;
965
+ flex-direction: column;
966
+ gap: 4px;
967
+ }
968
+
969
+ .upload-skill-name-input label {
970
+ font-size: 0.85rem;
971
+ color: var(--text-muted);
972
+ }
973
+
974
+ .upload-skill-name-input input {
975
+ padding: 9px 12px;
976
+ background: var(--bg-secondary);
977
+ border: 1px solid var(--border-color);
978
+ border-radius: 6px;
979
+ color: var(--text-secondary);
980
+ font-size: 0.9rem;
981
+ }
982
+
983
+ .upload-skill-name-input input:focus {
984
+ outline: none;
985
+ border-color: var(--accent);
986
+ }
987
+
988
+ .upload-progress {
989
+ display: flex;
990
+ align-items: center;
991
+ gap: 12px;
992
+ }
993
+
994
+ .upload-progress.hidden {
995
+ display: none;
996
+ }
997
+
998
+ .upload-progress-bar {
999
+ flex: 1;
1000
+ height: 8px;
1001
+ background: var(--bg-card);
1002
+ border-radius: 4px;
1003
+ overflow: hidden;
1004
+ }
1005
+
1006
+ .upload-progress-fill {
1007
+ height: 100%;
1008
+ width: 0%;
1009
+ background: var(--accent);
1010
+ border-radius: 4px;
1011
+ transition: width 0.3s;
1012
+ }
1013
+
1014
+ .upload-progress-text {
1015
+ font-size: 0.82rem;
1016
+ color: var(--text-muted);
1017
+ white-space: nowrap;
1018
+ }
1019
+
1020
+ .upload-preview {
1021
+ background: var(--bg-secondary);
1022
+ border: 1px solid var(--border-color);
1023
+ border-radius: 12px;
1024
+ padding: 24px;
1025
+ }
1026
+
1027
+ .upload-preview h2 {
1028
+ font-size: 1.4rem;
1029
+ color: var(--accent);
1030
+ margin-bottom: 4px;
1031
+ }
1032
+
1033
+ .upload-preview .upload-preview-id {
1034
+ font-size: 0.85rem;
1035
+ color: var(--text-muted);
1036
+ margin-bottom: 14px;
1037
+ }
1038
+
1039
+ .upload-preview .upload-preview-section {
1040
+ margin-bottom: 14px;
1041
+ padding-bottom: 12px;
1042
+ border-bottom: 1px solid var(--border-color);
1043
+ }
1044
+
1045
+ .upload-preview .upload-preview-section:last-child {
1046
+ border-bottom: none;
1047
+ margin-bottom: 0;
1048
+ padding-bottom: 0;
1049
+ }
1050
+
1051
+ .upload-preview .upload-preview-section h3 {
1052
+ font-size: 0.9rem;
1053
+ color: var(--text-secondary);
1054
+ margin-bottom: 6px;
1055
+ }
1056
+
1057
+ .upload-preview .upload-preview-section p,
1058
+ .upload-preview .upload-preview-section div {
1059
+ font-size: 0.85rem;
1060
+ color: var(--text-secondary);
1061
+ line-height: 1.5;
1062
+ }
1063
+
1064
+ .upload-preview .upload-preview-badge {
1065
+ display: inline-flex;
1066
+ align-items: center;
1067
+ gap: 4px;
1068
+ padding: 3px 8px;
1069
+ border-radius: 4px;
1070
+ font-size: 0.78rem;
1071
+ margin-right: 6px;
1072
+ margin-bottom: 4px;
1073
+ }
1074
+
1075
+ .upload-preview .upload-preview-badge.success {
1076
+ background: rgba(76, 175, 80, 0.2);
1077
+ color: var(--success);
1078
+ }
1079
+
1080
+ .upload-preview .upload-preview-badge.warning {
1081
+ background: rgba(255, 152, 0, 0.2);
1082
+ color: var(--warning);
1083
+ }
1084
+
1085
+ .upload-preview .upload-preview-stats {
1086
+ display: flex;
1087
+ gap: 16px;
1088
+ flex-wrap: wrap;
1089
+ }
1090
+
1091
+ .upload-preview .upload-preview-stat {
1092
+ text-align: center;
1093
+ min-width: 60px;
1094
+ }
1095
+
1096
+ .upload-preview .upload-preview-stat-value {
1097
+ font-size: 1.3rem;
1098
+ font-weight: 700;
1099
+ color: var(--accent);
1100
+ }
1101
+
1102
+ .upload-preview .upload-preview-stat-label {
1103
+ font-size: 0.72rem;
1104
+ color: var(--text-muted);
1105
+ text-transform: uppercase;
1106
+ }
1107
+
1108
+ .upload-actions {
1109
+ display: flex;
1110
+ gap: 10px;
1111
+ flex-wrap: wrap;
1112
+ justify-content: center;
1113
+ margin-top: 8px;
1114
+ }
1115
+
917
1116
  /* -----------------------------------------------------------------------------
918
1117
  滚动条
919
1118
  ----------------------------------------------------------------------------- */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "itismyskillmarket",
3
- "version": "1.3.26",
3
+ "version": "1.3.27",
4
4
  "description": "Cross-platform skill manager for AI coding tools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "test": "vitest"
13
13
  },
14
14
  "dependencies": {
15
+ "adm-zip": "^0.5.17",
15
16
  "commander": "^12.0.0",
16
17
  "fs-extra": "^11.2.0"
17
18
  },