@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,200 @@
1
+ /**
2
+ * 跨平台 SQLite 访问驱动
3
+ *
4
+ * 优先使用 Node 内置 node:sqlite(Node >= 22.5,零依赖);
5
+ * 不可用时回退到 sqlite3 CLI(跨平台均可用,依赖本机 sqlite3);
6
+ * 两者皆不可用时抛出带提示的错误。
7
+ *
8
+ * 统一接口(供清理逻辑使用):
9
+ * listBefore(boundaryMs) -> [{ id, title, time_created }](按时间倒序)
10
+ * listSessionsAsc() -> [{ id, title, time_created }](按时间正序,供 -n/--num 用)
11
+ * listAll() -> [{ id, title }]
12
+ * countBefore(boundaryMs) -> number
13
+ * deleteBefore(boundaryMs) -> void(删除会话 + 关联事件,并 VACUUM)
14
+ * deleteSessions(ids) -> void(删除指定 id 的会话 + 关联事件,并 VACUUM)
15
+ * checkpoint() -> void(尽力 WAL 落盘,失败忽略)
16
+ * close() -> void
17
+ */
18
+
19
+ import { execFileSync } from 'node:child_process'
20
+
21
+ const BOUNDARY_COL = 'time_created'
22
+
23
+ export async function detectDriver() {
24
+ try {
25
+ await import('node:sqlite')
26
+ return 'node'
27
+ } catch {}
28
+ if (hasSqlite3Cli()) {
29
+ return 'cli'
30
+ }
31
+ return null
32
+ }
33
+
34
+ export async function openDb(dbPath) {
35
+ const driver = await detectDriver()
36
+ if (driver === 'node') {
37
+ const { DatabaseSync } = await import('node:sqlite')
38
+ return new NodeSqliteDriver(new DatabaseSync(dbPath))
39
+ }
40
+ if (driver === 'cli') {
41
+ return new CliSqliteDriver(dbPath)
42
+ }
43
+ throw new Error(
44
+ 'no SQLite backend available: upgrade to Node >= 22.5 (built-in node:sqlite) ' +
45
+ 'or install the sqlite3 CLI to use "opm opencode clear"'
46
+ )
47
+ }
48
+
49
+ function hasSqlite3Cli() {
50
+ try {
51
+ execFileSync('sqlite3', ['--version'], { stdio: 'ignore' })
52
+ return true
53
+ } catch {
54
+ return false
55
+ }
56
+ }
57
+
58
+ export class NodeSqliteDriver {
59
+ constructor(db) {
60
+ this.db = db
61
+ this.db.exec('PRAGMA foreign_keys=ON')
62
+ }
63
+
64
+ listBefore(boundaryMs) {
65
+ const stmt = this.db.prepare(
66
+ `SELECT id, title, ${BOUNDARY_COL} AS time_created
67
+ FROM session WHERE ${BOUNDARY_COL} < ? ORDER BY ${BOUNDARY_COL} DESC`
68
+ )
69
+ return stmt.all(boundaryMs)
70
+ }
71
+
72
+ listSessionsAsc() {
73
+ const stmt = this.db.prepare(
74
+ `SELECT id, title, ${BOUNDARY_COL} AS time_created
75
+ FROM session ORDER BY ${BOUNDARY_COL} ASC`
76
+ )
77
+ return stmt.all()
78
+ }
79
+
80
+ listAll() {
81
+ const stmt = this.db.prepare('SELECT id, title FROM session ORDER BY time_created DESC')
82
+ return stmt.all()
83
+ }
84
+
85
+ countBefore(boundaryMs) {
86
+ const stmt = this.db.prepare(`SELECT COUNT(*) AS n FROM session WHERE ${BOUNDARY_COL} < ?`)
87
+ return Number(stmt.get(boundaryMs).n)
88
+ }
89
+
90
+ deleteBefore(boundaryMs) {
91
+ this._deleteWhere(`${BOUNDARY_COL} < ${boundaryMs}`)
92
+ }
93
+
94
+ deleteSessions(ids) {
95
+ const list = ids.map((id) => `'${String(id).replace(/'/g, "''")}'`).join(', ')
96
+ this._deleteWhere(`id IN (${list})`)
97
+ }
98
+
99
+ _deleteWhere(condition) {
100
+ this.db.exec('BEGIN')
101
+ try {
102
+ this.db.prepare(
103
+ `DELETE FROM event_sequence WHERE aggregate_id IN (SELECT id FROM session WHERE ${condition})`
104
+ ).run()
105
+ this.db.prepare(`DELETE FROM session WHERE ${condition}`).run()
106
+ this.db.exec('COMMIT')
107
+ } catch (err) {
108
+ this.db.exec('ROLLBACK')
109
+ throw err
110
+ }
111
+ this.db.exec('VACUUM')
112
+ }
113
+
114
+ checkpoint() {
115
+ try {
116
+ this.db.exec('PRAGMA wal_checkpoint(FULL)')
117
+ } catch {}
118
+ }
119
+
120
+ close() {
121
+ try {
122
+ this.db.close()
123
+ } catch {}
124
+ }
125
+ }
126
+
127
+ export class CliSqliteDriver {
128
+ constructor(dbPath) {
129
+ this.dbPath = dbPath
130
+ }
131
+
132
+ _run(sql) {
133
+ return execFileSync('sqlite3', [this.dbPath, sql], { encoding: 'utf8' })
134
+ }
135
+
136
+ _parseRows(out) {
137
+ const rows = []
138
+ for (const line of out.trim().split('\n')) {
139
+ if (!line) continue
140
+ const parts = line.split('\u0001')
141
+ if (parts.length >= 3) {
142
+ rows.push({ id: parts[0], title: parts[1], time_created: Number(parts[2]) })
143
+ }
144
+ }
145
+ return rows
146
+ }
147
+
148
+ listBefore(boundaryMs) {
149
+ const sql = `SELECT id || char(1) || title || char(1) || ${BOUNDARY_COL}
150
+ FROM session WHERE ${BOUNDARY_COL} < ${boundaryMs} ORDER BY ${BOUNDARY_COL} DESC;`
151
+ return this._parseRows(this._run(sql))
152
+ }
153
+
154
+ listSessionsAsc() {
155
+ const sql = `SELECT id || char(1) || title || char(1) || ${BOUNDARY_COL}
156
+ FROM session ORDER BY ${BOUNDARY_COL} ASC;`
157
+ return this._parseRows(this._run(sql))
158
+ }
159
+
160
+ listAll() {
161
+ const sql = `SELECT id || char(1) || title
162
+ FROM session ORDER BY time_created DESC;`
163
+ const out = this._run(sql)
164
+ const rows = []
165
+ for (const line of out.trim().split('\n')) {
166
+ if (!line) continue
167
+ const idx = line.indexOf('\u0001')
168
+ if (idx > 0) {
169
+ rows.push({ id: line.slice(0, idx), title: line.slice(idx + 1) })
170
+ }
171
+ }
172
+ return rows
173
+ }
174
+
175
+ countBefore(boundaryMs) {
176
+ const out = this._run(`SELECT count(*) FROM session WHERE ${BOUNDARY_COL} < ${boundaryMs};`).trim()
177
+ return Number(out || 0)
178
+ }
179
+
180
+ deleteBefore(boundaryMs) {
181
+ this._deleteWhere(`${BOUNDARY_COL} < ${boundaryMs}`)
182
+ }
183
+
184
+ deleteSessions(ids) {
185
+ const list = ids.map((id) => `'${String(id).replace(/'/g, "''")}'`).join(', ')
186
+ this._deleteWhere(`id IN (${list})`)
187
+ }
188
+
189
+ _deleteWhere(condition) {
190
+ const sql = `PRAGMA foreign_keys=ON;
191
+ DELETE FROM event_sequence WHERE aggregate_id IN (SELECT id FROM session WHERE ${condition});
192
+ DELETE FROM session WHERE ${condition};
193
+ VACUUM;`
194
+ this._run(sql)
195
+ }
196
+
197
+ checkpoint() {}
198
+
199
+ close() {}
200
+ }
package/src/index.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @wwkit/opm 库入口
3
+ *
4
+ * 统一命令行工具(包管理 npm/pip/dnf/apt/brew/composer/bun + 运行时版本管理
5
+ * node/php/python + 版本管理器 nvm/phpenv/uv + opencode 运维),
6
+ * CLI 与各组件的可复用 API 均从此导出。
7
+ */
8
+
9
+ export { CLI, GROUPS } from './cli/index.js'
10
+ export { PackageGroup, RuntimeGroup } from './cli/groups/package.js'
11
+ export { ConfigGroup } from './cli/groups/config.js'
12
+ export { HarnessGroup } from './harnesses/base.js'
13
+ export { opencodeHarness } from './harnesses/opencode/index.js'
14
+ export { BinaryGroup } from './binaries/base.js'
15
+ export { CftGroup } from './binaries/cft/index.js'
16
+ export { NvmGroup } from './cli/groups/nvm.js'
17
+ export { PhpenvGroup } from './cli/groups/phpenv.js'
18
+ export { UvGroup } from './cli/groups/uv.js'
19
+ export { ToolManagerGroup } from './cli/groups/version-manager.js'
20
+ export { parseFlags } from './cli/helpers/args.js'
21
+
22
+ export { PackageManager } from './managers/base.js'
23
+ export { RuntimeManager } from './managers/runtime.js'
24
+ export { NpmManager } from './managers/npm.js'
25
+ export { PipManager } from './managers/pip.js'
26
+ export { DnfManager } from './managers/dnf.js'
27
+ export { AptManager } from './managers/apt.js'
28
+ export { BrewManager } from './managers/brew.js'
29
+ export { ComposerManager } from './managers/composer.js'
30
+ export { BunManager } from './managers/bun.js'
31
+ export { NodeManager, isNvmInstalled, getNvmSh } from './managers/node.js'
32
+ export { PhpManager, isPhpenvInstalled, getPhpenvBin } from './managers/php.js'
33
+ export { PythonManager, isUvInstalled, getUvBin } from './managers/python.js'
34
+
35
+ export {
36
+ NPM_PRESETS, PIP_PRESETS, DNF_PRESETS, APT_PRESETS, BREW_PRESETS, COMPOSER_PRESETS,
37
+ BUN_PRESETS, NVM_PRESETS, NODE_PRESETS, PHPENV_PRESETS, PHP_PRESETS, PYTHON_PRESETS,
38
+ getPresets, resolveRegistryInput,
39
+ } from './presets.js'
40
+ export { PROXY_PRESETS } from './proxy-presets.js'
41
+ export { output } from './formatter.js'
42
+ export { downloadToFile, fetchText } from './managers/http.js'
43
+ export {
44
+ getConfig, getSection, copyBuiltinConfig, getActiveRegistry, setActiveRegistry,
45
+ getUserConfigDir, getUserConfigFile, getActiveProxy,
46
+ } from './config.js'
@@ -0,0 +1,377 @@
1
+ /**
2
+ * apt 包管理器实现
3
+ *
4
+ * 通过执行 apt / apt-cache / dpkg-query CLI 命令实现统一接口(Debian / Ubuntu 系)。
5
+ * apt 是系统级包管理器,所有操作均作用于系统(无用户级/local 概念)。
6
+ *
7
+ * 源查询使用 apt-cache,本地查询使用 dpkg-query / apt list。
8
+ */
9
+
10
+ import { execSync, spawnSync } from 'node:child_process'
11
+ import fs from 'node:fs'
12
+ import path from 'node:path'
13
+ import { PackageManager } from './base.js'
14
+ import { getActiveRegistry, setActiveRegistry } from '../config.js'
15
+
16
+ const SOURCES_LIST = '/etc/apt/sources.list'
17
+ const SOURCES_DIR = '/etc/apt/sources.list.d'
18
+
19
+ const SHOW_KEYS = new Set([
20
+ 'package', 'version', 'architecture', 'maintainer', 'installed-size',
21
+ 'homepage', 'priority', 'section', 'description'
22
+ ])
23
+
24
+ export class AptManager extends PackageManager {
25
+ constructor() {
26
+ super()
27
+ this.name = 'apt'
28
+ }
29
+
30
+ /**
31
+ * 对包含特殊字符的参数进行 shell 转义(单引号包裹)
32
+ * @param {string} arg
33
+ * @returns {string}
34
+ * @private
35
+ */
36
+ _quoteArg(arg) {
37
+ if (arg && /[\s'"$`\\]/.test(arg)) {
38
+ return `'${arg.replace(/'/g, `'\\''`)}'`
39
+ }
40
+ return arg
41
+ }
42
+
43
+ /**
44
+ * 执行 apt 命令,返回 stdout
45
+ * @param {string[]} args
46
+ * @param {object} [opts]
47
+ * @returns {string}
48
+ * @private
49
+ */
50
+ _exec(args, opts = {}) {
51
+ const cmd = ['apt', ...args].map((a) => this._quoteArg(a)).join(' ')
52
+ try {
53
+ return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], ...opts })
54
+ } catch (err) {
55
+ if (opts.allowNonZero) {
56
+ return err.stdout ? err.stdout.toString() : ''
57
+ }
58
+ const stderr = err.stderr ? err.stderr.toString().trim() : ''
59
+ if (stderr && !stderr.startsWith('W:') && !stderr.startsWith('N:')) {
60
+ throw new Error(stderr)
61
+ }
62
+ return err.stdout ? err.stdout.toString() : ''
63
+ }
64
+ }
65
+
66
+ /**
67
+ * 执行 apt 命令,输出直接继承到终端(用于 install/remove/upgrade)
68
+ * @param {string[]} args
69
+ * @private
70
+ */
71
+ _execInherit(args) {
72
+ const result = spawnSync('apt', args, { stdio: 'inherit', shell: true })
73
+ if (result.error) {
74
+ throw new Error(result.error.message)
75
+ }
76
+ if (result.status !== 0) {
77
+ throw new Error(`apt ${args.join(' ')} exited with code ${result.status}`)
78
+ }
79
+ }
80
+
81
+ /**
82
+ * 构造 apt 代理参数(http/https 代理)
83
+ * @param {string} [proxy] - 代理地址
84
+ * @returns {string[]}
85
+ * @private
86
+ */
87
+ _proxyArgs(proxy) {
88
+ if (!proxy) return []
89
+ return ['-o', `Acquire::http::Proxy=${proxy}`, '-o', `Acquire::https::Proxy=${proxy}`]
90
+ }
91
+
92
+ /**
93
+ * 过滤 apt 输出中的噪音行,返回有意义的行
94
+ * @param {string} out
95
+ * @returns {string[]}
96
+ * @private
97
+ */
98
+ _parseLines(out) {
99
+ const lines = []
100
+ for (const line of out.split('\n')) {
101
+ const trimmed = line.trim()
102
+ if (!trimmed) continue
103
+ if (trimmed.startsWith('W:') || trimmed.startsWith('N:')) continue
104
+ if (trimmed.startsWith('Listing')) continue
105
+ lines.push(trimmed)
106
+ }
107
+ return lines
108
+ }
109
+
110
+ /**
111
+ * 解析 apt 源文件文本(sources.list / *.list)中的二进制 deb 行
112
+ *
113
+ * 仅支持经典的 deb 行语法(含 [arch=...] 选项),不支持 deb822(*.sources)格式。
114
+ * 支持反斜杠续行。
115
+ *
116
+ * @param {string} text
117
+ * @returns {{ id: string, url: string }[]}
118
+ * @private
119
+ */
120
+ _parseSources(text) {
121
+ const repos = []
122
+ let continuation = ''
123
+ for (const rawLine of text.split('\n')) {
124
+ let line = rawLine.trim()
125
+ if (continuation) {
126
+ line = continuation + ' ' + line
127
+ continuation = ''
128
+ }
129
+ if (line.endsWith('\\')) {
130
+ continuation = line.slice(0, -1)
131
+ continue
132
+ }
133
+ if (!line || line.startsWith('#')) continue
134
+ const parts = line.split(/\s+/)
135
+ if (parts[0] !== 'deb') continue
136
+ const urlIdx = parts.findIndex((p) => /^https?:\/\//.test(p))
137
+ if (urlIdx < 0) continue
138
+ const suite = parts[urlIdx + 1] || 'deb'
139
+ repos.push({ id: suite, url: parts[urlIdx] })
140
+ }
141
+ return repos
142
+ }
143
+
144
+ /**
145
+ * 读取启用的 apt 源(不调用 apt,纯读配置文件)
146
+ *
147
+ * 解析 /etc/apt/sources.list 与 /etc/apt/sources.list.d/*.list,
148
+ * 提取 deb 行中的源 URL 与 suite 作为仓库 id。
149
+ *
150
+ * @param {string} [dir] - sources 目录(默认 /etc/apt/sources.list.d,测试可注入临时目录)
151
+ * @returns {{ id: string, url: string }[]}
152
+ * @private
153
+ */
154
+ _readEnabledRepos(dir = SOURCES_DIR) {
155
+ const repos = []
156
+ let files = [SOURCES_LIST]
157
+ try {
158
+ files = [SOURCES_LIST, ...fs.readdirSync(dir).map((f) => path.join(dir, f))]
159
+ } catch {}
160
+
161
+ for (const file of files) {
162
+ if (file !== SOURCES_LIST && !file.endsWith('.list')) continue
163
+ let text
164
+ try {
165
+ text = fs.readFileSync(file, 'utf8')
166
+ } catch {
167
+ continue
168
+ }
169
+ repos.push(...this._parseSources(text))
170
+ }
171
+ return repos
172
+ }
173
+
174
+ /**
175
+ * 查询某个包在所有源中的可用版本(去重,保留 madison 输出顺序)
176
+ * @param {string} name - 包名
177
+ * @param {{ proxy?: string }} [opts]
178
+ * @returns {string[]}
179
+ * @private
180
+ */
181
+ _availableVersions(name, opts = {}) {
182
+ const args = ['cache', 'madison', name, ...this._proxyArgs(opts.proxy)]
183
+ const out = this._exec(args, { allowNonZero: true })
184
+ const versions = []
185
+ for (const line of this._parseLines(out)) {
186
+ const parts = line.split('|').map((s) => s.trim())
187
+ if (parts.length >= 2 && !parts[1].includes(' ')) {
188
+ versions.push(parts[1])
189
+ }
190
+ }
191
+ return [...new Set(versions)]
192
+ }
193
+
194
+ async getVersion() {
195
+ try {
196
+ const out = this._exec(['--version'], { allowNonZero: true })
197
+ const match = out.match(/(\d+(?:\.\d+)+)/)
198
+ if (match) {
199
+ return { name: 'apt', installed: true, version: match[1] }
200
+ }
201
+ if (out.trim()) {
202
+ return { name: 'apt', installed: true, version: out.trim().split('\n')[0] }
203
+ }
204
+ } catch {}
205
+ return { name: 'apt', version: '', installed: false, hint: 'apt is the package manager for Debian / Ubuntu-based systems' }
206
+ }
207
+
208
+ async getRegistry() {
209
+ const registry = getActiveRegistry('apt') || '(no active registry in opm config)'
210
+ return { registry }
211
+ }
212
+
213
+ async setRegistry(url) {
214
+ const resolved = setActiveRegistry('apt', url)
215
+ const line = resolved.startsWith('deb ')
216
+ ? resolved
217
+ : `deb ${resolved} stable main`
218
+ const repoFile = path.join(SOURCES_DIR, 'opm.list')
219
+ try {
220
+ fs.writeFileSync(repoFile, `${line}\n`, 'utf8')
221
+ } catch (err) {
222
+ throw new Error(`failed to write ${repoFile}: ${err.message}`)
223
+ }
224
+ return { registry: line }
225
+ }
226
+
227
+ async searchPackage(name, version, opts = {}) {
228
+ const { registry } = await this.getRegistry()
229
+ const versions = this._availableVersions(name, opts)
230
+
231
+ if (versions.length === 0) {
232
+ if (version) {
233
+ return { name, version, exists: false, available: false, registry }
234
+ }
235
+ return { name, exists: false, registry }
236
+ }
237
+
238
+ if (version) {
239
+ return { name, version, exists: true, available: versions.includes(version), registry }
240
+ }
241
+
242
+ return {
243
+ name,
244
+ exists: true,
245
+ latest: versions[versions.length - 1],
246
+ versions,
247
+ registry
248
+ }
249
+ }
250
+
251
+ async isInstalled(name, opts = {}) {
252
+ const args = ['query', '-W', '-f=${Status} ${Version}', name]
253
+ const out = this._exec(args, { allowNonZero: true })
254
+ const m = out.trim().match(/^install ok installed\s+(\S+)$/)
255
+ if (m) {
256
+ return { name, installed: true, version: m[1] }
257
+ }
258
+ return { name, installed: false }
259
+ }
260
+
261
+ /**
262
+ * 解析 apt-cache show 输出为字段映射(取第一个包段)
263
+ * @param {string} out
264
+ * @returns {Record<string, string>|null}
265
+ * @private
266
+ */
267
+ _parseShow(out) {
268
+ const info = {}
269
+ let descriptionLines = []
270
+ let inDescription = false
271
+
272
+ for (const rawLine of out.split('\n')) {
273
+ const idx = rawLine.indexOf(':')
274
+ if (idx > 0) {
275
+ const key = rawLine.slice(0, idx).trim().toLowerCase()
276
+ if (SHOW_KEYS.has(key)) {
277
+ const val = rawLine.slice(idx + 1).trim()
278
+ if (key === 'description') {
279
+ descriptionLines = [val]
280
+ inDescription = true
281
+ continue
282
+ }
283
+ if (!(key in info)) {
284
+ info[key] = val
285
+ }
286
+ inDescription = false
287
+ continue
288
+ }
289
+ }
290
+ if (inDescription && rawLine.trim()) {
291
+ descriptionLines.push(rawLine.trim())
292
+ }
293
+ }
294
+
295
+ if (descriptionLines.length > 0) {
296
+ info.description = descriptionLines.join(' ')
297
+ }
298
+
299
+ return info.package ? info : null
300
+ }
301
+
302
+ async packageInfo(name, opts = {}) {
303
+ const out = this._exec(['cache', 'show', name, ...this._proxyArgs(opts.proxy)], { allowNonZero: true })
304
+ const info = this._parseShow(out)
305
+ if (!info) {
306
+ return { name, description: '(not found)' }
307
+ }
308
+
309
+ return {
310
+ name,
311
+ description: info.description || '',
312
+ latest: info.version || '',
313
+ homepage: info.homepage || '',
314
+ license: '',
315
+ author: info.maintainer || '',
316
+ repository: info.section || ''
317
+ }
318
+ }
319
+
320
+ async packageVersions(name, opts = {}) {
321
+ const { registry } = await this.getRegistry()
322
+ const versions = this._availableVersions(name, opts)
323
+ return { name, versions, registry }
324
+ }
325
+
326
+ async listInstalled(opts = {}) {
327
+ const out = this._exec(['list', '--installed'], { allowNonZero: true })
328
+ const items = []
329
+ for (const line of this._parseLines(out)) {
330
+ const m = line.match(/^(\S+?)\/(?:\S+,)?now\s+(\S+)\s+(\S+)\s+\[/)
331
+ if (m) {
332
+ items.push({ name: m[1], version: m[2] })
333
+ }
334
+ }
335
+ return items
336
+ }
337
+
338
+ async listOutdated(opts = {}) {
339
+ const out = this._exec(['list', '--upgradable', ...this._proxyArgs(opts.proxy)], { allowNonZero: true })
340
+ const items = []
341
+ for (const line of this._parseLines(out)) {
342
+ const m = line.match(/^(\S+?)\/(\S+)\s+(\S+)\s+(\S+)\s+\[(.*?)\]$/)
343
+ if (m) {
344
+ const from = m[5].match(/upgradable from:\s*(\S+)/)
345
+ items.push({
346
+ name: m[1],
347
+ current: from ? from[1] : '',
348
+ latest: m[3]
349
+ })
350
+ }
351
+ }
352
+ return items
353
+ }
354
+
355
+ async install(name, version, opts = {}) {
356
+ const spec = version ? `${name}=${version}` : name
357
+ const args = ['get', 'install', '-y', spec, ...this._proxyArgs(opts.proxy)]
358
+ this._execInherit(args)
359
+ return { name, version: version || 'latest', action: 'installed' }
360
+ }
361
+
362
+ async uninstall(name, opts = {}) {
363
+ this._execInherit(['get', 'remove', '-y', name])
364
+ return { name, action: 'uninstalled' }
365
+ }
366
+
367
+ async upgrade(name, opts = {}) {
368
+ const args = ['get', 'install', '--only-upgrade', '-y', name, ...this._proxyArgs(opts.proxy)]
369
+ this._execInherit(args)
370
+ return { name, version: 'latest', action: 'upgraded' }
371
+ }
372
+
373
+ async cleanCache() {
374
+ this._execInherit(['get', 'clean'])
375
+ return { action: 'cache cleaned' }
376
+ }
377
+ }