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 +114 -2
- package/gui/app.js +339 -0
- package/gui/index.html +40 -0
- package/gui/style.css +199 -0
- package/package.json +2 -1
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 & 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.
|
|
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
|
},
|