itismyskillmarket 1.3.26 → 1.3.29

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # SkillMarket
2
2
 
3
- Cross-platform skill manager for AI coding tools (Cursor, VSCode, Codex, OpenCode, Claude Code, Antigravity, OpenClaw, Hermes Agent).
3
+ > **v1.3.29** — Cross-platform skill manager for AI coding tools (Cursor, VSCode, Codex, OpenCode, Claude Code, Antigravity, OpenClaw, Hermes Agent).
4
4
 
5
5
  ## Installation
6
6
 
@@ -14,6 +14,25 @@ Or use directly:
14
14
  npx itismyskillmarket --help
15
15
  ```
16
16
 
17
+ ## Download Standalone .exe (Windows)
18
+
19
+ Download the **standalone Windows executable** (~86 MB) — no Node.js installation required:
20
+
21
+ ```bash
22
+ # Download from GitHub Releases
23
+ # https://github.com/wxc2004/market/releases/download/v1.3.29/skillmarket.exe
24
+
25
+ # Double-click the exe to start GUI + open browser automatically
26
+ # Or run from terminal:
27
+ .\skillmarket.exe # Start GUI (same as double-click)
28
+ .\skillmarket.exe --help # Show CLI help
29
+ .\skillmarket.exe ls # List available skills
30
+ ```
31
+
32
+ > **双击体验**: 直接双击 `skillmarket.exe` 会自动启动 Web GUI 界面并打开浏览器,无需打开终端输入命令。带参数运行则进入命令行模式。
33
+
34
+ All CLI commands work identically to the npm version. Built with Node.js SEA (Single Executable Applications).
35
+
17
36
  ## Usage
18
37
 
19
38
  ```bash
@@ -160,6 +179,14 @@ skm admin owner rm my-skill old-maintainer
160
179
  skm admin access my-skill restricted
161
180
  ```
162
181
 
182
+ ### GUI Upload Skill
183
+
184
+ The `skm gui` web interface includes an **Upload** view for publishing or installing skills from a `.zip` archive:
185
+
186
+ 1. Drag & drop or select a skill zip file
187
+ 2. Click "Upload & Parse" to extract and validate
188
+ 3. Choose action: **Publish to npm**, **Install Locally**, or **Both**
189
+
163
190
  ### GUI Admin Dashboard
164
191
 
165
192
  The `skm gui` web interface includes an **Admin** view with:
@@ -265,6 +292,11 @@ npm run build
265
292
 
266
293
  # Link for local testing
267
294
  npm link
295
+
296
+ # Build standalone .exe (Windows)
297
+ # Requires: Node.js 20+, esbuild, postject
298
+ node scripts/build-exe.mjs
299
+ # Output: dist/skillmarket.exe (~86 MB)
268
300
  ```
269
301
 
270
302
  ## Architecture
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { Command } from "commander";
5
5
  import { readFileSync as readFileSync3 } from "fs";
6
6
  import { fileURLToPath as fileURLToPath4 } from "url";
7
7
  import { dirname as dirname2, resolve } from "path";
8
+ import { execSync as execSync3 } from "child_process";
8
9
 
9
10
  // src/commands/registry.ts
10
11
  import fs2 from "fs-extra";
@@ -1575,9 +1576,10 @@ async function verifySkill(skillName) {
1575
1576
 
1576
1577
  // src/commands/ui.ts
1577
1578
  import { createServer } from "http";
1578
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
1579
- import { join as join3, extname, dirname } from "path";
1579
+ import { readFileSync as readFileSync2, existsSync as existsSync3, mkdirSync, rmSync } from "fs";
1580
+ import { join as join3, extname, dirname, basename } from "path";
1580
1581
  import { fileURLToPath as fileURLToPath3 } from "url";
1582
+ import AdmZip from "adm-zip";
1581
1583
 
1582
1584
  // src/commands/admin.ts
1583
1585
  import { execSync as execSync2 } from "child_process";
@@ -2537,6 +2539,117 @@ API_ROUTES.POST["/api/update"] = async (req, res, _url) => {
2537
2539
  jsonResponse(res, 500, { error: String(err) });
2538
2540
  }
2539
2541
  };
