@wwkit/opm 1.0.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 (62) hide show
  1. package/README.md +381 -0
  2. package/bin/index.js +9 -0
  3. package/docs/managers/apt.md +45 -0
  4. package/docs/managers/brew.md +92 -0
  5. package/docs/managers/bun.md +50 -0
  6. package/docs/managers/cft.md +57 -0
  7. package/docs/managers/composer.md +96 -0
  8. package/docs/managers/config.md +32 -0
  9. package/docs/managers/dnf.md +46 -0
  10. package/docs/managers/node.md +49 -0
  11. package/docs/managers/npm.md +100 -0
  12. package/docs/managers/nvm.md +34 -0
  13. package/docs/managers/php.md +48 -0
  14. package/docs/managers/phpenv.md +34 -0
  15. package/docs/managers/pip.md +98 -0
  16. package/docs/managers/python.md +53 -0
  17. package/docs/managers/uv.md +27 -0
  18. package/docs/opm.html +570 -0
  19. package/docs/packages/opencode.md +41 -0
  20. package/package.json +51 -0
  21. package/scripts/postinstall.js +7 -0
  22. package/src/binaries/base.js +508 -0
  23. package/src/binaries/cft/index.js +35 -0
  24. package/src/binaries/depchecker.js +172 -0
  25. package/src/binaries/exefinder.js +110 -0
  26. package/src/binaries/platform.js +17 -0
  27. package/src/cli/doc.js +335 -0
  28. package/src/cli/groups/config.js +78 -0
  29. package/src/cli/groups/ensure.js +96 -0
  30. package/src/cli/groups/homebrew.js +73 -0
  31. package/src/cli/groups/nvm.js +73 -0
  32. package/src/cli/groups/package.js +514 -0
  33. package/src/cli/groups/phpenv.js +89 -0
  34. package/src/cli/groups/uv.js +73 -0
  35. package/src/cli/groups/version-manager.js +186 -0
  36. package/src/cli/helpers/args.js +49 -0
  37. package/src/cli/index.js +129 -0
  38. package/src/config.js +133 -0
  39. package/src/config.json5 +158 -0
  40. package/src/formatter.js +13 -0
  41. package/src/harnesses/base.js +205 -0
  42. package/src/harnesses/opencode/clear.js +239 -0
  43. package/src/harnesses/opencode/db.js +39 -0
  44. package/src/harnesses/opencode/index.js +46 -0
  45. package/src/harnesses/opencode/sqlite.js +200 -0
  46. package/src/index.js +46 -0
  47. package/src/managers/apt.js +377 -0
  48. package/src/managers/base.js +167 -0
  49. package/src/managers/brew.js +374 -0
  50. package/src/managers/bun.js +342 -0
  51. package/src/managers/composer.js +280 -0
  52. package/src/managers/dnf.js +370 -0
  53. package/src/managers/http.js +432 -0
  54. package/src/managers/node.js +150 -0
  55. package/src/managers/npm.js +473 -0
  56. package/src/managers/php.js +157 -0
  57. package/src/managers/pip.js +555 -0
  58. package/src/managers/python.js +180 -0
  59. package/src/managers/runtime.js +309 -0
  60. package/src/managers/winget.js +268 -0
  61. package/src/presets.js +24 -0
  62. package/src/proxy-presets.js +12 -0
