dsh-plugin 1.0.0 → 1.1.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.
Files changed (39) hide show
  1. package/README.en.md +12 -7
  2. package/README.md +12 -7
  3. package/client/client.js +725 -223
  4. package/client/client.js.map +1 -1
  5. package/lib/http/routes.js +43 -7
  6. package/lib/services/installed-versions.js +16 -1
  7. package/lib/services/loader.js +12 -5
  8. package/lib/services/npm-resolve.js +67 -0
  9. package/lib/services/task-queue.js +55 -10
  10. package/lib/types/services/install-types.d.ts +8 -0
  11. package/lib/types/services/installed-versions.d.ts +10 -0
  12. package/lib/types/services/loader.d.ts +6 -4
  13. package/lib/types/services/npm-resolve.d.ts +6 -0
  14. package/lib/types/services/task-queue.d.ts +11 -5
  15. package/package.json +1 -1
  16. package/src/client/components/AboutModal.tsx +72 -0
  17. package/src/client/components/CatalogHeader.tsx +30 -5
  18. package/src/client/components/HubUpdateModal.tsx +84 -0
  19. package/src/client/components/NotificationsModal.tsx +84 -58
  20. package/src/client/components/PluginCard.tsx +3 -0
  21. package/src/client/components/PluginHubSection.tsx +73 -14
  22. package/src/client/components/modals.tsx +42 -18
  23. package/src/client/hooks/useCatalog.ts +96 -18
  24. package/src/client/hooks/useTaskQueue.ts +164 -54
  25. package/src/client/lib/catalog.ts +81 -13
  26. package/src/client/lib/failures.ts +14 -2
  27. package/src/client/lib/renderMarkdown.ts +116 -0
  28. package/src/client/locales.ts +47 -4
  29. package/src/client/styles/Header.module.css +83 -3
  30. package/src/client/styles/List.module.css +4 -4
  31. package/src/client/styles/Modal.module.css +320 -5
  32. package/src/client/styles/tokens.module.css +8 -3
  33. package/src/client/types.ts +3 -1
  34. package/src/server/http/routes.ts +42 -7
  35. package/src/server/services/install-types.ts +8 -0
  36. package/src/server/services/installed-versions.ts +20 -1
  37. package/src/server/services/loader.ts +12 -5
  38. package/src/server/services/npm-resolve.ts +67 -0
  39. package/src/server/services/task-queue.ts +57 -10
@@ -36,20 +36,25 @@ export interface ActiveTaskInfo {
36
36
  status: 'pending' | 'running'
37
37
  progress: number
38
38
  lines: string[]
39
+ /** 尝试过的安装方式(npm 反查 + 实际执行命令):失败时前端 issue 预填需要 */
40
+ attempts: string[]
39
41
  }
40
42
 
41
43
  /** All non-terminal tasks in queue order (running first, then pending). Lets the client resume a queue after a page reload. */
42
44
  export function activeTask(): ActiveTaskInfo[] {
43
45
  const found: ActiveTaskInfo[] = []
46
+ // 展示目标用 displayTarget(owner/repo):安装 target 可能被服务端切成 npm 包名
47
+ // (npm 反查命中),队列恢复/待重启展示对用户保持仓库名无感知
48
+ const show = (task: InstallTask): string => task.displayTarget ?? task.target
44
49
  for (const item of queue) {
45
50
  const task = item.task
46
51
  if (task.status === 'pending') {
47
- found.push({ id: task.id, target: task.target, action: task.action, status: 'pending', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES) })
52
+ found.push({ id: task.id, target: show(task), action: task.action, status: 'pending', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts })
48
53
  }
49
54
  }
50
55
  tasks.forEach((task) => {
51
56
  if (task.status === 'running') {
52
- found.unshift({ id: task.id, target: task.target, action: task.action, status: 'running', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES) })
57
+ found.unshift({ id: task.id, target: show(task), action: task.action, status: 'running', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts })
53
58
  }
54
59
  })
55
60
  return found
@@ -162,8 +167,10 @@ export function startPluginMutation(options: {
162
167
  displayTarget?: string
163
168
  /** 运行中 loader:卸载成功后主动移除条目、立即生效(缺失时卸载仍需重启清理) */
164
169
  uninstallLoader?: LoaderHandle
170
+ /** 入队前已尝试的安装方式(npm registry 反查等):失败提 Issue 时如实展示;实际执行的命令由 spawn 时追加 */
171
+ attempts?: string[]
165
172
  }): InstallTask {
166
- const task: InstallTask = { id: nextTaskId, target: options.target, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [] }
173
+ const task: InstallTask = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false }
167
174
  nextTaskId = nextTaskId >= Number.MAX_SAFE_INTEGER ? 1 : nextTaskId + 1
168
175
  tasks.set(task.id, task)
