dsh-m 0.1.0 → 0.2.0
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 +104 -0
- package/README.md +61 -23
- package/docs/DESIGN.md +172 -0
- package/lib/cli.js +134 -63
- package/lib/client.js +1254 -217
- package/lib/core/dsh-cli.js +118 -10
- package/lib/core/host-api.js +285 -0
- package/lib/core/httpx.js +93 -36
- package/lib/core/installed.js +69 -4
- package/lib/core/market.js +481 -104
- package/lib/core/npm-integrity.js +141 -0
- package/lib/core/progress.js +113 -0
- package/lib/core/registry-check.js +111 -0
- package/lib/core/registry-controller.js +321 -0
- package/lib/core/registry.js +634 -98
- package/lib/core/versions.js +80 -9
- package/lib/host.js +22 -161
- package/lib/tools.js +56 -41
- package/package.json +4 -2
- package/registry.json +95 -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,31 @@ 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
|
+
function addressLines(state) {
|
|
55
|
+
return [
|
|
56
|
+
`配置地址:${state.configuredAddress === '' ? '(默认官方清单)' : state.configuredAddress}`,
|
|
57
|
+
`实际生效:${state.activeAddress ?? '—'}`,
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
function unavailableLines(state) {
|
|
61
|
+
return [
|
|
62
|
+
'收录清单当前不可用:',
|
|
63
|
+
...addressLines(state),
|
|
64
|
+
state.errors.length ? `错误:${state.errors.join(';')}` : '',
|
|
65
|
+
].filter(Boolean);
|
|
66
|
+
}
|
|
52
67
|
const HELP = `dshm — DSH Marketplace(个人自用 DSH 插件市场)
|
|
53
68
|
|
|
54
69
|
用法:dshm <命令> [参数]
|
|
@@ -67,45 +82,74 @@ const HELP = `dshm — DSH Marketplace(个人自用 DSH 插件市场)
|
|
|
67
82
|
|
|
68
83
|
环境变量:DSHM_REGISTRY_URL(registry 源覆盖)、DSHM_TIMEOUT_MS、DSHM_CACHE_TTL_MIN、DSHM_CACHE_DIR
|
|
69
84
|
`;
|
|
70
|
-
async function
|
|
71
|
-
const
|
|
85
|
+
export async function runCli(argv, deps = {}, io = {}) {
|
|
86
|
+
const out = io.out ?? ((line) => console.log(line));
|
|
87
|
+
const err = io.err ?? ((line) => console.error(line));
|
|
88
|
+
try {
|
|
89
|
+
return await runCliDispatch(argv, deps, { out, err });
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
// 业务错误统一在这里打印并 exit 1(覆盖 install/upgrade/uninstall 等)
|
|
93
|
+
err(`错误:${e instanceof Error ? e.message : String(e)}`);
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function runCliDispatch(argv, deps, io) {
|
|
98
|
+
const { out, err } = io;
|
|
99
|
+
const d = {
|
|
100
|
+
listMarket: deps.listMarket ?? listMarket,
|
|
101
|
+
loadRegistry: deps.loadRegistry ?? loadRegistry,
|
|
102
|
+
listInstalledWithMeta: deps.listInstalledWithMeta ?? listInstalledWithMeta,
|
|
103
|
+
installFromRegistry: deps.installFromRegistry ?? installFromRegistry,
|
|
104
|
+
uninstallPlugin: deps.uninstallPlugin ?? uninstallPlugin,
|
|
105
|
+
upgradePlugin: deps.upgradePlugin ?? upgradePlugin,
|
|
106
|
+
};
|
|
107
|
+
const { cmd, flags } = parseArgs(argv);
|
|
72
108
|
const cfg = cliConfig();
|
|
73
109
|
switch (cmd) {
|
|
74
110
|
case 'help':
|
|
75
111
|
case '--help':
|
|
76
112
|
case '-h':
|
|
77
|
-
|
|
113
|
+
out(HELP);
|
|
78
114
|
return 0;
|
|
79
115
|
case 'search': {
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
116
|
+
// metadata-only:不构造全量 latest,query/category/limit 直接交给 core
|
|
117
|
+
const limit = Number.isFinite(Number(flags.limit)) && Number(flags.limit) > 0 ? Number(flags.limit) : undefined;
|
|
118
|
+
const result = await d.listMarket(cfg, {
|
|
119
|
+
query: typeof flags.query === 'string' ? flags.query : undefined,
|
|
120
|
+
category: typeof flags.category === 'string' ? flags.category : null,
|
|
121
|
+
offset: 0,
|
|
122
|
+
limit,
|
|
123
|
+
withLatest: false,
|
|
124
|
+
namespace: 'cli',
|
|
125
|
+
});
|
|
126
|
+
if (result.registryState.status === 'unavailable') {
|
|
127
|
+
for (const line of unavailableLines(result.registryState))
|
|
128
|
+
err(line);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
if (!result.items.length) {
|
|
132
|
+
out('没有匹配的收录条目。');
|
|
94
133
|
return 0;
|
|
95
134
|
}
|
|
96
|
-
for (const it of items) {
|
|
135
|
+
for (const it of result.items) {
|
|
97
136
|
const inst = it.installed ? ` [已安装 v${it.installedVersion || '?'}]` : '';
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
console.log(` ${it.description}`);
|
|
137
|
+
out(`• ${it.name} (${it.id}) · ${it.category} · ${it.source}${inst}`);
|
|
138
|
+
out(` ${it.description}`);
|
|
101
139
|
}
|
|
102
|
-
|
|
140
|
+
const s = result.registryState;
|
|
141
|
+
out(`\n来源:${s.source}${s.stale ? '(缓存)' : ''} · 更新:${s.fetchedAt ?? '—'} · 共 ${result.total} 条`);
|
|
103
142
|
return 0;
|
|
104
143
|
}
|
|
105
144
|
case 'list': {
|
|
106
|
-
const result = await listInstalledWithMeta(cfg);
|
|
145
|
+
const result = await d.listInstalledWithMeta(cfg, { namespace: 'cli' });
|
|
146
|
+
if (result.registryState.status === 'unavailable') {
|
|
147
|
+
for (const line of unavailableLines(result.registryState))
|
|
148
|
+
out(line);
|
|
149
|
+
out('(registry 不可用,仅列出已装插件)');
|
|
150
|
+
}
|
|
107
151
|
if (!result.items.length) {
|
|
108
|
-
|
|
152
|
+
out(`web profile(${result.profileDir})还没有已装的 dsh 插件。`);
|
|
109
153
|
return 0;
|
|
110
154
|
}
|
|
111
155
|
for (const it of result.items) {
|
|
@@ -113,84 +157,111 @@ async function main() {
|
|
|
113
157
|
it.registryId ? '市场' : '非市场',
|
|
114
158
|
it.outdated && it.latestVersion ? `可升级 → v${it.latestVersion}` : null,
|
|
115
159
|
].filter(Boolean).join(',');
|
|
116
|
-
|
|
160
|
+
out(`• ${it.name} (${it.pkg}) v${it.version || '?'} · ${sourceLabel(it.source)}${marks ? ` · ${marks}` : ''}`);
|
|
117
161
|
}
|
|
118
162
|
if (result.others)
|
|
119
|
-
|
|
163
|
+
out(`\n另有 ${result.others} 个非 dsh 依赖未列出。`);
|
|
120
164
|
return 0;
|
|
121
165
|
}
|
|
122
166
|
case 'outdated': {
|
|
123
|
-
const result = await listInstalledWithMeta(cfg);
|
|
167
|
+
const result = await d.listInstalledWithMeta(cfg, { namespace: 'cli' });
|
|
168
|
+
if (result.registryState.status === 'unavailable') {
|
|
169
|
+
for (const line of unavailableLines(result.registryState))
|
|
170
|
+
err(line);
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
124
173
|
const outdated = result.items.filter((it) => it.outdated);
|
|
125
174
|
if (!outdated.length) {
|
|
126
|
-
|
|
175
|
+
out(`全部 ${result.items.length} 个插件均已是最新版本。`);
|
|
127
176
|
return 0;
|
|
128
177
|
}
|
|
129
178
|
for (const it of outdated) {
|
|
130
|
-
|
|
179
|
+
out(`• ${it.name} (${it.pkg}):v${it.version} → ${it.latestVersion || '最新'}`);
|
|
131
180
|
}
|
|
132
|
-
|
|
181
|
+
out(`\n升级:dshm upgrade --pkg <包名> --yes`);
|
|
133
182
|
return 0;
|
|
134
183
|
}
|
|
135
184
|
case 'registry': {
|
|
136
|
-
const loaded = await loadRegistry(cfg, { force: flags.force === true });
|
|
137
|
-
|
|
185
|
+
const loaded = await d.loadRegistry(cfg, { namespace: 'cli', force: flags.force === true });
|
|
186
|
+
if (loaded.status === 'unavailable') {
|
|
187
|
+
for (const line of unavailableLines(loaded))
|
|
188
|
+
err(line);
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
out(`来源:${loaded.source} · 更新时间:${loaded.fetchedAt ?? '—'} · 条目:${loaded.count}`);
|
|
192
|
+
for (const line of addressLines(loaded))
|
|
193
|
+
out(line);
|
|
138
194
|
for (const e of loaded.registry.plugins) {
|
|
139
|
-
|
|
195
|
+
out(` • ${e.id} · ${e.name} · ${e.category} · ${e.source === 'npm' ? e.npm : e.github}`);
|
|
140
196
|
}
|
|
141
197
|
if (loaded.errors.length)
|
|
142
|
-
|
|
198
|
+
out(`提示:${loaded.errors.join(';')}`);
|
|
143
199
|
return 0;
|
|
144
200
|
}
|
|
145
201
|
case 'install': {
|
|
146
202
|
const id = needFlag(flags, 'id');
|
|
147
203
|
const version = typeof flags.version === 'string' ? flags.version : undefined;
|
|
148
|
-
const res = await withMutationLock(() => installFromRegistry(id, cfg, { version }));
|
|
149
|
-
|
|
204
|
+
const res = await withMutationLock(() => d.installFromRegistry(id, cfg, { version, namespace: 'cli' }));
|
|
205
|
+
out(`✅ 已安装 ${res.pkg}(${res.spec})`);
|
|
150
206
|
if (res.usedAllowAllBuilds)
|
|
151
|
-
|
|
152
|
-
|
|
207
|
+
out('⚠️ 该插件执行了构建脚本(已按策略放行)。');
|
|
208
|
+
out('需要重启 DSH Web 生效:dshm restart --yes');
|
|
153
209
|
return 0;
|
|
154
210
|
}
|
|
155
211
|
case 'upgrade': {
|
|
156
212
|
const target = needFlag(flags, 'pkg');
|
|
157
213
|
requireYes(flags, '升级');
|
|
158
|
-
const res = await withMutationLock(() => upgradePlugin(target, cfg));
|
|
214
|
+
const res = await withMutationLock(() => d.upgradePlugin(target, cfg, { namespace: 'cli' }));
|
|
159
215
|
const from = res.fromVersion ? `v${res.fromVersion} → ` : '';
|
|
160
216
|
const to = res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : '最新';
|
|
161
|
-
|
|
217
|
+
out(`✅ 已升级 ${res.pkg}(${from}${to})`);
|
|
162
218
|
if (res.usedAllowAllBuilds)
|
|
163
|
-
|
|
164
|
-
|
|
219
|
+
out('⚠️ 该插件执行了构建脚本(已按策略放行)。');
|
|
220
|
+
out('需要重启 DSH Web 生效:dshm restart --yes');
|
|
165
221
|
return 0;
|
|
166
222
|
}
|
|
167
223
|
case 'uninstall': {
|
|
168
224
|
const target = needFlag(flags, 'pkg');
|
|
169
225
|
requireYes(flags, '卸载');
|
|
170
|
-
const res = await withMutationLock(() => uninstallPlugin(target, cfg));
|
|
171
|
-
|
|
226
|
+
const res = await withMutationLock(() => d.uninstallPlugin(target, cfg, { namespace: 'cli' }));
|
|
227
|
+
out(`✅ 已卸载 ${res.pkg}${res.liveDisabled ? '(运行中的界面已先下线)' : ''}`);
|
|
172
228
|
if (res.leftovers.length)
|
|
173
|
-
|
|
174
|
-
|
|
229
|
+
out(`ℹ️ 疑似残留数据(未删除):${res.leftovers.join('、')}`);
|
|
230
|
+
out('需要重启 DSH Web 生效:dshm restart --yes');
|
|
175
231
|
return 0;
|
|
176
232
|
}
|
|
177
233
|
case 'restart': {
|
|
178
234
|
requireYes(flags, '重启 DSH Web');
|
|
179
235
|
const res = scheduleRestart(null);
|
|
180
|
-
|
|
236
|
+
out(`✅ 已请求重启(via ${res.via})。服务恢复后刷新页面即可。`);
|
|
181
237
|
return 0;
|
|
182
238
|
}
|
|
183
|
-
default:
|
|
184
|
-
|
|
185
|
-
|
|
239
|
+
default: {
|
|
240
|
+
err(`未知命令:${cmd}\n`);
|
|
241
|
+
out(HELP);
|
|
186
242
|
return 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** 仅在直接执行(bin/node lib/cli.js)时运行;被测试导入时无副作用。 */
|
|
247
|
+
const invokedDirectly = (() => {
|
|
248
|
+
const entry = process.argv[1];
|
|
249
|
+
if (!entry)
|
|
250
|
+
return false;
|
|
251
|
+
try {
|
|
252
|
+
return import.meta.url === pathToFileURL(resolve(entry)).href;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
187
256
|
}
|
|
257
|
+
})();
|
|
258
|
+
if (invokedDirectly) {
|
|
259
|
+
runCli(process.argv.slice(2))
|
|
260
|
+
.then((code) => {
|
|
261
|
+
process.exitCode = code;
|
|
262
|
+
})
|
|
263
|
+
.catch((err) => {
|
|
264
|
+
console.error('错误:', err instanceof Error ? err.message : String(err));
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
});
|
|
188
267
|
}
|
|
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
|
-
});
|