itismyskillmarket 1.3.12 → 1.3.13

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/gui/app.js CHANGED
@@ -71,6 +71,9 @@ function switchView(view) {
71
71
  case 'help':
72
72
  loadHelp();
73
73
  break;
74
+ case 'admin':
75
+ loadAdminDashboard();
76
+ break;
74
77
  }
75
78
  }
76
79
 
@@ -102,6 +105,8 @@ function initializeControls() {
102
105
  // 刷新按钮
103
106
  document.getElementById('refresh-skills').addEventListener('click', () => loadSkills());
104
107
  document.getElementById('update-all').addEventListener('click', updateAllSkills);
108
+ const refreshAdmin = document.getElementById('refresh-admin');
109
+ if (refreshAdmin) refreshAdmin.addEventListener('click', () => loadAdminDashboard());
105
110
 
106
111
  // 模态框关闭
107
112
  document.querySelector('.modal-close').addEventListener('click', closeModal);
@@ -526,3 +531,450 @@ function showToast(message, type = 'info') {
526
531
  toast.classList.add('hidden');
527
532
  }, 3000);
528
533
  }
534
+
535
+ // -----------------------------------------------------------------------------
536
+ // Admin Dashboard
537
+ // -----------------------------------------------------------------------------
538
+
539
+ async function loadAdminDashboard() {
540
+ await Promise.all([loadAdminStats(), loadAdminSkills()]);
541
+ }
542
+
543
+ async function loadAdminStats() {
544
+ const container = document.getElementById('admin-stats');
545
+ container.innerHTML = '<div class="loading">Loading stats...</div>';
546
+
547
+ try {
548
+ const response = await fetch('/api/admin/stats');
549
+ const data = await response.json();
550
+
551
+ if (data.error) {
552
+ container.innerHTML = `<div class="loading">Error: ${data.error}</div>`;
553
+ return;
554
+ }
555
+
556
+ const cards = [
557
+ { value: data.totalSkills, label: 'Published Skills' },
558
+ { value: data.totalVersions, label: 'Total Versions' },
559
+ { value: data.averageVersions, label: 'Avg Versions/Skill' },
560
+ { value: data.withMetadata, label: 'With Metadata' },
561
+ { value: `${data.totalSizeMB} MB`, label: 'Total Size' },
562
+ { value: data.platformCount, label: 'Platforms Covered' },
563
+ ];
564
+
565
+ container.innerHTML = cards.map(c => `
566
+ <div class="admin-stat-card">
567
+ <div class="admin-stat-value">${c.value}</div>
568
+ <div class="admin-stat-label">${c.label}</div>
569
+ </div>
570
+ `).join('');
571
+ } catch (err) {
572
+ container.innerHTML = `<div class="loading">Error: ${err.message}</div>`;
573
+ }
574
+ }
575
+
576
+ async function loadAdminSkills() {
577
+ const container = document.getElementById('admin-skills-list');
578
+ container.innerHTML = '<div class="loading">Loading published skills...</div>';
579
+
580
+ try {
581
+ const params = new URLSearchParams({ limit: '100' });
582
+ const response = await fetch(`/api/skills?${params}`);
583
+ const data = await response.json();
584
+
585
+ if (data.error) {
586
+ container.innerHTML = `<div class="loading">Error: ${data.error}</div>`;
587
+ return;
588
+ }
589
+
590
+ const skills = data.skills || [];
591
+ renderAdminSkills(skills, container);
592
+ } catch (err) {
593
+ container.innerHTML = `<div class="loading">Error: ${err.message}</div>`;
594
+ }
595
+ }
596
+
597
+ function renderAdminSkills(skills, container) {
598
+ if (!skills || skills.length === 0) {
599
+ container.innerHTML = '<div class="loading">No published skills found</div>';
600
+ return;
601
+ }
602
+
603
+ container.innerHTML = `
604
+ <h3 style="color: var(--text-secondary); margin-bottom: 12px; font-size: 1rem;">Published Skills (${skills.length})</h3>
605
+ ${skills.map(skill => {
606
+ const id = skill.id || skill.name;
607
+ const desc = (skill.description || '').slice(0, 80);
608
+ return `
609
+ <div class="admin-skill-row">
610
+ <div class="admin-skill-info">
611
+ <h4>${skill.displayName || id}</h4>
612
+ <div class="admin-skill-meta">${id}@${skill.version || 'latest'} ${skill.platforms && skill.platforms.length ? '— ' + skill.platforms.join(', ') : ''}</div>
613
+ ${desc ? `<div class="admin-skill-desc">${desc}</div>` : ''}
614
+ </div>
615
+ <div class="admin-actions">
616
+ <button class="admin-btn admin-btn-deprecate" onclick="showAdminDeprecateModal('${id}')">Deprecate</button>
617
+ <button class="admin-btn admin-btn-unpublish" onclick="showAdminUnpublishModal('${id}')">Unpublish</button>
618
+ <button class="admin-btn admin-btn-tag" onclick="showAdminTagModal('${id}')">Tags</button>
619
+ <button class="admin-btn admin-btn-owner" onclick="showAdminOwnerModal('${id}')">Owners</button>
620
+ <button class="admin-btn admin-btn-access" onclick="showAdminAccessModal('${id}')">Access</button>
621
+ </div>
622
+ </div>
623
+ `;
624
+ }).join('')}
625
+ `;
626
+ }
627
+
628
+ // -----------------------------------------------------------------------------
629
+ // Admin Action Modals
630
+ // -----------------------------------------------------------------------------
631
+
632
+ function showAdminDeprecateModal(skillId) {
633
+ const modal = document.getElementById('modal');
634
+ const modalBody = document.getElementById('modal-body');
635
+
636
+ modalBody.innerHTML = `
637
+ <h2>Deprecate: ${skillId}</h2>
638
+ <div class="admin-modal-section">
639
+ <div class="admin-input-group">
640
+ <label>Version</label>
641
+ <input type="text" id="deprecate-version" placeholder="(leave empty for all versions)">
642
+ </div>
643
+ <div class="admin-input-group">
644
+ <label>Message</label>
645
+ <input type="text" id="deprecate-msg" value="This skill is deprecated. Please use an alternative.">
646
+ </div>
647
+ <p class="admin-warning-text">⚠ Deprecating will mark this skill as deprecated in the npm registry.</p>
648
+ </div>
649
+ <div class="actions">
650
+ <button class="btn btn-danger" onclick="execAdminDeprecate('${skillId}')">Confirm Deprecate</button>
651
+ <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
652
+ </div>
653
+ `;
654
+ modal.classList.remove('hidden');
655
+ }
656
+
657
+ async function execAdminDeprecate(skillId) {
658
+ const version = document.getElementById('deprecate-version').value;
659
+ const message = document.getElementById('deprecate-msg').value;
660
+
661
+ try {
662
+ showToast(`Deprecating ${skillId}...`, 'info');
663
+ const response = await fetch('/api/admin/deprecate', {
664
+ method: 'POST',
665
+ headers: { 'Content-Type': 'application/json' },
666
+ body: JSON.stringify({ skillId, version, message }),
667
+ });
668
+ const result = await response.json();
669
+ if (result.error) {
670
+ showToast(`Error: ${result.error}`, 'error');
671
+ } else {
672
+ showToast(`✅ ${result.message}`, 'success');
673
+ closeModal();
674
+ loadAdminDashboard();
675
+ }
676
+ } catch (err) {
677
+ showToast(`Error: ${err.message}`, 'error');
678
+ }
679
+ }
680
+
681
+ function showAdminUnpublishModal(skillId) {
682
+ const modal = document.getElementById('modal');
683
+ const modalBody = document.getElementById('modal-body');
684
+
685
+ modalBody.innerHTML = `
686
+ <h2>Unpublish: ${skillId}</h2>
687
+ <div class="admin-modal-section">
688
+ <div class="admin-input-group">
689
+ <label>Version</label>
690
+ <input type="text" id="unpublish-version" placeholder="(leave empty for entire package)">
691
+ </div>
692
+ <div class="admin-checkbox-group">
693
+ <input type="checkbox" id="unpublish-force">
694
+ <label for="unpublish-force">Force unpublish entire package (required if no version specified)</label>
695
+ </div>
696
+ <p class="admin-danger-text">⚠ This action cannot be undone! Packages can be restored within 72 hours.</p>
697
+ </div>
698
+ <div class="actions">
699
+ <button class="btn btn-danger" onclick="execAdminUnpublish('${skillId}')">Confirm Unpublish</button>
700
+ <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
701
+ </div>
702
+ `;
703
+ modal.classList.remove('hidden');
704
+ }
705
+
706
+ async function execAdminUnpublish(skillId) {
707
+ const version = document.getElementById('unpublish-version').value;
708
+ const force = document.getElementById('unpublish-force').checked;
709
+
710
+ try {
711
+ showToast(`Unpublishing ${skillId}...`, 'info');
712
+ const response = await fetch('/api/admin/unpublish', {
713
+ method: 'POST',
714
+ headers: { 'Content-Type': 'application/json' },
715
+ body: JSON.stringify({ skillId, version, force }),
716
+ });
717
+ const result = await response.json();
718
+ if (result.error) {
719
+ showToast(`Error: ${result.error}`, 'error');
720
+ } else {
721
+ showToast(`✅ ${result.message}`, 'success');
722
+ closeModal();
723
+ loadAdminDashboard();
724
+ }
725
+ } catch (err) {
726
+ showToast(`Error: ${err.message}`, 'error');
727
+ }
728
+ }
729
+
730
+ function showAdminTagModal(skillId) {
731
+ const modal = document.getElementById('modal');
732
+ const modalBody = document.getElementById('modal-body');
733
+
734
+ modalBody.innerHTML = `
735
+ <h2>Tags: ${skillId}</h2>
736
+ <div id="admin-tag-current">
737
+ <div class="loading">Loading current tags...</div>
738
+ </div>
739
+ <div class="admin-modal-section" style="margin-top: 16px;">
740
+ <h3>Set Tag</h3>
741
+ <div class="admin-input-group">
742
+ <label>Tag</label>
743
+ <input type="text" id="tag-set-name" placeholder="e.g. beta, latest">
744
+ </div>
745
+ <div class="admin-input-group">
746
+ <label>Version</label>
747
+ <input type="text" id="tag-set-version" placeholder="e.g. 1.0.1">
748
+ </div>
749
+ <button class="btn btn-primary btn-sm" onclick="execAdminTagSet('${skillId}')">Set Tag</button>
750
+ </div>
751
+ <div class="admin-modal-section">
752
+ <h3>Remove Tag</h3>
753
+ <div class="admin-input-group">
754
+ <label>Tag</label>
755
+ <input type="text" id="tag-rm-name" placeholder="e.g. beta">
756
+ </div>
757
+ <button class="btn btn-danger btn-sm" onclick="execAdminTagRemove('${skillId}')">Remove Tag</button>
758
+ </div>
759
+ <div class="actions" style="margin-top: 16px;">
760
+ <button class="btn btn-secondary" onclick="closeModal()">Close</button>
761
+ </div>
762
+ `;
763
+ modal.classList.remove('hidden');
764
+
765
+ // Load current tags
766
+ loadAdminTags(skillId);
767
+ }
768
+
769
+ async function loadAdminTags(skillId) {
770
+ const container = document.getElementById('admin-tag-current');
771
+ try {
772
+ const response = await fetch('/api/admin/tag', {
773
+ method: 'POST',
774
+ headers: { 'Content-Type': 'application/json' },
775
+ body: JSON.stringify({ skillId, action: 'ls' }),
776
+ });
777
+ const result = await response.json();
778
+ if (result.success && result.tags) {
779
+ const entries = Object.entries(result.tags);
780
+ if (entries.length === 0) {
781
+ container.innerHTML = '<p style="color: var(--text-muted); font-size: 0.85rem;">No dist-tags found.</p>';
782
+ } else {
783
+ container.innerHTML = `
784
+ <h3>Current Tags</h3>
785
+ <div class="admin-tag-list">
786
+ ${entries.map(([tag, ver]) => `
787
+ <span class="admin-tag-item">
788
+ ${tag} <span class="tag-version">→ ${ver}</span>
789
+ ${tag === 'latest' ? '<span style="color: var(--accent); font-size: 0.75rem;"> (default)</span>' : ''}
790
+ </span>
791
+ `).join('')}
792
+ </div>
793
+ `;
794
+ }
795
+ } else {
796
+ container.innerHTML = '<p style="color: var(--text-muted); font-size: 0.85rem;">Could not load tags.</p>';
797
+ }
798
+ } catch (err) {
799
+ container.innerHTML = `<p style="color: #ff6666; font-size: 0.85rem;">Error: ${err.message}</p>`;
800
+ }
801
+ }
802
+
803
+ async function execAdminTagSet(skillId) {
804
+ const tag = document.getElementById('tag-set-name').value;
805
+ const version = document.getElementById('tag-set-version').value;
806
+
807
+ if (!tag || !version) {
808
+ showToast('Tag and version are required', 'error');
809
+ return;
810
+ }
811
+
812
+ try {
813
+ showToast(`Setting tag ${tag}...`, 'info');
814
+ const response = await fetch('/api/admin/tag', {
815
+ method: 'POST',
816
+ headers: { 'Content-Type': 'application/json' },
817
+ body: JSON.stringify({ skillId, action: 'set', tag, version }),
818
+ });
819
+ const result = await response.json();
820
+ if (result.error) {
821
+ showToast(`Error: ${result.error}`, 'error');
822
+ } else {
823
+ showToast(`✅ ${result.message}`, 'success');
824
+ loadAdminTags(skillId);
825
+ document.getElementById('tag-set-name').value = '';
826
+ document.getElementById('tag-set-version').value = '';
827
+ }
828
+ } catch (err) {
829
+ showToast(`Error: ${err.message}`, 'error');
830
+ }
831
+ }
832
+
833
+ async function execAdminTagRemove(skillId) {
834
+ const tag = document.getElementById('tag-rm-name').value;
835
+
836
+ if (!tag) {
837
+ showToast('Tag name is required', 'error');
838
+ return;
839
+ }
840
+
841
+ try {
842
+ showToast(`Removing tag ${tag}...`, 'info');
843
+ const response = await fetch('/api/admin/tag', {
844
+ method: 'POST',
845
+ headers: { 'Content-Type': 'application/json' },
846
+ body: JSON.stringify({ skillId, action: 'rm', tag }),
847
+ });
848
+ const result = await response.json();
849
+ if (result.error) {
850
+ showToast(`Error: ${result.error}`, 'error');
851
+ } else {
852
+ showToast(`✅ ${result.message}`, 'success');
853
+ loadAdminTags(skillId);
854
+ document.getElementById('tag-rm-name').value = '';
855
+ }
856
+ } catch (err) {
857
+ showToast(`Error: ${err.message}`, 'error');
858
+ }
859
+ }
860
+
861
+ function showAdminOwnerModal(skillId) {
862
+ const modal = document.getElementById('modal');
863
+ const modalBody = document.getElementById('modal-body');
864
+
865
+ modalBody.innerHTML = `
866
+ <h2>Owners: ${skillId}</h2>
867
+ <div class="admin-modal-section">
868
+ <h3>Add Owner</h3>
869
+ <div class="admin-input-group">
870
+ <label>npm User</label>
871
+ <input type="text" id="owner-add-name" placeholder="npm username">
872
+ </div>
873
+ <button class="btn btn-success btn-sm" onclick="execAdminOwnerAdd('${skillId}')">Add Owner</button>
874
+ </div>
875
+ <div class="admin-modal-section">
876
+ <h3>Remove Owner</h3>
877
+ <div class="admin-input-group">
878
+ <label>npm User</label>
879
+ <input type="text" id="owner-rm-name" placeholder="npm username">
880
+ </div>
881
+ <button class="btn btn-danger btn-sm" onclick="execAdminOwnerRemove('${skillId}')">Remove Owner</button>
882
+ </div>
883
+ <div class="actions">
884
+ <button class="btn btn-secondary" onclick="closeModal()">Close</button>
885
+ </div>
886
+ `;
887
+ modal.classList.remove('hidden');
888
+ }
889
+
890
+ async function execAdminOwnerAdd(skillId) {
891
+ const user = document.getElementById('owner-add-name').value;
892
+ if (!user) { showToast('Username is required', 'error'); return; }
893
+
894
+ try {
895
+ showToast(`Adding owner ${user}...`, 'info');
896
+ const response = await fetch('/api/admin/owner', {
897
+ method: 'POST',
898
+ headers: { 'Content-Type': 'application/json' },
899
+ body: JSON.stringify({ skillId, action: 'add', user }),
900
+ });
901
+ const result = await response.json();
902
+ if (result.error) {
903
+ showToast(`Error: ${result.error}`, 'error');
904
+ } else {
905
+ showToast(`✅ ${result.message}`, 'success');
906
+ document.getElementById('owner-add-name').value = '';
907
+ }
908
+ } catch (err) {
909
+ showToast(`Error: ${err.message}`, 'error');
910
+ }
911
+ }
912
+
913
+ async function execAdminOwnerRemove(skillId) {
914
+ const user = document.getElementById('owner-rm-name').value;
915
+ if (!user) { showToast('Username is required', 'error'); return; }
916
+
917
+ try {
918
+ showToast(`Removing owner ${user}...`, 'info');
919
+ const response = await fetch('/api/admin/owner', {
920
+ method: 'POST',
921
+ headers: { 'Content-Type': 'application/json' },
922
+ body: JSON.stringify({ skillId, action: 'rm', user }),
923
+ });
924
+ const result = await response.json();
925
+ if (result.error) {
926
+ showToast(`Error: ${result.error}`, 'error');
927
+ } else {
928
+ showToast(`✅ ${result.message}`, 'success');
929
+ document.getElementById('owner-rm-name').value = '';
930
+ }
931
+ } catch (err) {
932
+ showToast(`Error: ${err.message}`, 'error');
933
+ }
934
+ }
935
+
936
+ function showAdminAccessModal(skillId) {
937
+ const modal = document.getElementById('modal');
938
+ const modalBody = document.getElementById('modal-body');
939
+
940
+ modalBody.innerHTML = `
941
+ <h2>Access: ${skillId}</h2>
942
+ <div class="admin-modal-section">
943
+ <p style="color: var(--text-muted); font-size: 0.85rem; margin-bottom: 12px;">
944
+ Set the package access level. Public packages are visible to everyone.
945
+ Restricted packages require authentication to install.
946
+ </p>
947
+ <div class="admin-radio-group">
948
+ <label><input type="radio" name="access-level" value="public" checked> Public</label>
949
+ <label><input type="radio" name="access-level" value="restricted"> Restricted</label>
950
+ </div>
951
+ </div>
952
+ <div class="actions">
953
+ <button class="btn btn-primary" onclick="execAdminAccess('${skillId}')">Set Access</button>
954
+ <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
955
+ </div>
956
+ `;
957
+ modal.classList.remove('hidden');
958
+ }
959
+
960
+ async function execAdminAccess(skillId) {
961
+ const level = document.querySelector('input[name="access-level"]:checked').value;
962
+
963
+ try {
964
+ showToast(`Setting access to ${level}...`, 'info');
965
+ const response = await fetch('/api/admin/access', {
966
+ method: 'POST',
967
+ headers: { 'Content-Type': 'application/json' },
968
+ body: JSON.stringify({ skillId, level }),
969
+ });
970
+ const result = await response.json();
971
+ if (result.error) {
972
+ showToast(`Error: ${result.error}`, 'error');
973
+ } else {
974
+ showToast(`✅ ${result.message}`, 'success');
975
+ closeModal();
976
+ }
977
+ } catch (err) {
978
+ showToast(`Error: ${err.message}`, 'error');
979
+ }
980
+ }
package/gui/index.html CHANGED
@@ -26,9 +26,12 @@
26
26
  <button class="nav-btn" data-view="help">
27
27
  <span class="icon">📖</span> Help
28
28
  </button>
29
+ <button class="nav-btn" data-view="admin">
30
+ <span class="icon">⚙️</span> Admin
31
+ </button>
29
32
  </nav>
30
33
  <div class="sidebar-footer">
31
- <span class="version" id="gui-version">v1.3.12</span>
34
+ <span class="version" id="gui-version">v1.3.13</span>
32
35
  </div>
33
36
  </aside>
34
37
 
@@ -76,6 +79,16 @@
76
79
  </div>
77
80
  <div id="help-content"></div>
78
81
  </div>
82
+
83
+ <!-- Admin 视图 -->
84
+ <div id="view-admin" class="view">
85
+ <div class="view-header">
86
+ <h2>Admin Dashboard</h2>
87
+ <button id="refresh-admin" class="btn btn-secondary">🔄 Refresh</button>
88
+ </div>
89
+ <div id="admin-stats" class="admin-stats"></div>
90
+ <div id="admin-skills-list" class="admin-skills-list"></div>
91
+ </div>
79
92
  </main>
80
93
 
81
94
  <!-- Skill 详情模态框 -->