@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.
@@ -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
+ }