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/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
- console.error(`错误:缺少 --${name} 参数`);
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
- console.error(`拒绝执行:${action} 是变更操作,必须带 --yes 显式确认。`);
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 main() {
71
- const { cmd, flags } = parseArgs(process.argv.slice(2));
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
- console.log(HELP);
126
+ out(HELP);
78
127
  return 0;
79
128
  case 'search': {
80
- const { listMarket } = await import('./core/market.js');
81
- const result = await listMarket(cfg);
82
- const q = typeof flags.query === 'string' ? flags.query.toLowerCase() : '';
83
- const cat = typeof flags.category === 'string' ? flags.category : null;
84
- const limit = Number(flags.limit) || Infinity;
85
- const items = result.items.filter((it) => {
86
- if (cat && it.category !== cat)
87
- return false;
88
- if (!q)
89
- return true;
90
- return `${it.id} ${it.name} ${it.description} ${it.tags.join(' ')}`.toLowerCase().includes(q);
91
- }).slice(0, limit);
92
- if (!items.length) {
93
- console.log('没有匹配的收录条目。');
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
- const latest = it.latestVersion ? ` 最新 v${it.latestVersion}` : it.latestSha ? ` HEAD ${it.latestSha.slice(0, 7)}` : '';
99
- console.log(`• ${it.name} (${it.id}) · ${it.category} · ${it.source}${inst}${latest}`);
100
- console.log(` ${it.description}`);
150
+ out(`• ${it.name} (${it.id}) · ${it.category} · ${it.source}${inst}`);
151
+ out(` ${it.description}`);
101
152
  }
102
- console.log(`\n来源:${result.registrySource}(${result.registryFetchedAt ?? '—'}),共 ${result.items.length} 条`);
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
- console.log(`web profile(${result.profileDir})还没有已装的 dsh 插件。`);
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
- console.log(`• ${it.name} (${it.pkg}) v${it.version || '?'} · ${sourceLabel(it.source)}${marks ? ` · ${marks}` : ''}`);
173
+ out(`• ${it.name} (${it.pkg}) v${it.version || '?'} · ${sourceLabel(it.source)}${marks ? ` · ${marks}` : ''}`);
117
174
  }
118
175
  if (result.others)
119
- console.log(`\n另有 ${result.others} 个非 dsh 依赖未列出。`);
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
- console.log(`全部 ${result.items.length} 个插件均已是最新版本。`);
188
+ out(`全部 ${result.items.length} 个插件均已是最新版本。`);
127
189
  return 0;
128
190
  }
129
191
  for (const it of outdated) {
130
- console.log(`• ${it.name} (${it.pkg}):v${it.version} → ${it.latestVersion || '最新'}`);
192
+ out(`• ${it.name} (${it.pkg}):v${it.version} → ${it.latestVersion || '最新'}`);
131
193
  }
132
- console.log(`\n升级:dshm upgrade --pkg <包名> --yes`);
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
- console.log(`来源:${loaded.source} · 更新时间:${loaded.fetchedAt ?? ''} · 条目:${loaded.registry.plugins.length}`);
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
- console.log(` • ${e.id} · ${e.name} · ${e.category} · ${e.source === 'npm' ? e.npm : e.github}`);
208
+ out(` • ${e.id} · ${e.name} · ${e.category} · ${e.source === 'npm' ? e.npm : e.github}`);
140
209
  }
141
210
  if (loaded.errors.length)
142
- console.log(`提示:${loaded.errors.join(';')}`);
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
- console.log(`✅ 已安装 ${res.pkg}(${res.spec})`);
217
+ const res = await withMutationLock(() => d.installFromRegistry(id, cfg, { version, namespace: 'cli' }));
218
+ out(`✅ 已安装 ${res.pkg}(${res.spec})`);
150
219
  if (res.usedAllowAllBuilds)
151
- console.log('⚠️ 该插件执行了构建脚本(已按策略放行)。');
152
- console.log('需要重启 DSH Web 生效:dshm restart --yes');
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
- console.log(`✅ 已升级 ${res.pkg}(${from}${to})`);
230
+ out(`✅ 已升级 ${res.pkg}(${from}${to})`);
162
231
  if (res.usedAllowAllBuilds)
163
- console.log('⚠️ 该插件执行了构建脚本(已按策略放行)。');
164
- console.log('需要重启 DSH Web 生效:dshm restart --yes');
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
- console.log(`✅ 已卸载 ${res.pkg}${res.liveDisabled ? '(运行中的界面已先下线)' : ''}`);
239
+ const res = await withMutationLock(() => d.uninstallPlugin(target, cfg, { namespace: 'cli' }));
240
+ out(`✅ 已卸载 ${res.pkg}${res.liveDisabled ? '(运行中的界面已先下线)' : ''}`);
172
241
  if (res.leftovers.length)
173
- console.log(`ℹ️ 疑似残留数据(未删除):${res.leftovers.join('、')}`);
174
- console.log('需要重启 DSH Web 生效:dshm restart --yes');
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
- console.log(`✅ 已请求重启(via ${res.via})。服务恢复后刷新页面即可。`);
249
+ out(`✅ 已请求重启(via ${res.via})。服务恢复后刷新页面即可。`);
181
250
  return 0;
182
251
  }
183
- default:
184
- console.error(`未知命令:${cmd}\n`);
185
- console.log(HELP);
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
- });