dsh-plugin 1.3.3 → 1.3.5

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.
@@ -27,7 +27,7 @@ export interface InstallTask {
27
27
  attempts: string[]
28
28
  /** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时任务执行全局 npm 安装,不进任何 profile */
29
29
  globalNpm?: string[]
30
- action: 'add' | 'remove'
30
+ action: 'add' | 'remove' | 'update'
31
31
  /** 队列语义:pending 排队中 / running 执行中 / done / failed / cancelled(用户取消) */
32
32
  status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled'
33
33
  timedOut: boolean
@@ -51,7 +51,7 @@ export interface Invocation {
51
51
  /** 排队中的变更任务及其启动参数。 */
52
52
  export interface QueueItem {
53
53
  task: InstallTask
54
- options: { action: 'add' | 'remove'; profile: string; target: string; timeoutMs?: number; env?: NodeJS.ProcessEnv; globalNpm?: string[] }
54
+ options: { action: 'add' | 'remove' | 'update'; profile: string; target: string; timeoutMs?: number; env?: NodeJS.ProcessEnv; globalNpm?: string[] }
55
55
  }
56
56
 
57
57
  /**
@@ -23,7 +23,8 @@ export const NPM_MIN_VERSION = [11, 6, 0]
23
23
  /** 读本机 npm 版本(major.minor.patch);npm 不在 PATH 或执行失败/超时时返回 null(不妄下结论)。 */
24
24
  export function npmVersionOf(env: NodeJS.ProcessEnv | undefined): [number, number, number] | null {
25
25
  try {
26
- const res = spawnSync('npm', ['-v'], { env, encoding: 'utf8', timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
26
+ // Windows 下 npm 是 npm.cmd:spawn 直接 exec 会拒绝,须 shell:true 解析(与 task-queue 的 useShell 一致)
27
+ const res = spawnSync('npm', ['-v'], { env, encoding: 'utf8', timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'], shell: process.platform === 'win32' })
27
28
  if (res.error !== undefined || res.status !== 0) return null
28
29
  const m = /(\d+)\.(\d+)\.(\d+)/.exec(res.stdout ?? '')
29
30
  return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null
@@ -38,7 +38,7 @@ export function getTask(id: number): InstallTask | undefined {
38
38
  export interface ActiveTaskInfo {
39
39
  id: number
40
40
  target: string
41
- action: 'add' | 'remove'
41
+ action: 'add' | 'remove' | 'update'
42
42
  status: 'pending' | 'running'
43
43
  progress: number
44
44
  lines: string[]
@@ -158,13 +158,23 @@ function stopChild(child: ReturnType<typeof spawn>): void {
158
158
  child.kill('SIGKILL')
159
159
  }
160
160
 
161
+ /** 系统日志类别:安装/卸载/更新三分,针对性地判断「哪一步不兼容」。 */
162
+ function mutationCategory(action: 'add' | 'remove' | 'update'): 'install' | 'uninstall' | 'update' {
163
+ return action === 'remove' ? 'uninstall' : action === 'update' ? 'update' : 'install'
164
+ }
165
+
166
+ /** 系统日志动作词:开始安装/开始卸载/开始更新。 */
167
+ function mutationVerb(action: 'add' | 'remove' | 'update'): string {
168
+ return action === 'remove' ? '开始卸载' : action === 'update' ? '开始更新' : '开始安装'
169
+ }
170
+
161
171
  /**
162
172
  * Enqueue a plugin mutation and return its task. Tasks run strictly serially:
163
173
  * the queue worker starts the next one only after the previous finishes.
164
174
  * Progress is visible through `getTask(id)` / `activeTask()` until done.
165
175
  */
166
176
  export function startPluginMutation(options: {
167
- action: 'add' | 'remove'
177
+ action: 'add' | 'remove' | 'update'
168
178
  profile: string
169
179
  target: string
170
180
  timeoutMs?: number
@@ -193,14 +203,14 @@ export function startPluginMutation(options: {
193
203
  })
194
204
  }
195
205
  queue.push({ task, options: { ...options } })
196
- // 系统日志:记录任务入队(安装/卸载开始)
197
- const category = options.action === 'add' ? 'install' : 'uninstall'
206
+ // 系统日志:记录任务入队(安装/卸载/更新开始)
207
+ const category = mutationCategory(options.action)
198
208
  appendLog(options.profile, {
199
209
  at: Date.now(),
200
210
  level: 'info',
201
211
  category,
202
212
  event: `${category}.start`,
203
- message: `${options.action === 'add' ? '开始安装' : '开始卸载'} ${options.displayTarget ?? options.target}`,
213
+ message: `${mutationVerb(options.action)} ${options.displayTarget ?? options.target}`,
204
214
  })
205
215
  pumpQueue()
206
216
  return task
@@ -215,16 +225,17 @@ function pumpQueue(): void {
215
225
  task.status = 'running'
216
226
  void runPluginMutation({ ...options, task }).finally(() => {
217
227
  runningChildren.delete(task.id)
218
- // 系统日志:任务终态(成功/失败/取消),按动作归类安装/卸载日志
228
+ // 系统日志:任务终态(成功/失败/取消),按动作归类安装/卸载/更新日志
219
229
  const show = task.displayTarget ?? task.target
220
- const category = task.action === 'add' ? 'install' : 'uninstall'
230
+ const category = mutationCategory(task.action)
231
+ const verb = (v: '成功' | '失败') => task.action === 'remove' ? `卸载${v}` : task.action === 'update' ? `更新${v}` : `安装${v}`
221
232
  if (task.status === 'done') {
222
233
  appendLog(options.profile, {
223
234
  at: Date.now(),
224
235
  level: 'success',
225
236
  category,
226
237
  event: `${category}.done`,
227
- message: `${task.action === 'add' ? '安装成功' : '卸载成功'} ${show}${task.needsRestart ? '(待重启生效)' : ''}`,
238
+ message: `${verb('成功')} ${show}${task.needsRestart ? '(待重启生效)' : ''}`,
228
239
  })
229
240
  } else if (task.status === 'failed') {
230
241
  appendLog(options.profile, {
@@ -232,7 +243,7 @@ function pumpQueue(): void {
232
243
  level: 'error',
233
244
  category,
234
245
  event: `${category}.fail`,
235
- message: `${task.action === 'add' ? '安装失败' : '卸载失败'} ${show}`,
246
+ message: `${verb('失败')} ${show}`,
236
247
  })
237
248
  } else if (task.status === 'cancelled') {
238
249
  appendLog(options.profile, {
@@ -256,7 +267,7 @@ function pumpQueue(): void {
256
267
  * Never rejects; failures surface through the result / task state.
257
268
  */
258
269
  function spawnMutation(options: {
259
- action: 'add' | 'remove'
270
+ action: 'add' | 'remove' | 'update'
260
271
  profile: string
261
272
  target: string
262
273
  timeoutMs?: number
@@ -280,17 +291,21 @@ function spawnMutation(options: {
280
291
  // 更新已安装的 npm 包:显式指定 @latest。pnpm add <pkg>(无版本)对已存在依赖
281
292
  // (尤其精确 spec 如 "1.3.0")是幂等的 —— 输出 "Already up to date" 且不改 package.json,
282
293
  // 导致「更新成功」但实际仍停留在旧版本;git 地址与全局安装不受此影响。
283
- const npmUpdateTarget = updateNpm && !isGlobalNpm && githubRepoOf(target) === null
294
+ // update 动作实际执行 `add <target>@latest`(pnpm 对已存在依赖带显式版本即原位覆盖升级),
295
+ // CLI 动词仍透传 add —— 不执行官方 update 动词,避免与既有覆盖重装机制分叉。
296
+ const isUpdate = action === 'update' || updateNpm === true
297
+ const cliAction = action === 'update' ? 'add' : action
298
+ const npmUpdateTarget = isUpdate && !isGlobalNpm && githubRepoOf(target) === null
284
299
  ? `${target}@latest`
285
300
  : target
286
301
  const args = isGlobalNpm
287
302
  ? ['install', '-g', ...(globalNpm ?? [])]
288
- : [...invocation.prefixArgs, 'plugin', '--profile', profile, action, npmUpdateTarget]
303
+ : [...invocation.prefixArgs, 'plugin', '--profile', profile, cliAction, npmUpdateTarget]
289
304
  // 记录实际执行的命令(npm 通道显示包名,git 通道显示 HTTPS 源),
290
305
  // 失败提 Issue 时作为「已尝试的安装方式」随附
291
306
  const executed = isGlobalNpm
292
307
  ? `npm install -g ${(globalNpm ?? []).join(' ')}`
293
- : `dsh plugin --profile ${profile} ${action} ${npmUpdateTarget}`
308
+ : `dsh plugin --profile ${profile} ${cliAction} ${npmUpdateTarget}`
294
309
  if (task && !task.attempts.includes(executed)) {
295
310
  task.attempts.push(executed)
296
311
  }
@@ -443,10 +458,10 @@ function spawnMutation(options: {
443
458
  if (removed) clearPendingRestart(displayTarget ?? target)
444
459
  else addPendingRestart(displayTarget ?? target, 'uninstall')
445
460
  } else {
446
- // 安装成功(或卸载但无 loader 引用)→ 登记待重启:
461
+ // 安装/更新成功(或卸载但无 loader 引用)→ 登记待重启:
447
462
  // 插件要宿主重启才会挂载/卸载干净,重启前一直提醒;展示目标用 displayTarget(owner/repo)
448
463
  task.needsRestart = true
449
- addPendingRestart(displayTarget ?? target, action === 'add' ? 'install' : 'uninstall')
464
+ addPendingRestart(displayTarget ?? target, action === 'remove' ? 'uninstall' : 'install')
450
465
  }
451
466
  }
452
467
  pushLine(task, result.timedOut ? '[timed out]' : `[exit ${result.exitCode ?? '?'}]`)
@@ -513,11 +528,11 @@ function ignoredBuildKeys(output: string): string[] {
513
528
  * 前端据此展示「本机 npm 版本过低」的准确原因 —— 避免该报错被外层 ERR_PNPM_PREPARE_PACKAGE
514
529
  * 误判成插件打包/分发问题、误导用户提 Issue。检测逻辑在独立的 npm-check.ts(本机 npm 环境职责)。 */
515
530
  function annotateNpmTooLow(result: InstallResult, options: {
516
- action: 'add' | 'remove'
531
+ action: 'add' | 'remove' | 'update'
517
532
  task?: InstallTask
518
533
  env?: NodeJS.ProcessEnv
519
534
  }): InstallResult {
520
- if (options.action !== 'add' || result.exitCode === 0 || result.timedOut) return result
535
+ if (options.action === 'remove' || result.exitCode === 0 || result.timedOut) return result
521
536
  const line = npmTooLowMarker(`${result.stderr}\n${result.stdout}`, options.env)
522
537
  if (line === null) return result
523
538
  if (options.task && options.task.status !== 'cancelled') pushLine(options.task, line)
@@ -534,7 +549,7 @@ function annotateNpmTooLow(result: InstallResult, options: {
534
549
  * Other failures fall through to the single-spawn result.
535
550
  */
536
551
  export async function runPluginMutation(options: {
537
- action: 'add' | 'remove'
552
+ action: 'add' | 'remove' | 'update'
538
553
  profile: string
539
554
  target: string
540
555
  timeoutMs?: number
@@ -550,7 +565,8 @@ export async function runPluginMutation(options: {
550
565
  let result = await spawnMutation(options)
551
566
  // allowBuilds 放行 / 装后入口校验都是 profile 专属(pnpm 装进 profile 的场景);
552
567
  // 全局 npm 安装直接走 npm,无 pnpm allowBuilds 拦截、也不进 profile,跳过这两类恢复
553
- if (options.action === 'add' && result.exitCode !== 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
568
+ // 安装与更新(同为 add 通道执行)共享同一套恢复路径;卸载不适用
569
+ if (options.action !== 'remove' && result.exitCode !== 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
554
570
  const output = `${result.stderr}\n${result.stdout}`
555
571
  // git 源:pnpm 打印的 key@url 提示
556
572
  if (output.includes('ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED')) {
@@ -588,7 +604,7 @@ export async function runPluginMutation(options: {
588
604
  // pnpm 安装本身成功,但目标包入口文件缺失(git 分发缺构建产物)→ 判为失败:
589
605
  // 撤销刚登记的待重启,避免用户重启时宿主加载残缺包直接崩溃。
590
606
  // 全局 npm 安装不进 profile(profile deps 里没有这些包),跳过入口校验
591
- if (options.action === 'add' && result.exitCode === 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
607
+ if (options.action !== 'remove' && result.exitCode === 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
592
608
  const verified = verifyInstalledEntry(options.profile, options.target)
593
609
  if (verified.name !== null && verified.missing !== null) {
594
610
  clearPendingRestart(options.displayTarget ?? options.target)
@@ -30,17 +30,18 @@ export function githubTarget(repo: string): string | null {
30
30
  }
31
31
 
32
32
  /**
33
- * DSH 命令行前缀(官方唯一形式 `dsh plugin --profile <name> add <target>`)—— 提取其中的安装目标;
33
+ * DSH 命令行前缀(官方唯一形式 `dsh plugin --profile <name> <add|update> <target>`)—— 提取其中的安装目标;
34
34
  * 非命令输入原样返回。兼容「直接粘贴完整命令」(防用户输错):--profile 必填且只有长选项
35
35
  * (官方 CLI 无 -p 简写,省略即报错),支持 `--profile=<name>` 等号形式,命令大小写不敏感。
36
+ * update = 更新已安装目标到最新;remove 不在此列(卸载走 /uninstall 路由,按已装包名直卸)。
36
37
  * 与客户端 CustomInstallView 口径一致。
37
38
  */
38
- const DSH_PLUGIN_CMD_RE = /^dsh\s+plugin\s+--profile(?:\s+|=)\S+\s+add\s+(.+)$/i
39
+ const DSH_PLUGIN_CMD_RE = /^dsh\s+plugin\s+--profile(?:\s+|=)\S+\s+(add|update)\s+(.+)$/i
39
40
 
40
41
  export function installTargetOf(value: string): string {
41
42
  const input = typeof value === 'string' ? value.trim() : ''
42
43
  const match = DSH_PLUGIN_CMD_RE.exec(input)
43
- return match !== null ? match[1].trim() : input
44
+ return match !== null ? match[2].trim() : input
44
45
  }
45
46
 
46
47
  /**