@@ -0,0 +1,473 @@
1
+ /**
2
+ * npm 包管理器实现
3
+ *
4
+ * 通过执行 npm CLI 命令实现统一接口。
5
+ * npm view 命令查询源信息,npm list 查询本地安装状态。
6
+ */
7
+
8
+ import { execSync, spawnSync } from 'node:child_process'
9
+ import fs from 'node:fs'
10
+ import path from 'node:path'
11
+ import { createRequire } from 'node:module'
12
+ import { PackageManager } from './base.js'
13
+ import { getActiveRegistry, setActiveRegistry } from '../config.js'
14
+
15
+ export class NpmManager extends PackageManager {
16
+ constructor() {
17
+ super()
18
+ this.name = 'npm'
19
+ }
20
+
21
+ /**
22
+ * 执行 npm 命令,返回 stdout
23
+ * execSync 默认通过 shell 执行,兼容 Windows 的 npm.cmd
24
+ * @param {string[]} args
25
+ * @param {object} [opts]
26
+ * @returns {string}
27
+ * @private
28
+ */
29
+ _exec(args, opts = {}) {
30
+ const cmd = ['npm', ...args].join(' ')
31
+ try {
32
+ return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], ...opts })
33
+ } catch (err) {
34
+ if (opts.allowNonZero) {
35
+ return err.stdout ? err.stdout.toString() : ''
36
+ }
37
+ const stderr = err.stderr ? err.stderr.toString().trim() : ''
38
+ if (stderr && !stderr.startsWith('npm warn')) {
39
+ throw new Error(stderr)
40
+ }
41
+ return err.stdout ? err.stdout.toString() : ''
42
+ }
43
+ }
44
+
45
+ /**
46
+ * 执行 npm 命令并解析 JSON 输出
47
+ * @param {string[]} args
48
+ * @returns {*}
49
+ * @private
50
+ */
51
+ _execJson(args) {
52
+ const out = this._exec([...args, '--json', '--no-workspaces'], { allowNonZero: true })
53
+ try {
54
+ return JSON.parse(out)
55
+ } catch {
56
+ return null
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 执行 npm 命令,输出直接继承到终端(用于 install/uninstall 等)
62
+ * @param {string[]} args
63
+ * @private
64
+ */
65
+ _execInherit(args) {
66
+ const result = spawnSync('npm', args, { stdio: 'inherit', shell: true })
67
+ if (result.error) {
68
+ throw new Error(result.error.message)
69
+ }
70
+ if (result.status !== 0) {
71
+ throw new Error(`npm ${args.join(' ')} exited with code ${result.status}`)
72
+ }
73
+ }
74
+
75
+ /**
76
+ * 构造 npm proxy 参数
77
+ * @param {string} [proxy] - 代理地址
78
+ * @returns {string[]}
79
+ * @private
80
+ */
81
+ _proxyArgs(proxy) {
82
+ return proxy ? ['--proxy', proxy] : []
83
+ }
84
+
85
+ /**
86
+ * 构造 npm registry 参数(使用配置中 active 镜像)
87
+ * @returns {string[]}
88
+ * @private
89
+ */
90
+ _registryArgs() {
91
+ const registry = getActiveRegistry('npm')
92
+ return registry ? ['--registry', registry] : []
93
+ }
94
+
95
+ async getRegistry() {
96
+ return { registry: getActiveRegistry('npm') }
97
+ }
98
+
99
+ async getVersion() {
100
+ try {
101
+ const version = this._exec(['--version'], { allowNonZero: true }).trim()
102
+ if (version) {
103
+ return { name: 'npm', installed: true, version }
104
+ }
105
+ } catch {}
106
+ return { name: 'npm', version: '', installed: false, hint: 'Install Node.js from https://nodejs.org/ (includes npm)' }
107
+ }
108
+
109
+ async setRegistry(url) {
110
+ const resolved = setActiveRegistry('npm', url)
111
+ return { registry: resolved }
112
+ }
113
+
114
+ async searchPackage(name, version, opts = {}) {
115
+ const { registry } = await this.getRegistry()
116
+ const proxyArgs = this._proxyArgs(opts.proxy)
117
+ const regArgs = this._registryArgs()
118
+
119
+ if (version) {
120
+ try {
121
+ const data = this._execJson(['view', `${name}@${version}`, ...proxyArgs, ...regArgs])
122
+ if (data) {
123
+ return { name, version, exists: true, available: true, registry }
124
+ }
125
+ } catch {}
126
+ return { name, version, exists: false, available: false, registry }
127
+ }
128
+
129
+ try {
130
+ const data = this._execJson(['view', name, ...proxyArgs, ...regArgs])
131
+ if (data) {
132
+ const versions = Array.isArray(data.versions) ? data.versions : (data.versions ? Object.keys(data.versions) : [])
133
+ return {
134
+ name,
135
+ exists: true,
136
+ latest: data['dist-tags']?.latest || versions[versions.length - 1] || 'unknown',
137
+ versions,
138
+ registry
139
+ }
140
+ }
141
+ } catch {}
142
+
143
+ return { name, exists: false, registry }
144
+ }
145
+
146
+ /**
147
+ * 读取当前项目 package.json 中的所有依赖声明
148
+ * @returns {Record<string, string>} name -> versionRange
149
+ * @private
150
+ */
151
+ _readDeclaredDeps() {
152
+ const pkgPath = path.join(process.cwd(), 'package.json')
153
+ if (!fs.existsSync(pkgPath)) return {}
154
+ try {
155
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
156
+ return {
157
+ ...(pkg.dependencies || {}),
158
+ ...(pkg.devDependencies || {}),
159
+ ...(pkg.peerDependencies || {}),
160
+ ...(pkg.optionalDependencies || {})
161
+ }
162
+ } catch {
163
+ return {}
164
+ }
165
+ }
166
+
167
+ /**
168
+ * 读取 workspace 包名列表(兼容 npm/yarn workspaces 和 pnpm-workspace.yaml)
169
+ * @returns {{ name: string, version: string, dir: string }[]}
170
+ * @private
171
+ */
172
+ _readWorkspacePackages() {
173
+ const cwd = process.cwd()
174
+ const patterns = []
175
+
176
+ const pkgPath = path.join(cwd, 'package.json')
177
+ if (fs.existsSync(pkgPath)) {
178
+ try {
179
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
180
+ if (Array.isArray(pkg.workspaces)) {
181
+ patterns.push(...pkg.workspaces)
182
+ }
183
+ } catch {}
184
+ }
185
+
186
+ const pnpmWsPath = path.join(cwd, 'pnpm-workspace.yaml')
187
+ if (fs.existsSync(pnpmWsPath)) {
188
+ try {
189
+ const text = fs.readFileSync(pnpmWsPath, 'utf8')
190
+ const lines = text.split('\n').map((l) => l.trim())
191
+ let inPackages = false
192
+ for (const line of lines) {
193
+ if (line.startsWith('packages:')) {
194
+ inPackages = true
195
+ continue
196
+ }
197
+ if (inPackages && line.startsWith('-')) {
198
+ const val = line.slice(1).trim().replace(/^['"]|['"]$/g, '')
199
+ if (val) patterns.push(val)
200
+ } else if (inPackages && line && !line.startsWith('-') && !line.startsWith('#')) {
201
+ inPackages = false
202
+ }
203
+ }
204
+ } catch {}
205
+ }
206
+
207
+ const results = []
208
+ const seen = new Set()
209
+ for (const pattern of patterns) {
210
+ const dirs = this._globDirs(cwd, pattern)
211
+ for (const dir of dirs) {
212
+ const wpkgPath = path.join(dir, 'package.json')
213
+ if (!fs.existsSync(wpkgPath)) continue
214
+ try {
215
+ const wpkg = JSON.parse(fs.readFileSync(wpkgPath, 'utf8'))
216
+ if (wpkg.name && !seen.has(wpkg.name)) {
217
+ seen.add(wpkg.name)
218
+ results.push({ name: wpkg.name, version: wpkg.version || '0.0.0', dir })
219
+ }
220
+ } catch {}
221
+ }
222
+ }
223
+ return results
224
+ }
225
+
226
+ /**
227
+ * 简单 glob:将 packages/* 等模式展开为实际目录
228
+ * @param {string} base - 基目录
229
+ * @param {string} pattern - glob 模式(仅支持单层 * 通配)
230
+ * @returns {string[]}
231
+ * @private
232
+ */
233
+ _globDirs(base, pattern) {
234
+ if (!pattern.includes('*')) {
235
+ const full = path.resolve(base, pattern)
236
+ return fs.existsSync(full) ? [full] : []
237
+ }
238
+ const parts = pattern.split('*')
239
+ const prefix = parts[0]
240
+ const suffix = parts.slice(1).join('*')
241
+ const searchDir = path.resolve(base, prefix)
242
+ if (!fs.existsSync(searchDir)) return []
243
+ let entries = []
244
+ try {
245
+ entries = fs.readdirSync(searchDir, { withFileTypes: true })
246
+ } catch {
247
+ return []
248
+ }
249
+ return entries
250
+ .filter((e) => e.isDirectory())
251
+ .map((e) => path.join(searchDir, e.name) + (suffix || ''))
252
+ .filter((d) => fs.existsSync(d))
253
+ }
254
+
255
+ /**
256
+ * 通过 Node 模块解析获取已安装包的实际版本
257
+ * 兼容 npm / yarn / pnpm 的 node_modules 结构
258
+ * @param {string} name - 包名
259
+ * @returns {{ version: string, path: string } | null}
260
+ * @private
261
+ */
262
+ _resolveInstalled(name) {
263
+ const require = createRequire(path.join(process.cwd(), 'package.json'))
264
+
265
+ try {
266
+ const pkgPath = require.resolve(`${name}/package.json`)
267
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
268
+ return { version: pkg.version, path: path.dirname(pkgPath) }
269
+ } catch {}
270
+
271
+ try {
272
+ const mainPath = require.resolve(name)
273
+ const dir = path.dirname(mainPath)
274
+ const realDir = fs.realpathSync(dir)
275
+ const pkgPath = path.join(realDir, 'package.json')
276
+ if (fs.existsSync(pkgPath)) {
277
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
278
+ return { version: pkg.version, path: realDir }
279
+ }
280
+ const parentDir = path.dirname(realDir)
281
+ const parentPkgPath = path.join(parentDir, 'package.json')
282
+ if (fs.existsSync(parentPkgPath)) {
283
+ const pkg = JSON.parse(fs.readFileSync(parentPkgPath, 'utf8'))
284
+ if (pkg.name === name) {
285
+ return { version: pkg.version, path: parentDir }
286
+ }
287
+ }
288
+ } catch {}
289
+
290
+ return null
291
+ }
292
+
293
+ /**
294
+ * 查询全局安装的包
295
+ * @param {string} name - 包名
296
+ * @returns {Promise<InstalledResult>}
297
+ * @private
298
+ */
299
+ async _isGlobalInstalled(name) {
300
+ try {
301
+ const data = this._execJson(['list', '-g', name])
302
+ const deps = data?.dependencies || {}
303
+ const pkg = deps[name]
304
+ if (pkg) {
305
+ const globalRoot = this._exec(['root', '-g'], { allowNonZero: true }).trim()
306
+ return { name, installed: true, version: pkg.version || 'unknown', path: path.join(globalRoot, name), global: true }
307
+ }
308
+ } catch {}
309
+
310
+ try {
311
+ const globalRoot = this._exec(['root', '-g'], { allowNonZero: true }).trim()
312
+ const pkgPath = path.join(globalRoot, name, 'package.json')
313
+ if (fs.existsSync(pkgPath)) {
314
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
315
+ return { name, installed: true, version: pkg.version, path: path.dirname(pkgPath), global: true }
316
+ }
317
+ } catch {}
318
+
319
+ return { name, installed: false, global: true }
320
+ }
321
+
322
+ async isInstalled(name, opts = {}) {
323
+ if (opts.global) {
324
+ return this._isGlobalInstalled(name)
325
+ }
326
+
327
+ const resolved = this._resolveInstalled(name)
328
+ if (resolved) {
329
+ return { name, installed: true, version: resolved.version, path: resolved.path }
330
+ }
331
+
332
+ const workspaces = this._readWorkspacePackages()
333
+ const wsMatch = workspaces.find((w) => w.name === name)
334
+ if (wsMatch) {
335
+ return { name, installed: true, version: wsMatch.version, path: wsMatch.dir }
336
+ }
337
+
338
+ const declared = this._readDeclaredDeps()
339
+ if (declared[name]) {
340
+ return { name, installed: true, version: declared[name] }
341
+ }
342
+
343
+ return { name, installed: false }
344
+ }
345
+
346
+ async packageInfo(name, opts = {}) {
347
+ try {
348
+ const data = this._execJson(['view', name, ...this._proxyArgs(opts.proxy), ...this._registryArgs()])
349
+ if (!data) {
350
+ return { name, description: '(not found)' }
351
+ }
352
+ return {
353
+ name,
354
+ description: data.description || '',
355
+ latest: data['dist-tags']?.latest || '',
356
+ homepage: data.homepage || '',
357
+ license: data.license || '',
358
+ author: typeof data.author === 'object' ? data.author?.name || '' : (data.author || ''),
359
+ repository: typeof data.repository === 'object' ? data.repository?.url || '' : (data.repository || '')
360
+ }
361
+ } catch (err) {
362
+ throw new Error(`npm info "${name}" failed: ${err.message}`)
363
+ }
364
+ }
365
+
366
+ async packageVersions(name, opts = {}) {
367
+ const { registry } = await this.getRegistry()
368
+ try {
369
+ const data = this._execJson(['view', name, 'versions', ...this._proxyArgs(opts.proxy), ...this._registryArgs()])
370
+ if (data) {
371
+ const versions = Array.isArray(data) ? data : (typeof data === 'object' ? Object.keys(data) : [])
372
+ return { name, versions, registry }
373
+ }
374
+ } catch {}
375
+ return { name, versions: [], registry }
376
+ }
377
+
378
+ async listInstalled(opts = {}) {
379
+ if (opts.global) {
380
+ try {
381
+ const data = this._execJson(['list', '-g', '--depth=0'])
382
+ const deps = data?.dependencies || {}
383
+ return Object.entries(deps)
384
+ .filter(([, info]) => info && info.version)
385
+ .map(([name, info]) => ({ name, version: info.version, global: true }))
386
+ } catch (err) {
387
+ throw new Error(`npm list -g failed: ${err.message}`)
388
+ }
389
+ }
390
+
391
+ const declared = this._readDeclaredDeps()
392
+ const workspaces = this._readWorkspacePackages()
393
+ const items = []
394
+ const seen = new Set()
395
+
396
+ for (const { name, version, dir } of workspaces) {
397
+ if (seen.has(name)) continue
398
+ seen.add(name)
399
+ const resolved = this._resolveInstalled(name)
400
+ items.push({
401
+ name,
402
+ version: resolved ? resolved.version : version,
403
+ path: resolved ? resolved.path : dir
404
+ })
405
+ }
406
+
407
+ for (const [name, declaredVersion] of Object.entries(declared)) {
408
+ if (seen.has(name)) continue
409
+ seen.add(name)
410
+ const resolved = this._resolveInstalled(name)
411
+ items.push({
412
+ name,
413
+ version: resolved ? resolved.version : declaredVersion
414
+ })
415
+ }
416
+
417
+ return items
418
+ }
419
+
420
+ async listOutdated(opts = {}) {
421
+ try {
422
+ const args = ['outdated']
423
+ if (opts.global) args.push('-g')
424
+ args.push(...this._proxyArgs(opts.proxy))
425
+ args.push(...this._registryArgs())
426
+ const data = this._execJson(args)
427
+ if (!data) return []
428
+ const items = Object.entries(data).map(([name, info]) => ({
429
+ name,
430
+ current: info.current || 'missing',
431
+ wanted: info.wanted || '',
432
+ latest: info.latest || ''
433
+ }))
434
+ return items
435
+ } catch (err) {
436
+ throw new Error(`npm outdated failed: ${err.message}`)
437
+ }
438
+ }
439
+
440
+ async install(name, version, opts = {}) {
441
+ const spec = version ? `${name}@${version}` : name
442
+ const args = ['install', spec]
443
+ if (opts.global) args.push('-g')
444
+ args.push(...this._proxyArgs(opts.proxy))
445
+ args.push(...this._registryArgs())
446
+
447
+ this._execInherit(args)
448
+ return { name, version: version || 'latest', action: 'installed' }
449
+ }
450
+
451
+ async uninstall(name, opts = {}) {
452
+ const args = ['uninstall', name]
453
+ if (opts.global) args.push('-g')
454
+
455
+ this._execInherit(args)
456
+ return { name, action: 'uninstalled' }
457
+ }
458
+
459
+ async upgrade(name, opts = {}) {
460
+ const args = ['install', `${name}@latest`]
461
+ if (opts.global) args.push('-g')
462
+ args.push(...this._proxyArgs(opts.proxy))
463
+ args.push(...this._registryArgs())
464
+
465
+ this._execInherit(args)
466
+ return { name, version: 'latest', action: 'upgraded' }
467
+ }
468
+
469
+ async cleanCache() {
470
+ this._execInherit(['cache', 'clean', '--force'])
471
+ return { action: 'cache cleaned' }
472
+ }
473
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * php(PHP runtime)版本管理器实现(基于 phpenv)
3
+ *
4
+ * 通过 phpenv CLI 管理多个 PHP 版本。phpenv 是常规二进制(~/.phpenv/bin/phpenv),
5
+ * 非 shell function,直接调用即可(无需 source)。
6
+ *
7
+ * 源查询使用 phpenv install --list,本地查询使用 phpenv versions。
8
+ * 镜像通过 PHP_BUILD_MIRROR_URL 环境变量传递给 php-build。
9
+ */
10
+
11
+ import fs from 'node:fs'
12
+ import path from 'node:path'
13
+ import os from 'node:os'
14
+ import { execSync } from 'node:child_process'
15
+ import { RuntimeManager } from './runtime.js'
16
+ import { Shell } from '@wwkit/shared'
17
+ import { getActiveRegistry } from '../config.js'
18
+
19
+ const shell = new Shell()
20
+
21
+ const PHP_BUILD_GIT = 'https://github.com/php-build/php-build.git'
22
+
23
+ /**
24
+ * 获取 phpenv 根目录
25
+ * @returns {string}
26
+ */
27
+ function getPhpenvRoot() {
28
+ return process.env.PHPENV_ROOT || path.join(os.homedir(), '.phpenv')
29
+ }
30
+
31
+ /**
32
+ * 获取 phpenv 二进制路径
33
+ * @returns {string}
34
+ */
35
+ export function getPhpenvBin() {
36
+ return path.join(getPhpenvRoot(), 'bin', 'phpenv')
37
+ }
38
+
39
+ /**
40
+ * 检查 phpenv 是否已安装
41
+ * @returns {boolean}
42
+ */
43
+ export function isPhpenvInstalled() {
44
+ return fs.existsSync(getPhpenvBin())
45
+ }
46
+
47
+ /**
48
+ * 安装 phpenv(git clone + php-build 插件 + 写 shell profile)
49
+ * @returns {{ action: string, plugin: string, profile: string, hint: string }}
50
+ */
51
+ export function installPhpenv() {
52
+ const mirror = getActiveRegistry('phpenv')
53
+ const root = getPhpenvRoot()
54
+ execSync(`git clone "${mirror}" "${root}"`, { stdio: 'inherit', shell: shell.resolveBash() })
55
+ execSync(`git clone "${PHP_BUILD_GIT}" "${path.join(root, 'plugins', 'php-build')}"`, {
56
+ stdio: 'inherit',
57
+ shell: shell.resolveBash(),
58
+ })
59
+ const lines = [
60
+ `export PHPENV_ROOT="${root}"`,
61
+ `export PATH="$PHPENV_ROOT/bin:$PATH"`,
62
+ `eval "$(phpenv init -)"`,
63
+ ]
64
+ const wroteProfile = shell.writeProfile('PHPENV_ROOT', 'phpenv', lines)
65
+ return {
66
+ action: 'installed',
67
+ plugin: 'php-build',
68
+ profile: wroteProfile ? shell.getProfile() : '(already configured)',
69
+ hint: `Restart shell or run: eval "$(${getPhpenvBin()} init -)"`,
70
+ }
71
+ }
72
+
73
+ export class PhpManager extends RuntimeManager {
74
+ constructor() {
75
+ super({
76
+ name: 'php',
77
+ mirrorEnv: 'PHP_BUILD_MIRROR_URL',
78
+ prefix: getPhpenvBin(),
79
+ needsBash: true,
80
+ isInstalled: () => isPhpenvInstalled(),
81
+ installTool: () => installPhpenv(),
82
+ toolName: 'phpenv',
83
+ toolMissingMsg: 'phpenv not installed, run `opm phpenv install` first',
84
+ listCmd: ['versions'],
85
+ listRemoteCmd: ['install', '--list'],
86
+ currentCmd: ['version'],
87
+ parseCurrent: (out) => {
88
+ const m = out.trim().match(/^(\d+\.\d+\.\d+)/)
89
+ return m ? m[1] : ''
90
+ },
91
+ installCmd: (v) => ['install', v],
92
+ uninstallCmd: (n) => ['uninstall', n],
93
+ resultName: (v) => v,
94
+ useCmd: (v) => ['global', v],
95
+ useAction: 'switched',
96
+ defaultCmd: (v) => ['global', v],
97
+ noCacheMsg: 'no cache for phpenv-managed php',
98
+ installUsage: 'Usage: opm php install <version> (e.g. 8.3, 8.3.13)',
99
+ notInstalledHint: 'phpenv not installed or no PHP version active, run `opm phpenv install` and `opm php install <version>`',
100
+ upgradeNoVersionMsg: 'No PHP version available to upgrade to',
101
+ info: {
102
+ description: 'PHP Hypertext Preprocessor',
103
+ homepage: 'https://www.php.net',
104
+ license: 'PHP License',
105
+ author: 'The PHP Group',
106
+ repository: 'https://github.com/php/php-src',
107
+ },
108
+ })
109
+ }
110
+
111
+ /**
112
+ * 获取当前激活 php 的安装目录
113
+ * @returns {string}
114
+ */
115
+ getInstallDir() {
116
+ try {
117
+ const out = this._exec(['which'], { allowNonZero: true }).trim()
118
+ if (!out) return ''
119
+ return path.dirname(path.dirname(out))
120
+ } catch {}
121
+ return ''
122
+ }
123
+
124
+ /**
125
+ * 解析 phpenv versions 输出为已安装版本列表
126
+ * @param {string} out
127
+ * @returns {{ name: string, version: string, current: boolean }[]}
128
+ * @private
129
+ */
130
+ _parseInstalled(out) {
131
+ const items = []
132
+ for (const line of out.split('\n')) {
133
+ const m = line.match(/^(\*)?\s+(\d+\.\d+\.\d+)\b/)
134
+ if (m) {
135
+ items.push({ name: m[2], version: m[2], current: !!m[1] })
136
+ }
137
+ }
138
+ return items
139
+ }
140
+
141
+ /**
142
+ * 解析 phpenv install --list 输出为可用版本列表
143
+ * @param {string} out
144
+ * @returns {string[]}
145
+ * @private
146
+ */
147
+ _parseRemote(out) {
148
+ const versions = []
149
+ for (const line of out.split('\n')) {
150
+ const m = line.match(/^\s*(\d+\.\d+\.\d+)\s*$/)
151
+ if (m) {
152
+ versions.push(m[1])
153
+ }
154
+ }
155
+ return versions
156
+ }
157
+ }