@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,180 @@
1
+ /**
2
+ * python(Python runtime)版本管理器实现(基于 uv)
3
+ *
4
+ * 通过 uv CLI 管理多个 Python 版本。uv 是常规二进制(~/.local/bin/uv),
5
+ * 非 shell function,直接调用即可。
6
+ *
7
+ * 源查询使用 uv python list,本地查询使用 uv python list --only-installed。
8
+ * 镜像通过 UV_PYTHON_INSTALL_MIRROR 环境变量传递给 uv。
9
+ *
10
+ * 与 node/php 差异较大处由子类覆盖:
11
+ * - getVersion / currentVersion:直接读系统 `python3`/`python`(而非 uv 子命令)
12
+ * - setDefault:写 ~/.python-version 文件
13
+ */
14
+
15
+ import { execSync, spawnSync } from 'node:child_process'
16
+ import fs from 'node:fs'
17
+ import path from 'node:path'
18
+ import os from 'node:os'
19
+ import { RuntimeManager } from './runtime.js'
20
+
21
+ const UV_INSTALL_URL = 'https://astral.sh/uv/install.sh'
22
+ const UV_INSTALL_PS_URL = 'https://astral.sh/uv/install.ps1'
23
+
24
+ /**
25
+ * 获取 uv 二进制路径(默认 ~/.local/bin/uv,否则依赖 PATH)
26
+ * @returns {string}
27
+ */
28
+ export function getUvBin() {
29
+ const localBin = path.join(os.homedir(), '.local', 'bin', 'uv')
30
+ if (fs.existsSync(localBin)) return localBin
31
+ return 'uv'
32
+ }
33
+
34
+ /**
35
+ * 检查 uv 是否已安装
36
+ * @returns {boolean}
37
+ */
38
+ export function isUvInstalled() {
39
+ try {
40
+ execSync(`${getUvBin()} --version`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
41
+ return true
42
+ } catch {
43
+ return false
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 安装 uv(curl 安装脚本)
49
+ * @returns {{ action: string, hint: string }}
50
+ */
51
+ export function installUv() {
52
+ if (process.platform === 'win32') {
53
+ const r = spawnSync(
54
+ 'powershell',
55
+ ['-ExecutionPolicy', 'ByPass', '-c', `irm ${UV_INSTALL_PS_URL} | iex`],
56
+ { stdio: 'inherit' }
57
+ )
58
+ if (r.status !== 0) {
59
+ throw new Error('uv install failed (powershell exited non-zero)')
60
+ }
61
+ } else {
62
+ execSync(`curl -LsSf "${UV_INSTALL_URL}" | sh`, { stdio: 'inherit', shell: true })
63
+ }
64
+ return { action: 'installed', hint: 'Restart shell or ensure ~/.local/bin is in PATH' }
65
+ }
66
+
67
+ export class PythonManager extends RuntimeManager {
68
+ constructor() {
69
+ super({
70
+ name: 'python',
71
+ mirrorEnv: 'UV_PYTHON_INSTALL_MIRROR',
72
+ prefix: getUvBin(),
73
+ isInstalled: () => isUvInstalled(),
74
+ installTool: () => installUv(),
75
+ toolName: 'uv',
76
+ toolMissingMsg: 'uv not installed, run `opm uv install` first',
77
+ listCmd: ['python', 'list', '--only-installed'],
78
+ listRemoteCmd: ['python', 'list'],
79
+ installCmd: (v) => ['python', 'install', v],
80
+ uninstallCmd: (n) => ['python', 'uninstall', n],
81
+ resultName: (v) => v,
82
+ useCmd: (v) => ['python', 'pin', v],
83
+ useAction: 'pinned (current directory)',
84
+ noCacheMsg: 'no cache for uv-managed python',
85
+ installUsage: 'Usage: opm python install <version> (e.g. 3.12, 3.12.4)',
86
+ notInstalledHint: 'No Python found, run `opm uv install` and `opm python install <version>`',
87
+ upgradeNoVersionMsg: 'No Python version available to upgrade to',
88
+ info: {
89
+ description: 'Python programming language interpreter',
90
+ homepage: 'https://www.python.org',
91
+ license: 'PSF License',
92
+ author: 'Python Software Foundation',
93
+ repository: 'https://github.com/python/cpython',
94
+ },
95
+ })
96
+ }
97
+
98
+ /**
99
+ * 获取当前激活 python 的安装目录
100
+ * @returns {string}
101
+ */
102
+ getInstallDir() {
103
+ try {
104
+ const out = this._exec(['python', 'find'], { allowNonZero: true }).trim()
105
+ if (!out) return ''
106
+ return path.dirname(path.dirname(out))
107
+ } catch {}
108
+ return ''
109
+ }
110
+
111
+ /**
112
+ * 解析 uv python list --only-installed 输出为已安装版本列表
113
+ * 输出格式: cpython-3.12.4-linux-x86_64-gnu /path/to/python
114
+ * @param {string} out
115
+ * @returns {{ name: string, version: string, current: boolean }[]}
116
+ * @private
117
+ */
118
+ _parseInstalled(out) {
119
+ const items = []
120
+ for (const line of out.split('\n')) {
121
+ const m = line.match(/cpython-(\d+\.\d+\.\d+)/)
122
+ if (m) {
123
+ items.push({ name: m[1], version: m[1], current: false })
124
+ }
125
+ }
126
+ return items
127
+ }
128
+
129
+ /**
130
+ * 解析 uv python list 输出为可用版本列表
131
+ * @param {string} out
132
+ * @returns {string[]}
133
+ * @private
134
+ */
135
+ _parseRemote(out) {
136
+ const versions = []
137
+ for (const line of out.split('\n')) {
138
+ const m = line.match(/cpython-(\d+\.\d+\.\d+)/)
139
+ if (m && !versions.includes(m[1])) {
140
+ versions.push(m[1])
141
+ }
142
+ }
143
+ return versions
144
+ }
145
+
146
+ /**
147
+ * 查询系统 Python 版本
148
+ * @returns {{ version: string, installed: boolean } | null}
149
+ * @private
150
+ */
151
+ _systemVersion() {
152
+ for (const cmd of ['python3', 'python']) {
153
+ try {
154
+ const out = execSync(`${cmd} --version`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
155
+ const m = out.trim().match(/Python\s+(\d+\.\d+\.\d+)/)
156
+ if (m) return { version: m[1], installed: true }
157
+ } catch {}
158
+ }
159
+ return null
160
+ }
161
+
162
+ async getVersion() {
163
+ const v = this._systemVersion()
164
+ if (v) {
165
+ return { name: 'python', installed: true, version: v.version }
166
+ }
167
+ return { name: 'python', installed: false, hint: this.cfg.notInstalledHint }
168
+ }
169
+
170
+ async currentVersion() {
171
+ const v = this._systemVersion()
172
+ return { current: v ? v.version : 'unknown' }
173
+ }
174
+
175
+ async setDefault(version) {
176
+ const homeVersionFile = path.join(os.homedir(), '.python-version')
177
+ fs.writeFileSync(homeVersionFile, `${version}\n`, 'utf8')
178
+ return { version, action: 'default set (~/.python-version)' }
179
+ }
180
+ }
@@ -0,0 +1,309 @@
1
+ /**
2
+ * 运行时版本管理器基类(node / php / python 共用)
3
+ *
4
+ * 三个运行时均通过底层版本管理工具(nvm / phpenv / uv)管理多版本,
5
+ * 接口一致(registry/search/view/info/versions/list/install/uninstall/upgrade/use/current/default)。
6
+ * 差异(命令前缀、镜像环境变量、动作命令参数、解析函数)通过 cfg 配置注入;
7
+ * 解析函数由子类实现。python 的 getVersion/currentVersion/setDefault 等差异较大,
8
+ * 由子类覆盖。
9
+ */
10
+
11
+ import { execSync, spawnSync } from 'node:child_process'
12
+ import { PackageManager } from './base.js'
13
+ import { Shell } from '@wwkit/shared'
14
+ import { getActiveRegistry, setActiveRegistry, getConfig } from '../config.js'
15
+
16
+ const shell = new Shell()
17
+
18
+ /**
19
+ * @typedef {Object} RuntimeConfig
20
+ * @property {string} name
21
+ * @property {string} mirrorEnv - dist 镜像环境变量名
22
+ * @property {string} prefix - 工具调用前缀(如 `source ".../nvm.sh" && nvm`、bin 路径)
23
+ * @property {boolean} [needsBash=false] - 命令是否需要 bash(nvm source / phpenv 脚本);原生二进制(uv)为 false
24
+ * @property {() => boolean} isInstalled
25
+ * @property {string} toolMissingMsg
26
+ * @property {string[]} listCmd - 列出已安装版本的参数
27
+ * @property {string[]} listRemoteCmd - 列出可用版本的参数
28
+ * @property {string[]} [currentCmd] - 查询当前版本的参数(python 覆盖)
29
+ * @property {(out: string) => string} [parseCurrent] - 解析当前版本(空串 = 无有效版本)
30
+ * @property {(v: string) => string[]} installCmd
31
+ * @property {(n: string) => string[]} uninstallCmd
32
+ * @property {(v: string) => string} resultName - install 结果的 name 字段
33
+ * @property {string[]} [upgradeLts] - 升级为 LTS 的参数(node);缺省走「装最新版」逻辑
34
+ * @property {(v: string) => string[]} useCmd
35
+ * @property {string} useAction
36
+ * @property {(v: string) => string[]} defaultCmd
37
+ * @property {string} noCacheMsg
38
+ * @property {string} installUsage
39
+ * @property {string} notInstalledHint
40
+ * @property {string} upgradeNoVersionMsg
41
+ * @property {Object} info - packageInfo 静态元数据
42
+ */
43
+
44
+ export class RuntimeManager extends PackageManager {
45
+ /**
46
+ * @param {RuntimeConfig} cfg
47
+ */
48
+ constructor(cfg) {
49
+ super()
50
+ this.name = cfg.name
51
+ this.cfg = cfg
52
+ }
53
+
54
+ /**
55
+ * 构造工具调用环境(含 dist 镜像 + proxy)
56
+ * @param {string} [proxy]
57
+ * @returns {Record<string,string>}
58
+ * @private
59
+ */
60
+ _buildEnv(proxy) {
61
+ const env = { ...process.env }
62
+ const mirror = getActiveRegistry(this.name)
63
+ if (mirror) {
64
+ env[this.cfg.mirrorEnv] = mirror
65
+ }
66
+ if (proxy) {
67
+ Object.assign(env, {
68
+ HTTP_PROXY: proxy,
69
+ HTTPS_PROXY: proxy,
70
+ ALL_PROXY: proxy,
71
+ http_proxy: proxy,
72
+ https_proxy: proxy,
73
+ all_proxy: proxy,
74
+ })
75
+ }
76
+ return env
77
+ }
78
+
79
+ /**
80
+ * 构造完整 shell 命令
81
+ * @param {string[]} args
82
+ * @returns {string}
83
+ * @private
84
+ */
85
+ _cmd(args) {
86
+ return `${this.cfg.prefix} ${args.join(' ')}`
87
+ }
88
+
89
+ /**
90
+ * 执行工具命令,返回 stdout
91
+ * @param {string[]} args
92
+ * @param {{ allowNonZero?: boolean, proxy?: string }} [opts]
93
+ * @returns {string}
94
+ * @private
95
+ */
96
+ _exec(args, opts = {}) {
97
+ if (!this.cfg.isInstalled()) {
98
+ throw new Error(this.cfg.toolMissingMsg)
99
+ }
100
+ try {
101
+ return execSync(this._cmd(args), {
102
+ encoding: 'utf8',
103
+ stdio: ['pipe', 'pipe', 'pipe'],
104
+ shell: shell.shellForExec(this.cfg.needsBash),
105
+ env: this._buildEnv(opts.proxy),
106
+ })
107
+ } catch (err) {
108
+ if (opts.allowNonZero) {
109
+ return err.stdout ? err.stdout.toString() : ''
110
+ }
111
+ const stderr = err.stderr ? err.stderr.toString().trim() : ''
112
+ if (stderr) {
113
+ throw new Error(stderr)
114
+ }
115
+ return err.stdout ? err.stdout.toString() : ''
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 执行工具命令,输出直接继承到终端
121
+ * @param {string[]} args
122
+ * @param {{ proxy?: string }} [opts]
123
+ * @private
124
+ */
125
+ _execInherit(args, opts = {}) {
126
+ if (!this.cfg.isInstalled()) {
127
+ throw new Error(this.cfg.toolMissingMsg)
128
+ }
129
+ const result = spawnSync(this._cmd(args), {
130
+ stdio: 'inherit',
131
+ shell: shell.shellForExec(this.cfg.needsBash),
132
+ env: this._buildEnv(opts.proxy),
133
+ })
134
+ if (result.error) {
135
+ throw new Error(result.error.message)
136
+ }
137
+ if (result.status !== 0) {
138
+ throw new Error(`${this.name} ${args.join(' ')} exited with code ${result.status}`)
139
+ }
140
+ }
141
+
142
+ /**
143
+ * 解析已安装版本列表(子类实现)
144
+ * @param {string} out
145
+ * @returns {{ name: string, version: string, current: boolean }[]}
146
+ */
147
+ _parseInstalled(out) {
148
+ throw new Error(`${this.name}: _parseInstalled not implemented`)
149
+ }
150
+
151
+ /**
152
+ * 解析可用版本列表(子类实现)
153
+ * @param {string} out
154
+ * @returns {string[]}
155
+ */
156
+ _parseRemote(out) {
157
+ throw new Error(`${this.name}: _parseRemote not implemented`)
158
+ }
159
+
160
+ async getRegistry() {
161
+ return { registry: getActiveRegistry(this.name) }
162
+ }
163
+
164
+ async setRegistry(url) {
165
+ return { registry: setActiveRegistry(this.name, url) }
166
+ }
167
+
168
+ async getVersion() {
169
+ try {
170
+ const out = this._exec(this.cfg.currentCmd, { allowNonZero: true })
171
+ const parsed = this.cfg.parseCurrent(out)
172
+ if (parsed) {
173
+ return { name: this.name, installed: true, version: parsed }
174
+ }
175
+ } catch {}
176
+ return { name: this.name, version: '', installed: false, hint: this.cfg.notInstalledHint }
177
+ }
178
+
179
+ /**
180
+ * 获取当前激活版本的安装目录(子类实现)
181
+ * @returns {string}
182
+ */
183
+ getInstallDir() {
184
+ return ''
185
+ }
186
+
187
+ /**
188
+ * 获取配置中的默认安装版本
189
+ * @returns {string}
190
+ */
191
+ getDefaultVersion() {
192
+ return getConfig()[this.name]?.defaultVersion || ''
193
+ }
194
+
195
+ async searchPackage(name, version, opts = {}) {
196
+ const { registry } = await this.getRegistry()
197
+ const out = this._exec(this.cfg.listRemoteCmd, { allowNonZero: true, proxy: opts.proxy })
198
+ const versions = this._parseRemote(out)
199
+ if (versions.length === 0) {
200
+ if (version) return { name, version, exists: false, available: false, registry }
201
+ return { name, exists: false, registry }
202
+ }
203
+ if (version) {
204
+ return { name, version, exists: true, available: versions.includes(version), registry }
205
+ }
206
+ return {
207
+ name,
208
+ exists: true,
209
+ latest: versions[versions.length - 1],
210
+ versions,
211
+ registry,
212
+ }
213
+ }
214
+
215
+ async isInstalled(name, opts = {}) {
216
+ const out = this._exec(this.cfg.listCmd, { allowNonZero: true })
217
+ const items = this._parseInstalled(out)
218
+ const hit = items.find((i) => i.name === name)
219
+ if (hit) {
220
+ return { name, installed: true, version: hit.version, current: hit.current }
221
+ }
222
+ return { name, installed: false }
223
+ }
224
+
225
+ async packageInfo(name, opts = {}) {
226
+ return { name: this.name, ...this.cfg.info }
227
+ }
228
+
229
+ async packageVersions(name, opts = {}) {
230
+ const { registry } = await this.getRegistry()
231
+ const out = this._exec(this.cfg.listRemoteCmd, { allowNonZero: true, proxy: opts.proxy })
232
+ return { name, versions: this._parseRemote(out), registry }
233
+ }
234
+
235
+ async listInstalled(opts = {}) {
236
+ const out = this._exec(this.cfg.listCmd, { allowNonZero: true })
237
+ return this._parseInstalled(out)
238
+ }
239
+
240
+ async listOutdated(opts = {}) {
241
+ return []
242
+ }
243
+
244
+ async install(name, version, opts = {}) {
245
+ if (!version) {
246
+ throw new Error(this.cfg.installUsage)
247
+ }
248
+ if (!this.cfg.isInstalled() && this.cfg.installTool) {
249
+ console.log(`[opm] ${this.cfg.toolName} not installed, auto-installing...`)
250
+ this.cfg.installTool()
251
+ }
252
+ this._execInherit(this.cfg.installCmd(version), { proxy: opts.proxy })
253
+ return { name: this.cfg.resultName(version), version, action: 'installed' }
254
+ }
255
+
256
+ async uninstall(name, opts = {}) {
257
+ this._execInherit(this.cfg.uninstallCmd(name))
258
+ return { name, action: 'uninstalled' }
259
+ }
260
+
261
+ async upgrade(name, opts = {}) {
262
+ if (this.cfg.upgradeLts) {
263
+ this._execInherit(this.cfg.upgradeLts, { proxy: opts.proxy })
264
+ return { name: 'lts', version: 'latest', action: 'upgraded' }
265
+ }
266
+ const out = this._exec(this.cfg.listRemoteCmd, { allowNonZero: true, proxy: opts.proxy })
267
+ const versions = this._parseRemote(out)
268
+ const latest = versions[versions.length - 1]
269
+ if (!latest) {
270
+ throw new Error(this.cfg.upgradeNoVersionMsg)
271
+ }
272
+ this._execInherit(this.cfg.installCmd(latest), { proxy: opts.proxy })
273
+ return { name: latest, version: latest, action: 'upgraded' }
274
+ }
275
+
276
+ async cleanCache() {
277
+ return { action: this.cfg.noCacheMsg }
278
+ }
279
+
280
+ /**
281
+ * 切换当前版本
282
+ * @param {string} version
283
+ * @param {{ proxy?: string }} [opts]
284
+ * @returns {Promise<{ version: string, action: string }>}
285
+ */
286
+ async useVersion(version, opts = {}) {
287
+ this._execInherit(this.cfg.useCmd(version), { proxy: opts.proxy })
288
+ return { version, action: this.cfg.useAction }
289
+ }
290
+
291
+ /**
292
+ * 查询当前激活版本
293
+ * @returns {Promise<{ current: string }>}
294
+ */
295
+ async currentVersion() {
296
+ const out = this._exec(this.cfg.currentCmd, { allowNonZero: true })
297
+ return { current: this.cfg.parseCurrent(out) || out.trim() }
298
+ }
299
+
300
+ /**
301
+ * 设置默认版本
302
+ * @param {string} version
303
+ * @returns {Promise<{ version: string, action: string }>}
304
+ */
305
+ async setDefault(version) {
306
+ this._execInherit(this.cfg.defaultCmd(version))
307
+ return { version, action: 'default set' }
308
+ }
309
+ }