169
176
  if (tasks.size > MAX_TASKS) {
@@ -216,6 +223,12 @@ function spawnMutation(options: {
216
223
  const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, uninstallLoader } = options
217
224
  const invocation = cliInvocation()
218
225
  const args = [...invocation.prefixArgs, 'plugin', '--profile', profile, action, target]
226
+ // 记录实际执行的命令(npm 通道显示包名,git 通道显示 github: 源):失败提 Issue 时
227
+ // 「已尝试的安装方式」如实展示完整命令链,作者一眼看清我们跑过什么
228
+ const executed = `dsh plugin --profile ${profile} ${action} ${target}`
229
+ if (task && !task.attempts.includes(executed)) {
230
+ task.attempts.push(executed)
231
+ }
219
232
  return new Promise((resolvePromise) => {
220
233
  let stdout = ''
221
234
  let stderr = ''
@@ -332,14 +345,19 @@ function spawnMutation(options: {
332
345
  if (task.status === 'done') {
333
346
  task.progress = 100
334
347
  if (action === 'remove' && uninstallLoader) {
335
- // 卸载成功 → 主动从运行中 loader 移除该包条目,立即生效、无需重启;
336
- // 移除失败(或从未加载过但 loader 不可用)才登记「待重启清理」兜底
348
+ // 卸载成功 → 主动从运行中 loader 停用该包条目;
349
+ // 仅当宿主从未加载过它(磁盘已干净)才判「无需重启」;
350
+ // 只要曾在 loader 中存活(停用过 live entry)就仍要求重启——
351
+ // 带 UI 的插件(侧边栏面板等宿主启动时渲染的槽位)disable 后不会
352
+ // 主动消失,重启才能立即摘除面板
337
353
  const removed = await removeLoadedEntry(uninstallLoader, target)
354
+ task.needsRestart = !removed
338
355
  if (removed) clearPendingRestart(displayTarget ?? target)
339
356
  else addPendingRestart(displayTarget ?? target, 'uninstall')
340
357
  } else {
341
358
  // 安装成功(或卸载但无 loader 引用)→ 登记待重启:
342
359
  // 插件要宿主重启才会挂载/卸载干净,重启前一直提醒;展示目标用 displayTarget(owner/repo)
360
+ task.needsRestart = true
343
361
  addPendingRestart(displayTarget ?? target, action === 'add' ? 'install' : 'uninstall')
344
362
  }
345
363
  }
@@ -393,12 +411,24 @@ export function verifyInstalledEntry(profile: string, target: string): { name: s
393
411
  }
394
412
  }
395
413
 
414
+ /** 从 pnpm `Ignored build scripts:` 提示行解析被拦截的构建脚本 key(name@version,逗号分隔)。 */
415
+ function ignoredBuildKeys(output: string): string[] {
416
+ const m = /Ignored build scripts:\s*(.+)/i.exec(output)
417
+ if (m === null) return []
418
+ return m[1]
419
+ .split(',')
420
+ .map((s) => s.trim())
421
+ .filter((s) => s !== '')
422
+ }
423
+
396
424
  /**
397
- * Run a plugin mutation with one recovery path: when `add` fails because a
398
- * git-hosted package's `prepare` script is blocked by pnpm's allowBuilds
399
- * gate (`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`), read the exact allowlist
400
- * key pnpm printed, write it into the profile's pnpm-workspace.yaml, and
401
- * retry once. Other failures fall through to the single-spawn result.
425
+ * Run a plugin mutation with one recovery path: when `add` fails because
426
+ * pnpm's allowBuilds gate blocks a build script — either the git-hosted
427
+ * package's own `prepare` (`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`, key from
428
+ * the printed key@url hint) or a dependency's native build
429
+ * (`ERR_PNPM_IGNORED_BUILDS`, key = the listed name@version) — write the
430
+ * exact keys into the profile's pnpm-workspace.yaml and retry once.
431
+ * Other failures fall through to the single-spawn result.
402
432
  */
403
433
  export async function runPluginMutation(options: {
404
434
  action: 'add' | 'remove'
@@ -415,6 +445,7 @@ export async function runPluginMutation(options: {
415
445
  const result = await spawnMutation(options)
416
446
  if (options.action === 'add' && result.exitCode !== 0 && !result.timedOut) {
417
447
  const output = `${result.stderr}\n${result.stdout}`
448
+ // git 源:pnpm 打印的 key@url 提示
418
449
  if (output.includes('ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED')) {
419
450
  const key = parseAllowBuildsKey(output)
420
451
  if (key !== null) {
@@ -429,6 +460,22 @@ export async function runPluginMutation(options: {
429
460
  return spawnMutation(options)
430
461
  }
431
462
  }
463
+ // 依赖原生模块/构建脚本被 pnpm v11 拦截(ERR_PNPM_IGNORED_BUILDS):git 与 npm 源都会出现。
464
+ // allowBuilds 的 key 是输出里列出的 name@version(如 node-pty@1.1.0),逐个放行后重试一次;
465
+ // 输出解析不到时回退用目标名(插件自身构建脚本被拦的 npm 场景)。
466
+ if (output.includes('ERR_PNPM_IGNORED_BUILDS')) {
467
+ const keys = ignoredBuildKeys(output)
468
+ if (keys.length === 0 && !options.target.startsWith('github:')) keys.push(options.target)
469
+ if (keys.length > 0) {
470
+ if (options.task) {
471
+ options.task.status = 'running'
472
+ options.task.exitCode = null
473
+ pushLine(options.task, `[allow build] ${keys.join(', ')}`)
474
+ }
475
+ for (const key of keys) addAllowBuildsKey(options.profile, key)
476
+ return spawnMutation(options)
477
+ }
478
+ }
432
479
  }
433
480
  // pnpm 安装本身成功,但目标包入口文件缺失(git 分发缺构建产物)→ 判为失败:
434
481
  // 撤销刚登记的待重启,避免用户重启时宿主加载残缺包直接崩溃