@wwkit/opm 1.0.2 → 1.0.4

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,378 @@
1
+ /**
2
+ * opm git 命令组 — Git 配置管理
3
+ *
4
+ * 命令:init / config / user / log / export / import / help
5
+ * 聚焦冷门或多步骤操作,log 支持指定 workdir 免切换目录查看提交历史。
6
+ * export 保存两个 commit 之间的差异到文件,默认从 mark 到 HEAD。
7
+ * mark 用 lightweight git tag 标记基准 commit,对用户透明。
8
+ */
9
+
10
+ import path from 'node:path'
11
+ import fs from 'node:fs'
12
+ import { execSync } from 'node:child_process'
13
+ import { parseFlags } from '../../cli/helpers/args.js'
14
+ import { output } from '../../formatter.js'
15
+ import {
16
+ listConfig, getConfigValue, setConfigValue, unsetConfigValue,
17
+ getUser, setUser,
18
+ initGitConfig, saveDiff, importDiff,
19
+ getMark, setMark, unsetMark,
20
+ getGitVersion, installGit, uninstallGit, upgradeGit,
21
+ } from './config.js'
22
+
23
+ const ACTIONS = {
24
+ version: { desc: 'Show installed git version' },
25
+ install: { desc: 'Install git via system package manager (dnf/yum/apt/apk)' },
26
+ uninstall: { desc: 'Uninstall git via system package manager' },
27
+ upgrade: { desc: 'Upgrade git via system package manager' },
28
+ init: { desc: 'Initialize git global config from opm config (user/ssl/pull.rebase)' },
29
+ config: { desc: 'View/set/unset git config (with --scope global|local|system)' },
30
+ user: { desc: 'View/set user.name and user.email' },
31
+ log: { desc: 'Show commit history (with -d/--dir for workdir, -n for count)' },
32
+ export: { desc: 'Export diff to file (default: mark -> HEAD, or -f <n> override)' },
33
+ import: { desc: 'Import diff file into working tree (git apply --reject)' },
34
+ help: { desc: 'Show this help' },
35
+ }
36
+
37
+ export class GitGroup {
38
+ constructor() {
39
+ this.name = 'git'
40
+ this.desc = 'Git config management (init/config/user/log/export)'
41
+ }
42
+
43
+ async run(argv) {
44
+ const [action, ...rest] = argv
45
+
46
+ if (!action || action === '-h' || action === '--help' || action === 'help') {
47
+ this.printHelp()
48
+ return
49
+ }
50
+
51
+ const parsed = parseFlags(rest)
52
+
53
+ switch (action) {
54
+ case 'version':
55
+ return this._version()
56
+ case 'install':
57
+ return this._install(parsed)
58
+ case 'uninstall':
59
+ return this._uninstall()
60
+ case 'upgrade':
61
+ return this._upgrade(parsed)
62
+ case 'init':
63
+ return this._init(parsed)
64
+ case 'config':
65
+ return this._config(parsed)
66
+ case 'user':
67
+ return this._user(parsed)
68
+ case 'log':
69
+ return this._log(parsed)
70
+ case 'export':
71
+ return this._export(parsed)
72
+ case 'import':
73
+ return this._import(parsed)
74
+ default:
75
+ console.error(`Unknown action: ${action}`)
76
+ this.printHelp()
77
+ process.exit(1)
78
+ }
79
+ }
80
+
81
+ _resolveProxy(parsed) {
82
+ let proxy = parsed.flags.p || parsed.flags.proxy || ''
83
+ if (proxy === 'true') proxy = ''
84
+ return proxy
85
+ }
86
+
87
+ _version() {
88
+ const result = getGitVersion()
89
+ output(result)
90
+ }
91
+
92
+ _install(parsed) {
93
+ const proxy = this._resolveProxy(parsed)
94
+ const result = installGit(proxy)
95
+ output(result)
96
+ }
97
+
98
+ _uninstall() {
99
+ const result = uninstallGit()
100
+ output(result)
101
+ }
102
+
103
+ _upgrade(parsed) {
104
+ const proxy = this._resolveProxy(parsed)
105
+ const result = upgradeGit(proxy)
106
+ output(result)
107
+ }
108
+
109
+ _getScope(parsed) {
110
+ return parsed.flags.scope || 'global'
111
+ }
112
+
113
+ async _init(parsed) {
114
+ const force = parsed.flags.f === 'true' || parsed.flags.force === 'true'
115
+ const result = await initGitConfig(force)
116
+ output(result)
117
+ }
118
+
119
+ async _config(parsed) {
120
+ const scope = this._getScope(parsed)
121
+ const unsetKey = parsed.flags.unset || ''
122
+
123
+ if (unsetKey) {
124
+ const ok = unsetConfigValue(unsetKey, scope)
125
+ output({ scope, key: unsetKey, unset: ok })
126
+ return
127
+ }
128
+
129
+ const [key, value] = parsed.positional
130
+
131
+ if (key && value) {
132
+ const ok = setConfigValue(key, value, scope)
133
+ output({ scope, key, value, set: ok })
134
+ return
135
+ }
136
+
137
+ if (key) {
138
+ const val = getConfigValue(key, scope)
139
+ output({ scope, key, value: val })
140
+ return
141
+ }
142
+
143
+ const result = listConfig(scope)
144
+ output({ scope, ...result })
145
+ }
146
+
147
+ async _user(parsed) {
148
+ const scope = this._getScope(parsed)
149
+ const name = parsed.flags.n || parsed.flags.name || ''
150
+ const email = parsed.flags.e || parsed.flags.email || ''
151
+
152
+ if (name || email) {
153
+ const result = setUser(name, email, scope)
154
+ output({ scope, ...result })
155
+ return
156
+ }
157
+
158
+ const result = getUser(scope)
159
+ output({ scope, ...result })
160
+ }
161
+
162
+ _log(parsed) {
163
+ const workDir = parsed.flags.d || parsed.flags.dir || process.cwd()
164
+ const resolvedDir = path.resolve(workDir)
165
+ if (!this._getGitDir(resolvedDir)) {
166
+ console.error(`Not a git repository: ${resolvedDir}`)
167
+ process.exit(1)
168
+ }
169
+ const n = parseInt(parsed.flags.n || parsed.flags.count || '10', 10) || 10
170
+ const opts = { encoding: 'utf8', timeout: 5000, cwd: resolvedDir, stdio: ['pipe', 'pipe', 'pipe'] }
171
+ const raw = execSync(`git log --oneline -${n}`, opts).trim()
172
+ if (!raw) {
173
+ output({ commits: [], message: 'No commits yet' })
174
+ return
175
+ }
176
+ const commits = raw.split('\n').map((line) => {
177
+ const idx = line.indexOf(' ')
178
+ return { hash: line.slice(0, idx), subject: line.slice(idx + 1) }
179
+ })
180
+ output({ dir: resolvedDir, count: commits.length, commits })
181
+ }
182
+
183
+ _printRecentLog(workDir, count = 5) {
184
+ const resolvedDir = path.resolve(workDir)
185
+ if (!this._getGitDir(resolvedDir)) return
186
+ const opts = { encoding: 'utf8', timeout: 5000, cwd: resolvedDir, stdio: ['pipe', 'pipe', 'pipe'] }
187
+ try {
188
+ const raw = execSync(`git log --oneline -${count}`, opts).trim()
189
+ if (raw) {
190
+ console.error(`\nRecent ${count} commits:`)
191
+ for (const line of raw.split('\n')) {
192
+ console.error(` ${line}`)
193
+ }
194
+ }
195
+ } catch {}
196
+ }
197
+
198
+ async _export(parsed) {
199
+ const [sub] = parsed.positional
200
+
201
+ if (sub === 'mark') {
202
+ return this._exportMark(parsed)
203
+ }
204
+
205
+ return this._exportDiff(parsed)
206
+ }
207
+
208
+ _exportMark(parsed) {
209
+ const workDir = parsed.flags.d || parsed.flags.dir || process.cwd()
210
+ const ref = parsed.positional[1]
211
+
212
+ if (parsed.flags.unset === 'true') {
213
+ const ok = unsetMark(workDir)
214
+ output({ mark: 'removed', ok })
215
+ return
216
+ }
217
+
218
+ if (!ref) {
219
+ const mark = getMark(workDir)
220
+ if (!mark) {
221
+ output({ mark: 'not-set' })
222
+ console.error('\nTips: mark not set, set a diff base first:')
223
+ console.error(' opm git export mark HEAD # mark current HEAD as diff base')
224
+ console.error(' opm git export mark <commit> # mark a commit as diff base')
225
+ console.error(' opm git log -d <dir> -n 10 # view commit history without cd')
226
+ this._printRecentLog(workDir)
227
+ return
228
+ }
229
+ output({ mark: 'set', ...mark })
230
+ console.error('\nTips:')
231
+ console.error(' opm git log -d <dir> -n 10 # view commit history without cd')
232
+ console.error(' opm git export mark <commit> # change diff base to another commit')
233
+ console.error(' opm git export mark --unset # remove current mark')
234
+ console.error(' opm git export # export diff (mark -> HEAD)')
235
+ this._printRecentLog(workDir)
236
+ return
237
+ }
238
+
239
+ const resolvedRef = ref === '0' || ref.toLowerCase() === 'head' ? 'HEAD' : ref
240
+ const result = setMark(workDir, resolvedRef)
241
+ output({ mark: 'set', ...result })
242
+ }
243
+
244
+ _exportDiff(parsed) {
245
+ const workDir = parsed.flags.d || parsed.flags.dir || process.cwd()
246
+ const resolvedDir = path.resolve(workDir)
247
+ const gitDir = this._getGitDir(resolvedDir)
248
+ if (!gitDir) {
249
+ console.error(`Not a git repository: ${resolvedDir}`)
250
+ process.exit(1)
251
+ }
252
+
253
+ const fromRaw = parsed.flags.f || parsed.flags.from || ''
254
+ let fromRef
255
+ if (fromRaw) {
256
+ const n = parseInt(fromRaw, 10)
257
+ fromRef = isNaN(n) ? fromRaw : `HEAD~${n}`
258
+ } else {
259
+ const mark = getMark(resolvedDir)
260
+ if (!mark) {
261
+ console.error('No mark found. Set a mark first:')
262
+ console.error(' opm git export mark HEAD # mark current HEAD as diff base')
263
+ console.error(' opm git export mark <commit> # mark a commit as diff base')
264
+ console.error(' opm git log -d <dir> -n 10 # view commit history without cd')
265
+ process.exit(1)
266
+ }
267
+ fromRef = mark.commit
268
+ }
269
+
270
+ let outFile = parsed.flags.o || parsed.flags.output || ''
271
+ if (!outFile) {
272
+ const opts = { encoding: 'utf8', timeout: 5000, cwd: resolvedDir, stdio: ['pipe', 'pipe', 'pipe'] }
273
+ const gitRoot = execSync('git rev-parse --show-toplevel', opts).trim()
274
+ const name = parsed.flags.n || parsed.flags.name || ''
275
+ const fileName = name ? `${name}.diff` : (() => {
276
+ const fromShort = execSync(`git rev-parse --short ${fromRef}`, opts).trim()
277
+ const toShort = execSync('git rev-parse --short HEAD', opts).trim()
278
+ return `changes-${fromShort}-to-${toShort}.diff`
279
+ })()
280
+ outFile = path.join(gitRoot, '.webwork', 'git', fileName)
281
+ }
282
+
283
+ const filePath = path.resolve(outFile)
284
+ const dir = path.dirname(filePath)
285
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
286
+
287
+ const result = saveDiff(resolvedDir, filePath, fromRef)
288
+ output(result)
289
+ }
290
+
291
+ async _import(parsed) {
292
+ const diffFile = parsed.positional[0]
293
+ if (!diffFile) {
294
+ console.error('Usage: opm git import <difffile> [-d <workdir>]')
295
+ process.exit(1)
296
+ }
297
+
298
+ const workDir = parsed.flags.d || parsed.flags.dir || process.cwd()
299
+ const resolvedDir = path.resolve(workDir)
300
+ const gitDir = this._getGitDir(resolvedDir)
301
+ if (!gitDir) {
302
+ console.error(`Not a git repository: ${resolvedDir}`)
303
+ process.exit(1)
304
+ }
305
+
306
+ const result = importDiff(resolvedDir, diffFile)
307
+ output(result)
308
+ }
309
+
310
+ _getGitDir(dir) {
311
+ try {
312
+ execSync('git rev-parse --git-dir', { encoding: 'utf8', timeout: 5000, cwd: dir, stdio: ['pipe', 'pipe', 'pipe'] })
313
+ return true
314
+ } catch {
315
+ return false
316
+ }
317
+ }
318
+
319
+ printHelp() {
320
+ const actionLines = Object.entries(ACTIONS)
321
+ .map(([sig, { desc }]) => ` ${sig.padEnd(12)} ${desc}`)
322
+ .join('\n')
323
+
324
+ console.log(`
325
+ Usage: opm git <action> [options]
326
+
327
+ Actions:
328
+ ${actionLines}
329
+
330
+ Export subcommands:
331
+ export mark [-d dir] Show current diff base mark
332
+ export mark 0|HEAD [-d dir] Set diff base mark to current HEAD
333
+ export mark <commit> [-d dir] Set diff base mark to a commit
334
+ export mark --unset [-d dir] Remove the diff base mark
335
+ export [-d dir] Export diff (mark -> HEAD)
336
+ export -f <n> [-d dir] Export diff (HEAD~n -> HEAD, override mark)
337
+ export -f <ref> [-d dir] Export diff (<ref> -> HEAD, override mark)
338
+
339
+ Options:
340
+ -d, --dir <dir> Git repository directory (default: cwd)
341
+ -p, --proxy <url> Proxy for install/upgrade
342
+ --scope <global|local|system> Config scope (default: global)
343
+ -n, --name <name> User name (user) / diff file name (export)
344
+ -n, --count <n> Number of commits to show (log, default: 10)
345
+ -e, --email <email> User email (user)
346
+ -o, --output <file> Output file path (export)
347
+ -f, --from <n>|<ref> Override from ref (export)
348
+ -f, --force Force overwrite existing config (init)
349
+ --unset <key> Unset a config key (config)
350
+ -h, --help Show this help
351
+
352
+ Examples:
353
+ opm git version Show installed git version
354
+ opm git install Install git via system package manager
355
+ opm git upgrade Upgrade git to latest
356
+ opm git uninstall Uninstall git
357
+ opm git init Initialize global config from opm config
358
+ opm git config List all global config
359
+ opm git config pull.rebase true Set pull to use rebase
360
+ opm git user Show current user.name and user.email
361
+ opm git log Show recent 10 commits (cwd)
362
+ opm git log -n 5 Show recent 5 commits (cwd)
363
+ opm git log -d /path/to/repo Show commits in specified repo
364
+ opm git export mark Show current diff base mark (cwd)
365
+ opm git export mark 0 Set diff base to current HEAD (cwd)
366
+ opm git export mark HEAD Set diff base to current HEAD (cwd)
367
+ opm git export mark 3af967b Set diff base at commit (cwd)
368
+ opm git export mark --unset Remove diff base mark (cwd)
369
+ opm git export mark abc -d /repo Set diff base at commit in /repo
370
+ opm git export Export diff in current repo (mark -> HEAD)
371
+ opm git export -f 2 Override: HEAD~2 -> HEAD (current repo)
372
+ opm git export -d /path/to/repo Export diff in specified repo
373
+ opm git export -n feature Save as feature.diff
374
+ opm git import file.diff Import diff file (current repo)
375
+ opm git import file.diff -d /repo Import diff file into specified repo
376
+ `)
377
+ }
378
+ }
@@ -0,0 +1,20 @@
1
+ # opm share server — Apache URL rewrite
2
+ # 将所有 /share/* 请求路由到 server.php
3
+
4
+ <IfModule mod_rewrite.c>
5
+ RewriteEngine On
6
+
7
+ # 如果请求的是已存在的文件,直接返回
8
+ RewriteCond %{REQUEST_FILENAME} -f [OR]
9
+ RewriteCond %{REQUEST_FILENAME} -d
10
+ RewriteRule ^ - [L]
11
+
12
+ # /share/* 路由到 server.php
13
+ RewriteRule ^share/?$ server.php [L,QSA]
14
+ RewriteRule ^share/(.+)$ server.php [L,QSA]
15
+ </IfModule>
16
+
17
+ # 保护 data 目录,禁止直接访问
18
+ <IfModule mod_rewrite.c>
19
+ RewriteRule ^data/ - [F]
20
+ </IfModule>
@@ -0,0 +1,172 @@
1
+ /**
2
+ * share HTTP 客户端 — 上传/取回/列表
3
+ *
4
+ * 通过 fetch 调用 share server API。
5
+ * 服务地址由 config.share.server.active 解析(key → URL,URL 原样返回)。
6
+ */
7
+
8
+ /**
9
+ * 解析 active server URL
10
+ * @param {object} shareConfig - config.share 段
11
+ * @returns {string} base URL(无尾斜杠)
12
+ */
13
+ export function getShareServer(shareConfig) {
14
+ const server = shareConfig?.server ?? {}
15
+ const active = server.active
16
+ if (!active) return server.official || ''
17
+ return server[active] || active
18
+ }
19
+
20
+ /**
21
+ * ping 检测 server 可达性
22
+ * @param {string} baseUrl
23
+ * @returns {Promise<{reachable: boolean, status: number, message: string}>}
24
+ */
25
+ export async function pingServer(baseUrl) {
26
+ try {
27
+ const res = await fetch(`${baseUrl}/share/list?n=1`, {
28
+ signal: AbortSignal.timeout(5000),
29
+ })
30
+ if (res.ok) {
31
+ return { reachable: true, status: res.status, message: 'OK' }
32
+ }
33
+ if (res.status === 404) {
34
+ return { reachable: true, status: res.status, message: 'OK (404 on list endpoint is acceptable)' }
35
+ }
36
+ return { reachable: false, status: res.status, message: `HTTP ${res.status}` }
37
+ } catch (err) {
38
+ return { reachable: false, status: 0, message: err.message }
39
+ }
40
+ }
41
+
42
+ /**
43
+ * 上传分享内容
44
+ * @param {string} baseUrl
45
+ * @param {{title?: string, content: string, password?: string, id?: string, type?: string, filename?: string}} data
46
+ * @returns {Promise<{id: string, url: string, htmlUrl: string}>}
47
+ */
48
+ export async function uploadShare(baseUrl, data) {
49
+ const res = await fetch(`${baseUrl}/share`, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify(data),
53
+ signal: AbortSignal.timeout(30000),
54
+ })
55
+ if (!res.ok) {
56
+ const body = await res.text()
57
+ throw new Error(`Upload failed: HTTP ${res.status} ${body}`)
58
+ }
59
+ return res.json()
60
+ }
61
+
62
+ /**
63
+ * 取回分享内容
64
+ * @param {string} baseUrl
65
+ * @param {string} id
66
+ * @param {string} [password]
67
+ * @returns {Promise<{id: string, title: string, content: string, created: string}>}
68
+ */
69
+ export async function getShare(baseUrl, id, password) {
70
+ const url = password
71
+ ? `${baseUrl}/share/${id}/${password}`
72
+ : `${baseUrl}/share/${id}`
73
+ const res = await fetch(url, {
74
+ headers: { 'Accept': 'application/json' },
75
+ signal: AbortSignal.timeout(10000),
76
+ })
77
+ if (!res.ok) {
78
+ const body = await res.text()
79
+ throw new Error(`Get failed: HTTP ${res.status} ${body}`)
80
+ }
81
+ return res.json()
82
+ }
83
+
84
+ /**
85
+ * 取回最近一条分享内容
86
+ * @param {string} baseUrl
87
+ * @param {string} [password]
88
+ * @returns {Promise<{id: string, title: string, content: string, created: string}>}
89
+ */
90
+ export async function getLatestShare(baseUrl, password) {
91
+ const url = password
92
+ ? `${baseUrl}/share/latest/${password}`
93
+ : `${baseUrl}/share/latest`
94
+ const res = await fetch(url, {
95
+ headers: { 'Accept': 'application/json' },
96
+ signal: AbortSignal.timeout(10000),
97
+ })
98
+ if (!res.ok) {
99
+ const body = await res.text()
100
+ throw new Error(`Get latest failed: HTTP ${res.status} ${body}`)
101
+ }
102
+ return res.json()
103
+ }
104
+
105
+ /**
106
+ * 获取 HTML 页面 URL
107
+ * @param {string} baseUrl
108
+ * @param {string} id
109
+ * @param {string} [password]
110
+ * @returns {string}
111
+ */
112
+ export function getHtmlUrl(baseUrl, id, password) {
113
+ return password
114
+ ? `${baseUrl}/share/html/${id}/${password}`
115
+ : `${baseUrl}/share/html/${id}`
116
+ }
117
+
118
+ /**
119
+ * 列出最近分享
120
+ * @param {string} baseUrl
121
+ * @param {number} n
122
+ * @returns {Promise<{count: number, items: Array<{id: string, title: string, created: string}>}>}
123
+ */
124
+ export async function listShares(baseUrl, n) {
125
+ const res = await fetch(`${baseUrl}/share/list?n=${n}`, {
126
+ signal: AbortSignal.timeout(10000),
127
+ })
128
+ if (!res.ok) {
129
+ const body = await res.text()
130
+ throw new Error(`List failed: HTTP ${res.status} ${body}`)
131
+ }
132
+ return res.json()
133
+ }
134
+
135
+ /**
136
+ * 删除指定分享
137
+ * @param {string} baseUrl
138
+ * @param {string} id
139
+ * @returns {Promise<{deleted: boolean, id: string}>}
140
+ */
141
+ export async function deleteShare(baseUrl, id) {
142
+ const res = await fetch(`${baseUrl}/share/delete/${id}`, {
143
+ method: 'POST',
144
+ signal: AbortSignal.timeout(10000),
145
+ })
146
+ if (!res.ok) {
147
+ const body = await res.text()
148
+ throw new Error(`Delete failed: HTTP ${res.status} ${body}`)
149
+ }
150
+ return res.json()
151
+ }
152
+
153
+ /**
154
+ * 清理过期/超额分享
155
+ * @param {string} baseUrl
156
+ * @param {{days?: number, keep?: number}} opts
157
+ * @returns {Promise<{deleted: number, ids: string[]}>}
158
+ */
159
+ export async function clearShares(baseUrl, opts) {
160
+ const params = new URLSearchParams()
161
+ if (opts.days) params.set('days', String(opts.days))
162
+ if (opts.keep) params.set('keep', String(opts.keep))
163
+ const res = await fetch(`${baseUrl}/share/clear?${params}`, {
164
+ method: 'POST',
165
+ signal: AbortSignal.timeout(30000),
166
+ })
167
+ if (!res.ok) {
168
+ const body = await res.text()
169
+ throw new Error(`Clear failed: HTTP ${res.status} ${body}`)
170
+ }
171
+ return res.json()
172
+ }