@wwkit/opm 1.0.18 → 1.0.20

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.
@@ -335,10 +335,29 @@ export class BrewManager extends PackageManager {
335
335
  }
336
336
 
337
337
  async cleanCache() {
338
- this._execInherit(['cleanup', '-s', '--prune=all'])
338
+ this._execInherit(['cleanup', '-s'])
339
339
  return { action: 'cache cleaned' }
340
340
  }
341
341
 
342
+ /**
343
+ * 列出已安装的 Homebrew 包(系统级,无项目作用域)
344
+ * @returns {Promise<{ name: string, version: string }[]>}
345
+ */
346
+ async listInstalled(opts = {}) {
347
+ if (opts.global === false) return []
348
+ const out = this._exec(['list', '--versions'], { allowNonZero: true })
349
+ const items = []
350
+ // 行格式: name v1 v2 ...(多版本取最新)
351
+ for (const line of out.split('\n')) {
352
+ const m = line.trim().match(/^(\S+)\s+(.+)$/)
353
+ if (m) {
354
+ const versions = m[2].trim().split(/\s+/)
355
+ items.push({ name: m[1], version: versions[versions.length - 1] })
356
+ }
357
+ }
358
+ return items
359
+ }
360
+
342
361
  /**
343
362
  * 查询 brew 安装路径和包目录
344
363
  * @returns {{ name: string, install: string, prefix: string, cellar: string }}
@@ -272,6 +272,29 @@ export class BunManager extends PackageManager {
272
272
  return { action: 'cache cleaned' }
273
273
  }
274
274
 
275
+ /**
276
+ * 列出已安装包(bun pm ls 文本树解析;-g 列全局)
277
+ * @param {{ global?: boolean }} [opts]
278
+ * @returns {Promise<{ name: string, version: string }[]>}
279
+ */
280
+ async listInstalled(opts = {}) {
281
+ const args = opts.global ? ['pm', 'ls', '-g', '--all'] : ['pm', 'ls', '--all']
282
+ const out = this._exec(args, { allowNonZero: true })
283
+ const items = []
284
+ for (const line of out.split('\n')) {
285
+ const cleaned = line.replace(/^[\s├└─│]+/, '').trim()
286
+ if (!cleaned) continue
287
+ // scoped 包:@babel/core@7.0.0 → 最后一个 @ 为版本分隔
288
+ const idx = cleaned.lastIndexOf('@')
289
+ if (idx > 0) {
290
+ const name = cleaned.slice(0, idx)
291
+ const version = cleaned.slice(idx + 1)
292
+ if (name && version) items.push({ name, version: version.replace(/^v/, '') })
293
+ }
294
+ }
295
+ return items
296
+ }
297
+
275
298
  /**
276
299
  * 查询 bun 安装路径和包目录
277
300
  * @returns {{ name: string, install: string, globalDir: string, cacheDir: string }}
@@ -344,6 +344,17 @@ export class ComposerManager extends PackageManager {
344
344
  return { action: 'cache cleaned' }
345
345
  }
346
346
 
347
+ /**
348
+ * 列出已安装包(缺省读当前项目 vendor/composer/installed.json,global 读全局 installed.json)
349
+ * @param {{ global?: boolean }} [opts]
350
+ * @returns {Promise<{ name: string, version: string }[]>}
351
+ */
352
+ async listInstalled(opts = {}) {
353
+ const packages = opts.global ? this._readGlobalInstalledJson() : this._readInstalledJson()
354
+ if (!packages) return []
355
+ return packages.map((p) => ({ name: p.name, version: p.version || 'unknown' }))
356
+ }
357
+
347
358
  /**
348
359
  * 查询 composer 安装路径和包目录
349
360
  * @returns {{ name: string, install: string, globalHome: string, vendorDir: string }}
@@ -342,6 +342,22 @@ export class DnfManager extends PackageManager {
342
342
  return { action: 'cache cleaned' }
343
343
  }
344
344
 
345
+ /**
346
+ * 列出系统已安装包(系统级管理器,无项目作用域)
347
+ * @returns {Promise<{ name: string, version: string }[]>}
348
+ */
349
+ async listInstalled(opts = {}) {
350
+ if (opts.global === false) return []
351
+ const out = this._exec(['list', 'installed', '--quiet'], { allowNonZero: true })
352
+ const items = []
353
+ // 行格式: name.arch version repo
354
+ for (const line of out.split('\n')) {
355
+ const m = line.trim().match(/^(\S+?)\.\S+\s+(\S+)\s+\S+/)
356
+ if (m) items.push({ name: m[1], version: m[2] })
357
+ }
358
+ return items
359
+ }
360
+
345
361
  /**
346
362
  * 查询 dnf 安装路径和包目录
347
363
  * @returns {{ name: string, install: string, cacheDir: string, repoDir: string }}
@@ -55,7 +55,7 @@ export function installNvm(proxy) {
55
55
  env.http_proxy = proxy
56
56
  env.https_proxy = proxy
57
57
  }
58
- execSync(`git clone "${mirror}" "${nvmDir}"`, { stdio: 'inherit', shell: shell.resolveBash(), env })
58
+ execSync(`git clone "${mirror}" "${nvmDir}"`, { stdio: shell.stdioForExec(true), shell: shell.resolveBash(), env })
59
59
  const lines = [
60
60
  `export NVM_DIR="${nvmDir}"`,
61
61
  `[ -s "\\$NVM_DIR/nvm.sh" ] && \\. "\\$NVM_DIR/nvm.sh"`,
@@ -505,6 +505,18 @@ export class NpmManager extends PackageManager {
505
505
  return { action: 'cache cleaned' }
506
506
  }
507
507
 
508
+ /**
509
+ * 列出已安装包
510
+ * @param {{ global?: boolean }} [opts] - global=true 列全局,缺省列当前项目
511
+ * @returns {Promise<{ name: string, version: string }[]>}
512
+ */
513
+ async listInstalled(opts = {}) {
514
+ const args = opts.global ? ['ls', '-g', '--depth=0'] : ['ls', '--depth=0']
515
+ const data = this._execJson(args)
516
+ const deps = (data && data.dependencies) || {}
517
+ return Object.entries(deps).map(([name, d]) => ({ name, version: (d.version || '').replace(/^v/, '') }))
518
+ }
519
+
508
520
  /**
509
521
  * 查询 npm 安装路径和包目录
510
522
  * @returns {{ name: string, install: string, globalDir: string, localDir: string, cacheDir: string, prefix: string }}
@@ -51,9 +51,9 @@ export function isPhpenvInstalled() {
51
51
  export function installPhpenv() {
52
52
  const mirror = getActiveRegistry('phpenv')
53
53
  const root = getPhpenvRoot()
54
- execSync(`git clone "${mirror}" "${root}"`, { stdio: 'inherit', shell: shell.resolveBash() })
54
+ execSync(`git clone "${mirror}" "${root}"`, { stdio: shell.stdioForExec(true), shell: shell.resolveBash() })
55
55
  execSync(`git clone "${PHP_BUILD_GIT}" "${path.join(root, 'plugins', 'php-build')}"`, {
56
- stdio: 'inherit',
56
+ stdio: shell.stdioForExec(true),
57
57
  shell: shell.resolveBash(),
58
58
  })
59
59
  const lines = [
@@ -557,6 +557,22 @@ export class PipManager extends PackageManager {
557
557
  return { action: 'cache cleaned' }
558
558
  }
559
559
 
560
+ /**
561
+ * 列出已安装包
562
+ * @param {{ global?: boolean }} [opts] - global=true 用系统级 Python 环境,缺省当前环境
563
+ * @returns {Promise<{ name: string, version: string }[]>}
564
+ */
565
+ async listInstalled(opts = {}) {
566
+ const out = this._execPip(['list', '--format=json', '--disable-pip-version-check'], { allowNonZero: true, global: opts.global })
567
+ try {
568
+ const data = JSON.parse(out)
569
+ if (!Array.isArray(data)) return []
570
+ return data.map((i) => ({ name: i.name, version: i.version || '' }))
571
+ } catch {
572
+ return []
573
+ }
574
+ }
575
+
560
576
  /**
561
577
  * 查询 pip 安装路径和包目录
562
578
  * @returns {{ name: string, install: string, sitePackages: string, userSite: string }}
@@ -127,7 +127,7 @@ export class RuntimeManager extends PackageManager {
127
127
  throw new Error(this.cfg.toolMissingMsg)
128
128
  }
129
129
  const result = spawnSync(this._cmd(args), {
130
- stdio: 'inherit',
130
+ stdio: shell.stdioForExec(this.cfg.needsBash),
131
131
  shell: shell.shellForExec(this.cfg.needsBash),
132
132
  env: this._buildEnv(opts.proxy),
133
133
  })
@@ -242,6 +242,18 @@ export class WingetManager extends PackageManager {
242
242
  return { action: 'winget has no cache clean command' }
243
243
  }
244
244
 
245
+ /**
246
+ * 列出系统已安装包(winget list 表格解析)
247
+ * @returns {Promise<{ name: string, version: string }[]>}
248
+ */
249
+ async listInstalled(opts = {}) {
250
+ const out = this._exec(['list'], { allowNonZero: true })
251
+ const rows = this._parseTable(out)
252
+ return rows
253
+ .filter((r) => r.Name)
254
+ .map((r) => ({ name: r.Name, version: r.Version || '' }))
255
+ }
256
+
245
257
  /**
246
258
  * 查询 winget 安装路径
247
259
  * @returns {{ name: string, install: string }}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * 系统剪切板读写工具
3
+ *
4
+ * 跨平台读取/写入系统级剪切板:
5
+ * - darwin: pbpaste / pbcopy
6
+ * - linux: 优先 wl-paste / wl-copy(Wayland),回退 xclip / xsel
7
+ * - win32: PowerShell Get-Clipboard / Set-Clipboard
8
+ * 读取失败(命令不存在/非零退出)返回空字符串,调用方用 trim 判断空。
9
+ */
10
+
11
+ import { spawnSync } from 'node:child_process'
12
+
13
+ /**
14
+ * 执行外部命令并捕获输出
15
+ * @param {string} cmd
16
+ * @param {string[]} args
17
+ * @param {{input?: string}} [opts]
18
+ * @returns {{ok: boolean, stdout: string, stderr: string}}
19
+ */
20
+ function run(cmd, args, opts = {}) {
21
+ const res = spawnSync(cmd, args, { encoding: 'utf8', ...opts })
22
+ if (res.error) {
23
+ return { ok: false, stdout: '', stderr: res.error.message }
24
+ }
25
+ return { ok: res.status === 0, stdout: res.stdout || '', stderr: res.stderr || '' }
26
+ }
27
+
28
+ /**
29
+ * 读取系统剪切板内容(失败返回空字符串)
30
+ * @returns {string}
31
+ */
32
+ export function readClipboard() {
33
+ const p = process.platform
34
+ if (p === 'darwin') {
35
+ return run('pbpaste', []).stdout
36
+ }
37
+ if (p === 'win32') {
38
+ const r = run('powershell', [
39
+ '-NoProfile', '-Command',
40
+ '[Console]::Out.Write((Get-Clipboard -Raw -ErrorAction SilentlyContinue))',
41
+ ])
42
+ return r.ok ? r.stdout : ''
43
+ }
44
+ // linux
45
+ const r = run('bash', [
46
+ '-c',
47
+ 'wl-paste --no-newline 2>/dev/null || xclip -selection clipboard -o 2>/dev/null || xsel --clipboard --output 2>/dev/null',
48
+ ])
49
+ return r.ok ? r.stdout : ''
50
+ }
51
+
52
+ /**
53
+ * 写入系统剪切板
54
+ * @param {string} text
55
+ */
56
+ export function writeClipboard(text) {
57
+ const input = String(text ?? '')
58
+ const p = process.platform
59
+ if (p === 'darwin') {
60
+ run('pbcopy', [], { input })
61
+ return
62
+ }
63
+ if (p === 'win32') {
64
+ run('powershell', ['-NoProfile', '-Command', 'Set-Clipboard -Value $input'], { input })
65
+ return
66
+ }
67
+ run('bash', ['-c', 'wl-copy 2>/dev/null || xclip -selection clipboard 2>/dev/null || xsel --clipboard --input 2>/dev/null'], { input })
68
+ }
@@ -196,7 +196,7 @@ export class FtpGroup {
196
196
  console.error('Usage: opm ftp upload <file> [-n name] [-s subdir]')
197
197
  process.exit(1)
198
198
  }
