@wwkit/opm 1.0.18 → 1.0.19
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 +26 -23
- package/bin/index.js +8 -0
- package/package.json +2 -2
- package/src/binaries/base.js +5 -10
- package/src/cli/groups/env.js +15 -14
- package/src/cli/groups/overlay.js +7 -7
- package/src/cli/groups/package.js +62 -20
- package/src/cli/groups/ping.js +81 -83
- package/src/cli/groups/proc.js +14 -14
- package/src/cli/groups/version-manager.js +6 -10
- package/src/cli/helpers/args.js +4 -46
- package/src/cli/index.js +10 -2
- package/src/harnesses/base.js +2 -1
- package/src/harnesses/opencode/clear.js +29 -26
- package/src/harnesses/opencode/index.js +1 -2
- package/src/managers/apt.js +16 -0
- package/src/managers/brew.js +20 -1
- package/src/managers/bun.js +23 -0
- package/src/managers/composer.js +11 -0
- package/src/managers/dnf.js +16 -0
- package/src/managers/npm.js +12 -0
- package/src/managers/pip.js +16 -0
- package/src/managers/winget.js +12 -0
- package/src/tools/clipboard.js +68 -0
- package/src/tools/ftp/index.js +11 -11
- package/src/tools/git/index.js +30 -13
- package/src/tools/share/index.js +68 -12
- package/src/tools/share/server.js +16 -2
- package/src/tools/webwork/index.js +6 -2
package/src/managers/bun.js
CHANGED
|
@@ -272,6 +272,29 @@ export class BunManager extends PackageManager {
|
|
|
272
272
|
return { action: 'cache cleaned' }
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
/**
|
|
276
|
+
* 列出已安装包(bun pm ls 文本树解析;-g 列全局)
|
|
277
|
+
* @param {{ global?: boolean }} [opts]
|
|
278
|
+
* @returns {Promise<{ name: string, version: string }[]>}
|
|
279
|
+
*/
|
|
280
|
+
async listInstalled(opts = {}) {
|
|
281
|
+
const args = opts.global ? ['pm', 'ls', '-g', '--all'] : ['pm', 'ls', '--all']
|
|
282
|
+
const out = this._exec(args, { allowNonZero: true })
|
|
283
|
+
const items = []
|
|
284
|
+
for (const line of out.split('\n')) {
|
|
285
|
+
const cleaned = line.replace(/^[\s├└─│]+/, '').trim()
|
|
286
|
+
if (!cleaned) continue
|
|
287
|
+
// scoped 包:@babel/core@7.0.0 → 最后一个 @ 为版本分隔
|
|
288
|
+
const idx = cleaned.lastIndexOf('@')
|
|
289
|
+
if (idx > 0) {
|
|
290
|
+
const name = cleaned.slice(0, idx)
|
|
291
|
+
const version = cleaned.slice(idx + 1)
|
|
292
|
+
if (name && version) items.push({ name, version: version.replace(/^v/, '') })
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return items
|
|
296
|
+
}
|
|
297
|
+
|
|
275
298
|
/**
|
|
276
299
|
* 查询 bun 安装路径和包目录
|
|
277
300
|
* @returns {{ name: string, install: string, globalDir: string, cacheDir: string }}
|
package/src/managers/composer.js
CHANGED
|
@@ -344,6 +344,17 @@ export class ComposerManager extends PackageManager {
|
|
|
344
344
|
return { action: 'cache cleaned' }
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
/**
|
|
348
|
+
* 列出已安装包(缺省读当前项目 vendor/composer/installed.json,global 读全局 installed.json)
|
|
349
|
+
* @param {{ global?: boolean }} [opts]
|
|
350
|
+
* @returns {Promise<{ name: string, version: string }[]>}
|
|
351
|
+
*/
|
|
352
|
+
async listInstalled(opts = {}) {
|
|
353
|
+
const packages = opts.global ? this._readGlobalInstalledJson() : this._readInstalledJson()
|
|
354
|
+
if (!packages) return []
|
|
355
|
+
return packages.map((p) => ({ name: p.name, version: p.version || 'unknown' }))
|
|
356
|
+
}
|
|
357
|
+
|
|
347
358
|
/**
|
|
348
359
|
* 查询 composer 安装路径和包目录
|
|
349
360
|
* @returns {{ name: string, install: string, globalHome: string, vendorDir: string }}
|
package/src/managers/dnf.js
CHANGED
|
@@ -342,6 +342,22 @@ export class DnfManager extends PackageManager {
|
|
|
342
342
|
return { action: 'cache cleaned' }
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
/**
|
|
346
|
+
* 列出系统已安装包(系统级管理器,无项目作用域)
|
|
347
|
+
* @returns {Promise<{ name: string, version: string }[]>}
|
|
348
|
+
*/
|
|
349
|
+
async listInstalled(opts = {}) {
|
|
350
|
+
if (opts.global === false) return []
|
|
351
|
+
const out = this._exec(['list', 'installed', '--quiet'], { allowNonZero: true })
|
|
352
|
+
const items = []
|
|
353
|
+
// 行格式: name.arch version repo
|
|
354
|
+
for (const line of out.split('\n')) {
|
|
355
|
+
const m = line.trim().match(/^(\S+?)\.\S+\s+(\S+)\s+\S+/)
|
|
356
|
+
if (m) items.push({ name: m[1], version: m[2] })
|
|
357
|
+
}
|
|
358
|
+
return items
|
|
359
|
+
}
|
|
360
|
+
|
|
345
361
|
/**
|
|
346
362
|
* 查询 dnf 安装路径和包目录
|
|
347
363
|
* @returns {{ name: string, install: string, cacheDir: string, repoDir: string }}
|
package/src/managers/npm.js
CHANGED
|
@@ -505,6 +505,18 @@ export class NpmManager extends PackageManager {
|
|
|
505
505
|
return { action: 'cache cleaned' }
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
/**
|
|
509
|
+
* 列出已安装包
|
|
510
|
+
* @param {{ global?: boolean }} [opts] - global=true 列全局,缺省列当前项目
|
|
511
|
+
* @returns {Promise<{ name: string, version: string }[]>}
|
|
512
|
+
*/
|
|
513
|
+
async listInstalled(opts = {}) {
|
|
514
|
+
const args = opts.global ? ['ls', '-g', '--depth=0'] : ['ls', '--depth=0']
|
|
515
|
+
const data = this._execJson(args)
|
|
516
|
+
const deps = (data && data.dependencies) || {}
|
|
517
|
+
return Object.entries(deps).map(([name, d]) => ({ name, version: (d.version || '').replace(/^v/, '') }))
|
|
518
|
+
}
|
|
519
|
+
|
|
508
520
|
/**
|
|
509
521
|
* 查询 npm 安装路径和包目录
|
|
510
522
|
* @returns {{ name: string, install: string, globalDir: string, localDir: string, cacheDir: string, prefix: string }}
|
package/src/managers/pip.js
CHANGED
|
@@ -557,6 +557,22 @@ export class PipManager extends PackageManager {
|
|
|
557
557
|
return { action: 'cache cleaned' }
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
/**
|
|
561
|
+
* 列出已安装包
|
|
562
|
+
* @param {{ global?: boolean }} [opts] - global=true 用系统级 Python 环境,缺省当前环境
|
|
563
|
+
* @returns {Promise<{ name: string, version: string }[]>}
|
|
564
|
+
*/
|
|
565
|
+
async listInstalled(opts = {}) {
|
|
566
|
+
const out = this._execPip(['list', '--format=json', '--disable-pip-version-check'], { allowNonZero: true, global: opts.global })
|
|
567
|
+
try {
|
|
568
|
+
const data = JSON.parse(out)
|
|
569
|
+
if (!Array.isArray(data)) return []
|
|
570
|
+
return data.map((i) => ({ name: i.name, version: i.version || '' }))
|
|
571
|
+
} catch {
|
|
572
|
+
return []
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
560
576
|
/**
|
|
561
577
|
* 查询 pip 安装路径和包目录
|
|
562
578
|
* @returns {{ name: string, install: string, sitePackages: string, userSite: string }}
|
package/src/managers/winget.js
CHANGED
|
@@ -242,6 +242,18 @@ export class WingetManager extends PackageManager {
|
|
|
242
242
|
return { action: 'winget has no cache clean command' }
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
/**
|
|
246
|
+
* 列出系统已安装包(winget list 表格解析)
|
|
247
|
+
* @returns {Promise<{ name: string, version: string }[]>}
|
|
248
|
+
*/
|
|
249
|
+
async listInstalled(opts = {}) {
|
|
250
|
+
const out = this._exec(['list'], { allowNonZero: true })
|
|
251
|
+
const rows = this._parseTable(out)
|
|
252
|
+
return rows
|
|
253
|
+
.filter((r) => r.Name)
|
|
254
|
+
.map((r) => ({ name: r.Name, version: r.Version || '' }))
|
|
255
|
+
}
|
|
256
|
+
|
|
245
257
|
/**
|
|
246
258
|
* 查询 winget 安装路径
|
|
247
259
|
* @returns {{ name: string, install: string }}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 系统剪切板读写工具
|
|
3
|
+
*
|
|
4
|
+
* 跨平台读取/写入系统级剪切板:
|
|
5
|
+
* - darwin: pbpaste / pbcopy
|
|
6
|
+
* - linux: 优先 wl-paste / wl-copy(Wayland),回退 xclip / xsel
|
|
7
|
+
* - win32: PowerShell Get-Clipboard / Set-Clipboard
|
|
8
|
+
* 读取失败(命令不存在/非零退出)返回空字符串,调用方用 trim 判断空。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawnSync } from 'node:child_process'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 执行外部命令并捕获输出
|
|
15
|
+
* @param {string} cmd
|
|
16
|
+
* @param {string[]} args
|
|
17
|
+
* @param {{input?: string}} [opts]
|
|
18
|
+
* @returns {{ok: boolean, stdout: string, stderr: string}}
|
|
19
|
+
*/
|
|
20
|
+
function run(cmd, args, opts = {}) {
|
|
21
|
+
const res = spawnSync(cmd, args, { encoding: 'utf8', ...opts })
|
|
22
|
+
if (res.error) {
|
|
23
|
+
return { ok: false, stdout: '', stderr: res.error.message }
|
|
24
|
+
}
|
|
25
|
+
return { ok: res.status === 0, stdout: res.stdout || '', stderr: res.stderr || '' }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 读取系统剪切板内容(失败返回空字符串)
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function readClipboard() {
|
|
33
|
+
const p = process.platform
|
|
34
|
+
if (p === 'darwin') {
|
|
35
|
+
return run('pbpaste', []).stdout
|
|
36
|
+
}
|
|
37
|
+
if (p === 'win32') {
|
|
38
|
+
const r = run('powershell', [
|
|
39
|
+
'-NoProfile', '-Command',
|
|
40
|
+
'[Console]::Out.Write((Get-Clipboard -Raw -ErrorAction SilentlyContinue))',
|
|
41
|
+
])
|
|
42
|
+
return r.ok ? r.stdout : ''
|
|
43
|
+
}
|
|
44
|
+
// linux
|
|
45
|
+
const r = run('bash', [
|
|
46
|
+
'-c',
|
|
47
|
+
'wl-paste --no-newline 2>/dev/null || xclip -selection clipboard -o 2>/dev/null || xsel --clipboard --output 2>/dev/null',
|
|
48
|
+
])
|
|
49
|
+
return r.ok ? r.stdout : ''
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 写入系统剪切板
|
|
54
|
+
* @param {string} text
|
|
55
|
+
*/
|
|
56
|
+
export function writeClipboard(text) {
|
|
57
|
+
const input = String(text ?? '')
|
|
58
|
+
const p = process.platform
|
|
59
|
+
if (p === 'darwin') {
|
|
60
|
+
run('pbcopy', [], { input })
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
if (p === 'win32') {
|
|
64
|
+
run('powershell', ['-NoProfile', '-Command', 'Set-Clipboard -Value $input'], { input })
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
run('bash', ['-c', 'wl-copy 2>/dev/null || xclip -selection clipboard 2>/dev/null || xsel --clipboard --input 2>/dev/null'], { input })
|
|
68
|
+
}
|
package/src/tools/ftp/index.js
CHANGED
|
@@ -196,7 +196,7 @@ export class FtpGroup {
|
|
|
196
196
|
console.error('Usage: opm ftp upload <file> [-n name] [-s subdir]')
|
|
197
197
|
process.exit(1)
|
|
198
198
|
}
|
|
199
|
-
const name = parsed.flags.name ||
|
|
199
|
+
const name = parsed.flags.name || ''
|
|
200
200
|
const subdir = parsed.flags.subdir || parsed.flags.s || ''
|
|
201
201
|
const cfg = getFtpConfig()
|
|
202
202
|
|
|
@@ -242,14 +242,14 @@ export class FtpGroup {
|
|
|
242
242
|
async _rsync(parsed) {
|
|
243
243
|
const local = parsed.positional[0]
|
|
244
244
|
if (!local) {
|
|
245
|
-
console.error('Usage: opm ftp rsync <local> [-s subdir] [-x pattern] [--dry-run] [--
|
|
245
|
+
console.error('Usage: opm ftp rsync <local> [-s subdir] [-x pattern] [--dry-run] [--delete]')
|
|
246
246
|
process.exit(1)
|
|
247
247
|
}
|
|
248
248
|
const subdir = parsed.flags.subdir || parsed.flags.s || ''
|
|
249
249
|
const excludes = parsed.flags.exclude || parsed.flags.x || []
|
|
250
250
|
const excludeList = Array.isArray(excludes) ? excludes : [excludes]
|
|
251
251
|
const dryRun = parsed.flags['dry-run'] === 'true'
|
|
252
|
-
const
|
|
252
|
+
const deleteRemote = parsed.flags.delete === 'true'
|
|
253
253
|
|
|
254
254
|
const cfg = getFtpConfig()
|
|
255
255
|
const localDir = path.resolve(local)
|
|
@@ -282,7 +282,7 @@ export class FtpGroup {
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
const deletePlan = []
|
|
285
|
-
if (
|
|
285
|
+
if (deleteRemote) {
|
|
286
286
|
for (const [rel] of remoteFiles) {
|
|
287
287
|
if (!toUpload.includes(rel)) {
|
|
288
288
|
deletePlan.push(rel)
|
|
@@ -335,9 +335,9 @@ export class FtpGroup {
|
|
|
335
335
|
skippedSame: skippedSame.length,
|
|
336
336
|
skippedExcluded,
|
|
337
337
|
failed,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
338
|
+
deleted: deleted.length,
|
|
339
|
+
deletedFiles: deleted,
|
|
340
|
+
deleteFailed,
|
|
341
341
|
})
|
|
342
342
|
}
|
|
343
343
|
|
|
@@ -370,7 +370,7 @@ Actions:
|
|
|
370
370
|
uninstall Uninstall basic-ftp
|
|
371
371
|
upgrade Upgrade basic-ftp to latest (uses opm npm registry + proxy)
|
|
372
372
|
connect Test FTP connection
|
|
373
|
-
upload <file> [
|
|
373
|
+
upload <file> [--name name] [-s subdir] Upload file (local path or HTTP URL)
|
|
374
374
|
rsync <local> [options] Recursive upload directory (FTP-side dedup)
|
|
375
375
|
download <remote> <local> Download file from FTP
|
|
376
376
|
root Show configured remote root directory
|
|
@@ -380,7 +380,7 @@ rsync options:
|
|
|
380
380
|
-s, --subdir <dir> Remote subdirectory (under remote_root)
|
|
381
381
|
-x, --exclude <pattern> Exclude glob pattern (repeatable)
|
|
382
382
|
--dry-run Preview without uploading
|
|
383
|
-
--
|
|
383
|
+
--delete Delete remote files not in local
|
|
384
384
|
|
|
385
385
|
Options:
|
|
386
386
|
-p, --proxy <url> Proxy for install/upgrade (overrides config proxy.active)
|
|
@@ -393,12 +393,12 @@ Examples:
|
|
|
393
393
|
opm ftp upgrade
|
|
394
394
|
opm ftp connect
|
|
395
395
|
opm ftp upload ./img.png
|
|
396
|
-
opm ftp upload ./img.png
|
|
396
|
+
opm ftp upload ./img.png --name cover -s banner
|
|
397
397
|
opm ftp upload https://example.com/a.jpg -n cover.jpg
|
|
398
398
|
opm ftp rsync ./webwork/
|
|
399
399
|
opm ftp rsync ./webwork/ -s deploy -x "vendor/*" -x "*.log"
|
|
400
400
|
opm ftp rsync ./webwork/ --dry-run
|
|
401
|
-
opm ftp rsync ./webwork/ --
|
|
401
|
+
opm ftp rsync ./webwork/ --delete
|
|
402
402
|
opm ftp download 20260913/cover.jpg ./cover.jpg
|
|
403
403
|
opm ftp root
|
|
404
404
|
`)
|
package/src/tools/git/index.js
CHANGED
|
@@ -40,6 +40,20 @@ export class GitGroup {
|
|
|
40
40
|
this.desc = 'Git config management (init/config/user/log/export)'
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* 当前 action 下的上下文布尔标志
|
|
45
|
+
* 同名标志在不同 action 中语义不同(-f:init=force / export=from;--unset:config 带值 / export mark 布尔),
|
|
46
|
+
* 由 action 决定解析方式,避免布尔标志吞掉后续参数。
|
|
47
|
+
* @param {string} action
|
|
48
|
+
* @returns {string[]}
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
_booleanFlags(action) {
|
|
52
|
+
if (action === 'init') return ['f', 'force']
|
|
53
|
+
if (action === 'export') return ['remove', 'unset']
|
|
54
|
+
return []
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
async run(argv) {
|
|
44
58
|
const [action, ...rest] = argv
|
|
45
59
|
|
|
@@ -48,7 +62,7 @@ export class GitGroup {
|
|
|
48
62
|
return
|
|
49
63
|
}
|
|
50
64
|
|
|
51
|
-
const parsed = parseFlags(rest)
|
|
65
|
+
const parsed = parseFlags(rest, { booleans: this._booleanFlags(action) })
|
|
52
66
|
|
|
53
67
|
switch (action) {
|
|
54
68
|
case 'version':
|
|
@@ -146,7 +160,7 @@ export class GitGroup {
|
|
|
146
160
|
|
|
147
161
|
async _user(parsed) {
|
|
148
162
|
const scope = this._getScope(parsed)
|
|
149
|
-
const name = parsed.flags.
|
|
163
|
+
const name = parsed.flags.name || ''
|
|
150
164
|
const email = parsed.flags.e || parsed.flags.email || ''
|
|
151
165
|
|
|
152
166
|
if (name || email) {
|
|
@@ -209,7 +223,7 @@ export class GitGroup {
|
|
|
209
223
|
const workDir = parsed.flags.d || parsed.flags.dir || process.cwd()
|
|
210
224
|
const ref = parsed.positional[1]
|
|
211
225
|
|
|
212
|
-
if (parsed.flags.unset === 'true') {
|
|
226
|
+
if (parsed.flags.remove === 'true' || parsed.flags.unset === 'true') {
|
|
213
227
|
const ok = unsetMark(workDir)
|
|
214
228
|
output({ mark: 'removed', ok })
|
|
215
229
|
return
|
|
@@ -230,7 +244,7 @@ export class GitGroup {
|
|
|
230
244
|
console.error('\nTips:')
|
|
231
245
|
console.error(' opm git log -d <dir> -n 10 # view commit history without cd')
|
|
232
246
|
console.error(' opm git export mark <commit> # change diff base to another commit')
|
|
233
|
-
|
|
247
|
+
console.error(' opm git export mark --remove # remove current mark')
|
|
234
248
|
console.error(' opm git export # export diff (mark -> HEAD)')
|
|
235
249
|
this._printRecentLog(workDir)
|
|
236
250
|
return
|
|
@@ -250,7 +264,7 @@ export class GitGroup {
|
|
|
250
264
|
process.exit(1)
|
|
251
265
|
}
|
|
252
266
|
|
|
253
|
-
const fromRaw = parsed.flags.
|
|
267
|
+
const fromRaw = parsed.flags.from || ''
|
|
254
268
|
let fromRef
|
|
255
269
|
if (fromRaw) {
|
|
256
270
|
const n = parseInt(fromRaw, 10)
|
|
@@ -271,7 +285,7 @@ export class GitGroup {
|
|
|
271
285
|
if (!outFile) {
|
|
272
286
|
const opts = { encoding: 'utf8', timeout: 5000, cwd: resolvedDir, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
273
287
|
const gitRoot = execSync('git rev-parse --show-toplevel', opts).trim()
|
|
274
|
-
const name = parsed.flags.
|
|
288
|
+
const name = parsed.flags.name || ''
|
|
275
289
|
const fileName = name ? `${name}.diff` : (() => {
|
|
276
290
|
const fromShort = execSync(`git rev-parse --short ${fromRef}`, opts).trim()
|
|
277
291
|
const toShort = execSync('git rev-parse --short HEAD', opts).trim()
|
|
@@ -331,7 +345,7 @@ Export subcommands:
|
|
|
331
345
|
export mark [-d dir] Show current diff base mark
|
|
332
346
|
export mark 0|HEAD [-d dir] Set diff base mark to current HEAD
|
|
333
347
|
export mark <commit> [-d dir] Set diff base mark to a commit
|
|
334
|
-
export mark --
|
|
348
|
+
export mark --remove [-d dir] Remove the diff base mark
|
|
335
349
|
export [-d dir] Export diff (mark -> HEAD)
|
|
336
350
|
export -f <n> [-d dir] Export diff (HEAD~n -> HEAD, override mark)
|
|
337
351
|
export -f <ref> [-d dir] Export diff (<ref> -> HEAD, override mark)
|
|
@@ -344,11 +358,15 @@ Options:
|
|
|
344
358
|
-n, --count <n> Number of commits to show (log, default: 10)
|
|
345
359
|
-e, --email <email> User email (user)
|
|
346
360
|
-o, --output <file> Output file path (export)
|
|
347
|
-
|
|
361
|
+
--from <n>|<ref> Override from ref (export)
|
|
348
362
|
-f, --force Force overwrite existing config (init)
|
|
349
|
-
--unset <key> Unset a config key (config)
|
|
363
|
+
--unset <key> Unset a config key (config only)
|
|
364
|
+
--remove Remove the diff base mark (export mark)
|
|
350
365
|
-h, --help Show this help
|
|
351
366
|
|
|
367
|
+
Note: -n 在 log 中为 --count 数量,user/export 中用 --name 名称(无短形式);
|
|
368
|
+
-f 在 init 中为 --force 布尔,export 中用 --from 带值(无短形式)。
|
|
369
|
+
|
|
352
370
|
Examples:
|
|
353
371
|
opm git version Show installed git version
|
|
354
372
|
opm git install Install git via system package manager
|
|
@@ -365,12 +383,11 @@ Examples:
|
|
|
365
383
|
opm git export mark 0 Set diff base to current HEAD (cwd)
|
|
366
384
|
opm git export mark HEAD Set diff base to current HEAD (cwd)
|
|
367
385
|
opm git export mark 3af967b Set diff base at commit (cwd)
|
|
368
|
-
opm git export mark --
|
|
369
|
-
opm git export mark abc -d /repo Set diff base at commit in /repo
|
|
386
|
+
opm git export mark --remove Remove diff base mark (cwd) opm git export mark abc -d /repo Set diff base at commit in /repo
|
|
370
387
|
opm git export Export diff in current repo (mark -> HEAD)
|
|
371
|
-
opm git export
|
|
388
|
+
opm git export --from 2 Override: HEAD~2 -> HEAD (current repo)
|
|
372
389
|
opm git export -d /path/to/repo Export diff in specified repo
|
|
373
|
-
opm git export
|
|
390
|
+
opm git export --name feature Save as feature.diff
|
|
374
391
|
opm git import file.diff Import diff file (current repo)
|
|
375
392
|
opm git import file.diff -d /repo Import diff file into specified repo
|
|
376
393
|
`)
|
package/src/tools/share/index.js
CHANGED
|
@@ -18,9 +18,11 @@ import { getConfig, getSection, getUserConfigDir } from '../../config.js'
|
|
|
18
18
|
import { output } from '../../formatter.js'
|
|
19
19
|
import { getShareServer, pingServer, uploadShare, getShare, getLatestShare, getHtmlUrl, listShares, deleteShare, clearShares } from './client.js'
|
|
20
20
|
import { startServer } from './server.js'
|
|
21
|
+
import { readClipboard, writeClipboard } from '../clipboard.js'
|
|
21
22
|
|
|
22
23
|
const DEFAULT_PASSWORD = '0000'
|
|
23
24
|
const ZIP_TMP_PREFIX = 'opm-share-'
|
|
25
|
+
const CLIPBOARD_ID = 'clipboard'
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* 生成默认标题(内容前 30 字符折叠空白 + ...,≤30 用原文)
|
|
@@ -109,9 +111,11 @@ const ACTIONS = {
|
|
|
109
111
|
open: { desc: 'Open shared content in browser (id optional; defaults to latest)' },
|
|
110
112
|
list: { desc: 'List recent shares (-n count, default 10)' },
|
|
111
113
|
delete: { desc: 'Delete a share by id' },
|
|
112
|
-
clear: { desc: '
|
|
114
|
+
clear: { desc: 'Keep shares by days and/or count (--days n keeps last n days, --keep n keeps n most recent)' },
|
|
113
115
|
config: { desc: 'Show current share config section' },
|
|
114
116
|
server: { desc: 'Manage local server (start/stop/status)' },
|
|
117
|
+
copy: { desc: 'Copy system clipboard to server (id=clipboard; skips when clipboard is empty)' },
|
|
118
|
+
paste: { desc: 'Fetch server clipboard (id=clipboard) back into system clipboard and print' },
|
|
115
119
|
help: { desc: 'Show this help' },
|
|
116
120
|
}
|
|
117
121
|
|
|
@@ -152,6 +156,10 @@ export class ShareGroup {
|
|
|
152
156
|
return this._config(parsed)
|
|
153
157
|
case 'server':
|
|
154
158
|
return this._server(parsed)
|
|
159
|
+
case 'copy':
|
|
160
|
+
return this._copy(parsed)
|
|
161
|
+
case 'paste':
|
|
162
|
+
return this._paste(parsed)
|
|
155
163
|
default:
|
|
156
164
|
return this._upload(parseFlags([action, ...rest]))
|
|
157
165
|
}
|
|
@@ -188,7 +196,7 @@ export class ShareGroup {
|
|
|
188
196
|
* @returns {string}
|
|
189
197
|
*/
|
|
190
198
|
_resolvePassword(parsed, positional = '') {
|
|
191
|
-
const flag = parsed.flags.
|
|
199
|
+
const flag = parsed.flags.password || ''
|
|
192
200
|
if (flag && flag !== 'true') return flag
|
|
193
201
|
const cfgPassword = this._getShareConfig().password
|
|
194
202
|
if (cfgPassword) return cfgPassword
|
|
@@ -234,7 +242,7 @@ export class ShareGroup {
|
|
|
234
242
|
|
|
235
243
|
async _upload(parsed) {
|
|
236
244
|
const title = parsed.flags.t || parsed.flags.title || ''
|
|
237
|
-
const file = parsed.flags.
|
|
245
|
+
const file = parsed.flags.file || ''
|
|
238
246
|
const id = parsed.flags.i || parsed.flags.id || ''
|
|
239
247
|
const password = this._resolvePassword(parsed)
|
|
240
248
|
|
|
@@ -271,7 +279,7 @@ export class ShareGroup {
|
|
|
271
279
|
}
|
|
272
280
|
|
|
273
281
|
async _zip(parsed) {
|
|
274
|
-
const inputPath = parsed.positional[0] || parsed.flags.
|
|
282
|
+
const inputPath = parsed.positional[0] || parsed.flags.file || ''
|
|
275
283
|
const title = parsed.flags.t || parsed.flags.title || ''
|
|
276
284
|
const id = parsed.flags.i || parsed.flags.id || ''
|
|
277
285
|
const password = this._resolvePassword(parsed)
|
|
@@ -477,12 +485,12 @@ export class ShareGroup {
|
|
|
477
485
|
}
|
|
478
486
|
|
|
479
487
|
async _clear(parsed) {
|
|
480
|
-
const days =
|
|
488
|
+
const days = parseFloat(parsed.flags.days || '0') || 0
|
|
481
489
|
const keep = parseInt(parsed.flags.keep || parsed.flags.n || '0', 10) || 0
|
|
482
490
|
|
|
483
491
|
if (!days && !keep) {
|
|
484
492
|
console.error('Usage: opm share clear [--days <n>] [--keep <n>]')
|
|
485
|
-
console.error(' --days <n>
|
|
493
|
+
console.error(' --days <n> Keep shares from the last n days (integer = aligned to local midnight, fraction = hour window; default keeps today)')
|
|
486
494
|
console.error(' --keep <n> Keep only the most recent n shares')
|
|
487
495
|
console.error(' Both can be combined: clear --days 7 --keep 50')
|
|
488
496
|
process.exit(1)
|
|
@@ -667,6 +675,50 @@ Environment variables (for server.js direct execution):
|
|
|
667
675
|
output({ running: true, ...pidInfo })
|
|
668
676
|
}
|
|
669
677
|
|
|
678
|
+
/**
|
|
679
|
+
* opm copy — 将系统剪切板内容以 id=clipboard 上传到服务器
|
|
680
|
+
* 剪切板为空(trim 后)时提示并停止。
|
|
681
|
+
* @param {object} parsed
|
|
682
|
+
*/
|
|
683
|
+
async _copy(parsed) {
|
|
684
|
+
const content = readClipboard()
|
|
685
|
+
if (!content.trim()) {
|
|
686
|
+
console.error('[opm share] Clipboard is empty, nothing to upload.')
|
|
687
|
+
process.exit(1)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const password = this._resolvePassword(parsed)
|
|
691
|
+
const baseUrl = await this._ensureReachable()
|
|
692
|
+
const result = await uploadShare(baseUrl, {
|
|
693
|
+
title: defaultTitle(content),
|
|
694
|
+
content,
|
|
695
|
+
password,
|
|
696
|
+
id: CLIPBOARD_ID,
|
|
697
|
+
})
|
|
698
|
+
output({ ...result, copied: true })
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* opm paste — 取回服务器上 id=clipboard 的内容,写回系统剪切板并显示
|
|
703
|
+
* @param {object} parsed
|
|
704
|
+
*/
|
|
705
|
+
async _paste(parsed) {
|
|
706
|
+
const password = this._resolvePassword(parsed, parsed.positional[0] || '')
|
|
707
|
+
const baseUrl = await this._ensureReachable()
|
|
708
|
+
const result = await getShare(baseUrl, CLIPBOARD_ID, password)
|
|
709
|
+
|
|
710
|
+
const content = result.content || ''
|
|
711
|
+
writeClipboard(content)
|
|
712
|
+
output({
|
|
713
|
+
pasted: true,
|
|
714
|
+
id: result.id,
|
|
715
|
+
title: result.title,
|
|
716
|
+
created: result.created,
|
|
717
|
+
length: content.length,
|
|
718
|
+
content,
|
|
719
|
+
})
|
|
720
|
+
}
|
|
721
|
+
|
|
670
722
|
printHelp() {
|
|
671
723
|
const actionLines = Object.entries(ACTIONS)
|
|
672
724
|
.map(([sig, { desc }]) => ` ${sig.padEnd(12)} ${desc}`)
|
|
@@ -690,9 +742,9 @@ Shorthand:
|
|
|
690
742
|
|
|
691
743
|
Options:
|
|
692
744
|
-t, --title <title> Title (default: first 30 chars of content)
|
|
693
|
-
|
|
745
|
+
--file <path> Read content from file
|
|
694
746
|
-i, --id <id> Custom share id (upload/zip; easier to remember)
|
|
695
|
-
|
|
747
|
+
--password <password> Password (default: config share.password, fallback 0000)
|
|
696
748
|
-n, --count <n> Number of items (list, default: 10)
|
|
697
749
|
-d, --dir <dir> Extract zip to this directory (get)
|
|
698
750
|
-h, --help Show this help
|
|
@@ -703,14 +755,18 @@ Server config (config.json5):
|
|
|
703
755
|
share.serve.host / .port / .dataDir Local serve parameters
|
|
704
756
|
|
|
705
757
|
Examples:
|
|
758
|
+
opm share copy Copy system clipboard to server (id=clipboard)
|
|
759
|
+
opm share paste Fetch server clipboard back to system clipboard
|
|
760
|
+
opm copy Top-level shortcut for "opm share copy"
|
|
761
|
+
opm paste Top-level shortcut for "opm share paste"
|
|
706
762
|
opm share config Show current share config
|
|
707
763
|
opm share server start Start local server (background)
|
|
708
764
|
opm share server status Check if server is running
|
|
709
765
|
opm share server stop Stop local server
|
|
710
766
|
opm share "hello world" -t test Upload text
|
|
711
767
|
opm share "hello world" -i myid Upload text with custom id "myid"
|
|
712
|
-
opm share "hello world"
|
|
713
|
-
opm share
|
|
768
|
+
opm share "hello world" --password 1234 Upload text with password override
|
|
769
|
+
opm share --file doc.md Upload file (title auto from content)
|
|
714
770
|
opm share -f doc.md -t "README" Upload file with custom title
|
|
715
771
|
echo "hello" | opm share Upload from stdin
|
|
716
772
|
opm share zip ./src Upload folder as zip
|
|
@@ -718,7 +774,7 @@ Examples:
|
|
|
718
774
|
opm share get Get the latest share (JSON)
|
|
719
775
|
opm share get myid Get share by custom id
|
|
720
776
|
opm share get abc12345 Get share by random id
|
|
721
|
-
opm share get abc12345
|
|
777
|
+
opm share get abc12345 --password 1234 Get with password override
|
|
722
778
|
opm share get abc12345 -d ./out Download and extract zip to ./out
|
|
723
779
|
opm share view View the latest share content
|
|
724
780
|
opm share view myid View share by custom id
|
|
@@ -727,7 +783,7 @@ Examples:
|
|
|
727
783
|
opm share list List recent 10 shares
|
|
728
784
|
opm share list -n 5 List recent 5 shares
|
|
729
785
|
opm share delete abc12345 Delete a share by id
|
|
730
|
-
opm share clear --days 7
|
|
786
|
+
opm share clear --days 7 Keep shares from the last 7 days
|
|
731
787
|
opm share clear --keep 50 Keep only the most recent 50 shares
|
|
732
788
|
opm share clear --days 7 --keep 50 Both combined
|
|
733
789
|
`)
|
|
@@ -300,8 +300,18 @@ function handleDelete(req, res, dataDir, id) {
|
|
|
300
300
|
sendJson(res, 200, { deleted: true, id })
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
/**
|
|
304
|
+
* 某毫秒时间戳所在「天」的本地零点(与 opencode clear 语义一致)
|
|
305
|
+
* @param {number} ts - epoch ms
|
|
306
|
+
* @returns {number}
|
|
307
|
+
*/
|
|
308
|
+
function startOfDayLocal(ts) {
|
|
309
|
+
const d = new Date(ts)
|
|
310
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
|
|
311
|
+
}
|
|
312
|
+
|
|
303
313
|
function handleClear(req, res, dataDir, url) {
|
|
304
|
-
const days =
|
|
314
|
+
const days = parseFloat(url.searchParams.get('days') || '0') || 0
|
|
305
315
|
const keep = parseInt(url.searchParams.get('keep') || '0', 10) || 0
|
|
306
316
|
|
|
307
317
|
let files = fs.readdirSync(dataDir)
|
|
@@ -315,7 +325,11 @@ function handleClear(req, res, dataDir, url) {
|
|
|
315
325
|
const now = Date.now()
|
|
316
326
|
|
|
317
327
|
if (days > 0) {
|
|
318
|
-
|
|
328
|
+
// 语义与 opencode clear 统一:保留最近 n 天(含今天)。
|
|
329
|
+
// 整数天对齐本地零点(删除 今天零点-(n-1)天 之前);小数天 = 小时级滚动窗口(0.5 = 12h)
|
|
330
|
+
const cutoff = Number.isInteger(days)
|
|
331
|
+
? startOfDayLocal(now) - (days - 1) * 86400000
|
|
332
|
+
: now - days * 86400000
|
|
319
333
|
for (const f of files) {
|
|
320
334
|
const createdMs = new Date(f.created).getTime()
|
|
321
335
|
if (createdMs < cutoff) {
|
|
@@ -33,7 +33,7 @@ const COMPONENTS = [
|
|
|
33
33
|
|
|
34
34
|
const ACTIONS = {
|
|
35
35
|
version: { desc: 'Show installed version of each component' },
|
|
36
|
-
versions: { desc: 'Show latest available versions (default 10, -n <
|
|
36
|
+
versions: { desc: 'Show latest available versions (default 10, -n <count>)' },
|
|
37
37
|
installed: { desc: 'Show install status (true/false) of each component' },
|
|
38
38
|
install: { desc: 'Mirror check + ensure runtime + write registry env + blues-lib + ww init' },
|
|
39
39
|
uninstall: { desc: 'Uninstall all components in reverse order' },
|
|
@@ -96,7 +96,7 @@ export class WebworkGroup {
|
|
|
96
96
|
|
|
97
97
|
async _versions(parsed) {
|
|
98
98
|
const proxy = this._resolveProxy(parsed)
|
|
99
|
-
const num = parseInt(parsed.flags.n || parsed.flags.num || '10', 10)
|
|
99
|
+
const num = parseInt(parsed.flags.n || parsed.flags.count || parsed.flags.num || '10', 10)
|
|
100
100
|
|
|
101
101
|
const result = {}
|
|
102
102
|
|
|
@@ -274,6 +274,10 @@ export class WebworkGroup {
|
|
|
274
274
|
await this._runBatch('uninstall', reversed, proxy, (key, p) => this._uninstallComponent(key, p))
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
// TODO(P2-6): webwork 自身需要提供真正的 uninstall 命令。
|
|
278
|
+
// 当前 _uninstall 仅卸载 blues-lib(python/node 作为系统依赖跳过);
|
|
279
|
+
// 自身的完整卸载方案(registry env 清理、ww 相关配置移除等)待实现。
|
|
280
|
+
|
|
277
281
|
/**
|
|
278
282
|
* 检查镜像可达性(HEAD 请求,不可达则抛错)
|
|
279
283
|
* 必查:npm / pip(blues-lib 依赖,环境必然可用)
|