@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/cli/groups/ping.js
CHANGED
|
@@ -91,7 +91,9 @@ function checkByPing(host, count) {
|
|
|
91
91
|
|
|
92
92
|
let child
|
|
93
93
|
try {
|
|
94
|
-
|
|
94
|
+
// 直接传 args(不经 shell 拼接):ping.exe 为真实可执行文件,
|
|
95
|
+
// Windows 下 shell:true 会走 cmd.exe,存在参数被二次解析的风险
|
|
96
|
+
child = spawn('ping', args, { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
95
97
|
} catch {
|
|
96
98
|
resolve({
|
|
97
99
|
reachable: false, sent: count, received: 0, loss: count,
|
|
@@ -159,6 +161,47 @@ function checkByPing(host, count) {
|
|
|
159
161
|
})
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
/**
|
|
165
|
+
* 解析 curl writeout 输出(-w '%{http_code} %{time_total}')
|
|
166
|
+
*
|
|
167
|
+
* 兜底:writeout 输出缺失/被破坏(httpCode 0)时,退出码 0 仍代表成功收到
|
|
168
|
+
* HTTP 响应(curl 未加 -f,404/429 等也算正常完成),不应误判 FAIL。
|
|
169
|
+
* 修复背景:spawn 经 shell 拼接时,%{} 与空格被 shell/cmd 破坏导致 stdout
|
|
170
|
+
* 为空,镜像实际可达却全部报 HTTP 0。
|
|
171
|
+
*
|
|
172
|
+
* @param {string} stdout - curl stdout
|
|
173
|
+
* @param {number} code - curl 进程退出码
|
|
174
|
+
* @returns {{ httpCode: number, timeTotal: number, writeoutBroken: boolean, proxyAuthRequired: boolean, reachable: boolean, rttMs: string, raw: string }}
|
|
175
|
+
*/
|
|
176
|
+
function parseCurlWriteout(stdout, code) {
|
|
177
|
+
const parts = String(stdout || '').trim().split(/\s+/)
|
|
178
|
+
const httpCode = parseInt(parts[0], 10) || 0
|
|
179
|
+
const timeTotal = parseFloat(parts[1]) || 0
|
|
180
|
+
const writeoutBroken = httpCode === 0 && code === 0
|
|
181
|
+
const proxyAuthRequired = httpCode === 407
|
|
182
|
+
const reachable = writeoutBroken || httpReachable(httpCode)
|
|
183
|
+
const rttMs = reachable && timeTotal > 0 ? String(Math.round(timeTotal * 1000)) : ''
|
|
184
|
+
const raw = writeoutBroken
|
|
185
|
+
? 'OK (curl exit 0; writeout unavailable)'
|
|
186
|
+
: `HTTP ${httpCode} (${timeTotal}s)`
|
|
187
|
+
return { httpCode, timeTotal, writeoutBroken, proxyAuthRequired, reachable, rttMs, raw }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 构造 curl 探活参数(导出:win32 命令行往返测试直接锁定同一份生产参数,
|
|
192
|
+
* 避免测试与真实 args 漂移;Windows 上含空白的参数由 libuv 双引号包裹,
|
|
193
|
+
* cmd 不参与,%{} 与空格原样到达 curl.exe)
|
|
194
|
+
* @param {string} url - 完整 URL(含协议)
|
|
195
|
+
* @param {string} [proxy] - 代理地址,设置时追加 -x <proxy>
|
|
196
|
+
* @returns {string[]}
|
|
197
|
+
*/
|
|
198
|
+
function buildCurlArgs(url, proxy) {
|
|
199
|
+
const args = ['-k', '-L', '-s', '-o', NULL_DEV, '-w', '%{http_code} %{time_total}', '--connect-timeout', '2', '--max-time', '5']
|
|
200
|
+
if (proxy) args.push('-x', proxy)
|
|
201
|
+
args.push(url)
|
|
202
|
+
return args
|
|
203
|
+
}
|
|
204
|
+
|
|
162
205
|
/**
|
|
163
206
|
* 用 curl 检查 URL 可访问性(HTTP,跳过 SSL 校验,跟随跳转)
|
|
164
207
|
* @param {string} url - 完整 URL
|
|
@@ -167,12 +210,13 @@ function checkByPing(host, count) {
|
|
|
167
210
|
*/
|
|
168
211
|
function checkByCurl(url, proxy) {
|
|
169
212
|
return new Promise((resolve) => {
|
|
170
|
-
const args =
|
|
171
|
-
if (proxy) args.push('-x', proxy)
|
|
172
|
-
args.push(url)
|
|
213
|
+
const args = buildCurlArgs(url, proxy)
|
|
173
214
|
let child
|
|
174
215
|
try {
|
|
175
|
-
|
|
216
|
+
// 直接传 args(不经 shell 拼接):-w 格式串含 %{} 与空格,
|
|
217
|
+
// shell 模式会按空白拆参并交由 cmd/sh 做变量展开,导致 stdout 无法解析
|
|
218
|
+
// (Windows cmd 下实测输出空 → HTTP 0 (0s),镜像实际可达也被误判 FAIL)
|
|
219
|
+
child = spawn('curl', args, { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
176
220
|
} catch {
|
|
177
221
|
resolve({
|
|
178
222
|
reachable: false, sent: 1, received: 0, loss: 1,
|
|
@@ -185,23 +229,18 @@ function checkByCurl(url, proxy) {
|
|
|
185
229
|
child.stdout.on('data', (d) => { stdout += d })
|
|
186
230
|
child.stderr.on('data', (d) => { stderr += d })
|
|
187
231
|
child.on('close', (code) => {
|
|
188
|
-
const
|
|
189
|
-
const httpCode = parseInt(parts[0], 10) || 0
|
|
190
|
-
const timeTotal = parseFloat(parts[1]) || 0
|
|
191
|
-
const proxyAuthRequired = httpCode === 407
|
|
192
|
-
const reachable = httpReachable(httpCode)
|
|
193
|
-
const rttMs = reachable ? String(Math.round(timeTotal * 1000)) : ''
|
|
232
|
+
const r = parseCurlWriteout(stdout, code)
|
|
194
233
|
resolve({
|
|
195
|
-
reachable,
|
|
196
|
-
proxyAuthRequired,
|
|
234
|
+
reachable: r.reachable,
|
|
235
|
+
proxyAuthRequired: r.proxyAuthRequired,
|
|
197
236
|
sent: 1,
|
|
198
|
-
received: reachable ? 1 : 0,
|
|
199
|
-
loss: reachable ? 0 : 1,
|
|
200
|
-
lossPercent: reachable ? 0 : 100,
|
|
201
|
-
rttMin: rttMs,
|
|
202
|
-
rttAvg: rttMs,
|
|
203
|
-
rttMax: rttMs,
|
|
204
|
-
raw:
|
|
237
|
+
received: r.reachable ? 1 : 0,
|
|
238
|
+
loss: r.reachable ? 0 : 1,
|
|
239
|
+
lossPercent: r.reachable ? 0 : 100,
|
|
240
|
+
rttMin: r.rttMs,
|
|
241
|
+
rttAvg: r.rttMs,
|
|
242
|
+
rttMax: r.rttMs,
|
|
243
|
+
raw: r.raw,
|
|
205
244
|
})
|
|
206
245
|
})
|
|
207
246
|
child.on('error', () => {
|
|
@@ -228,7 +267,7 @@ function checkByWget(url, proxy) {
|
|
|
228
267
|
args.push(url)
|
|
229
268
|
let child
|
|
230
269
|
try {
|
|
231
|
-
child = spawn('wget', args, { stdio: ['pipe', 'pipe', 'pipe']
|
|
270
|
+
child = spawn('wget', args, { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
232
271
|
} catch {
|
|
233
272
|
resolve({
|
|
234
273
|
reachable: false, sent: 1, received: 0, loss: 1,
|
|
@@ -302,7 +341,7 @@ async function checkReachability(address, tool, count, proxy) {
|
|
|
302
341
|
return { reachable: false, sent: 0, received: 0, loss: 0, lossPercent: 0, rttMin: '', rttAvg: '', rttMax: '', raw: 'No tool available' }
|
|
303
342
|
}
|
|
304
343
|
|
|
305
|
-
export { checkReachability, extractHostPort, httpReachable }
|
|
344
|
+
export { checkReachability, extractHostPort, httpReachable, parseCurlWriteout, buildCurlArgs }
|
|
306
345
|
|
|
307
346
|
export class PingGroup {
|
|
308
347
|
constructor() {
|
|
@@ -311,7 +350,7 @@ export class PingGroup {
|
|
|
311
350
|
}
|
|
312
351
|
|
|
313
352
|
async run(argv) {
|
|
314
|
-
const parsed = parseFlags(argv)
|
|
353
|
+
const parsed = parseFlags(argv, { booleans: ['detail'] })
|
|
315
354
|
const [target] = parsed.positional
|
|
316
355
|
|
|
317
356
|
if (!target || target === '-h' || target === '--help' || target === 'help') {
|
|
@@ -327,7 +366,7 @@ export class PingGroup {
|
|
|
327
366
|
|
|
328
367
|
const count = parseInt(parsed.flags.c || parsed.flags.count || '4', 10)
|
|
329
368
|
const n = (isNaN(count) || count < 1) ? 4 : count
|
|
330
|
-
const detail = !!(parsed.flags.
|
|
369
|
+
const detail = !!(parsed.flags.detail)
|
|
331
370
|
|
|
332
371
|
let proxy = parsed.flags.proxy || parsed.flags.p || ''
|
|
333
372
|
if (proxy === 'true') proxy = ''
|
|
@@ -342,7 +381,9 @@ export class PingGroup {
|
|
|
342
381
|
}
|
|
343
382
|
|
|
344
383
|
if (target === 'source') {
|
|
345
|
-
|
|
384
|
+
console.error('[opm] Example check commands have moved into `opm ping help`.')
|
|
385
|
+
this.printHelp()
|
|
386
|
+
return
|
|
346
387
|
}
|
|
347
388
|
|
|
348
389
|
if (proxy) {
|
|
@@ -434,58 +475,6 @@ export class PingGroup {
|
|
|
434
475
|
output(detail ? items : items.map(compact))
|
|
435
476
|
}
|
|
436
477
|
|
|
437
|
-
async _pingSource() {
|
|
438
|
-
const url = getActiveRegistry('npm')
|
|
439
|
-
const activeProxy = getActiveProxy()
|
|
440
|
-
const tool = shell.getNetProbeTool()
|
|
441
|
-
|
|
442
|
-
if (!tool) {
|
|
443
|
-
console.error('[opm] No network check tool available (ping/curl/wget).')
|
|
444
|
-
return
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const cleanProxy = activeProxy.replace(/^(https?:\/\/)[^@]*@/, '$1')
|
|
448
|
-
const authProxy = cleanProxy.replace(/^(https?:\/\/)/, '$1user:password@')
|
|
449
|
-
|
|
450
|
-
if (tool === 'curl') {
|
|
451
|
-
console.log(`# tool: ${tool}
|
|
452
|
-
# Windows (PowerShell)
|
|
453
|
-
curl.exe -k --max-time 10 ${url}
|
|
454
|
-
curl.exe -k --max-time 10 --proxy ${cleanProxy} ${url}
|
|
455
|
-
curl.exe -k --max-time 10 --proxy ${authProxy} ${url}
|
|
456
|
-
|
|
457
|
-
# Linux / macOS
|
|
458
|
-
curl -k --max-time 10 ${url}
|
|
459
|
-
curl -k --max-time 10 --proxy ${cleanProxy} ${url}
|
|
460
|
-
curl -k --max-time 10 --proxy ${authProxy} ${url}`)
|
|
461
|
-
return
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (tool === 'wget') {
|
|
465
|
-
console.log(`# tool: ${tool}
|
|
466
|
-
# Windows (PowerShell)
|
|
467
|
-
wget.exe -q --spider --timeout=10 ${url}
|
|
468
|
-
wget.exe -q --spider --timeout=10 -e use_proxy=yes -e https_proxy=${cleanProxy} ${url}
|
|
469
|
-
wget.exe -q --spider --timeout=10 -e use_proxy=yes -e https_proxy=${authProxy} ${url}
|
|
470
|
-
|
|
471
|
-
# Linux / macOS
|
|
472
|
-
wget -q --spider --timeout=10 ${url}
|
|
473
|
-
wget -q --spider --timeout=10 -e use_proxy=yes -e https_proxy=${cleanProxy} ${url}
|
|
474
|
-
wget -q --spider --timeout=10 -e use_proxy=yes -e https_proxy=${authProxy} ${url}`)
|
|
475
|
-
return
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (tool === 'ping') {
|
|
479
|
-
const { host } = extractHostPort(url)
|
|
480
|
-
console.log(`# tool: ${tool}
|
|
481
|
-
# Windows
|
|
482
|
-
ping -n 4 -w 10000 ${host}
|
|
483
|
-
|
|
484
|
-
# Linux / macOS
|
|
485
|
-
ping -c 4 -W 10 ${host}`)
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
478
|
printHelp() {
|
|
490
479
|
console.log(`
|
|
491
480
|
Usage: opm ping <host|proxy|registry|source> [options]
|
|
@@ -494,13 +483,13 @@ Actions:
|
|
|
494
483
|
ping <host> Check a host, IP, or URL for accessibility
|
|
495
484
|
ping proxy Check the active proxy address (from config)
|
|
496
485
|
ping registry Check all tools' active registry addresses in parallel
|
|
497
|
-
ping source
|
|
486
|
+
ping source Show this help (example system commands listed below)
|
|
498
487
|
|
|
499
488
|
Options:
|
|
500
489
|
-c, --count <n> Number of packets to send (ping only; default: 4)
|
|
501
490
|
-p, --proxy <url> Proxy address for the check (overrides config proxy.active;
|
|
502
491
|
routes curl/wget requests through the proxy and pre-validates usability)
|
|
503
|
-
|
|
492
|
+
--detail Output full JSON details (default: compact [{tool, address, proxy, reachable}])
|
|
504
493
|
-h, --help Show this help
|
|
505
494
|
|
|
506
495
|
Tool detection: curl > wget > ping (auto-detected)
|
|
@@ -514,11 +503,20 @@ Examples:
|
|
|
514
503
|
opm ping https://mirrors.tools.huawei.com/npm/
|
|
515
504
|
opm ping proxy
|
|
516
505
|
opm ping registry
|
|
517
|
-
opm ping source
|
|
518
506
|
opm ping 8.8.8.8 -c 2
|
|
519
|
-
opm ping proxy
|
|
520
|
-
opm ping registry
|
|
521
|
-
opm ping sample
|
|
507
|
+
opm ping proxy --detail
|
|
508
|
+
opm ping registry --detail
|
|
509
|
+
opm ping sample --detail
|
|
510
|
+
|
|
511
|
+
Manual check commands (for reference; run manually in your shell):
|
|
512
|
+
# curl (npm active registry, proxy variants)
|
|
513
|
+
curl -k --max-time 10 https://registry.npmjs.org/
|
|
514
|
+
curl -k --max-time 10 --proxy http://proxy:port https://registry.npmjs.org/
|
|
515
|
+
curl -k --max-time 10 --proxy http://user:password@proxy:port https://registry.npmjs.org/
|
|
516
|
+
# wget
|
|
517
|
+
wget -q --spider --timeout=10 https://registry.npmjs.org/
|
|
518
|
+
# ping
|
|
519
|
+
ping -c 4 -W 10 registry.npmjs.org
|
|
522
520
|
`)
|
|
523
521
|
}
|
|
524
522
|
}
|
package/src/cli/groups/proc.js
CHANGED
|
@@ -52,7 +52,7 @@ export class ProcGroup {
|
|
|
52
52
|
return
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const parsed = parseFlags(rest)
|
|
55
|
+
const parsed = parseFlags(rest, { booleans: ['f', 'force', 'exact'] })
|
|
56
56
|
|
|
57
57
|
switch (action) {
|
|
58
58
|
case 'info':
|
|
@@ -91,8 +91,8 @@ export class ProcGroup {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
async _search(parsed) {
|
|
94
|
-
const name = parsed.flags.
|
|
95
|
-
const port = parsed.flags.
|
|
94
|
+
const name = parsed.flags.name || ''
|
|
95
|
+
const port = parsed.flags.port || ''
|
|
96
96
|
const exact = parsed.flags.exact === 'true'
|
|
97
97
|
|
|
98
98
|
if (!name && !port) {
|
|
@@ -106,15 +106,15 @@ export class ProcGroup {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
async _port(parsed) {
|
|
109
|
-
const port = parsed.flags.
|
|
109
|
+
const port = parsed.flags.port || ''
|
|
110
110
|
const result = await listPorts(port || undefined)
|
|
111
111
|
output(result)
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
async _kill(parsed) {
|
|
115
115
|
const id = parsed.flags.i || parsed.flags.id || ''
|
|
116
|
-
const port = parsed.flags.
|
|
117
|
-
const name = parsed.flags.
|
|
116
|
+
const port = parsed.flags.port || ''
|
|
117
|
+
const name = parsed.flags.name || ''
|
|
118
118
|
const force = parsed.flags.f === 'true' || parsed.flags.force === 'true'
|
|
119
119
|
const yes = parsed.flags.y === 'true' || parsed.flags.yes === 'true'
|
|
120
120
|
const signal = force ? 'SIGKILL' : 'SIGTERM'
|
|
@@ -203,9 +203,9 @@ ${actionLines}
|
|
|
203
203
|
|
|
204
204
|
Options:
|
|
205
205
|
-n, --count <n> Number of processes (list, default: 10)
|
|
206
|
-
|
|
206
|
+
--name <name> Process name pattern (search/kill, fuzzy by default)
|
|
207
207
|
-b, --by <mem|cpu> Sort by memory or CPU (list, default: mem)
|
|
208
|
-
|
|
208
|
+
--port <port> Port number (search/kill/port)
|
|
209
209
|
-i, --id <pid> Process ID (kill)
|
|
210
210
|
--exact Exact match (search, default: fuzzy)
|
|
211
211
|
-f, --force Use SIGKILL instead of SIGTERM (kill)
|
|
@@ -216,14 +216,14 @@ Examples:
|
|
|
216
216
|
opm proc info
|
|
217
217
|
opm proc list
|
|
218
218
|
opm proc list -n 20 -b cpu
|
|
219
|
-
opm proc search
|
|
220
|
-
opm proc search
|
|
221
|
-
opm proc search
|
|
219
|
+
opm proc search --name node
|
|
220
|
+
opm proc search --name node --port 3000
|
|
221
|
+
opm proc search --name node --exact
|
|
222
222
|
opm proc port
|
|
223
|
-
opm proc port
|
|
223
|
+
opm proc port --port 8080
|
|
224
224
|
opm proc kill -i 1234
|
|
225
|
-
opm proc kill
|
|
226
|
-
opm proc kill
|
|
225
|
+
opm proc kill --port 8080 -f -y
|
|
226
|
+
opm proc kill --name node -y
|
|
227
227
|
opm proc tree
|
|
228
228
|
opm proc tree 1234
|
|
229
229
|
opm proc detail 1234
|
|
@@ -47,7 +47,7 @@ export class ToolManagerGroup {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async run(argv) {
|
|
50
|
-
const parsed = parseFlags(argv)
|
|
50
|
+
const parsed = parseFlags(argv, { booleans: ['force'] })
|
|
51
51
|
const [action, ...rest] = parsed.positional
|
|
52
52
|
|
|
53
53
|
if (!action || action === '-h' || action === '--help' || action === 'help') {
|
|
@@ -198,25 +198,21 @@ const out = execSync(this.cfg.versionCmd(), { encoding: 'utf8', shell: shell.she
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
/**
|
|
201
|
-
*
|
|
201
|
+
* 列出工具已安装的版本(数组格式输出)
|
|
202
202
|
* @returns {void}
|
|
203
203
|
* @private
|
|
204
204
|
*/
|
|
205
205
|
_list() {
|
|
206
|
-
if (!this.cfg.isInstalled()) {
|
|
207
|
-
output(
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
if (!this.cfg.listCmd) {
|
|
211
|
-
output({ name: this.name, count: 0, versions: [] })
|
|
206
|
+
if (!this.cfg.isInstalled() || !this.cfg.listCmd) {
|
|
207
|
+
output([])
|
|
212
208
|
return
|
|
213
209
|
}
|
|
214
210
|
try {
|
|
215
211
|
const out = execSync(this.cfg.listCmd(), { encoding: 'utf8', shell: shell.shellForExec(this.cfg.needsBash) })
|
|
216
212
|
const versions = this.cfg.parseList ? this.cfg.parseList(out) : []
|
|
217
|
-
output(
|
|
213
|
+
output(versions)
|
|
218
214
|
} catch {
|
|
219
|
-
output(
|
|
215
|
+
output([])
|
|
220
216
|
}
|
|
221
217
|
}
|
|
222
218
|
|
package/src/cli/helpers/args.js
CHANGED
|
@@ -1,49 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* opm 参数解析器 — 从 @wwkit/shared re-export
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* --flag=value 长标志(等号形式)
|
|
7
|
-
* -f value 短标志(空格分隔)
|
|
8
|
-
* -f=value 短标志(等号形式)
|
|
9
|
-
* positional 位置参数(不以 - 开头)
|
|
10
|
-
*
|
|
11
|
-
* @param {string[]} argv - 原始参数数组
|
|
12
|
-
* @returns {{ flags: Record<string, string>, positional: string[] }}
|
|
4
|
+
* parseFlags 已提升到 @wwkit/shared(packages/shared/src/args.js),
|
|
5
|
+
* 此文件保留 re-export 以维持 opm 内部 import 路径不变(向后兼容)。
|
|
13
6
|
*/
|
|
14
|
-
export
|
|
15
|
-
const flags = {}
|
|
16
|
-
const positional = []
|
|
17
|
-
|
|
18
|
-
for (let i = 0; i < argv.length; i++) {
|
|
19
|
-
const arg = argv[i]
|
|
20
|
-
|
|
21
|
-
if (arg.startsWith('--') && arg.includes('=')) {
|
|
22
|
-
const idx = arg.indexOf('=')
|
|
23
|
-
flags[arg.slice(2, idx)] = arg.slice(idx + 1)
|
|
24
|
-
} else if (arg.startsWith('--')) {
|
|
25
|
-
const next = argv[i + 1]
|
|
26
|
-
if (next !== undefined && !next.startsWith('-')) {
|
|
27
|
-
flags[arg.slice(2)] = next
|
|
28
|
-
i++
|
|
29
|
-
} else {
|
|
30
|
-
flags[arg.slice(2)] = 'true'
|
|
31
|
-
}
|
|
32
|
-
} else if (arg.startsWith('-') && arg.includes('=')) {
|
|
33
|
-
const idx = arg.indexOf('=')
|
|
34
|
-
flags[arg.slice(1, idx)] = arg.slice(idx + 1)
|
|
35
|
-
} else if (arg.startsWith('-') && arg.length > 1) {
|
|
36
|
-
const next = argv[i + 1]
|
|
37
|
-
if (next !== undefined && !next.startsWith('-')) {
|
|
38
|
-
flags[arg.slice(1)] = next
|
|
39
|
-
i++
|
|
40
|
-
} else {
|
|
41
|
-
flags[arg.slice(1)] = 'true'
|
|
42
|
-
}
|
|
43
|
-
} else {
|
|
44
|
-
positional.push(arg)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return { flags, positional }
|
|
49
|
-
}
|
|
7
|
+
export { parseFlags, DEFAULT_BOOLEAN_FLAGS } from '@wwkit/shared'
|
package/src/cli/index.js
CHANGED
|
@@ -101,6 +101,11 @@ if (groupName === 'version' || groupName === '-v' || groupName === '--version')
|
|
|
101
101
|
return
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// 剪切板快捷命令 → share 组的 copy/paste(id=clipboard)
|
|
105
|
+
if (groupName === 'copy' || groupName === 'paste') {
|
|
106
|
+
return GROUPS.share.run([groupName])
|
|
107
|
+
}
|
|
108
|
+
|
|
104
109
|
const group = GROUPS[groupName]
|
|
105
110
|
if (!group) {
|
|
106
111
|
console.error(`Unknown command: ${groupName}`)
|
|
@@ -126,13 +131,15 @@ Usage: opm <command> [args] [options]
|
|
|
126
131
|
|
|
127
132
|
Commands:
|
|
128
133
|
${groupLines}
|
|
134
|
+
copy Copy system clipboard to the share server (id=clipboard)
|
|
135
|
+
paste Paste the server clipboard back to system clipboard
|
|
129
136
|
version Show opm version
|
|
130
137
|
upgrade Upgrade opm itself (npm install -g @wwkit/opm@latest)
|
|
131
138
|
doc Build docs HTML and open in browser
|
|
132
139
|
|
|
133
140
|
Options:
|
|
134
|
-
-g, --global Global operation (
|
|
135
|
-
-p, --proxy <url> Proxy address (search/info/versions/install/upgrade)
|
|
141
|
+
-g, --global Global operation (package managers only: npm global node_modules; pip system Python)
|
|
142
|
+
-p, --proxy <url> Proxy address (network commands: search/info/versions/install/upgrade)
|
|
136
143
|
-h, --help Show help
|
|
137
144
|
|
|
138
145
|
Run "opm <command> help" for command details.
|
|
@@ -143,6 +150,7 @@ Examples:
|
|
|
143
150
|
opm config Show common config (proxy + registries)
|
|
144
151
|
opm opencode clear Clean up opencode sessions before today
|
|
145
152
|
opm opencode clear --days 7 Keep sessions from the last 7 days
|
|
153
|
+
opm opencode clear --days 0.5 Keep sessions from the last 12 hours
|
|
146
154
|
`)
|
|
147
155
|
}
|
|
148
156
|
}
|
package/src/harnesses/base.js
CHANGED
|
@@ -211,7 +211,8 @@ export class HarnessGroup {
|
|
|
211
211
|
|
|
212
212
|
async _clear(rest) {
|
|
213
213
|
const parsed = parseFlags(rest)
|
|
214
|
-
|
|
214
|
+
// -h 统一表示 help(各 harness clear 实现不再占用 -h 作为业务参数)
|
|
215
|
+
if (parsed.flags.help === 'true' || parsed.flags.h === 'true') {
|
|
215
216
|
this.printHelp()
|
|
216
217
|
return
|
|
217
218
|
}
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* 5. 删除过期会话(event_sequence + session)并 VACUUM
|
|
10
10
|
* 6. 清空日志、报告清理结果
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
12
|
+
* 保留策略参数(二选一,互斥;缺省 --days 1 即只保留今天):
|
|
13
|
+
* --days <n> 保留最近 n 天(整数对齐本地零点含今天;小数 = 小时级滚动窗口,如 0.5 = 12 小时)
|
|
14
|
+
* -n, --keep <n> 保留最近 n 个会话(按 time_created 倒序取最新 n 个)
|
|
15
|
+
* -h/--help 显示帮助
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import fs from 'node:fs'
|
|
@@ -35,15 +35,19 @@ export function startOfDayLocal(ts) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* 解析
|
|
38
|
+
* 解析 --days | -n/--keep 的计数参数
|
|
39
39
|
* @param {string|undefined} value
|
|
40
|
-
* @param {string} name - 参数名(days/
|
|
41
|
-
* @
|
|
40
|
+
* @param {string} name - 参数名(days/keep),用于报错提示
|
|
41
|
+
* @param {{ allowFraction?: boolean }} [opts] - allowFraction=true 时接受正小数(days 支持小时级)
|
|
42
|
+
* @returns {number|null} 未提供返回 null,否则返回正数
|
|
42
43
|
*/
|
|
43
|
-
export function parseCount(value, name) {
|
|
44
|
+
export function parseCount(value, name, { allowFraction = false } = {}) {
|
|
44
45
|
if (value === undefined) return null
|
|
45
46
|
const n = Number(value)
|
|
46
|
-
if (!Number.
|
|
47
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
48
|
+
throw new Error(`invalid --${name} value: "${value}" (expect ${allowFraction ? 'a positive number' : 'a positive integer'})`)
|
|
49
|
+
}
|
|
50
|
+
if (!allowFraction && !Number.isInteger(n)) {
|
|
47
51
|
throw new Error(`invalid --${name} value: "${value}" (expect a positive integer)`)
|
|
48
52
|
}
|
|
49
53
|
return n
|
|
@@ -51,18 +55,18 @@ export function parseCount(value, name) {
|
|
|
51
55
|
|
|
52
56
|
/**
|
|
53
57
|
* 计算时间清理边界(毫秒)
|
|
54
|
-
* days: 最近 n 天,对齐本地零点(含今天),边界 = 今天零点 - (n-1)
|
|
55
|
-
*
|
|
56
|
-
* @param {{ days?: number|null
|
|
58
|
+
* days: 整数 = 最近 n 天,对齐本地零点(含今天),边界 = 今天零点 - (n-1) 天;
|
|
59
|
+
* 小数 = 小时级滚动窗口,边界 = now - n*24h(如 0.5 = 最近 12 小时)
|
|
60
|
+
* @param {{ days?: number|null }} opts
|
|
57
61
|
* @returns {number}
|
|
58
62
|
*/
|
|
59
|
-
export function computeBoundary({ days = null
|
|
63
|
+
export function computeBoundary({ days = null } = {}) {
|
|
60
64
|
const now = Date.now()
|
|
61
65
|
if (days !== null) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return now -
|
|
66
|
+
if (Number.isInteger(days)) {
|
|
67
|
+
return startOfDayLocal(now) - (days - 1) * 86400000
|
|
68
|
+
}
|
|
69
|
+
return now - days * 86400000
|
|
66
70
|
}
|
|
67
71
|
return startOfDayLocal(now)
|
|
68
72
|
}
|
|
@@ -148,17 +152,16 @@ export async function runClear(argv) {
|
|
|
148
152
|
const backup = flags.backup === 'true'
|
|
149
153
|
const dbPath = flags.db || getDbPath()
|
|
150
154
|
|
|
151
|
-
const days = parseCount(flags.days
|
|
152
|
-
const
|
|
153
|
-
const num = parseCount(flags.num ?? flags.n, 'num')
|
|
155
|
+
const days = parseCount(flags.days, 'days', { allowFraction: true })
|
|
156
|
+
const num = parseCount(flags.keep ?? flags.num ?? flags.n, 'keep')
|
|
154
157
|
|
|
155
|
-
const given = ['days', '
|
|
158
|
+
const given = ['days', 'keep'].filter((k) => ({ days, keep: num })[k] !== null)
|
|
156
159
|
if (given.length > 1) {
|
|
157
|
-
throw new Error('--days / --
|
|
160
|
+
throw new Error('--days / --keep are mutually exclusive')
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
const mode = given[0] || 'days'
|
|
161
|
-
const count = mode === 'days' ? (days ?? 1) :
|
|
164
|
+
const count = mode === 'days' ? (days ?? 1) : num
|
|
162
165
|
|
|
163
166
|
if (!fs.existsSync(dbPath)) {
|
|
164
167
|
throw new Error(`opencode database not found: ${dbPath}`)
|
|
@@ -170,15 +173,15 @@ export async function runClear(argv) {
|
|
|
170
173
|
let scopeDesc
|
|
171
174
|
let boundary = null
|
|
172
175
|
|
|
173
|
-
if (mode === '
|
|
176
|
+
if (mode === 'keep') {
|
|
174
177
|
const all = driver.listSessionsAsc()
|
|
175
178
|
const keep = Math.min(count, all.length)
|
|
176
179
|
rows = all.slice(0, all.length - keep)
|
|
177
180
|
scopeDesc = `仅保留最新 ${keep} 个会话`
|
|
178
181
|
} else {
|
|
179
|
-
boundary = computeBoundary({ days
|
|
182
|
+
boundary = computeBoundary({ days })
|
|
180
183
|
rows = driver.listBefore(boundary)
|
|
181
|
-
scopeDesc =
|
|
184
|
+
scopeDesc = `删除最近 ${count} 天(${formatLocal(boundary)})之前的会话`
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
console.log(`==> DB: ${dbPath}`)
|
|
@@ -93,8 +93,7 @@ export const opencodeHarness = {
|
|
|
93
93
|
getLogPath,
|
|
94
94
|
clear: runClear,
|
|
95
95
|
clearOptions: [
|
|
96
|
-
['-d, --days <n>', 'Keep sessions from the last n days (default 1)'],
|
|
97
|
-
['-h, --hours <n>', 'Keep sessions from the last n hours'],
|
|
96
|
+
['-d, --days <n>', 'Keep sessions from the last n days (default 1); integer = aligned to local midnight, fraction = hour window (0.5 = 12h)'],
|
|
98
97
|
['-n, --num <n>', 'Keep the most recent n sessions'],
|
|
99
98
|
['--dry-run', 'Preview only, do not modify anything'],
|
|
100
99
|
['-y, --yes', 'Skip the confirmation prompt'],
|
package/src/managers/apt.js
CHANGED
|
@@ -349,6 +349,22 @@ export class AptManager extends PackageManager {
|
|
|
349
349
|
return { action: 'cache cleaned' }
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
/**
|
|
353
|
+
* 列出系统已安装包(系统级管理器,无项目作用域)
|
|
354
|
+
* @returns {Promise<{ name: string, version: string }[]>}
|
|
355
|
+
*/
|
|
356
|
+
async listInstalled(opts = {}) {
|
|
357
|
+
if (opts.global === false) return []
|
|
358
|
+
const out = this._exec(['list', '--installed'], { allowNonZero: true })
|
|
359
|
+
const items = []
|
|
360
|
+
// 行格式: name/arch version status
|
|
361
|
+
for (const line of out.split('\n')) {
|
|
362
|
+
const m = line.trim().match(/^(\S+?)\/\S+\s+(\S+)\s+\S*installed/)
|
|
363
|
+
if (m) items.push({ name: m[1], version: m[2] })
|
|
364
|
+
}
|
|
365
|
+
return items
|
|
366
|
+
}
|
|
367
|
+
|
|
352
368
|
/**
|
|
353
369
|
* 查询 apt 安装路径和包目录
|
|
354
370
|
* @returns {{ name: string, install: string, listsDir: string, sourcesList: string, sourcesDir: string }}
|
package/src/managers/brew.js
CHANGED
|
@@ -335,10 +335,29 @@ export class BrewManager extends PackageManager {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
async cleanCache() {
|
|
338
|
-
this._execInherit(['cleanup', '-s'
|
|
338
|
+
this._execInherit(['cleanup', '-s'])
|
|
339
339
|
return { action: 'cache cleaned' }
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
/**
|
|
343
|
+
* 列出已安装的 Homebrew 包(系统级,无项目作用域)
|
|
344
|
+
* @returns {Promise<{ name: string, version: string }[]>}
|
|
345
|
+
*/
|
|
346
|
+
async listInstalled(opts = {}) {
|
|
347
|
+
if (opts.global === false) return []
|
|
348
|
+
const out = this._exec(['list', '--versions'], { allowNonZero: true })
|
|
349
|
+
const items = []
|
|
350
|
+
// 行格式: name v1 v2 ...(多版本取最新)
|
|
351
|
+
for (const line of out.split('\n')) {
|
|
352
|
+
const m = line.trim().match(/^(\S+)\s+(.+)$/)
|
|
353
|
+
if (m) {
|
|
354
|
+
const versions = m[2].trim().split(/\s+/)
|
|
355
|
+
items.push({ name: m[1], version: versions[versions.length - 1] })
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return items
|
|
359
|
+
}
|
|
360
|
+
|
|
342
361
|
/**
|
|
343
362
|
* 查询 brew 安装路径和包目录
|
|
344
363
|
* @returns {{ name: string, install: string, prefix: string, cellar: string }}
|