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/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
- 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
+ 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 main() {
71
- const { cmd, flags } = parseArgs(process.argv.slice(2));
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
- console.log(HELP);
113
+ out(HELP);
78
114
  return 0;
79
115
  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('没有匹配的收录条目。');
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
- 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}`);
137
+ out(`• ${it.name} (${it.id}) · ${it.category} · ${it.source}${inst}`);
138
+ out(` ${it.description}`);
101
139
  }
102
- console.log(`\n来源:${result.registrySource}(${result.registryFetchedAt ?? '—'}),共 ${result.items.length} 条`);
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
- console.log(`web profile(${result.profileDir})还没有已装的 dsh 插件。`);
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
- console.log(`• ${it.name} (${it.pkg}) v${it.version || '?'} · ${sourceLabel(it.source)}${marks ? ` · ${marks}` : ''}`);
160
+ out(`• ${it.name} (${it.pkg}) v${it.version || '?'} · ${sourceLabel(it.source)}${marks ? ` · ${marks}` : ''}`);
117
161
  }
118
162
  if (result.others)
119
- console.log(`\n另有 ${result.others} 个非 dsh 依赖未列出。`);
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
- console.log(`全部 ${result.items.length} 个插件均已是最新版本。`);
175
+ out(`全部 ${result.items.length} 个插件均已是最新版本。`);
127
176
  return 0;
128
177
  }
129
178
  for (const it of outdated) {
130
- console.log(`• ${it.name} (${it.pkg}):v${it.version} → ${it.latestVersion || '最新'}`);
179
+ out(`• ${it.name} (${it.pkg}):v${it.version} → ${it.latestVersion || '最新'}`);
131
180
  }
132
- console.log(`\n升级:dshm upgrade --pkg <包名> --yes`);
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
- console.log(`来源:${loaded.source} · 更新时间:${loaded.fetchedAt ?? ''} · 条目:${loaded.registry.plugins.length}`);
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
- console.log(` • ${e.id} · ${e.name} · ${e.category} · ${e.source === 'npm' ? e.npm : e.github}`);
195
+ out(` • ${e.id} · ${e.name} · ${e.category} · ${e.source === 'npm' ? e.npm : e.github}`);
140
196
  }
141
197
  if (loaded.errors.length)
142
- console.log(`提示:${loaded.errors.join(';')}`);
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
- console.log(`✅ 已安装 ${res.pkg}(${res.spec})`);
204
+ const res = await withMutationLock(() => d.installFromRegistry(id, cfg, { version, namespace: 'cli' }));
205
+ out(`✅ 已安装 ${res.pkg}(${res.spec})`);
150
206
  if (res.usedAllowAllBuilds)
151
- console.log('⚠️ 该插件执行了构建脚本(已按策略放行)。');
152
- console.log('需要重启 DSH Web 生效:dshm restart --yes');
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
- console.log(`✅ 已升级 ${res.pkg}(${from}${to})`);
217
+ out(`✅ 已升级 ${res.pkg}(${from}${to})`);
162
218
  if (res.usedAllowAllBuilds)
163
- console.log('⚠️ 该插件执行了构建脚本(已按策略放行)。');
164
- console.log('需要重启 DSH Web 生效:dshm restart --yes');
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
- console.log(`✅ 已卸载 ${res.pkg}${res.liveDisabled ? '(运行中的界面已先下线)' : ''}`);
226
+ const res = await withMutationLock(() => d.uninstallPlugin(target, cfg, { namespace: 'cli' }));
227
+ out(`✅ 已卸载 ${res.pkg}${res.liveDisabled ? '(运行中的界面已先下线)' : ''}`);
172
228
  if (res.leftovers.length)
173
- console.log(`ℹ️ 疑似残留数据(未删除):${res.leftovers.join('、')}`);
174
- console.log('需要重启 DSH Web 生效:dshm restart --yes');
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
- console.log(`✅ 已请求重启(via ${res.via})。服务恢复后刷新页面即可。`);
236
+ out(`✅ 已请求重启(via ${res.via})。服务恢复后刷新页面即可。`);
181
237
  return 0;
182
238
  }
183
- default:
184
- console.error(`未知命令:${cmd}\n`);
185
- console.log(HELP);
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
- });