2542
+ var PROJECT_ROOT = join3(__dirname2, "..");
2543
+ API_ROUTES.POST["/api/upload"] = async (req, res, _url) => {
2544
+ try {
2545
+ const body = await parseBody(req);
2546
+ const fileData = String(body.fileData || "");
2547
+ const fileName = String(body.fileName || "upload.zip");
2548
+ const skillNameOverride = body.skillNameOverride ? String(body.skillNameOverride).trim() : "";
2549
+ if (!fileData) {
2550
+ jsonResponse(res, 400, { error: "Missing fileData" });
2551
+ return;
2552
+ }
2553
+ const buffer = Buffer.from(fileData, "base64");
2554
+ if (buffer.length === 0) {
2555
+ jsonResponse(res, 400, { error: "Empty file data" });
2556
+ return;
2557
+ }
2558
+ const zip = new AdmZip(buffer);
2559
+ const entries = zip.getEntries();
2560
+ if (entries.length === 0) {
2561
+ jsonResponse(res, 400, { error: "ZIP archive is empty" });
2562
+ return;
2563
+ }
2564
+ const pkgEntry = entries.find((e) => e.entryName === "package.json" || e.entryName.endsWith("/package.json"));
2565
+ let skillName = "";
2566
+ let pkgInfo = {};
2567
+ if (pkgEntry) {
2568
+ try {
2569
+ pkgInfo = JSON.parse(pkgEntry.getData().toString("utf-8"));
2570
+ skillName = pkgInfo.skillmarket?.id || pkgInfo.name?.replace(/^@[^/]+\//, "") || "";
2571
+ } catch {
2572
+ }
2573
+ }
2574
+ if (skillNameOverride) {
2575
+ skillName = skillNameOverride;
2576
+ }
2577
+ if (!skillName) {
2578
+ const rootDirs = [...new Set(entries.map((e) => e.entryName.split("/")[0]))].filter(Boolean);
2579
+ skillName = rootDirs.length === 1 ? rootDirs[0] : basename(fileName, ".zip");
2580
+ }
2581
+ skillName = skillName.replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase();
2582
+ if (!skillName) skillName = "untitled-skill";
2583
+ const skillDir = join3(PROJECT_ROOT, "skills", skillName);
2584
+ if (existsSync3(skillDir)) {
2585
+ rmSync(skillDir, { recursive: true, force: true });
2586
+ }
2587
+ mkdirSync(skillDir, { recursive: true });
2588
+ zip.extractAllTo(skillDir, true);
2589
+ const skillMdExists = existsSync3(join3(skillDir, "SKILL.md")) || entries.some((e) => e.entryName.endsWith("SKILL.md"));
2590
+ const pkgJsonPath = join3(skillDir, "package.json");
2591
+ if (existsSync3(pkgJsonPath)) {
2592
+ try {
2593
+ pkgInfo = JSON.parse(readFileSync2(pkgJsonPath, "utf-8"));
2594
+ } catch {
2595
+ }
2596
+ }
2597
+ const meta = pkgInfo?.skillmarket || {};
2598
+ const result = {
2599
+ skillName,
2600
+ displayName: meta.displayName || pkgInfo.displayName || skillName,
2601
+ version: pkgInfo.version || "0.0.0",
2602
+ description: pkgInfo.description || meta.description || "",
2603
+ platforms: meta.platforms || [],
2604
+ hasPackageJson: existsSync3(pkgJsonPath),
2605
+ hasSkillMd: skillMdExists,
2606
+ fileCount: entries.length
2607
+ };
2608
+ jsonResponse(res, 200, result);
2609
+ } catch (err) {
2610
+ jsonResponse(res, 500, { error: String(err) });
2611
+ }
2612
+ };
2613
+ API_ROUTES.POST["/api/upload/action"] = async (req, res, _url) => {
2614
+ try {
2615
+ const body = await parseBody(req);
2616
+ const skillName = String(body.skillName || "");
2617
+ const action = String(body.action || "");
2618
+ if (!skillName) {
2619
+ jsonResponse(res, 400, { error: "Missing skillName" });
2620
+ return;
2621
+ }
2622
+ if (!["publish", "install", "both"].includes(action)) {
2623
+ jsonResponse(res, 400, { error: 'action must be "publish", "install", or "both"' });
2624
+ return;
2625
+ }
2626
+ const skillDir = join3(PROJECT_ROOT, "skills", skillName);
2627
+ if (!existsSync3(skillDir)) {
2628
+ jsonResponse(res, 404, { error: `Skill "${skillName}" not found in skills/ directory. Upload first.` });
2629
+ return;
2630
+ }
2631
+ const results = {};
2632
+ if (action === "publish" || action === "both") {
2633
+ try {
2634
+ await publishSkill(skillName);
2635
+ results.publish = { success: true, message: `${skillName} published to npm` };
2636
+ } catch (err) {
2637
+ results.publish = { success: false, message: String(err) };
2638
+ }
2639
+ }
2640
+ if (action === "install" || action === "both") {
2641
+ try {
2642
+ await installSkill(skillName, void 0, { force: true });
2643
+ results.install = { success: true, message: `${skillName} installed locally` };
2644
+ } catch (err) {
2645
+ results.install = { success: false, message: String(err) };
2646
+ }
2647
+ }
2648
+ jsonResponse(res, 200, { success: true, skillName, action, results });
2649
+ } catch (err) {
2650
+ jsonResponse(res, 500, { error: String(err) });
2651
+ }
2652
+ };
2540
2653
  function serveStaticFile(res, filePath) {
2541
2654
  if (!existsSync3(filePath)) {
2542
2655
  res.writeHead(404, { "Content-Type": "text/plain" });
@@ -3176,4 +3289,16 @@ admin.command("access <skill> <level>").description("Set package access (public|
3176
3289
  process.exit(1);
3177
3290
  }
3178
3291
  });
3179
- program.parse();
3292
+ var hasArgs = process.argv.slice(2).length > 0;
3293
+ if (!hasArgs) {
3294
+ const port = 18770;
3295
+ setTimeout(() => {
3296
+ try {
3297
+ execSync3(`start http://localhost:${port}`, { timeout: 5e3 });
3298
+ } catch {
3299
+ }
3300
+ }, 1500);
3301
+ startGuiServer(port);
3302
+ } else {
3303
+ program.parse();
3304
+ }
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.29",
4
4
  "description": "Cross-platform skill manager for AI coding tools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,14 +9,18 @@
9
9
  "scripts": {
10
10
  "build": "tsup",
11
11
  "dev": "tsup --watch",
12
- "test": "vitest"
12
+ "test": "vitest",
13
+ "build:exe": "node scripts/build-exe.mjs"
13
14
  },
14
15
  "dependencies": {
16
+ "adm-zip": "^0.5.17",
15
17
  "commander": "^12.0.0",
16
18
  "fs-extra": "^11.2.0"
17
19
  },
18
20
  "devDependencies": {
19
21
  "@types/fs-extra": "^11.0.4",
22
+ "@yao-pkg/pkg": "^6.19.0",
23
+ "postject": "^1.0.0-alpha.6",
20
24
  "tsup": "^8.0.0",
21
25
  "typescript": "^5.3.0",
22
26
  "vitest": "^1.2.0"