199
- const name = parsed.flags.name || parsed.flags.n || ''
199
+ const name = parsed.flags.name || ''
200
200
  const subdir = parsed.flags.subdir || parsed.flags.s || ''
201
201
  const cfg = getFtpConfig()
202
202
 
@@ -242,14 +242,14 @@ export class FtpGroup {
242
242
  async _rsync(parsed) {
243
243
  const local = parsed.positional[0]
244
244
  if (!local) {
245
- console.error('Usage: opm ftp rsync <local> [-s subdir] [-x pattern] [--dry-run] [--prune]')
245
+ console.error('Usage: opm ftp rsync <local> [-s subdir] [-x pattern] [--dry-run] [--delete]')
246
246
  process.exit(1)
247
247
  }
248
248
  const subdir = parsed.flags.subdir || parsed.flags.s || ''
249
249
  const excludes = parsed.flags.exclude || parsed.flags.x || []
250
250
  const excludeList = Array.isArray(excludes) ? excludes : [excludes]
251
251
  const dryRun = parsed.flags['dry-run'] === 'true'
252
- const prune = parsed.flags.prune === 'true'
252
+ const deleteRemote = parsed.flags.delete === 'true'
253
253
 
254
254
  const cfg = getFtpConfig()
255
255
  const localDir = path.resolve(local)
@@ -282,7 +282,7 @@ export class FtpGroup {
282
282
  }
283
283
 
284
284
  const deletePlan = []
285
- if (prune) {
285
+ if (deleteRemote) {
286
286
  for (const [rel] of remoteFiles) {
287
287
  if (!toUpload.includes(rel)) {
288
288
  deletePlan.push(rel)
@@ -335,9 +335,9 @@ export class FtpGroup {
335
335
  skippedSame: skippedSame.length,
336
336
  skippedExcluded,
337
337
  failed,
338
- pruned: deleted.length,
339
- prunedFiles: deleted,
340
- pruneFailed: deleteFailed,
338
+ deleted: deleted.length,
339
+ deletedFiles: deleted,
340
+ deleteFailed,
341
341
  })
342
342
  }
343
343
 
@@ -370,7 +370,7 @@ Actions:
370
370
  uninstall Uninstall basic-ftp
371
371
  upgrade Upgrade basic-ftp to latest (uses opm npm registry + proxy)
372
372
  connect Test FTP connection
373
- upload <file> [-n name] [-s subdir] Upload file (local path or HTTP URL)
373
+ upload <file> [--name name] [-s subdir] Upload file (local path or HTTP URL)
374
374
  rsync <local> [options] Recursive upload directory (FTP-side dedup)
375
375
  download <remote> <local> Download file from FTP
376
376
  root Show configured remote root directory
@@ -380,7 +380,7 @@ rsync options:
380
380
  -s, --subdir <dir> Remote subdirectory (under remote_root)
381
381
  -x, --exclude <pattern> Exclude glob pattern (repeatable)
382
382
  --dry-run Preview without uploading
383
- --prune Delete remote files not in local
383
+ --delete Delete remote files not in local
384
384
 
385
385
  Options:
386
386
  -p, --proxy <url> Proxy for install/upgrade (overrides config proxy.active)
@@ -393,12 +393,12 @@ Examples:
393
393
  opm ftp upgrade
394
394
  opm ftp connect
395
395
  opm ftp upload ./img.png
396
- opm ftp upload ./img.png -n cover -s banner
396
+ opm ftp upload ./img.png --name cover -s banner
397
397
  opm ftp upload https://example.com/a.jpg -n cover.jpg
398
398
  opm ftp rsync ./webwork/
399
399
  opm ftp rsync ./webwork/ -s deploy -x "vendor/*" -x "*.log"
400
400
  opm ftp rsync ./webwork/ --dry-run
401
- opm ftp rsync ./webwork/ --prune
401
+ opm ftp rsync ./webwork/ --delete
402
402
  opm ftp download 20260913/cover.jpg ./cover.jpg
403
403
  opm ftp root
404
404
  `)
@@ -40,6 +40,20 @@ export class GitGroup {
40
40
  this.desc = 'Git config management (init/config/user/log/export)'
41
41
  }
42
42
 
43
+ /**
44
+ * 当前 action 下的上下文布尔标志
45
+ * 同名标志在不同 action 中语义不同(-f:init=force / export=from;--unset:config 带值 / export mark 布尔),
46
+ * 由 action 决定解析方式,避免布尔标志吞掉后续参数。
47
+ * @param {string} action
48
+ * @returns {string[]}
49
+ * @private
50
+ */
51
+ _booleanFlags(action) {
52
+ if (action === 'init') return ['f', 'force']
53
+ if (action === 'export') return ['remove', 'unset']
54
+ return []
55
+ }
56
+
43
57
  async run(argv) {
44
58
  const [action, ...rest] = argv
45
59
 
@@ -48,7 +62,7 @@ export class GitGroup {
48
62
  return
49
63
  }
50
64
 
51
- const parsed = parseFlags(rest)
65
+ const parsed = parseFlags(rest, { booleans: this._booleanFlags(action) })
52
66
 
53
67
  switch (action) {
54
68
  case 'version':
@@ -146,7 +160,7 @@ export class GitGroup {
146
160
 
147
161
  async _user(parsed) {
148
162
  const scope = this._getScope(parsed)
149
- const name = parsed.flags.n || parsed.flags.name || ''
163
+ const name = parsed.flags.name || ''
150
164
  const email = parsed.flags.e || parsed.flags.email || ''
151
165
 
152
166
  if (name || email) {
@@ -209,7 +223,7 @@ export class GitGroup {
209
223
  const workDir = parsed.flags.d || parsed.flags.dir || process.cwd()
210
224
  const ref = parsed.positional[1]
211
225
 
212
- if (parsed.flags.unset === 'true') {
226
+ if (parsed.flags.remove === 'true' || parsed.flags.unset === 'true') {
213
227
  const ok = unsetMark(workDir)
214
228
  output({ mark: 'removed', ok })
215
229
  return
@@ -230,7 +244,7 @@ export class GitGroup {
230
244
  console.error('\nTips:')
231
245
  console.error(' opm git log -d <dir> -n 10 # view commit history without cd')
232
246
  console.error(' opm git export mark <commit> # change diff base to another commit')
233
- console.error(' opm git export mark --unset # remove current mark')
247
+ console.error(' opm git export mark --remove # remove current mark')
234
248
  console.error(' opm git export # export diff (mark -> HEAD)')
235
249
  this._printRecentLog(workDir)
236
250
  return
@@ -250,7 +264,7 @@ export class GitGroup {
250
264
  process.exit(1)
251
265
  }
252
266
 
253
- const fromRaw = parsed.flags.f || parsed.flags.from || ''
267
+ const fromRaw = parsed.flags.from || ''
254
268
  let fromRef
255
269
  if (fromRaw) {
256
270
  const n = parseInt(fromRaw, 10)
@@ -271,7 +285,7 @@ export class GitGroup {
271
285
  if (!outFile) {
272
286
  const opts = { encoding: 'utf8', timeout: 5000, cwd: resolvedDir, stdio: ['pipe', 'pipe', 'pipe'] }
273
287
  const gitRoot = execSync('git rev-parse --show-toplevel', opts).trim()
274
- const name = parsed.flags.n || parsed.flags.name || ''
288
+ const name = parsed.flags.name || ''
275
289
  const fileName = name ? `${name}.diff` : (() => {
276
290
  const fromShort = execSync(`git rev-parse --short ${fromRef}`, opts).trim()
277
291
  const toShort = execSync('git rev-parse --short HEAD', opts).trim()
@@ -331,7 +345,7 @@ Export subcommands:
331
345
  export mark [-d dir] Show current diff base mark
332
346
  export mark 0|HEAD [-d dir] Set diff base mark to current HEAD
333
347
  export mark <commit> [-d dir] Set diff base mark to a commit
334
- export mark --unset [-d dir] Remove the diff base mark
348
+ export mark --remove [-d dir] Remove the diff base mark
335
349
  export [-d dir] Export diff (mark -> HEAD)
336
350
  export -f <n> [-d dir] Export diff (HEAD~n -> HEAD, override mark)
337
351
  export -f <ref> [-d dir] Export diff (<ref> -> HEAD, override mark)
@@ -344,11 +358,15 @@ Options:
344
358
  -n, --count <n> Number of commits to show (log, default: 10)
345
359
  -e, --email <email> User email (user)
346
360
  -o, --output <file> Output file path (export)
347
- -f, --from <n>|<ref> Override from ref (export)
361
+ --from <n>|<ref> Override from ref (export)
348
362
  -f, --force Force overwrite existing config (init)
349
- --unset <key> Unset a config key (config)
363
+ --unset <key> Unset a config key (config only)
364
+ --remove Remove the diff base mark (export mark)
350
365
  -h, --help Show this help
351
366
 
367
+ Note: -n 在 log 中为 --count 数量,user/export 中用 --name 名称(无短形式);
368
+ -f 在 init 中为 --force 布尔,export 中用 --from 带值(无短形式)。
369
+
352
370
  Examples:
353
371
  opm git version Show installed git version
354
372
  opm git install Install git via system package manager
@@ -365,12 +383,11 @@ Examples:
365
383
  opm git export mark 0 Set diff base to current HEAD (cwd)
366
384
  opm git export mark HEAD Set diff base to current HEAD (cwd)
367
385
  opm git export mark 3af967b Set diff base at commit (cwd)
368
- opm git export mark --unset Remove diff base mark (cwd)
369
- opm git export mark abc -d /repo Set diff base at commit in /repo
386
+ opm git export mark --remove Remove diff base mark (cwd) opm git export mark abc -d /repo Set diff base at commit in /repo
370
387
  opm git export Export diff in current repo (mark -> HEAD)
371
- opm git export -f 2 Override: HEAD~2 -> HEAD (current repo)
388
+ opm git export --from 2 Override: HEAD~2 -> HEAD (current repo)
372
389
  opm git export -d /path/to/repo Export diff in specified repo
373
- opm git export -n feature Save as feature.diff
390
+ opm git export --name feature Save as feature.diff
374
391
  opm git import file.diff Import diff file (current repo)
375
392
  opm git import file.diff -d /repo Import diff file into specified repo
376
393
  `)