@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.
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * opm ftp 命令组 — FTP 文件管理
3
3
  *
4
- * 命令:connect / upload / rsync / download / root / config
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 { getConfig } from '../../config.js'
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
+ }