@wwkit/opm 1.0.2 → 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 +3 -2
- package/src/cli/groups/docker.js +10 -5
- 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/index.js +6 -0
- package/src/config.json5 +66 -7
- package/src/harnesses/base.js +34 -0
- package/src/harnesses/opencode/index.js +56 -0
- package/src/index.js +4 -0
- package/src/managers/brew.js +6 -6
- package/src/managers/docker.js +10 -7
- package/src/managers/node.js +8 -2
- package/src/managers/proc.js +424 -0
- package/src/managers/python.js +7 -4
- package/src/presets.js +3 -0
- package/src/tools/ftp/index.js +151 -16
- package/src/tools/git/config.js +374 -0
- package/src/tools/git/index.js +378 -0
package/src/tools/ftp/index.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* opm ftp 命令组 — FTP 文件管理
|
|
3
3
|
*
|
|
4
|
-
* 命令:
|
|
5
|
-
*
|
|
4
|
+
* 命令:version / installed / install / uninstall / upgrade /
|
|
5
|
+
* connect / upload / rsync / download / root
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import fs from 'node:fs'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
import { createRequire } from 'node:module'
|
|
12
|
+
import { spawnSync } from 'node:child_process'
|
|
10
13
|
import { parseFlags } from '../../cli/helpers/args.js'
|
|
11
|
-
import {
|
|
14
|
+
import { getActiveProxy, getActiveRegistry } from '../../config.js'
|
|
12
15
|
import { output } from '../../formatter.js'
|
|
13
16
|
import {
|
|
14
17
|
getFtpConfig, checkConnection, uploadBuffer, uploadFile,
|
|
@@ -19,6 +22,31 @@ import {
|
|
|
19
22
|
todayStr, readData, collectFiles,
|
|
20
23
|
} from './util.js'
|
|
21
24
|
|
|
25
|
+
const FTP_DEP = 'basic-ftp'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 检查 basic-ftp 是否已安装,返回版本号或 null
|
|
29
|
+
* @returns {string|null}
|
|
30
|
+
*/
|
|
31
|
+
function checkBasicFtp() {
|
|
32
|
+
try {
|
|
33
|
+
const require = createRequire(import.meta.url)
|
|
34
|
+
const pkg = require(`${FTP_DEP}/package.json`)
|
|
35
|
+
return pkg.version || 'unknown'
|
|
36
|
+
} catch {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 获取 opm 包根目录(package.json 所在目录)
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
function getPkgRoot() {
|
|
46
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
47
|
+
return path.resolve(here, '../../..')
|
|
48
|
+
}
|
|
49
|
+
|
|
22
50
|
export class FtpGroup {
|
|
23
51
|
constructor() {
|
|
24
52
|
this.name = 'ftp'
|
|
@@ -34,18 +62,26 @@ export class FtpGroup {
|
|
|
34
62
|
}
|
|
35
63
|
|
|
36
64
|
switch (action) {
|
|
65
|
+
case 'version':
|
|
66
|
+
return this._version()
|
|
67
|
+
case 'installed':
|
|
68
|
+
return this._installed()
|
|
69
|
+
case 'install':
|
|
70
|
+
return this._install(rest)
|
|
71
|
+
case 'uninstall':
|
|
72
|
+
return this._uninstall()
|
|
73
|
+
case 'upgrade':
|
|
74
|
+
return this._upgrade(rest)
|
|
37
75
|
case 'connect':
|
|
38
|
-
return this._connect()
|
|
76
|
+
return this._guard(() => this._connect())
|
|
39
77
|
case 'upload':
|
|
40
|
-
return this._upload(parseFlags(rest))
|
|
78
|
+
return this._guard(() => this._upload(parseFlags(rest)))
|
|
41
79
|
case 'rsync':
|
|
42
|
-
return this._rsync(parseFlags(rest))
|
|
80
|
+
return this._guard(() => this._rsync(parseFlags(rest)))
|
|
43
81
|
case 'download':
|
|
44
|
-
return this._download(parseFlags(rest))
|
|
82
|
+
return this._guard(() => this._download(parseFlags(rest)))
|
|
45
83
|
case 'root':
|
|
46
84
|
return this._root()
|
|
47
|
-
case 'config':
|
|
48
|
-
return this._config()
|
|
49
85
|
default:
|
|
50
86
|
console.error(`Unknown action: ${action}`)
|
|
51
87
|
this.printHelp()
|
|
@@ -53,6 +89,102 @@ export class FtpGroup {
|
|
|
53
89
|
}
|
|
54
90
|
}
|
|
55
91
|
|
|
92
|
+
/**
|
|
93
|
+
* 检查 basic-ftp 是否可用,不可用则提示安装并退出
|
|
94
|
+
* @param {() => Promise<void>} fn
|
|
95
|
+
* @private
|
|
96
|
+
*/
|
|
97
|
+
async _guard(fn) {
|
|
98
|
+
const ver = checkBasicFtp()
|
|
99
|
+
if (!ver) {
|
|
100
|
+
console.error(`basic-ftp is not installed. Run \`opm ftp install\` to install it.`)
|
|
101
|
+
process.exit(1)
|
|
102
|
+
}
|
|
103
|
+
return fn()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 解析 proxy(-p/--proxy 优先,否则回退配置 active proxy)
|
|
108
|
+
* @param {string[]} argv
|
|
109
|
+
* @returns {string}
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
_resolveProxy(argv) {
|
|
113
|
+
const parsed = parseFlags(argv)
|
|
114
|
+
let proxy = parsed.flags.proxy || parsed.flags.p || ''
|
|
115
|
+
if (proxy === 'true') proxy = ''
|
|
116
|
+
if (!proxy) proxy = getActiveProxy()
|
|
117
|
+
return proxy
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 在 opm 包根目录执行 npm 命令(自动附加 registry + proxy)
|
|
122
|
+
* @param {string[]} args - npm 子命令参数
|
|
123
|
+
* @param {string} proxy
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
_npmExec(args, proxy) {
|
|
127
|
+
const registry = getActiveRegistry('npm')
|
|
128
|
+
const fullArgs = [...args]
|
|
129
|
+
if (registry) fullArgs.push('--registry', registry)
|
|
130
|
+
if (proxy) fullArgs.push('--proxy', proxy)
|
|
131
|
+
|
|
132
|
+
const pkgRoot = getPkgRoot()
|
|
133
|
+
const result = spawnSync('npm', fullArgs, { stdio: 'inherit', shell: true, cwd: pkgRoot })
|
|
134
|
+
if (result.error) {
|
|
135
|
+
throw new Error(result.error.message)
|
|
136
|
+
}
|
|
137
|
+
if (result.status !== 0) {
|
|
138
|
+
throw new Error(`npm ${fullArgs.join(' ')} exited with code ${result.status}`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async _version() {
|
|
143
|
+
const ver = checkBasicFtp()
|
|
144
|
+
output(ver || '')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async _installed() {
|
|
148
|
+
const ver = checkBasicFtp()
|
|
149
|
+
output({
|
|
150
|
+
name: FTP_DEP,
|
|
151
|
+
installed: !!ver,
|
|
152
|
+
...(ver ? { version: ver } : { hint: `Run \`opm ftp install\` to install` }),
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async _install(rest) {
|
|
157
|
+
const ver = checkBasicFtp()
|
|
158
|
+
if (ver) {
|
|
159
|
+
output({ name: FTP_DEP, installed: true, version: ver })
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
const proxy = this._resolveProxy(rest)
|
|
163
|
+
console.log(`Installing ${FTP_DEP} ...`)
|
|
164
|
+
this._npmExec(['install', FTP_DEP], proxy)
|
|
165
|
+
const newVer = checkBasicFtp()
|
|
166
|
+
output({ name: FTP_DEP, installed: true, version: newVer || 'unknown', action: 'installed' })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async _uninstall() {
|
|
170
|
+
const ver = checkBasicFtp()
|
|
171
|
+
if (!ver) {
|
|
172
|
+
output({ name: FTP_DEP, installed: false, action: 'not installed' })
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
console.log(`Uninstalling ${FTP_DEP} ...`)
|
|
176
|
+
this._npmExec(['uninstall', FTP_DEP], '')
|
|
177
|
+
output({ name: FTP_DEP, action: 'uninstalled' })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async _upgrade(rest) {
|
|
181
|
+
const proxy = this._resolveProxy(rest)
|
|
182
|
+
console.log(`Upgrading ${FTP_DEP} ...`)
|
|
183
|
+
this._npmExec(['install', `${FTP_DEP}@latest`], proxy)
|
|
184
|
+
const newVer = checkBasicFtp()
|
|
185
|
+
output({ name: FTP_DEP, installed: true, version: newVer || 'unknown', action: 'upgraded' })
|
|
186
|
+
}
|
|
187
|
+
|
|
56
188
|
async _connect() {
|
|
57
189
|
const ok = await checkConnection()
|
|
58
190
|
output({ connected: ok })
|
|
@@ -227,22 +359,21 @@ export class FtpGroup {
|
|
|
227
359
|
output({ remote_root: cfg.remote_root })
|
|
228
360
|
}
|
|
229
361
|
|
|
230
|
-
_config() {
|
|
231
|
-
const cfg = getFtpConfig()
|
|
232
|
-
output(cfg)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
362
|
printHelp() {
|
|
236
363
|
console.log(`
|
|
237
364
|
Usage: opm ftp <action> [args] [options]
|
|
238
365
|
|
|
239
366
|
Actions:
|
|
367
|
+
version Show installed basic-ftp version
|
|
368
|
+
installed Check if basic-ftp is installed
|
|
369
|
+
install Install basic-ftp (uses opm npm registry + proxy)
|
|
370
|
+
uninstall Uninstall basic-ftp
|
|
371
|
+
upgrade Upgrade basic-ftp to latest (uses opm npm registry + proxy)
|
|
240
372
|
connect Test FTP connection
|
|
241
373
|
upload <file> [-n name] [-s subdir] Upload file (local path or HTTP URL)
|
|
242
374
|
rsync <local> [options] Recursive upload directory (FTP-side dedup)
|
|
243
375
|
download <remote> <local> Download file from FTP
|
|
244
376
|
root Show configured remote root directory
|
|
245
|
-
config Show FTP config
|
|
246
377
|
help Show this help
|
|
247
378
|
|
|
248
379
|
rsync options:
|
|
@@ -252,9 +383,14 @@ rsync options:
|
|
|
252
383
|
--prune Delete remote files not in local
|
|
253
384
|
|
|
254
385
|
Options:
|
|
386
|
+
-p, --proxy <url> Proxy for install/upgrade (overrides config proxy.active)
|
|
255
387
|
-h, --help Show this help
|
|
256
388
|
|
|
257
389
|
Examples:
|
|
390
|
+
opm ftp version
|
|
391
|
+
opm ftp installed
|
|
392
|
+
opm ftp install
|
|
393
|
+
opm ftp upgrade
|
|
258
394
|
opm ftp connect
|
|
259
395
|
opm ftp upload ./img.png
|
|
260
396
|
opm ftp upload ./img.png -n cover -s banner
|
|
@@ -265,7 +401,6 @@ Examples:
|
|
|
265
401
|
opm ftp rsync ./webwork/ --prune
|
|
266
402
|
opm ftp download 20260913/cover.jpg ./cover.jpg
|
|
267
403
|
opm ftp root
|
|
268
|
-
opm ftp config
|
|
269
404
|
`)
|
|
270
405
|
}
|
|
271
406
|
}
|
|
@@ -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
|
+
}
|