coding-tool-x 3.8.7 → 3.8.8
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/web/assets/{AgentsDrawer-C7ePByZx.js → AgentsDrawer-DRQuwi0m.js} +1 -1
- package/dist/web/assets/Analytics-Cf5KwEdj.js +1 -0
- package/dist/web/assets/AssetPathField-DyniM0pF.js +1 -0
- package/dist/web/assets/{ChatHistoryDrawer-Ccl4rhdz.js → ChatHistoryDrawer-CrfS0KYV.js} +1 -1
- package/dist/web/assets/{CollapseItem-MxG_UD4o.js → CollapseItem-ClMsJuUu.js} +1 -1
- package/dist/web/assets/{CommandsDrawer-DWbaxHRZ.js → CommandsDrawer-CzHelf56.js} +1 -1
- package/dist/web/assets/ConfigExportDrawer-BEZ8wTby.js +1 -0
- package/dist/web/assets/ConfigTemplates-Crg3xWe5.js +1 -0
- package/dist/web/assets/{ConfigTemplatesDrawer-CKMFsfEF.js → ConfigTemplatesDrawer-BqM32Im7.js} +1 -1
- package/dist/web/assets/{ConfigTemplatesPanel-Cvbnkvpt.js → ConfigTemplatesPanel-lTdutC8H.js} +1 -1
- package/dist/web/assets/{Divider-D28g1gwj.js → Divider-DsdbkHrG.js} +1 -1
- package/dist/web/assets/{FavoritesDrawer-DA-dR_vP.js → FavoritesDrawer-DDyWUBJR.js} +1 -1
- package/dist/web/assets/Forward-DLCL7aMY.js +1 -0
- package/dist/web/assets/{GatewayConvertDrawer-402_raGb.js → GatewayConvertDrawer-Rigtt_5J.js} +1 -1
- package/dist/web/assets/Home-tCSKmSHV.js +1 -0
- package/dist/web/assets/{MarkdownEditor-BvkDECwH.js → MarkdownEditor-BMMUufSc.js} +1 -1
- package/dist/web/assets/McpDrawer-CmE-d7VW.js +1 -0
- package/dist/web/assets/{McpFormDrawer-DPMnyt5L.js → McpFormDrawer-ByvS18od.js} +1 -1
- package/dist/web/assets/{PluginManager-FKVeugr4.js → PluginManager-C4kJlpVX.js} +1 -1
- package/dist/web/assets/{PluginsDrawer-MvM1R55d.js → PluginsDrawer-05R4k15x.js} +1 -1
- package/dist/web/assets/PluginsPanel-BCoEs7Og.js +1 -0
- package/dist/web/assets/PluginsPanel-CzGMK-Ap.css +1 -0
- package/dist/web/assets/ProjectList-BJt1OIaI.js +1 -0
- package/dist/web/assets/ProjectList-DG3yu8xZ.css +1 -0
- package/dist/web/assets/PromptsDrawer-DnwBf_oW.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-B7d-38wH.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-yrAD4p1A.css +1 -0
- package/dist/web/assets/SessionCard-BUgjHQwX.js +1 -0
- package/dist/web/assets/SessionList-B-bXZRFk.js +1 -0
- package/dist/web/assets/SessionList-qG_XpPBJ.css +1 -0
- package/dist/web/assets/{SettingsDrawer-lMjttbkD.js → SettingsDrawer-BITzjQLl.js} +1 -1
- package/dist/web/assets/SkillManager-D9jkNuRM.js +1 -0
- package/dist/web/assets/{SkillsDrawer-DF6JsSyj.js → SkillsDrawer-0Z4IefxC.js} +1 -1
- package/dist/web/assets/SkillsPanel-BKIVOj7n.js +1 -0
- package/dist/web/assets/{SkillsPanel-B3Nm5j32.css → SkillsPanel-C4ke56nF.css} +1 -1
- package/dist/web/assets/{SpeedTestDrawer-BaZkZqPP.js → SpeedTestDrawer-Bx1bYf5i.js} +1 -1
- package/dist/web/assets/Upload-CzC4rGI4.js +1 -0
- package/dist/web/assets/{WorkspaceDrawer-BFSwZ5yU.js → WorkspaceDrawer-eujtWpoq.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-BHL1uYct.js → WorkspaceManager-CQ-8Ej68.js} +1 -1
- package/dist/web/assets/{WorkspacePanel-B5PooBtg.js → WorkspacePanel-Cql5qE2R.js} +1 -1
- package/dist/web/assets/headers-BqDmkMjy.js +1 -0
- package/dist/web/assets/{icons-B2Vg-IOC.js → icons-DmFe4b5j.js} +1 -1
- package/dist/web/assets/index-Cmmwj3Fx.js +2 -0
- package/dist/web/assets/{index-BohDDAy_.css → index-Cnbe696C.css} +1 -1
- package/dist/web/assets/{light-Cp0t7v32.js → light-0eavxNvt.js} +1 -1
- package/dist/web/assets/platforms-BmzzwnSw.js +1 -0
- package/dist/web/assets/platforms-DWXgmy2w.js +1 -0
- package/dist/web/assets/session-visibility-BAmxh3cK.js +1 -0
- package/dist/web/assets/sessions-D4yQtfKb.js +1 -0
- package/dist/web/assets/sessions-DqlbocLJ.js +1 -0
- package/dist/web/assets/use-message-DqCwefG6.js +1 -0
- package/dist/web/assets/{useResponsiveDrawer-D-DDpbnI.js → useResponsiveDrawer-DZd5rNk5.js} +1 -1
- package/dist/web/index.html +7 -7
- package/package.json +1 -1
- package/src/commands/search.js +17 -2
- package/src/config/paths.js +2 -0
- package/src/platforms/driver-registry.js +9 -1
- package/src/platforms/drivers/claude/session-history-adapter.js +72 -1
- package/src/platforms/drivers/claude/sessions-implementation.js +49 -7
- package/src/platforms/drivers/claude/sessions.js +30 -6
- package/src/platforms/drivers/codex/parser.js +57 -1
- package/src/platforms/drivers/codex/session-history-adapter.js +8 -2
- package/src/platforms/drivers/codex/sessions-implementation.js +25 -3
- package/src/platforms/drivers/codex/sessions.js +18 -1
- package/src/platforms/drivers/dsh/api-operations.js +396 -0
- package/src/platforms/drivers/dsh/channels-implementation.js +458 -0
- package/src/platforms/drivers/dsh/channels.js +191 -0
- package/src/platforms/drivers/dsh/common.js +571 -0
- package/src/platforms/drivers/dsh/native-config.js +93 -0
- package/src/platforms/drivers/dsh/native-logs.js +116 -0
- package/src/platforms/drivers/dsh/plugin-manager.js +117 -0
- package/src/platforms/drivers/dsh/projects.js +23 -0
- package/src/platforms/drivers/dsh/proxy-implementation.js +260 -0
- package/src/platforms/drivers/dsh/proxy.js +23 -0
- package/src/platforms/drivers/dsh/resources.js +346 -0
- package/src/platforms/drivers/dsh/sessions.js +587 -0
- package/src/platforms/drivers/dsh/statistics-implementation.js +238 -0
- package/src/platforms/drivers/dsh/statistics.js +24 -0
- package/src/platforms/drivers/gemini/session-history-adapter.js +49 -1
- package/src/platforms/drivers/gemini/sessions-implementation.js +46 -1
- package/src/platforms/drivers/gemini/sessions.js +2 -1
- package/src/platforms/drivers/omp/session-history-adapter.js +42 -1
- package/src/platforms/drivers/omp/sessions-implementation.js +51 -3
- package/src/platforms/drivers/omp/sessions.js +33 -2
- package/src/platforms/drivers/opencode/sessions-implementation.js +89 -9
- package/src/platforms/drivers/opencode/sessions.js +37 -1
- package/src/platforms/manifest-schema.js +43 -3
- package/src/platforms/manifests/dsh.json +542 -0
- package/src/platforms/path-resolver.js +2 -0
- package/src/platforms/platform-path-context.js +12 -2
- package/src/platforms/registry.js +16 -1
- package/src/platforms/runtime.js +5 -0
- package/src/platforms/session-history-adapters.js +1 -0
- package/src/server/api/platform-api-config.js +23 -2
- package/src/server/api/platform-route-factory.js +9 -2
- package/src/server/api/platforms.js +19 -1
- package/src/server/api/plugins.js +108 -13
- package/src/server/services/config-registry-service.js +10 -1
- package/src/server/services/config-templates-service.js +1 -0
- package/src/server/services/plugins-service.js +474 -103
- package/src/server/services/session-history-index.js +900 -140
- package/src/server/services/session-history-worker.js +3 -1
- package/src/server/services/skill-service.js +214 -7
- package/src/server/services/usage-log-utils.js +1 -0
- package/src/server/services/workspace-service.js +39 -10
- package/src/shared/driver-factories/api.js +22 -6
- package/src/shared/driver-factories/projects.js +8 -0
- package/dist/web/assets/Analytics-BVG6ehFv.js +0 -1
- package/dist/web/assets/AssetPathField-BZI9NvEr.js +0 -1
- package/dist/web/assets/ConfigExportDrawer-DrawKwxE.js +0 -1
- package/dist/web/assets/ConfigTemplates-AiRPv2cd.js +0 -1
- package/dist/web/assets/Home-CTF8ZWKW.js +0 -1
- package/dist/web/assets/McpDrawer-DZGHkuDm.js +0 -1
- package/dist/web/assets/PluginsPanel-CKuBs38B.js +0 -1
- package/dist/web/assets/PluginsPanel-D6ZqITcR.css +0 -1
- package/dist/web/assets/ProjectList-677RLZ8O.css +0 -1
- package/dist/web/assets/ProjectList-tsjwjXV1.js +0 -1
- package/dist/web/assets/PromptsDrawer-B-X9ci8n.js +0 -1
- package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +0 -1
- package/dist/web/assets/RecentSessionsDrawer-wLktMF_c.js +0 -1
- package/dist/web/assets/SessionCard-DkJPy0TY.js +0 -1
- package/dist/web/assets/SessionList-BpYRz1cX.css +0 -1
- package/dist/web/assets/SessionList-Gzhh4FOZ.js +0 -1
- package/dist/web/assets/SkillManager-CzFxWxx-.js +0 -1
- package/dist/web/assets/SkillsPanel--lLEts9y.js +0 -1
- package/dist/web/assets/Upload-BEQ-3Kqq.js +0 -1
- package/dist/web/assets/headers-cf-QapUq.js +0 -1
- package/dist/web/assets/index-BFInlZGI.js +0 -2
- package/dist/web/assets/platforms-CBkB45JP.js +0 -1
- package/dist/web/assets/platforms-FTpjxAT6.js +0 -1
- package/dist/web/assets/sessions-Cmy31slD.js +0 -1
- package/dist/web/assets/sessions-yC_oVVh8.js +0 -1
- package/dist/web/assets/use-message-DoLM8C1C.js +0 -1
|
@@ -9,6 +9,7 @@ const os = require('os');
|
|
|
9
9
|
const http = require('http');
|
|
10
10
|
const https = require('https');
|
|
11
11
|
const path = require('path');
|
|
12
|
+
const crypto = require('crypto');
|
|
12
13
|
const { execFileSync } = require('child_process');
|
|
13
14
|
const toml = require('toml');
|
|
14
15
|
const tomlStringify = require('@iarna/toml').stringify;
|
|
@@ -48,7 +49,6 @@ const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
|
48
49
|
const REPO_SOURCE_META_FILE = '.cc-tool-plugin-source.json';
|
|
49
50
|
const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
|
|
50
51
|
const PLUGIN_LIST_TTL_MS = 1000;
|
|
51
|
-
const MARKET_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
52
52
|
const DEFAULT_GITHUB_HOST = 'https://github.com';
|
|
53
53
|
const DEFAULT_GITLAB_HOST = 'https://gitlab.com';
|
|
54
54
|
const DEFAULT_REPOS_BY_PLATFORM = {
|
|
@@ -56,7 +56,8 @@ const DEFAULT_REPOS_BY_PLATFORM = {
|
|
|
56
56
|
codex: [],
|
|
57
57
|
gemini: [],
|
|
58
58
|
opencode: [],
|
|
59
|
-
omp: []
|
|
59
|
+
omp: [],
|
|
60
|
+
dsh: []
|
|
60
61
|
};
|
|
61
62
|
const PLATFORM_CAPABILITIES = {
|
|
62
63
|
claude: {
|
|
@@ -73,12 +74,12 @@ const PLATFORM_CAPABILITIES = {
|
|
|
73
74
|
},
|
|
74
75
|
codex: {
|
|
75
76
|
platform: 'codex',
|
|
76
|
-
supportsPlugins:
|
|
77
|
-
repositories:
|
|
78
|
-
market:
|
|
79
|
-
install:
|
|
80
|
-
uninstall:
|
|
81
|
-
toggle:
|
|
77
|
+
supportsPlugins: false,
|
|
78
|
+
repositories: false,
|
|
79
|
+
market: false,
|
|
80
|
+
install: false,
|
|
81
|
+
uninstall: false,
|
|
82
|
+
toggle: false,
|
|
82
83
|
config: false,
|
|
83
84
|
import: false,
|
|
84
85
|
syncRepos: false
|
|
@@ -124,6 +125,19 @@ const PLATFORM_CAPABILITIES = {
|
|
|
124
125
|
repositoryToggle: false,
|
|
125
126
|
repositoryAuth: false,
|
|
126
127
|
installMetadataMode: 'omp'
|
|
128
|
+
},
|
|
129
|
+
dsh: {
|
|
130
|
+
platform: 'dsh',
|
|
131
|
+
supportsPlugins: true,
|
|
132
|
+
repositories: true,
|
|
133
|
+
market: false,
|
|
134
|
+
install: false,
|
|
135
|
+
uninstall: false,
|
|
136
|
+
toggle: false,
|
|
137
|
+
config: false,
|
|
138
|
+
import: false,
|
|
139
|
+
syncRepos: false,
|
|
140
|
+
disabledReason: 'DSH 插件由 profile 管理,当前面板仅提供只读查看'
|
|
127
141
|
}
|
|
128
142
|
};
|
|
129
143
|
|
|
@@ -442,7 +456,7 @@ function stripJsonComments(input = '') {
|
|
|
442
456
|
}
|
|
443
457
|
|
|
444
458
|
class PluginsService {
|
|
445
|
-
constructor(platform = 'claude', { registry } = {}) {
|
|
459
|
+
constructor(platform = 'claude', { registry, runtime } = {}) {
|
|
446
460
|
this.platform = resolveManagedPlatform(platform).platform;
|
|
447
461
|
this.registry = registry || (() => {
|
|
448
462
|
try {
|
|
@@ -451,6 +465,13 @@ class PluginsService {
|
|
|
451
465
|
return null;
|
|
452
466
|
}
|
|
453
467
|
})();
|
|
468
|
+
this.runtime = runtime || (() => {
|
|
469
|
+
try {
|
|
470
|
+
return require('../platform-context').getPlatformContext().runtime;
|
|
471
|
+
} catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
})();
|
|
454
475
|
this.pathContext = null;
|
|
455
476
|
try {
|
|
456
477
|
if (typeof this.registry?.resolvePathContext === 'function') {
|
|
@@ -486,6 +507,10 @@ class PluginsService {
|
|
|
486
507
|
this.opencodePluginsDir = path.join(this.opencodeConfigDir, 'plugins');
|
|
487
508
|
this.opencodeLegacyPluginsDir = path.join(this.opencodeConfigDir, 'plugin');
|
|
488
509
|
this.marketCachePath = getPlatformStatePath('pluginMarketCache', this.platform);
|
|
510
|
+
this.pluginContentCacheRoot = path.join(
|
|
511
|
+
path.dirname(this.marketCachePath),
|
|
512
|
+
`${this.platform}-repo-content`
|
|
513
|
+
);
|
|
489
514
|
this._marketCache = null;
|
|
490
515
|
this._marketCacheByKey = new Map();
|
|
491
516
|
this._pluginListCache = new Map();
|
|
@@ -494,6 +519,9 @@ class PluginsService {
|
|
|
494
519
|
this._marketFetchedAt = 0;
|
|
495
520
|
this._marketRepoFingerprint = null;
|
|
496
521
|
this._marketLastUsedStale = false;
|
|
522
|
+
this._marketReadmeCache = new Map();
|
|
523
|
+
this._marketReadmeCacheLoaded = false;
|
|
524
|
+
this._lastFetchedRepoTrees = new Map();
|
|
497
525
|
this.ompNativeAdapter = this.platform === 'omp'
|
|
498
526
|
? new OmpNativePluginAdapter({ pathContext: this.pathContext })
|
|
499
527
|
: null;
|
|
@@ -706,6 +734,8 @@ class PluginsService {
|
|
|
706
734
|
this._marketCacheByKey.clear();
|
|
707
735
|
this._marketFetchedAt = 0;
|
|
708
736
|
this._marketRepoFingerprint = null;
|
|
737
|
+
this._marketReadmeCache.clear();
|
|
738
|
+
this._marketReadmeCacheLoaded = false;
|
|
709
739
|
this._invalidatePluginList();
|
|
710
740
|
if (removeFile) {
|
|
711
741
|
try {
|
|
@@ -723,13 +753,14 @@ class PluginsService {
|
|
|
723
753
|
if (!fs.existsSync(this.marketCachePath)) return null;
|
|
724
754
|
const data = JSON.parse(fs.readFileSync(this.marketCachePath, 'utf-8'));
|
|
725
755
|
if (Array.isArray(data)) {
|
|
726
|
-
return { plugins: data, fetchedAt: 0, reposFingerprint: null };
|
|
756
|
+
return { plugins: data, fetchedAt: 0, reposFingerprint: null, readmes: {} };
|
|
727
757
|
}
|
|
728
758
|
if (Array.isArray(data?.plugins)) {
|
|
729
759
|
return {
|
|
730
760
|
plugins: data.plugins,
|
|
731
761
|
fetchedAt: Number(data.fetchedAt || data.time || 0) || 0,
|
|
732
|
-
reposFingerprint: typeof data.reposFingerprint === 'string' ? data.reposFingerprint : null
|
|
762
|
+
reposFingerprint: typeof data.reposFingerprint === 'string' ? data.reposFingerprint : null,
|
|
763
|
+
readmes: data.readmes && typeof data.readmes === 'object' ? data.readmes : {}
|
|
733
764
|
};
|
|
734
765
|
}
|
|
735
766
|
} catch (err) {
|
|
@@ -740,22 +771,55 @@ class PluginsService {
|
|
|
740
771
|
|
|
741
772
|
loadMarketCacheFromFile() {
|
|
742
773
|
const cached = this.loadMarketCacheEnvelope();
|
|
743
|
-
return cached?.
|
|
744
|
-
|
|
745
|
-
|
|
774
|
+
return Array.isArray(cached?.plugins) ? cached.plugins : null;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
_pluginReadmeCacheKey(plugin = {}) {
|
|
778
|
+
return [
|
|
779
|
+
plugin.name || '',
|
|
780
|
+
plugin.repoId || '',
|
|
781
|
+
plugin.repoProvider || '',
|
|
782
|
+
plugin.repoHost || '',
|
|
783
|
+
plugin.repoOwner || '',
|
|
784
|
+
plugin.repoName || '',
|
|
785
|
+
plugin.repoBranch || '',
|
|
786
|
+
plugin.repoProjectPath || '',
|
|
787
|
+
plugin.repoLocalPath || '',
|
|
788
|
+
plugin.directory || '',
|
|
789
|
+
plugin.source || ''
|
|
790
|
+
].join('::');
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
_loadMarketReadmeCache() {
|
|
794
|
+
if (this._marketReadmeCacheLoaded) return this._marketReadmeCache;
|
|
795
|
+
const envelope = this.loadMarketCacheEnvelope();
|
|
796
|
+
this._marketReadmeCache = new Map(Object.entries(envelope?.readmes || {}));
|
|
797
|
+
this._marketReadmeCacheLoaded = true;
|
|
798
|
+
return this._marketReadmeCache;
|
|
746
799
|
}
|
|
747
800
|
|
|
748
|
-
saveMarketCacheToFile(plugins, fetchedAt = Date.now(), reposFingerprint = null) {
|
|
801
|
+
saveMarketCacheToFile(plugins, fetchedAt = Date.now(), reposFingerprint = null, readmes = null) {
|
|
749
802
|
try {
|
|
750
803
|
this._ensureDir(path.dirname(this.marketCachePath));
|
|
751
|
-
|
|
804
|
+
const payload = {
|
|
752
805
|
fetchedAt,
|
|
753
806
|
time: fetchedAt,
|
|
754
807
|
reposFingerprint,
|
|
755
|
-
plugins
|
|
756
|
-
|
|
808
|
+
plugins,
|
|
809
|
+
readmes: readmes || Object.fromEntries(this._loadMarketReadmeCache())
|
|
810
|
+
};
|
|
811
|
+
const tempPath = `${this.marketCachePath}.tmp-${process.pid}-${Date.now()}`;
|
|
812
|
+
try {
|
|
813
|
+
fs.writeFileSync(tempPath, JSON.stringify(payload), 'utf-8');
|
|
814
|
+
fs.renameSync(tempPath, this.marketCachePath);
|
|
815
|
+
} catch (error) {
|
|
816
|
+
try {
|
|
817
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
818
|
+
} catch (_) {}
|
|
819
|
+
throw error;
|
|
820
|
+
}
|
|
757
821
|
} catch (err) {
|
|
758
|
-
// ignore cache write errors
|
|
822
|
+
// ignore cache write errors, but do not leave a partial cache behind
|
|
759
823
|
}
|
|
760
824
|
}
|
|
761
825
|
|
|
@@ -784,26 +848,166 @@ class PluginsService {
|
|
|
784
848
|
].join('::');
|
|
785
849
|
if (seen.has(key)) continue;
|
|
786
850
|
seen.add(key);
|
|
787
|
-
deduped.push({
|
|
851
|
+
deduped.push(this.sanitizePluginListItem({
|
|
788
852
|
...plugin,
|
|
789
853
|
isInstalled: installedNames.has(plugin.name)
|
|
790
|
-
});
|
|
854
|
+
}));
|
|
791
855
|
}
|
|
792
856
|
|
|
793
857
|
deduped.sort((a, b) => (a.name || '').toLowerCase().localeCompare((b.name || '').toLowerCase()));
|
|
794
858
|
return deduped;
|
|
795
859
|
}
|
|
796
860
|
|
|
861
|
+
sanitizePluginListItem(plugin = {}) {
|
|
862
|
+
const sanitized = { ...plugin };
|
|
863
|
+
for (const key of Object.keys(sanitized)) {
|
|
864
|
+
const normalizedKey = key.toLowerCase();
|
|
865
|
+
if (
|
|
866
|
+
normalizedKey === 'content'
|
|
867
|
+
|| normalizedKey === 'fullcontent'
|
|
868
|
+
|| normalizedKey === 'manifest'
|
|
869
|
+
|| normalizedKey === 'rawmanifest'
|
|
870
|
+
|| normalizedKey === 'files'
|
|
871
|
+
|| normalizedKey === 'filecontents'
|
|
872
|
+
|| (normalizedKey.includes('readme') && normalizedKey !== 'readmeurl')
|
|
873
|
+
) {
|
|
874
|
+
delete sanitized[key];
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return sanitized;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
sanitizePluginList(plugins = []) {
|
|
881
|
+
return (Array.isArray(plugins) ? plugins : []).map(plugin => this.sanitizePluginListItem(plugin));
|
|
882
|
+
}
|
|
883
|
+
|
|
797
884
|
_ensureDir(dirPath) {
|
|
798
885
|
if (!fs.existsSync(dirPath)) {
|
|
799
886
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
800
887
|
}
|
|
801
888
|
}
|
|
802
889
|
|
|
890
|
+
_pluginContentScopeKey(options = {}) {
|
|
891
|
+
if (options.scope !== 'project') return 'user';
|
|
892
|
+
const cwd = options.cwd ? path.resolve(options.cwd) : '';
|
|
893
|
+
if (!cwd) return 'project-unknown';
|
|
894
|
+
return `project-${crypto.createHash('sha256').update(cwd).digest('hex').slice(0, 24)}`;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
_pluginContentRepoKey(repo = {}) {
|
|
898
|
+
const identity = JSON.stringify({
|
|
899
|
+
provider: repo.provider || '',
|
|
900
|
+
host: repo.host || '',
|
|
901
|
+
owner: repo.owner || '',
|
|
902
|
+
name: repo.name || '',
|
|
903
|
+
projectPath: repo.projectPath || '',
|
|
904
|
+
localPath: repo.localPath || '',
|
|
905
|
+
branch: repo.branch || 'main'
|
|
906
|
+
});
|
|
907
|
+
return crypto.createHash('sha256').update(identity).digest('hex').slice(0, 32);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
_pluginContentCacheDir(repo, options = {}) {
|
|
911
|
+
return path.join(
|
|
912
|
+
this.pluginContentCacheRoot,
|
|
913
|
+
this._pluginContentScopeKey(options),
|
|
914
|
+
this._pluginContentRepoKey(repo)
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
_readCachedRepoFile(repo, filePath, options = {}) {
|
|
919
|
+
try {
|
|
920
|
+
const normalizedPath = normalizePluginRepoDirectory(filePath, 'Plugin repository file path');
|
|
921
|
+
if (!normalizedPath) return null;
|
|
922
|
+
const cacheDir = this._pluginContentCacheDir(repo, options);
|
|
923
|
+
const metaPath = path.join(cacheDir, 'index.json');
|
|
924
|
+
if (!fs.existsSync(metaPath)) return null;
|
|
925
|
+
const metadata = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
926
|
+
if (!metadata.ready || !metadata.files?.includes(normalizedPath)) return null;
|
|
927
|
+
const cachedPath = resolveInsideRoot(
|
|
928
|
+
cacheDir,
|
|
929
|
+
normalizedPath,
|
|
930
|
+
'Cached plugin repository file path',
|
|
931
|
+
{ allowHiddenSegments: true }
|
|
932
|
+
);
|
|
933
|
+
if (!fs.existsSync(cachedPath)) return null;
|
|
934
|
+
return fs.readFileSync(cachedPath, 'utf8');
|
|
935
|
+
} catch {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async _cacheRemoteRepoContent(repo, tree, options = {}) {
|
|
941
|
+
if (repo.provider === 'local') return { fetchedFiles: 0 };
|
|
942
|
+
const files = (Array.isArray(tree) ? tree : [])
|
|
943
|
+
.filter(item => item?.type === 'blob' && item.path)
|
|
944
|
+
.map(item => ({ ...item, path: normalizePluginRepoDirectory(item.path, 'Plugin repository file path') }))
|
|
945
|
+
.filter(item => item.path);
|
|
946
|
+
const uniqueFiles = Array.from(new Map(files.map(file => [file.path, file])).values());
|
|
947
|
+
const cacheDir = this._pluginContentCacheDir(repo, options);
|
|
948
|
+
const tempDir = `${cacheDir}.tmp-${process.pid}-${Date.now()}`;
|
|
949
|
+
let backupDir = null;
|
|
950
|
+
|
|
951
|
+
this._ensureDir(path.dirname(cacheDir));
|
|
952
|
+
this._ensureDir(tempDir);
|
|
953
|
+
try {
|
|
954
|
+
let nextFileIndex = 0;
|
|
955
|
+
const fetchWorker = async () => {
|
|
956
|
+
while (nextFileIndex < uniqueFiles.length) {
|
|
957
|
+
const file = uniqueFiles[nextFileIndex++];
|
|
958
|
+
const content = await this.fetchRepoFileContent(repo, file.path, file);
|
|
959
|
+
const targetPath = resolveInsideRoot(
|
|
960
|
+
tempDir,
|
|
961
|
+
file.path,
|
|
962
|
+
'Cached plugin repository file path',
|
|
963
|
+
{ allowHiddenSegments: true }
|
|
964
|
+
);
|
|
965
|
+
this._ensureDir(path.dirname(targetPath));
|
|
966
|
+
fs.writeFileSync(targetPath, content, 'utf8');
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
await Promise.all(Array.from({ length: Math.min(4, Math.max(uniqueFiles.length, 1)) }, fetchWorker));
|
|
970
|
+
|
|
971
|
+
fs.writeFileSync(
|
|
972
|
+
path.join(tempDir, 'index.json'),
|
|
973
|
+
JSON.stringify({
|
|
974
|
+
ready: true,
|
|
975
|
+
repoId: repo.id || '',
|
|
976
|
+
branch: repo.branch || 'main',
|
|
977
|
+
fetchedAt: Date.now(),
|
|
978
|
+
files: uniqueFiles.map(file => file.path)
|
|
979
|
+
}),
|
|
980
|
+
'utf8'
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
if (fs.existsSync(cacheDir)) {
|
|
984
|
+
backupDir = `${cacheDir}.backup-${process.pid}-${Date.now()}`;
|
|
985
|
+
fs.renameSync(cacheDir, backupDir);
|
|
986
|
+
}
|
|
987
|
+
fs.renameSync(tempDir, cacheDir);
|
|
988
|
+
if (backupDir) fs.rmSync(backupDir, { recursive: true, force: true });
|
|
989
|
+
return { fetchedFiles: uniqueFiles.length };
|
|
990
|
+
} catch (error) {
|
|
991
|
+
try {
|
|
992
|
+
if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
|
|
993
|
+
} catch (_) {}
|
|
994
|
+
if (backupDir && !fs.existsSync(cacheDir) && fs.existsSync(backupDir)) {
|
|
995
|
+
try {
|
|
996
|
+
fs.renameSync(backupDir, cacheDir);
|
|
997
|
+
} catch (_) {}
|
|
998
|
+
}
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
803
1003
|
_isOpenCode() {
|
|
804
1004
|
return this.platform === 'opencode';
|
|
805
1005
|
}
|
|
806
1006
|
|
|
1007
|
+
_isDsh() {
|
|
1008
|
+
return this.platform === 'dsh';
|
|
1009
|
+
}
|
|
1010
|
+
|
|
807
1011
|
_isOmp() {
|
|
808
1012
|
return this.platform === 'omp';
|
|
809
1013
|
}
|
|
@@ -1337,19 +1541,58 @@ class PluginsService {
|
|
|
1337
1541
|
return this._clone(value);
|
|
1338
1542
|
}
|
|
1339
1543
|
|
|
1544
|
+
_listDshPlugins(options = {}) {
|
|
1545
|
+
const driver = this.runtime?.getDriver?.('dsh', 'api');
|
|
1546
|
+
if (!driver?.listPlugins) return { plugins: [] };
|
|
1547
|
+
const request = { query: { ...(options.cwd ? { cwd: options.cwd } : {}) } };
|
|
1548
|
+
const result = driver.listPlugins(request);
|
|
1549
|
+
if (result && typeof result.then === 'function') {
|
|
1550
|
+
// The DSH Driver is normally synchronous for this operation. Keep the
|
|
1551
|
+
// service contract synchronous, matching the existing plugin API.
|
|
1552
|
+
return { plugins: [] };
|
|
1553
|
+
}
|
|
1554
|
+
if (result?.status !== 'ok') return { plugins: [] };
|
|
1555
|
+
const profiles = Array.isArray(result.data?.profiles) ? result.data.profiles : [];
|
|
1556
|
+
const plugins = [];
|
|
1557
|
+
for (const profile of profiles) {
|
|
1558
|
+
for (const plugin of Array.isArray(profile?.plugins) ? profile.plugins : []) {
|
|
1559
|
+
const name = String(plugin.name || '').trim();
|
|
1560
|
+
if (!name) continue;
|
|
1561
|
+
const profileName = String(profile.profile || plugin.profile || '').trim();
|
|
1562
|
+
const key = `dsh:${profileName}:${name}`;
|
|
1563
|
+
plugins.push({
|
|
1564
|
+
...plugin,
|
|
1565
|
+
key,
|
|
1566
|
+
pluginId: key,
|
|
1567
|
+
name,
|
|
1568
|
+
profile: profileName,
|
|
1569
|
+
source: 'dsh-profile',
|
|
1570
|
+
pluginType: 'dsh-profile',
|
|
1571
|
+
enabled: true,
|
|
1572
|
+
readonly: true,
|
|
1573
|
+
description: plugin.description || '',
|
|
1574
|
+
directory: plugin.name || name
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
return { plugins };
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1340
1581
|
/**
|
|
1341
1582
|
* List all installed plugins with their status
|
|
1342
1583
|
* Reads from Claude Code's native installed_plugins.json
|
|
1343
1584
|
* @returns {Object} { plugins: Array }
|
|
1344
1585
|
*/
|
|
1345
1586
|
_listPluginsUncached(options = {}) {
|
|
1346
|
-
if (
|
|
1347
|
-
return
|
|
1587
|
+
if (this._isDsh()) {
|
|
1588
|
+
return this._listDshPlugins(options);
|
|
1348
1589
|
}
|
|
1349
|
-
|
|
1350
1590
|
if (this._isCodex()) {
|
|
1351
1591
|
return { plugins: this._listCodexCachedPlugins() };
|
|
1352
1592
|
}
|
|
1593
|
+
if (!this._pluginsSupported()) {
|
|
1594
|
+
return { plugins: [] };
|
|
1595
|
+
}
|
|
1353
1596
|
|
|
1354
1597
|
if (this._isOpenCode()) {
|
|
1355
1598
|
const plugins = [];
|
|
@@ -1518,6 +1761,11 @@ class PluginsService {
|
|
|
1518
1761
|
* @returns {Object|null} Plugin details or null
|
|
1519
1762
|
*/
|
|
1520
1763
|
getPlugin(name, options = {}) {
|
|
1764
|
+
if (this._isDsh()) {
|
|
1765
|
+
return this.listPlugins(options).plugins.find(plugin => (
|
|
1766
|
+
plugin.key === name || plugin.pluginId === name || plugin.name === name
|
|
1767
|
+
)) || null;
|
|
1768
|
+
}
|
|
1521
1769
|
if (this._isCodex()) {
|
|
1522
1770
|
const plugin = this.listPlugins().plugins.find(p => p.name === name || `${p.name}@${p.marketplace}` === name);
|
|
1523
1771
|
if (!plugin) return null;
|
|
@@ -1583,19 +1831,24 @@ class PluginsService {
|
|
|
1583
1831
|
* @returns {Promise<Object>} Installation result
|
|
1584
1832
|
*/
|
|
1585
1833
|
async installPlugin(source, repoInfo = null, options = {}) {
|
|
1834
|
+
const installFromRepo = (repo, installOptions = {}) => (
|
|
1835
|
+
Object.keys(installOptions || {}).length > 0
|
|
1836
|
+
? this._installFromRepoDirectory(repo, installOptions)
|
|
1837
|
+
: this._installFromRepoDirectory(repo)
|
|
1838
|
+
);
|
|
1586
1839
|
if (this._isCodex()) {
|
|
1587
1840
|
if (hasRepoInstallInfo(repoInfo)) {
|
|
1588
|
-
return
|
|
1841
|
+
return installFromRepo(repoInfo, options);
|
|
1589
1842
|
}
|
|
1590
1843
|
|
|
1591
1844
|
const parsedSource = this.parseRepoTreeSource(source);
|
|
1592
1845
|
if (parsedSource) {
|
|
1593
|
-
return
|
|
1846
|
+
return installFromRepo(parsedSource, options);
|
|
1594
1847
|
}
|
|
1595
1848
|
|
|
1596
1849
|
const parsedRepo = this._repoFromGitUrl(source, 'main');
|
|
1597
1850
|
if (parsedRepo) {
|
|
1598
|
-
return
|
|
1851
|
+
return installFromRepo({ ...parsedRepo, directory: '' }, options);
|
|
1599
1852
|
}
|
|
1600
1853
|
|
|
1601
1854
|
return {
|
|
@@ -1604,6 +1857,13 @@ class PluginsService {
|
|
|
1604
1857
|
};
|
|
1605
1858
|
}
|
|
1606
1859
|
|
|
1860
|
+
if (!this.getCapabilities().install) {
|
|
1861
|
+
return {
|
|
1862
|
+
success: false,
|
|
1863
|
+
error: `${this.platform} plugin installation is not supported`
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1607
1867
|
if (!this._pluginsSupported()) {
|
|
1608
1868
|
return {
|
|
1609
1869
|
success: false,
|
|
@@ -1627,17 +1887,17 @@ class PluginsService {
|
|
|
1627
1887
|
|
|
1628
1888
|
if (this._isOpenCode()) {
|
|
1629
1889
|
if (hasRepoInstallInfo(repoInfo)) {
|
|
1630
|
-
return
|
|
1890
|
+
return installFromRepo(repoInfo, { ...options, installRoot: this._getOpenCodePluginsDir() });
|
|
1631
1891
|
}
|
|
1632
1892
|
|
|
1633
1893
|
const parsedSource = this.parseRepoTreeSource(source);
|
|
1634
1894
|
if (parsedSource) {
|
|
1635
|
-
return
|
|
1895
|
+
return installFromRepo(parsedSource, { ...options, installRoot: this._getOpenCodePluginsDir() });
|
|
1636
1896
|
}
|
|
1637
1897
|
|
|
1638
1898
|
const parsedRepo = this._repoFromGitUrl(source, 'main');
|
|
1639
1899
|
if (parsedRepo) {
|
|
1640
|
-
return
|
|
1900
|
+
return installFromRepo({ ...parsedRepo, directory: '' }, { ...options, installRoot: this._getOpenCodePluginsDir() });
|
|
1641
1901
|
}
|
|
1642
1902
|
|
|
1643
1903
|
// OpenCode 原生支持 npm 包名,通过 opencode.json 的 plugin 数组管理
|
|
@@ -1660,17 +1920,17 @@ class PluginsService {
|
|
|
1660
1920
|
}
|
|
1661
1921
|
|
|
1662
1922
|
if (hasRepoInstallInfo(repoInfo)) {
|
|
1663
|
-
return await
|
|
1923
|
+
return await installFromRepo(repoInfo, options);
|
|
1664
1924
|
}
|
|
1665
1925
|
|
|
1666
1926
|
const parsedSource = this.parseRepoTreeSource(source);
|
|
1667
1927
|
if (parsedSource) {
|
|
1668
|
-
return await
|
|
1928
|
+
return await installFromRepo(parsedSource, options);
|
|
1669
1929
|
}
|
|
1670
1930
|
|
|
1671
1931
|
const parsedRepo = this._repoFromGitUrl(source, 'main');
|
|
1672
1932
|
if (parsedRepo) {
|
|
1673
|
-
return await
|
|
1933
|
+
return await installFromRepo({ ...parsedRepo, directory: '' }, options);
|
|
1674
1934
|
}
|
|
1675
1935
|
|
|
1676
1936
|
// Fallback to original git clone method
|
|
@@ -1688,12 +1948,25 @@ class PluginsService {
|
|
|
1688
1948
|
const manifestCandidates = this._getManifestCandidates(directory);
|
|
1689
1949
|
|
|
1690
1950
|
try {
|
|
1951
|
+
if (normalizedRepo.provider !== 'local') {
|
|
1952
|
+
const cacheIndexPath = path.join(this._pluginContentCacheDir(normalizedRepo, options), 'index.json');
|
|
1953
|
+
if (!fs.existsSync(cacheIndexPath)) {
|
|
1954
|
+
throw new Error('Plugin repository content is not cached; click refresh before installing');
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1691
1958
|
let manifest;
|
|
1692
1959
|
let manifestPath = '';
|
|
1693
1960
|
for (const candidate of manifestCandidates) {
|
|
1694
1961
|
try {
|
|
1695
1962
|
manifestPath = joinRepoPath(directory, candidate);
|
|
1696
|
-
|
|
1963
|
+
if (normalizedRepo.provider === 'local') {
|
|
1964
|
+
manifest = await this.fetchRepoJson(normalizedRepo, manifestPath);
|
|
1965
|
+
} else {
|
|
1966
|
+
const cachedManifest = this._readCachedRepoFile(normalizedRepo, manifestPath, options);
|
|
1967
|
+
if (cachedManifest === null) throw new Error(`File not found in cached repository: ${manifestPath}`);
|
|
1968
|
+
manifest = JSON.parse(stripJsonComments(cachedManifest));
|
|
1969
|
+
}
|
|
1697
1970
|
break;
|
|
1698
1971
|
} catch {
|
|
1699
1972
|
manifestPath = '';
|
|
@@ -1730,52 +2003,14 @@ class PluginsService {
|
|
|
1730
2003
|
}
|
|
1731
2004
|
this.copyDirRecursive(sourceDir, pluginDir);
|
|
1732
2005
|
} else {
|
|
1733
|
-
const
|
|
1734
|
-
const
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
let zipHeaders = {};
|
|
1740
|
-
|
|
1741
|
-
if (normalizedRepo.provider === 'gitlab') {
|
|
1742
|
-
const projectId = encodeURIComponent(normalizedRepo.projectPath);
|
|
1743
|
-
zipUrl = `${normalizedRepo.host}/api/v4/projects/${projectId}/repository/archive.zip?sha=${encodeURIComponent(normalizedRepo.branch)}`;
|
|
1744
|
-
const token = this.getGitLabToken(normalizedRepo);
|
|
1745
|
-
if (token) {
|
|
1746
|
-
zipHeaders['PRIVATE-TOKEN'] = token;
|
|
1747
|
-
}
|
|
1748
|
-
} else {
|
|
1749
|
-
zipUrl = `https://api.github.com/repos/${normalizedRepo.owner}/${normalizedRepo.name}/zipball/${encodeURIComponent(normalizedRepo.branch)}`;
|
|
1750
|
-
const token = this.getGitHubToken(normalizedRepo);
|
|
1751
|
-
zipHeaders.Accept = 'application/vnd.github+json';
|
|
1752
|
-
if (token) {
|
|
1753
|
-
zipHeaders.Authorization = `token ${token}`;
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
await this.downloadFile(zipUrl, zipPath, zipHeaders);
|
|
1758
|
-
const zip = new AdmZip(zipPath);
|
|
1759
|
-
zip.extractAllTo(tempDir, true);
|
|
1760
|
-
|
|
1761
|
-
const extractedDir = fs.readdirSync(tempDir).find(item =>
|
|
1762
|
-
fs.statSync(path.join(tempDir, item)).isDirectory()
|
|
1763
|
-
);
|
|
1764
|
-
if (!extractedDir) {
|
|
1765
|
-
throw new Error('Empty archive');
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
const extractedRoot = path.join(tempDir, extractedDir);
|
|
1769
|
-
const sourceDir = directory
|
|
1770
|
-
? resolveInsideRoot(extractedRoot, directory, 'Plugin directory', { allowHiddenSegments: true })
|
|
1771
|
-
: extractedRoot;
|
|
1772
|
-
if (!fs.existsSync(sourceDir)) {
|
|
1773
|
-
throw new Error(`Plugin directory not found: ${directory}`);
|
|
1774
|
-
}
|
|
1775
|
-
this.copyDirRecursive(sourceDir, pluginDir);
|
|
1776
|
-
} finally {
|
|
1777
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
2006
|
+
const cachedRepoDir = this._pluginContentCacheDir(normalizedRepo, options);
|
|
2007
|
+
const sourceDir = directory
|
|
2008
|
+
? resolveInsideRoot(cachedRepoDir, directory, 'Plugin directory', { allowHiddenSegments: true })
|
|
2009
|
+
: cachedRepoDir;
|
|
2010
|
+
if (!fs.existsSync(sourceDir)) {
|
|
2011
|
+
throw new Error(`Plugin directory not found in cached repository: ${directory}`);
|
|
1778
2012
|
}
|
|
2013
|
+
this.copyDirRecursive(sourceDir, pluginDir, { excludeNames: directory ? [] : ['index.json'] });
|
|
1779
2014
|
}
|
|
1780
2015
|
|
|
1781
2016
|
// Write plugin.json if not exists
|
|
@@ -2042,6 +2277,13 @@ class PluginsService {
|
|
|
2042
2277
|
};
|
|
2043
2278
|
}
|
|
2044
2279
|
|
|
2280
|
+
if (!this.getCapabilities().uninstall) {
|
|
2281
|
+
return {
|
|
2282
|
+
success: false,
|
|
2283
|
+
error: `${this.platform} plugin uninstallation is not supported`
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2045
2287
|
if (!this._pluginsSupported()) {
|
|
2046
2288
|
return {
|
|
2047
2289
|
success: false,
|
|
@@ -2183,6 +2425,10 @@ class PluginsService {
|
|
|
2183
2425
|
};
|
|
2184
2426
|
}
|
|
2185
2427
|
|
|
2428
|
+
if (!this.getCapabilities().toggle) {
|
|
2429
|
+
throw new Error(`${this.platform} plugin toggles are not supported`);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2186
2432
|
if (!this._pluginsSupported()) {
|
|
2187
2433
|
throw new Error(`${this.platform} plugin management is not supported`);
|
|
2188
2434
|
}
|
|
@@ -2457,6 +2703,7 @@ class PluginsService {
|
|
|
2457
2703
|
* @returns {Array} Updated repos list
|
|
2458
2704
|
*/
|
|
2459
2705
|
addRepo(repo, options = {}) {
|
|
2706
|
+
if (!this.getCapabilities().repositories) return [];
|
|
2460
2707
|
if (this._isOmp()) {
|
|
2461
2708
|
const source = repo.source || repo.sourceUri || repo.repoUrl || repo.url
|
|
2462
2709
|
|| repo.localPath || [repo.owner, repo.name].filter(Boolean).join('/');
|
|
@@ -2488,6 +2735,7 @@ class PluginsService {
|
|
|
2488
2735
|
* @returns {Array} Updated repos list
|
|
2489
2736
|
*/
|
|
2490
2737
|
removeRepo(owner, name, repoId = '', options = {}) {
|
|
2738
|
+
if (!this.getCapabilities().repositories) return [];
|
|
2491
2739
|
if (this._isOmp()) {
|
|
2492
2740
|
return this.ompNativeAdapter.removeMarketplace(repoId || name, options);
|
|
2493
2741
|
}
|
|
@@ -2511,6 +2759,7 @@ class PluginsService {
|
|
|
2511
2759
|
* @returns {Array} Updated repos list
|
|
2512
2760
|
*/
|
|
2513
2761
|
toggleRepo(owner, name, enabled, repoId = '', options = {}) {
|
|
2762
|
+
if (!this.getCapabilities().repositories) return [];
|
|
2514
2763
|
if (this._isOmp()) {
|
|
2515
2764
|
throw new Error('OMP native marketplaces do not support repository toggles');
|
|
2516
2765
|
}
|
|
@@ -2529,6 +2778,7 @@ class PluginsService {
|
|
|
2529
2778
|
}
|
|
2530
2779
|
|
|
2531
2780
|
updateRepoAuth(owner, name, token = '', clearToken = false, repoId = '') {
|
|
2781
|
+
if (!this.getCapabilities().repositories) return [];
|
|
2532
2782
|
if (this._isOmp()) {
|
|
2533
2783
|
throw new Error('OMP native marketplaces do not support per-repository authentication');
|
|
2534
2784
|
}
|
|
@@ -3029,7 +3279,13 @@ class PluginsService {
|
|
|
3029
3279
|
* @param {Object} plugin - Plugin object with name, repoUrl, source, or repoInfo
|
|
3030
3280
|
* @returns {Promise<string>} README content or empty string
|
|
3031
3281
|
*/
|
|
3032
|
-
async getPluginReadme(plugin
|
|
3282
|
+
async getPluginReadme(plugin, {
|
|
3283
|
+
allowRemote = false,
|
|
3284
|
+
cacheRemote = false,
|
|
3285
|
+
forceRemote = false,
|
|
3286
|
+
scope = 'user',
|
|
3287
|
+
cwd = null
|
|
3288
|
+
} = {}) {
|
|
3033
3289
|
try {
|
|
3034
3290
|
const normalizedDirectory = normalizeRepoPath(plugin.directory || '');
|
|
3035
3291
|
const readmeCandidates = [];
|
|
@@ -3058,6 +3314,10 @@ class PluginsService {
|
|
|
3058
3314
|
}
|
|
3059
3315
|
}
|
|
3060
3316
|
|
|
3317
|
+
const readmeCache = this._loadMarketReadmeCache();
|
|
3318
|
+
const cachedReadme = readmeCache.get(this._pluginReadmeCacheKey(plugin));
|
|
3319
|
+
if (!forceRemote && typeof cachedReadme === 'string') return cachedReadme;
|
|
3320
|
+
|
|
3061
3321
|
let repo = null;
|
|
3062
3322
|
if (plugin.repoProvider || plugin.repoLocalPath || plugin.repoProjectPath || plugin.repoOwner) {
|
|
3063
3323
|
try {
|
|
@@ -3098,9 +3358,23 @@ class PluginsService {
|
|
|
3098
3358
|
|
|
3099
3359
|
if (!repo) return '';
|
|
3100
3360
|
|
|
3361
|
+
for (const candidate of readmeCandidates) {
|
|
3362
|
+
const cachedReadme = this._readCachedRepoFile(repo, candidate, { scope, cwd });
|
|
3363
|
+
if (cachedReadme !== null) {
|
|
3364
|
+
if (cacheRemote) readmeCache.set(this._pluginReadmeCacheKey(plugin), cachedReadme);
|
|
3365
|
+
return cachedReadme;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
if (!allowRemote) return '';
|
|
3370
|
+
|
|
3101
3371
|
for (const candidate of readmeCandidates) {
|
|
3102
3372
|
try {
|
|
3103
|
-
|
|
3373
|
+
const readme = await this.fetchRepoFileContent(repo, candidate);
|
|
3374
|
+
if (cacheRemote && typeof readme === 'string') {
|
|
3375
|
+
readmeCache.set(this._pluginReadmeCacheKey(plugin), readme);
|
|
3376
|
+
}
|
|
3377
|
+
return readme;
|
|
3104
3378
|
} catch {
|
|
3105
3379
|
// try next candidate
|
|
3106
3380
|
}
|
|
@@ -3424,36 +3698,31 @@ class PluginsService {
|
|
|
3424
3698
|
const key = this._marketKey(options);
|
|
3425
3699
|
const repos = this._isOmp() ? [] : this.getRepos(options).filter(repo => repo.enabled);
|
|
3426
3700
|
const reposFingerprint = this._marketReposFingerprint(repos);
|
|
3427
|
-
const now = Date.now();
|
|
3428
3701
|
const cached = this._marketCacheByKey.get(key);
|
|
3429
|
-
|
|
3430
|
-
? cached.plugins
|
|
3431
|
-
: (Array.isArray(this._marketCache) ? this._marketCache : null);
|
|
3432
|
-
if (!forceRefresh && cached && now - cached.fetchedAt < MARKET_CACHE_TTL_MS
|
|
3433
|
-
&& cached.reposFingerprint === reposFingerprint) {
|
|
3702
|
+
if (!forceRefresh && cached && Array.isArray(cached.plugins)) {
|
|
3434
3703
|
return this._clone(cached.plugins);
|
|
3435
3704
|
}
|
|
3436
3705
|
|
|
3437
3706
|
const disk = this.loadMarketCacheEnvelope();
|
|
3438
3707
|
if (!forceRefresh && Array.isArray(disk?.plugins)) {
|
|
3439
3708
|
const diskPlugins = this.prepareMarketPlugins(disk.plugins);
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
this._marketFetchedAt = disk.fetchedAt;
|
|
3452
|
-
this._marketRepoFingerprint = disk.reposFingerprint || reposFingerprint;
|
|
3453
|
-
return this._clone(diskPlugins);
|
|
3454
|
-
}
|
|
3709
|
+
this._marketCacheByKey.set(key, {
|
|
3710
|
+
plugins: diskPlugins,
|
|
3711
|
+
fetchedAt: disk.fetchedAt,
|
|
3712
|
+
reposFingerprint: disk.reposFingerprint || reposFingerprint
|
|
3713
|
+
});
|
|
3714
|
+
this._marketCache = diskPlugins;
|
|
3715
|
+
this._marketFetchedAt = disk.fetchedAt;
|
|
3716
|
+
this._marketRepoFingerprint = disk.reposFingerprint || reposFingerprint;
|
|
3717
|
+
this._marketReadmeCache = new Map(Object.entries(disk.readmes || {}));
|
|
3718
|
+
this._marketReadmeCacheLoaded = true;
|
|
3719
|
+
return this._clone(diskPlugins);
|
|
3455
3720
|
}
|
|
3456
3721
|
|
|
3722
|
+
// A normal read is deliberately local-only. Missing or stale cache must
|
|
3723
|
+
// never fall through to repository/network discovery.
|
|
3724
|
+
if (!forceRefresh) return [];
|
|
3725
|
+
|
|
3457
3726
|
if (this._marketRefreshPromise) return this._marketRefreshPromise;
|
|
3458
3727
|
const promise = this._getMarketPluginsUncached(forceRefresh, options, { reposFingerprint })
|
|
3459
3728
|
.then(plugins => {
|
|
@@ -3477,6 +3746,106 @@ class PluginsService {
|
|
|
3477
3746
|
return promise;
|
|
3478
3747
|
}
|
|
3479
3748
|
|
|
3749
|
+
/**
|
|
3750
|
+
* Explicitly refresh every remote resource currently exposed by the plugin
|
|
3751
|
+
* UI, then persist the market index and README content for offline reads.
|
|
3752
|
+
*/
|
|
3753
|
+
async refreshRemotePlugins({ platform = this.platform, scope = 'user', projectPath = null } = {}) {
|
|
3754
|
+
if (platform !== this.platform) throw new Error(`PluginsService platform mismatch: ${platform}`);
|
|
3755
|
+
if (!this.getCapabilities().market) {
|
|
3756
|
+
return { status: 'succeeded', fetchedPlugins: 0, fetchedReadmes: 0, failedReadmes: [] };
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
const refreshOptions = {
|
|
3760
|
+
scope,
|
|
3761
|
+
...(projectPath ? { cwd: projectPath } : {})
|
|
3762
|
+
};
|
|
3763
|
+
const previousEnvelope = this.loadMarketCacheEnvelope();
|
|
3764
|
+
const plugins = await this.getMarketPlugins(true, refreshOptions);
|
|
3765
|
+
const readmes = this._loadMarketReadmeCache();
|
|
3766
|
+
const failedReadmes = [];
|
|
3767
|
+
const failedRepositories = [];
|
|
3768
|
+
let fetchedFiles = 0;
|
|
3769
|
+
|
|
3770
|
+
const repos = this._isOmp() ? [] : this.getRepos(refreshOptions).filter(repo => repo.enabled);
|
|
3771
|
+
for (const repo of repos.filter(item => item.provider !== 'local')) {
|
|
3772
|
+
let fetched = this._lastFetchedRepoTrees.get(repo.id);
|
|
3773
|
+
try {
|
|
3774
|
+
if (!fetched) {
|
|
3775
|
+
throw new Error('Repository tree was not fetched during market refresh');
|
|
3776
|
+
}
|
|
3777
|
+
const result = await this._cacheRemoteRepoContent(repo, fetched.tree, refreshOptions);
|
|
3778
|
+
fetchedFiles += result.fetchedFiles;
|
|
3779
|
+
} catch (error) {
|
|
3780
|
+
failedRepositories.push({ repoId: repo.id, error: error.message });
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
let fetchedReadmes = 0;
|
|
3785
|
+
|
|
3786
|
+
for (const plugin of plugins) {
|
|
3787
|
+
try {
|
|
3788
|
+
const readme = await this.getPluginReadme(plugin, {
|
|
3789
|
+
allowRemote: true,
|
|
3790
|
+
cacheRemote: true,
|
|
3791
|
+
forceRemote: true
|
|
3792
|
+
});
|
|
3793
|
+
if (readme) fetchedReadmes += 1;
|
|
3794
|
+
} catch (error) {
|
|
3795
|
+
failedReadmes.push({
|
|
3796
|
+
pluginId: plugin.pluginId || plugin.id || plugin.name,
|
|
3797
|
+
error: error.message
|
|
3798
|
+
});
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
const failed = failedRepositories.length > 0 || failedReadmes.length > 0;
|
|
3803
|
+
const status = failed ? 'failed' : 'succeeded';
|
|
3804
|
+
if (failed) {
|
|
3805
|
+
// The market envelope is the read boundary for normal reads. Restore the
|
|
3806
|
+
// previous snapshot when any part of a full refresh failed so the UI
|
|
3807
|
+
// never observes a half-updated market index.
|
|
3808
|
+
if (previousEnvelope?.plugins) {
|
|
3809
|
+
const previousPlugins = this.prepareMarketPlugins(previousEnvelope.plugins);
|
|
3810
|
+
this._marketCache = previousPlugins;
|
|
3811
|
+
this._marketCacheByKey.clear();
|
|
3812
|
+
this._marketCacheByKey.set(this._marketKey(refreshOptions), {
|
|
3813
|
+
plugins: previousPlugins,
|
|
3814
|
+
fetchedAt: previousEnvelope.fetchedAt || 0,
|
|
3815
|
+
reposFingerprint: previousEnvelope.reposFingerprint || null
|
|
3816
|
+
});
|
|
3817
|
+
this._marketFetchedAt = previousEnvelope.fetchedAt || 0;
|
|
3818
|
+
this._marketRepoFingerprint = previousEnvelope.reposFingerprint || null;
|
|
3819
|
+
this._marketReadmeCache = new Map(Object.entries(previousEnvelope.readmes || {}));
|
|
3820
|
+
this._marketReadmeCacheLoaded = true;
|
|
3821
|
+
this.saveMarketCacheToFile(
|
|
3822
|
+
previousEnvelope.plugins,
|
|
3823
|
+
previousEnvelope.fetchedAt,
|
|
3824
|
+
previousEnvelope.reposFingerprint,
|
|
3825
|
+
previousEnvelope.readmes || {}
|
|
3826
|
+
);
|
|
3827
|
+
} else {
|
|
3828
|
+
this.clearMarketCache();
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
this.saveMarketCacheToFile(
|
|
3833
|
+
failed ? (previousEnvelope?.plugins || []) : plugins,
|
|
3834
|
+
failed ? (previousEnvelope?.fetchedAt || 0) : Date.now(),
|
|
3835
|
+
failed ? (previousEnvelope?.reposFingerprint || null) : this._marketReposFingerprint(repos),
|
|
3836
|
+
failed ? (previousEnvelope?.readmes || {}) : Object.fromEntries(readmes)
|
|
3837
|
+
);
|
|
3838
|
+
|
|
3839
|
+
return {
|
|
3840
|
+
status,
|
|
3841
|
+
fetchedPlugins: failed ? 0 : plugins.length,
|
|
3842
|
+
fetchedFiles,
|
|
3843
|
+
fetchedReadmes,
|
|
3844
|
+
failedReadmes,
|
|
3845
|
+
failedRepositories
|
|
3846
|
+
};
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3480
3849
|
/**
|
|
3481
3850
|
* Get market plugins from configured repositories
|
|
3482
3851
|
* @returns {Promise<Array>} List of available market plugins
|
|
@@ -3486,6 +3855,7 @@ class PluginsService {
|
|
|
3486
3855
|
return [];
|
|
3487
3856
|
}
|
|
3488
3857
|
this._marketLastUsedStale = false;
|
|
3858
|
+
if (forceRefresh) this._lastFetchedRepoTrees.clear();
|
|
3489
3859
|
if (this._isOmp()) {
|
|
3490
3860
|
if (forceRefresh) {
|
|
3491
3861
|
this.ompNativeAdapter.updateMarketplaces('', options);
|
|
@@ -3499,7 +3869,7 @@ class PluginsService {
|
|
|
3499
3869
|
|
|
3500
3870
|
const fileCache = this.loadMarketCacheEnvelope()?.plugins || null;
|
|
3501
3871
|
|
|
3502
|
-
const repos = this.getRepos().filter(r => r.enabled);
|
|
3872
|
+
const repos = this.getRepos(options).filter(r => r.enabled);
|
|
3503
3873
|
const marketPlugins = [];
|
|
3504
3874
|
let repoFailureCount = 0;
|
|
3505
3875
|
|
|
@@ -3511,6 +3881,7 @@ class PluginsService {
|
|
|
3511
3881
|
const pluginsBefore = marketPlugins.length;
|
|
3512
3882
|
try {
|
|
3513
3883
|
const tree = await this.fetchRepoTree(repo);
|
|
3884
|
+
this._lastFetchedRepoTrees.set(repo.id, { repo, tree });
|
|
3514
3885
|
const files = tree.filter(item => item.type === 'blob');
|
|
3515
3886
|
const fileMap = new Map(files.map(file => [normalizeRepoPath(file.path), file]));
|
|
3516
3887
|
const readJson = async (filePath) => {
|