@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.
- package/README.md +381 -0
- package/bin/index.js +9 -0
- package/docs/managers/apt.md +45 -0
- package/docs/managers/brew.md +92 -0
- package/docs/managers/bun.md +50 -0
- package/docs/managers/cft.md +57 -0
- package/docs/managers/composer.md +96 -0
- package/docs/managers/config.md +32 -0
- package/docs/managers/dnf.md +46 -0
- package/docs/managers/node.md +49 -0
- package/docs/managers/npm.md +100 -0
- package/docs/managers/nvm.md +34 -0
- package/docs/managers/php.md +48 -0
- package/docs/managers/phpenv.md +34 -0
- package/docs/managers/pip.md +98 -0
- package/docs/managers/python.md +53 -0
- package/docs/managers/uv.md +27 -0
- package/docs/opm.html +570 -0
- package/docs/packages/opencode.md +41 -0
- package/package.json +51 -0
- package/scripts/postinstall.js +7 -0
- package/src/binaries/base.js +508 -0
- package/src/binaries/cft/index.js +35 -0
- package/src/binaries/depchecker.js +172 -0
- package/src/binaries/exefinder.js +110 -0
- package/src/binaries/platform.js +17 -0
- package/src/cli/doc.js +335 -0
- package/src/cli/groups/config.js +78 -0
- package/src/cli/groups/ensure.js +96 -0
- package/src/cli/groups/homebrew.js +73 -0
- package/src/cli/groups/nvm.js +73 -0
- package/src/cli/groups/package.js +514 -0
- package/src/cli/groups/phpenv.js +89 -0
- package/src/cli/groups/uv.js +73 -0
- package/src/cli/groups/version-manager.js +186 -0
- package/src/cli/helpers/args.js +49 -0
- package/src/cli/index.js +129 -0
- package/src/config.js +133 -0
- package/src/config.json5 +158 -0
- package/src/formatter.js +13 -0
- package/src/harnesses/base.js +205 -0
- package/src/harnesses/opencode/clear.js +239 -0
- package/src/harnesses/opencode/db.js +39 -0
- package/src/harnesses/opencode/index.js +46 -0
- package/src/harnesses/opencode/sqlite.js +200 -0
- package/src/index.js +46 -0
- package/src/managers/apt.js +377 -0
- package/src/managers/base.js +167 -0
- package/src/managers/brew.js +374 -0
- package/src/managers/bun.js +342 -0
- package/src/managers/composer.js +280 -0
- package/src/managers/dnf.js +370 -0
- package/src/managers/http.js +432 -0
- package/src/managers/node.js +150 -0
- package/src/managers/npm.js +473 -0
- package/src/managers/php.js +157 -0
- package/src/managers/pip.js +555 -0
- package/src/managers/python.js +180 -0
- package/src/managers/runtime.js +309 -0
- package/src/managers/winget.js +268 -0
- package/src/presets.js +24 -0
- 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
|
+
}
|