dsh-m 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +35 -14
- package/README.md +35 -14
- package/docs/DESIGN.md +172 -0
- package/lib/cli.js +147 -63
- package/lib/client.js +780 -116
- package/lib/core/host-api.js +285 -0
- package/lib/core/httpx.js +93 -36
- package/lib/core/market.js +477 -105
- package/lib/core/npm-integrity.js +141 -0
- package/lib/core/registry-check.js +111 -0
- package/lib/core/registry-controller.js +321 -0
- package/lib/core/registry.js +637 -100
- package/lib/core/versions.js +66 -23
- package/lib/host.js +22 -173
- package/lib/tools.js +54 -40
- package/package.json +3 -2
- package/registry.json +29 -7
- package/DESIGN.md +0 -140
package/lib/cli.js
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* dshm — DSH Marketplace 薄 CLI(DESIGN.md §5)。与 dshm_* agent 工具同核。
|
|
4
4
|
* 变更类命令(uninstall/upgrade/restart)必须带 --yes 显式确认。
|
|
5
|
+
* 独立 CLI 固定 namespace:'cli'(与 Host 的 host namespace cache 互不影响);
|
|
6
|
+
* registry/search/list/outdated 在清单不可用时打印配置/实际生效地址并 exit 1。
|
|
5
7
|
*/
|
|
6
|
-
import { installFromRegistry, listInstalledWithMeta, uninstallPlugin, upgradePlugin, withMutationLock, } from './core/market.js';
|
|
8
|
+
import { installFromRegistry, listInstalledWithMeta, listMarket, uninstallPlugin, upgradePlugin, withMutationLock, } from './core/market.js';
|
|
7
9
|
import { loadRegistry } from './core/registry.js';
|
|
8
10
|
import { scheduleRestart } from './core/restart.js';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import { resolve } from 'node:path';
|
|
9
13
|
function parseArgs(argv) {
|
|
10
14
|
const cmd = argv[0] || 'help';
|
|
11
15
|
const flags = {};
|
|
@@ -35,20 +39,44 @@ function cliConfig() {
|
|
|
35
39
|
function needFlag(flags, name) {
|
|
36
40
|
const v = flags[name];
|
|
37
41
|
if (typeof v !== 'string' || !v.trim()) {
|
|
38
|
-
|
|
39
|
-
process.exit(1);
|
|
42
|
+
throw new Error(`缺少 --${name} 参数`);
|
|
40
43
|
}
|
|
41
44
|
return v.trim();
|
|
42
45
|
}
|
|
43
46
|
function requireYes(flags, action) {
|
|
44
47
|
if (flags.yes !== true) {
|
|
45
|
-
|
|
46
|
-
process.exit(1);
|
|
48
|
+
throw new Error(`拒绝执行:${action} 是变更操作,必须带 --yes 显式确认。`);
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
function sourceLabel(s) {
|
|
50
52
|
return { npm: 'npm', github: 'github', link: '本地 link', file: '本地 file', unknown: '未知' }[s] || s;
|
|
51
53
|
}
|
|
54
|
+
/** registry 来源的用户友好名称(CLI 本地终端可见)。 */
|
|
55
|
+
function registrySourceLabel(s) {
|
|
56
|
+
return {
|
|
57
|
+
'default-raw': 'GitHub 原始文件(@main)',
|
|
58
|
+
'default-jsdelivr': 'GitHub 镜像(备用)',
|
|
59
|
+
'default-cache': '默认清单缓存',
|
|
60
|
+
bundled: '包内快照(兜底)',
|
|
61
|
+
'custom-url': '自定义 URL 源',
|
|
62
|
+
'custom-file': '本地文件源',
|
|
63
|
+
'custom-cache': '自定义源(缓存)',
|
|
64
|
+
'custom-unavailable': '自定义源(不可用)',
|
|
65
|
+
}[s] || s;
|
|
66
|
+
}
|
|
67
|
+
function addressLines(state) {
|
|
68
|
+
return [
|
|
69
|
+
`配置地址:${state.configuredAddress === '' ? '(默认官方清单)' : state.configuredAddress}`,
|
|
70
|
+
`实际生效:${state.activeAddress ?? '—'}`,
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
function unavailableLines(state) {
|
|
74
|
+
return [
|
|
75
|
+
'收录清单当前不可用:',
|
|
76
|
+
...addressLines(state),
|
|
77
|
+
state.errors.length ? `错误:${state.errors.join(';')}` : '',
|
|
78
|
+
].filter(Boolean);
|
|
79
|
+
}
|
|
52
80
|
const HELP = `dshm — DSH Marketplace(个人自用 DSH 插件市场)
|
|
53
81
|
|
|
54
82
|
用法:dshm <命令> [参数]
|
|
@@ -67,45 +95,74 @@ const HELP = `dshm — DSH Marketplace(个人自用 DSH 插件市场)
|
|
|
67
95
|
|
|
68
96
|
环境变量:DSHM_REGISTRY_URL(registry 源覆盖)、DSHM_TIMEOUT_MS、DSHM_CACHE_TTL_MIN、DSHM_CACHE_DIR
|
|
69
97
|
`;
|
|
70
|
-
async function
|
|
71
|
-
const
|
|
98
|
+
export async function runCli(argv, deps = {}, io = {}) {
|
|
99
|
+
const out = io.out ?? ((line) => console.log(line));
|
|
100
|
+
const err = io.err ?? ((line) => console.error(line));
|
|
101
|
+
try {
|
|
102
|
+
return await runCliDispatch(argv, deps, { out, err });
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
// 业务错误统一在这里打印并 exit 1(覆盖 install/upgrade/uninstall 等)
|
|
106
|
+
err(`错误:${e instanceof Error ? e.message : String(e)}`);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function runCliDispatch(argv, deps, io) {
|
|
111
|
+
const { out, err } = io;
|
|
112
|
+
const d = {
|
|
113
|
+
listMarket: deps.listMarket ?? listMarket,
|
|
114
|
+
loadRegistry: deps.loadRegistry ?? loadRegistry,
|
|
115
|
+
listInstalledWithMeta: deps.listInstalledWithMeta ?? listInstalledWithMeta,
|
|
116
|
+
installFromRegistry: deps.installFromRegistry ?? installFromRegistry,
|
|
117
|
+
uninstallPlugin: deps.uninstallPlugin ?? uninstallPlugin,
|
|
118
|
+
upgradePlugin: deps.upgradePlugin ?? upgradePlugin,
|
|
119
|
+
};
|
|
120
|
+
const { cmd, flags } = parseArgs(argv);
|
|
72
121
|
const cfg = cliConfig();
|
|
73
122
|
switch (cmd) {
|
|
74
123
|
case 'help':
|
|
75
124
|
case '--help':
|
|
76
125
|
case '-h':
|
|
77
|
-
|
|
126
|
+
out(HELP);
|
|
78
127
|
return 0;
|
|
79
128
|
case 'search': {
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
129
|
+
// metadata-only:不构造全量 latest,query/category/limit 直接交给 core
|
|
130
|
+
const limit = Number.isFinite(Number(flags.limit)) && Number(flags.limit) > 0 ? Number(flags.limit) : undefined;
|
|
131
|
+
const result = await d.listMarket(cfg, {
|
|
132
|
+
query: typeof flags.query === 'string' ? flags.query : undefined,
|
|
133
|
+
category: typeof flags.category === 'string' ? flags.category : null,
|
|
134
|
+
offset: 0,
|
|
135
|
+
limit,
|
|
136
|
+
withLatest: false,
|
|
137
|
+
namespace: 'cli',
|
|
138
|
+
});
|
|
139
|
+
if (result.registryState.status === 'unavailable') {
|
|
140
|
+
for (const line of unavailableLines(result.registryState))
|
|
141
|
+
err(line);
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
if (!result.items.length) {
|
|
145
|
+
out('没有匹配的收录条目。');
|
|
94
146
|
return 0;
|
|
95
147
|
}
|
|
96
|
-
for (const it of items) {
|
|
148
|
+
for (const it of result.items) {
|
|
97
149
|
const inst = it.installed ? ` [已安装 v${it.installedVersion || '?'}]` : '';
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
console.log(` ${it.description}`);
|
|
150
|
+
out(`• ${it.name} (${it.id}) · ${it.category} · ${it.source}${inst}`);
|
|
151
|
+
out(` ${it.description}`);
|
|
101
152
|
}
|
|
102
|
-
|
|
153
|
+
const s = result.registryState;
|
|
154
|
+
out(`\n来源:${registrySourceLabel(s.source)}${s.stale ? '(缓存)' : ''} · 更新:${s.fetchedAt ?? '—'} · 共 ${result.total} 条`);
|
|
103
155
|
return 0;
|
|
104
156
|
}
|
|
105
157
|
case 'list': {
|
|
106
|
-
const result = await listInstalledWithMeta(cfg);
|
|
158
|
+
const result = await d.listInstalledWithMeta(cfg, { namespace: 'cli' });
|
|
159
|
+
if (result.registryState.status === 'unavailable') {
|
|
160
|
+
for (const line of unavailableLines(result.registryState))
|
|
161
|
+
out(line);
|
|
162
|
+
out('(registry 不可用,仅列出已装插件)');
|
|
163
|
+
}
|
|
107
164
|
if (!result.items.length) {
|
|
108
|
-
|
|
165
|
+
out(`web profile(${result.profileDir})还没有已装的 dsh 插件。`);
|
|
109
166
|
return 0;
|
|
110
167
|
}
|
|
111
168
|
for (const it of result.items) {
|
|
@@ -113,84 +170,111 @@ async function main() {
|
|
|
113
170
|
it.registryId ? '市场' : '非市场',
|
|
114
171
|
it.outdated && it.latestVersion ? `可升级 → v${it.latestVersion}` : null,
|
|
115
172
|
].filter(Boolean).join(',');
|
|
116
|
-
|
|
173
|
+
out(`• ${it.name} (${it.pkg}) v${it.version || '?'} · ${sourceLabel(it.source)}${marks ? ` · ${marks}` : ''}`);
|
|
117
174
|
}
|
|
118
175
|
if (result.others)
|
|
119
|
-
|
|
176
|
+
out(`\n另有 ${result.others} 个非 dsh 依赖未列出。`);
|
|
120
177
|
return 0;
|
|
121
178
|
}
|
|
122
179
|
case 'outdated': {
|
|
123
|
-
const result = await listInstalledWithMeta(cfg);
|
|
180
|
+
const result = await d.listInstalledWithMeta(cfg, { namespace: 'cli' });
|
|
181
|
+
if (result.registryState.status === 'unavailable') {
|
|
182
|
+
for (const line of unavailableLines(result.registryState))
|
|
183
|
+
err(line);
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
124
186
|
const outdated = result.items.filter((it) => it.outdated);
|
|
125
187
|
if (!outdated.length) {
|
|
126
|
-
|
|
188
|
+
out(`全部 ${result.items.length} 个插件均已是最新版本。`);
|
|
127
189
|
return 0;
|
|
128
190
|
}
|
|
129
191
|
for (const it of outdated) {
|
|
130
|
-
|
|
192
|
+
out(`• ${it.name} (${it.pkg}):v${it.version} → ${it.latestVersion || '最新'}`);
|
|
131
193
|
}
|
|
132
|
-
|
|
194
|
+
out(`\n升级:dshm upgrade --pkg <包名> --yes`);
|
|
133
195
|
return 0;
|
|
134
196
|
}
|
|
135
197
|
case 'registry': {
|
|
136
|
-
const loaded = await loadRegistry(cfg, { force: flags.force === true });
|
|
137
|
-
|
|
198
|
+
const loaded = await d.loadRegistry(cfg, { namespace: 'cli', force: flags.force === true });
|
|
199
|
+
if (loaded.status === 'unavailable') {
|
|
200
|
+
for (const line of unavailableLines(loaded))
|
|
201
|
+
err(line);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
out(`来源:${registrySourceLabel(loaded.source)} · 更新时间:${loaded.fetchedAt ?? '—'} · 条目:${loaded.count}`);
|
|
205
|
+
for (const line of addressLines(loaded))
|
|
206
|
+
out(line);
|
|
138
207
|
for (const e of loaded.registry.plugins) {
|
|
139
|
-
|
|
208
|
+
out(` • ${e.id} · ${e.name} · ${e.category} · ${e.source === 'npm' ? e.npm : e.github}`);
|
|
140
209
|
}
|
|
141
210
|
if (loaded.errors.length)
|
|
142
|
-
|
|
211
|
+
out(`提示:${loaded.errors.join(';')}`);
|
|
143
212
|
return 0;
|
|
144
213
|
}
|
|
145
214
|
case 'install': {
|
|
146
215
|
const id = needFlag(flags, 'id');
|
|
147
216
|
const version = typeof flags.version === 'string' ? flags.version : undefined;
|
|
148
|
-
const res = await withMutationLock(() => installFromRegistry(id, cfg, { version }));
|
|
149
|
-
|
|
217
|
+
const res = await withMutationLock(() => d.installFromRegistry(id, cfg, { version, namespace: 'cli' }));
|
|
218
|
+
out(`✅ 已安装 ${res.pkg}(${res.spec})`);
|
|
150
219
|
if (res.usedAllowAllBuilds)
|
|
151
|
-
|
|
152
|
-
|
|
220
|
+
out('⚠️ 该插件执行了构建脚本(已按策略放行)。');
|
|
221
|
+
out('需要重启 DSH Web 生效:dshm restart --yes');
|
|
153
222
|
return 0;
|
|
154
223
|
}
|
|
155
224
|
case 'upgrade': {
|
|
156
225
|
const target = needFlag(flags, 'pkg');
|
|
157
226
|
requireYes(flags, '升级');
|
|
158
|
-
const res = await withMutationLock(() => upgradePlugin(target, cfg));
|
|
227
|
+
const res = await withMutationLock(() => d.upgradePlugin(target, cfg, { namespace: 'cli' }));
|
|
159
228
|
const from = res.fromVersion ? `v${res.fromVersion} → ` : '';
|
|
160
229
|
const to = res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : '最新';
|
|
161
|
-
|
|
230
|
+
out(`✅ 已升级 ${res.pkg}(${from}${to})`);
|
|
162
231
|
if (res.usedAllowAllBuilds)
|
|
163
|
-
|
|
164
|
-
|
|
232
|
+
out('⚠️ 该插件执行了构建脚本(已按策略放行)。');
|
|
233
|
+
out('需要重启 DSH Web 生效:dshm restart --yes');
|
|
165
234
|
return 0;
|
|
166
235
|
}
|
|
167
236
|
case 'uninstall': {
|
|
168
237
|
const target = needFlag(flags, 'pkg');
|
|
169
238
|
requireYes(flags, '卸载');
|
|
170
|
-
const res = await withMutationLock(() => uninstallPlugin(target, cfg));
|
|
171
|
-
|
|
239
|
+
const res = await withMutationLock(() => d.uninstallPlugin(target, cfg, { namespace: 'cli' }));
|
|
240
|
+
out(`✅ 已卸载 ${res.pkg}${res.liveDisabled ? '(运行中的界面已先下线)' : ''}`);
|
|
172
241
|
if (res.leftovers.length)
|
|
173
|
-
|
|
174
|
-
|
|
242
|
+
out(`ℹ️ 疑似残留数据(未删除):${res.leftovers.join('、')}`);
|
|
243
|
+
out('需要重启 DSH Web 生效:dshm restart --yes');
|
|
175
244
|
return 0;
|
|
176
245
|
}
|
|
177
246
|
case 'restart': {
|
|
178
247
|
requireYes(flags, '重启 DSH Web');
|
|
179
248
|
const res = scheduleRestart(null);
|
|
180
|
-
|
|
249
|
+
out(`✅ 已请求重启(via ${res.via})。服务恢复后刷新页面即可。`);
|
|
181
250
|
return 0;
|
|
182
251
|
}
|
|
183
|
-
default:
|
|
184
|
-
|
|
185
|
-
|
|
252
|
+
default: {
|
|
253
|
+
err(`未知命令:${cmd}\n`);
|
|
254
|
+
out(HELP);
|
|
186
255
|
return 1;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** 仅在直接执行(bin/node lib/cli.js)时运行;被测试导入时无副作用。 */
|
|
260
|
+
const invokedDirectly = (() => {
|
|
261
|
+
const entry = process.argv[1];
|
|
262
|
+
if (!entry)
|
|
263
|
+
return false;
|
|
264
|
+
try {
|
|
265
|
+
return import.meta.url === pathToFileURL(resolve(entry)).href;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return false;
|
|
187
269
|
}
|
|
270
|
+
})();
|
|
271
|
+
if (invokedDirectly) {
|
|
272
|
+
runCli(process.argv.slice(2))
|
|
273
|
+
.then((code) => {
|
|
274
|
+
process.exitCode = code;
|
|
275
|
+
})
|
|
276
|
+
.catch((err) => {
|
|
277
|
+
console.error('错误:', err instanceof Error ? err.message : String(err));
|
|
278
|
+
process.exitCode = 1;
|
|
279
|
+
});
|
|
188
280
|
}
|
|
189
|
-
main()
|
|
190
|
-
.then((code) => {
|
|
191
|
-
process.exitCode = code;
|
|
192
|
-
})
|
|
193
|
-
.catch((err) => {
|
|
194
|
-
console.error('错误:', err instanceof Error ? err.message : String(err));
|
|
195
|
-
process.exitCode = 1;
|
|
196
|
-
});
|