@wwkit/opm 1.0.1 → 1.0.3
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 +194 -11
- package/bin/index.js +23 -0
- package/docs/managers/git.md +103 -0
- package/docs/managers/proc.md +77 -0
- package/docs/managers/uv.md +9 -3
- package/docs/opm.html +154 -11
- package/docs/packages/opencode.md +46 -15
- package/docs/test/linux-environment-test-report.md +147 -0
- package/package.json +4 -2
- package/src/cli/groups/docker.js +121 -0
- package/src/cli/groups/homebrew.js +9 -5
- package/src/cli/groups/nvm.js +6 -2
- package/src/cli/groups/ping.js +461 -0
- package/src/cli/groups/proc.js +232 -0
- package/src/cli/groups/uv.js +9 -8
- package/src/cli/groups/version-manager.js +34 -8
- package/src/cli/groups/wsl.js +220 -0
- package/src/cli/index.js +12 -0
- package/src/config.json5 +80 -6
- package/src/harnesses/base.js +34 -0
- package/src/harnesses/opencode/index.js +56 -0
- package/src/index.js +19 -0
- package/src/managers/brew.js +6 -6
- package/src/managers/docker.js +298 -0
- package/src/managers/node.js +8 -2
- package/src/managers/proc.js +424 -0
- package/src/managers/python.js +7 -4
- package/src/managers/wsl.js +258 -0
- package/src/presets.js +3 -0
- package/src/tools/ftp/client.js +214 -0
- package/src/tools/ftp/index.js +406 -0
- package/src/tools/ftp/util.js +174 -0
- package/src/tools/git/config.js +374 -0
- package/src/tools/git/index.js +378 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FTP 工具函数 — exclude 匹配、MD5、图片检测、自动命名、HTTP 源下载
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import crypto from 'node:crypto'
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
|
|
9
|
+
const IMAGE_MAGIC = {
|
|
10
|
+
jpg: Buffer.from([0xff, 0xd8, 0xff]),
|
|
11
|
+
png: Buffer.from([0x89, 0x50, 0x4e, 0x47]),
|
|
12
|
+
gif: Buffer.from([0x47, 0x49, 0x46, 0x38]),
|
|
13
|
+
webp: Buffer.from([0x52, 0x49, 0x46, 0x46]),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* glob 通配符匹配(支持 * 和 **)
|
|
18
|
+
* @param {string} relPath - 相对路径(posix 风格)
|
|
19
|
+
* @param {string} pattern - glob 模式
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
export function matchGlob(relPath, pattern) {
|
|
23
|
+
const regex = globToRegex(pattern)
|
|
24
|
+
return regex.test(relPath)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function globToRegex(pattern) {
|
|
28
|
+
let re = ''
|
|
29
|
+
let i = 0
|
|
30
|
+
while (i < pattern.length) {
|
|
31
|
+
if (pattern[i] === '*' && pattern[i + 1] === '*') {
|
|
32
|
+
re += '.*'
|
|
33
|
+
i += 2
|
|
34
|
+
} else if (pattern[i] === '*') {
|
|
35
|
+
re += '[^/]*'
|
|
36
|
+
i++
|
|
37
|
+
} else if (pattern[i] === '?') {
|
|
38
|
+
re += '[^/]'
|
|
39
|
+
i++
|
|
40
|
+
} else if ('.+^$(){}|[]\\'.includes(pattern[i])) {
|
|
41
|
+
re += '\\' + pattern[i]
|
|
42
|
+
i++
|
|
43
|
+
} else {
|
|
44
|
+
re += pattern[i]
|
|
45
|
+
i++
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return new RegExp('^' + re + '$')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 判断路径是否被排除
|
|
53
|
+
* @param {string} relPath - 相对路径(posix)
|
|
54
|
+
* @param {string[]} excludes - 排除模式列表
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
export function isExcluded(relPath, excludes) {
|
|
58
|
+
for (const pattern of excludes) {
|
|
59
|
+
const p = pattern.trim()
|
|
60
|
+
if (!p) continue
|
|
61
|
+
if (matchGlob(relPath, p) || matchGlob(relPath, `${p}/**`)) {
|
|
62
|
+
return true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 计算文件 MD5(流式,避免大文件占内存)
|
|
70
|
+
* @param {string} filePath
|
|
71
|
+
* @returns {Promise<string>}
|
|
72
|
+
*/
|
|
73
|
+
export async function md5File(filePath) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const hash = crypto.createHash('md5')
|
|
76
|
+
const stream = fs.createReadStream(filePath)
|
|
77
|
+
stream.on('data', (chunk) => hash.update(chunk))
|
|
78
|
+
stream.on('end', () => resolve(hash.digest('hex')))
|
|
79
|
+
stream.on('error', reject)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 计算 Buffer MD5
|
|
85
|
+
* @param {Buffer} data
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
export function md5Buffer(data) {
|
|
89
|
+
return crypto.createHash('md5').update(data).digest('hex')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 从 magic bytes 检测图片扩展名
|
|
94
|
+
* @param {Buffer} data
|
|
95
|
+
* @returns {string|null}
|
|
96
|
+
*/
|
|
97
|
+
export function detectImageExt(data) {
|
|
98
|
+
for (const [ext, magic] of Object.entries(IMAGE_MAGIC)) {
|
|
99
|
+
if (data.subarray(0, magic.length).equals(magic)) {
|
|
100
|
+
return ext
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 从文件名提取扩展名(仅限图片类型)
|
|
108
|
+
* @param {string} name
|
|
109
|
+
* @returns {string|null}
|
|
110
|
+
*/
|
|
111
|
+
export function extFromName(name) {
|
|
112
|
+
const clean = name.split('?')[0]
|
|
113
|
+
const ext = path.extname(clean).slice(1).toLowerCase()
|
|
114
|
+
return ext in IMAGE_MAGIC ? ext : null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 生成随机文件名
|
|
119
|
+
* @param {string} ext
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
export function makeFilename(ext) {
|
|
123
|
+
const id = crypto.randomUUID().replace(/-/g, '').slice(0, 8)
|
|
124
|
+
return `${id}.${ext}`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 今天的日期字符串 YYYYMMDD
|
|
129
|
+
* @returns {string}
|
|
130
|
+
*/
|
|
131
|
+
export function todayStr() {
|
|
132
|
+
const d = new Date()
|
|
133
|
+
const y = d.getFullYear()
|
|
134
|
+
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
135
|
+
const day = String(d.getDate()).padStart(2, '0')
|
|
136
|
+
return `${y}${m}${day}`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 读取本地文件或下载 HTTP URL 为 Buffer
|
|
141
|
+
* @param {string} urlOrPath
|
|
142
|
+
* @param {number} [timeout=120000]
|
|
143
|
+
* @returns {Promise<Buffer>}
|
|
144
|
+
*/
|
|
145
|
+
export async function readData(urlOrPath, timeout = 120000) {
|
|
146
|
+
if (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://')) {
|
|
147
|
+
const resp = await fetch(urlOrPath, { signal: AbortSignal.timeout(timeout) })
|
|
148
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${urlOrPath}`)
|
|
149
|
+
return Buffer.from(await resp.arrayBuffer())
|
|
150
|
+
}
|
|
151
|
+
if (!fs.existsSync(urlOrPath)) throw new Error(`File not found: ${urlOrPath}`)
|
|
152
|
+
return fs.readFileSync(urlOrPath)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 递归收集目录下所有文件(posix 相对路径)
|
|
157
|
+
* @param {string} dir
|
|
158
|
+
* @returns {string[]}
|
|
159
|
+
*/
|
|
160
|
+
export function collectFiles(dir) {
|
|
161
|
+
const result = []
|
|
162
|
+
function walk(base) {
|
|
163
|
+
for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
|
|
164
|
+
const full = path.join(base, entry.name)
|
|
165
|
+
if (entry.isDirectory()) {
|
|
166
|
+
walk(full)
|
|
167
|
+
} else if (entry.isFile()) {
|
|
168
|
+
result.push(path.relative(dir, full).split(path.sep).join('/'))
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
walk(dir)
|
|
173
|
+
return result.sort()
|
|
174
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git 配置管理 — config/user/init/export/mark/version/install/uninstall/upgrade 实现
|
|
3
|
+
*
|
|
4
|
+
* 通过 git config CLI 读写配置,支持 --global/--local/--system 作用域。
|
|
5
|
+
* init 根据 opm config 的 git 段一键初始化全局配置。
|
|
6
|
+
* export 保存两个 commit 之间的差异到文件(默认从 mark 到 HEAD)。
|
|
7
|
+
* mark 用 lightweight git tag(opm-diff-base)标记基准 commit。
|
|
8
|
+
* version/install/uninstall/upgrade 管理 git 自身的系统安装。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync, spawnSync } from 'node:child_process'
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { getConfig } from '../../config.js'
|
|
15
|
+
|
|
16
|
+
const MARK_TAG = 'opm-diff-base'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 作用域映射到 git config 参数
|
|
20
|
+
* @param {string} scope - global|local|system
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function scopeArg(scope) {
|
|
24
|
+
if (scope === 'local') return '--local'
|
|
25
|
+
if (scope === 'system') return '--system'
|
|
26
|
+
return '--global'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 执行 git config 命令(静默 stderr)
|
|
31
|
+
* @param {string[]} args
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
function gitConfig(args) {
|
|
35
|
+
try {
|
|
36
|
+
return execSync(`git config ${args.join(' ')}`, { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
37
|
+
} catch {
|
|
38
|
+
return ''
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 获取配置文件路径
|
|
44
|
+
* @param {string} scope
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function getConfigFile(scope) {
|
|
48
|
+
const raw = gitConfig([scopeArg(scope), '--list', '--show-origin'])
|
|
49
|
+
const firstLine = raw.split('\n')[0] || ''
|
|
50
|
+
const m = firstLine.match(/^file:([^\t]+)/)
|
|
51
|
+
return m ? m[1] : ''
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 列出全部配置
|
|
56
|
+
* @param {string} scope
|
|
57
|
+
* @returns {{file: string, config: Record<string, string>}}
|
|
58
|
+
*/
|
|
59
|
+
export function listConfig(scope) {
|
|
60
|
+
const arg = scopeArg(scope)
|
|
61
|
+
const raw = gitConfig([arg, '--list'])
|
|
62
|
+
const config = {}
|
|
63
|
+
for (const line of raw.split('\n')) {
|
|
64
|
+
if (!line) continue
|
|
65
|
+
const idx = line.indexOf('=')
|
|
66
|
+
if (idx > 0) {
|
|
67
|
+
config[line.slice(0, idx)] = line.slice(idx + 1)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { file: getConfigFile(scope), config }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 获取单个配置值
|
|
75
|
+
* @param {string} key
|
|
76
|
+
* @param {string} scope
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
export function getConfigValue(key, scope) {
|
|
80
|
+
return gitConfig([scopeArg(scope), '--get', key])
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 设置配置值
|
|
85
|
+
* @param {string} key
|
|
86
|
+
* @param {string} value
|
|
87
|
+
* @param {string} scope
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
export function setConfigValue(key, value, scope) {
|
|
91
|
+
try {
|
|
92
|
+
execSync(`git config ${scopeArg(scope)} ${key} "${value}"`, { timeout: 5000 })
|
|
93
|
+
return true
|
|
94
|
+
} catch {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 取消配置项
|
|
101
|
+
* @param {string} key
|
|
102
|
+
* @param {string} scope
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
105
|
+
export function unsetConfigValue(key, scope) {
|
|
106
|
+
try {
|
|
107
|
+
execSync(`git config ${scopeArg(scope)} --unset ${key}`, { timeout: 5000 })
|
|
108
|
+
return true
|
|
109
|
+
} catch {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 获取 user 信息
|
|
116
|
+
* @param {string} scope
|
|
117
|
+
* @returns {{name: string, email: string}}
|
|
118
|
+
*/
|
|
119
|
+
export function getUser(scope) {
|
|
120
|
+
return {
|
|
121
|
+
name: getConfigValue('user.name', scope),
|
|
122
|
+
email: getConfigValue('user.email', scope),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 设置 user 信息
|
|
128
|
+
* @param {string} name
|
|
129
|
+
* @param {string} email
|
|
130
|
+
* @param {string} scope
|
|
131
|
+
* @returns {{name: boolean, email: boolean}}
|
|
132
|
+
*/
|
|
133
|
+
export function setUser(name, email, scope) {
|
|
134
|
+
const result = {}
|
|
135
|
+
if (name) result.name = setConfigValue('user.name', name, scope)
|
|
136
|
+
if (email) result.email = setConfigValue('user.email', email, scope)
|
|
137
|
+
return result
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 一键初始化 git 全局配置
|
|
142
|
+
* 根据 opm config 的 git 段设置 user/ssl/pull.rebase
|
|
143
|
+
* @param {boolean} force - 强制覆盖已有配置
|
|
144
|
+
* @returns {Promise<object>}
|
|
145
|
+
*/
|
|
146
|
+
export async function initGitConfig(force) {
|
|
147
|
+
const cfg = getConfig().git || {}
|
|
148
|
+
const scope = 'global'
|
|
149
|
+
const items = []
|
|
150
|
+
let set = 0
|
|
151
|
+
let skip = 0
|
|
152
|
+
let overwrite = 0
|
|
153
|
+
|
|
154
|
+
const tasks = [
|
|
155
|
+
{ key: 'user.name', value: cfg.user?.name || '' },
|
|
156
|
+
{ key: 'user.email', value: cfg.user?.email || '' },
|
|
157
|
+
{ key: 'http.sslVerify', value: cfg.sslVerify === false ? 'false' : 'true' },
|
|
158
|
+
{ key: 'pull.rebase', value: cfg.pullRebase === true ? 'true' : 'false' },
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
for (const task of tasks) {
|
|
162
|
+
if (!task.value && task.value !== 'false') continue
|
|
163
|
+
const existing = getConfigValue(task.key, scope)
|
|
164
|
+
if (existing && !force) {
|
|
165
|
+
items.push({ key: task.key, value: task.value, action: 'skip', existing })
|
|
166
|
+
skip++
|
|
167
|
+
} else if (existing && force) {
|
|
168
|
+
setConfigValue(task.key, task.value, scope)
|
|
169
|
+
items.push({ key: task.key, value: task.value, action: 'overwrite', existing })
|
|
170
|
+
overwrite++
|
|
171
|
+
} else {
|
|
172
|
+
setConfigValue(task.key, task.value, scope)
|
|
173
|
+
items.push({ key: task.key, value: task.value, action: 'set' })
|
|
174
|
+
set++
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { scope, items, summary: { set, skip, overwrite } }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 获取 mark 标记的 commit 信息
|
|
183
|
+
* @param {string} workDir
|
|
184
|
+
* @returns {{commit: string, message: string}|null}
|
|
185
|
+
*/
|
|
186
|
+
export function getMark(workDir) {
|
|
187
|
+
const opts = { encoding: 'utf8', timeout: 5000, cwd: workDir, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
188
|
+
try {
|
|
189
|
+
const hash = execSync(`git rev-list -n 1 ${MARK_TAG}`, opts).trim()
|
|
190
|
+
const message = execSync(`git log -1 --format=%s ${hash}`, opts).trim()
|
|
191
|
+
return { commit: hash, message }
|
|
192
|
+
} catch {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 设置 mark 标记(在指定 commit 上打 tag,已存在则先删后打)
|
|
199
|
+
* @param {string} workDir
|
|
200
|
+
* @param {string} [ref='HEAD'] - commit ref
|
|
201
|
+
* @returns {{commit: string, message: string}}
|
|
202
|
+
*/
|
|
203
|
+
export function setMark(workDir, ref = 'HEAD') {
|
|
204
|
+
const opts = { encoding: 'utf8', timeout: 5000, cwd: workDir, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
205
|
+
try {
|
|
206
|
+
execSync(`git tag -d ${MARK_TAG}`, opts)
|
|
207
|
+
} catch {}
|
|
208
|
+
execSync(`git tag ${MARK_TAG} ${ref}`, opts)
|
|
209
|
+
const hash = execSync(`git rev-list -n 1 ${MARK_TAG}`, opts).trim()
|
|
210
|
+
const message = execSync(`git log -1 --format=%s ${hash}`, opts).trim()
|
|
211
|
+
return { commit: hash, message }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 删除 mark 标记
|
|
216
|
+
* @param {string} workDir
|
|
217
|
+
* @returns {boolean}
|
|
218
|
+
*/
|
|
219
|
+
export function unsetMark(workDir) {
|
|
220
|
+
const opts = { encoding: 'utf8', timeout: 5000, cwd: workDir, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
221
|
+
try {
|
|
222
|
+
execSync(`git tag -d ${MARK_TAG}`, opts)
|
|
223
|
+
return true
|
|
224
|
+
} catch {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 导入 diff 文件到工作目录
|
|
231
|
+
* @param {string} workDir - git 仓库工作目录
|
|
232
|
+
* @param {string} diffFile - diff 文件路径
|
|
233
|
+
* @returns {{workDir: string, diffFile: string, applied: boolean, rejected: string[]}}
|
|
234
|
+
*/
|
|
235
|
+
export function importDiff(workDir, diffFile) {
|
|
236
|
+
const opts = { encoding: 'utf8', timeout: 30000, cwd: workDir, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
237
|
+
const resolved = path.resolve(diffFile)
|
|
238
|
+
if (!fs.existsSync(resolved)) {
|
|
239
|
+
return { workDir, diffFile: resolved, applied: false, error: 'File not found' }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
execSync(`git apply --reject ${resolved}`, opts)
|
|
244
|
+
return { workDir, diffFile: resolved, applied: true, rejected: [] }
|
|
245
|
+
} catch (err) {
|
|
246
|
+
const stderr = err.stderr ? err.stderr.toString() : ''
|
|
247
|
+
const rejFiles = stderr.match(/\.rej/g) ? fs.readdirSync(workDir).filter(f => f.endsWith('.rej')) : []
|
|
248
|
+
return { workDir, diffFile: resolved, applied: false, rejected: rejFiles, error: stderr.trim() || err.message }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 保存两个 commit 之间的差异到文件
|
|
254
|
+
* 执行前若有未提交变更,自动 commit(含未跟踪文件),确保所有变动都在 HEAD 中。
|
|
255
|
+
* @param {string} workDir - git 仓库工作目录
|
|
256
|
+
* @param {string} outputFile - 输出文件路径
|
|
257
|
+
* @param {string} fromRef - 起始 commit ref(mark tag 或 HEAD~n)
|
|
258
|
+
* @returns {{workDir: string, file: string, lines: number, from: string, to: string, autoCommitted: boolean}}
|
|
259
|
+
*/
|
|
260
|
+
export function saveDiff(workDir, outputFile, fromRef) {
|
|
261
|
+
const opts = { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024, timeout: 30000, cwd: workDir, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
262
|
+
|
|
263
|
+
let autoCommitted = false
|
|
264
|
+
const status = execSync('git status --porcelain', { ...opts, timeout: 5000 }).trim()
|
|
265
|
+
if (status) {
|
|
266
|
+
execSync('git add -A', { ...opts, timeout: 5000 })
|
|
267
|
+
execSync('git commit -m "wip: auto-commit before export"', { ...opts, timeout: 10000 })
|
|
268
|
+
autoCommitted = true
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const fromMessage = execSync(`git log -1 --format=%s ${fromRef}`, { ...opts, timeout: 5000 }).trim()
|
|
272
|
+
const diff = execSync(`git diff ${fromRef} HEAD`, opts)
|
|
273
|
+
fs.writeFileSync(outputFile, diff, 'utf8')
|
|
274
|
+
const lines = diff ? diff.split('\n').length : 0
|
|
275
|
+
return { workDir, file: outputFile, lines, from: fromRef, fromMessage, to: 'HEAD', autoCommitted }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 检测系统包管理器
|
|
280
|
+
* @returns {{cmd: string, installArgs: string[], uninstallArgs: string[], upgradeArgs: string[]}|null}
|
|
281
|
+
*/
|
|
282
|
+
function detectPm() {
|
|
283
|
+
const candidates = [
|
|
284
|
+
{ cmd: 'dnf', install: ['install', '-y'], uninstall: ['remove', '-y'], upgrade: ['upgrade', '-y'] },
|
|
285
|
+
{ cmd: 'yum', install: ['install', '-y'], uninstall: ['remove', '-y'], upgrade: ['upgrade', '-y'] },
|
|
286
|
+
{ cmd: 'apt-get', install: ['install', '-y'], uninstall: ['remove', '-y'], upgrade: ['install', '--only-upgrade', '-y'] },
|
|
287
|
+
{ cmd: 'apk', install: ['add'], uninstall: ['del'], upgrade: ['upgrade'] },
|
|
288
|
+
]
|
|
289
|
+
for (const c of candidates) {
|
|
290
|
+
try {
|
|
291
|
+
execSync(`command -v ${c.cmd}`, { encoding: 'utf8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
292
|
+
return {
|
|
293
|
+
cmd: c.cmd,
|
|
294
|
+
installArgs: c.install,
|
|
295
|
+
uninstallArgs: c.uninstall,
|
|
296
|
+
upgradeArgs: c.upgrade,
|
|
297
|
+
}
|
|
298
|
+
} catch {}
|
|
299
|
+
}
|
|
300
|
+
return null
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 查询 git 版本
|
|
305
|
+
* @returns {{installed: boolean, version?: string, path?: string, hint?: string}}
|
|
306
|
+
*/
|
|
307
|
+
export function getGitVersion() {
|
|
308
|
+
try {
|
|
309
|
+
const out = execSync('git --version', { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
310
|
+
const match = out.match(/(\d+(?:\.\d+)+)/)
|
|
311
|
+
let path = ''
|
|
312
|
+
try {
|
|
313
|
+
path = execSync('command -v git', { encoding: 'utf8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
314
|
+
} catch {}
|
|
315
|
+
return { installed: true, version: match ? match[1] : out, path }
|
|
316
|
+
} catch {
|
|
317
|
+
return { installed: false, hint: 'Run `opm git install` to install git' }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 安装 git(通过系统包管理器)
|
|
323
|
+
* @param {string} [proxy]
|
|
324
|
+
* @returns {{action: string, pm?: string, error?: string}}
|
|
325
|
+
*/
|
|
326
|
+
export function installGit(proxy) {
|
|
327
|
+
const pm = detectPm()
|
|
328
|
+
if (!pm) {
|
|
329
|
+
return { action: 'install', error: 'No supported package manager found (dnf/yum/apt-get/apk)' }
|
|
330
|
+
}
|
|
331
|
+
const args = [...pm.installArgs, 'git']
|
|
332
|
+
if (proxy && pm.cmd !== 'apk') args.push(`--proxy=${proxy}`)
|
|
333
|
+
const result = spawnSync(pm.cmd, args, { stdio: 'inherit', shell: true })
|
|
334
|
+
if (result.error || result.status !== 0) {
|
|
335
|
+
return { action: 'install', pm: pm.cmd, error: result.error ? result.error.message : `exited with code ${result.status}` }
|
|
336
|
+
}
|
|
337
|
+
return { action: 'installed', pm: pm.cmd }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 卸载 git(通过系统包管理器)
|
|
342
|
+
* @returns {{action: string, pm?: string, error?: string}}
|
|
343
|
+
*/
|
|
344
|
+
export function uninstallGit() {
|
|
345
|
+
const pm = detectPm()
|
|
346
|
+
if (!pm) {
|
|
347
|
+
return { action: 'uninstall', error: 'No supported package manager found (dnf/yum/apt-get/apk)' }
|
|
348
|
+
}
|
|
349
|
+
const args = [...pm.uninstallArgs, 'git']
|
|
350
|
+
const result = spawnSync(pm.cmd, args, { stdio: 'inherit', shell: true })
|
|
351
|
+
if (result.error || result.status !== 0) {
|
|
352
|
+
return { action: 'uninstall', pm: pm.cmd, error: result.error ? result.error.message : `exited with code ${result.status}` }
|
|
353
|
+
}
|
|
354
|
+
return { action: 'uninstalled', pm: pm.cmd }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 升级 git(通过系统包管理器)
|
|
359
|
+
* @param {string} [proxy]
|
|
360
|
+
* @returns {{action: string, pm?: string, error?: string}}
|
|
361
|
+
*/
|
|
362
|
+
export function upgradeGit(proxy) {
|
|
363
|
+
const pm = detectPm()
|
|
364
|
+
if (!pm) {
|
|
365
|
+
return { action: 'upgrade', error: 'No supported package manager found (dnf/yum/apt-get/apk)' }
|
|
366
|
+
}
|
|
367
|
+
const args = [...pm.upgradeArgs, 'git']
|
|
368
|
+
if (proxy && pm.cmd !== 'apk') args.push(`--proxy=${proxy}`)
|
|
369
|
+
const result = spawnSync(pm.cmd, args, { stdio: 'inherit', shell: true })
|
|
370
|
+
if (result.error || result.status !== 0) {
|
|
371
|
+
return { action: 'upgrade', pm: pm.cmd, error: result.error ? result.error.message : `exited with code ${result.status}` }
|
|
372
|
+
}
|
|
373
|
+
return { action: 'upgraded', pm: pm.cmd }
|
|
374
|
+
}
|