@wwkit/opm 1.0.18 → 1.0.20
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/nvm.js +1 -1
- package/src/cli/groups/overlay.js +7 -7
- package/src/cli/groups/package.js +62 -20
- package/src/cli/groups/phpenv.js +2 -2
- package/src/cli/groups/ping.js +83 -85
- 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 +24 -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/node.js +1 -1
- package/src/managers/npm.js +12 -0
- package/src/managers/php.js +2 -2
- package/src/managers/pip.js +16 -0
- package/src/managers/runtime.js +1 -1
- 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 +327 -139
- package/src/tools/share/server.js +21 -5
- package/src/tools/webwork/index.js +6 -2
package/src/tools/share/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* opm share 命令组 — 文本分享服务
|
|
3
3
|
*
|
|
4
|
-
* 命令:upload / zip / get /
|
|
4
|
+
* 命令:upload / zip / get / unzip / sync / open / list / delete / clear / config / server / help
|
|
5
5
|
* 服务地址由 config.share.server.active 解析(不支持命令行 -s 参数)。
|
|
6
6
|
* 需要服务调用的命令执行前先 ping 验证可达性;
|
|
7
7
|
* 若 active=local 且服务未启动,自动后台启动后再执行。
|
|
8
8
|
* server 子命令:start(后台启动)/ stop(停止)/ status(查询状态)。
|
|
9
|
-
*
|
|
9
|
+
* 未设 -i 时 id 默认 opm_share(临时槽,下次无 -i 上传即覆盖);zip 上传后用 unzip/sync 取回。
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import fs from 'node:fs'
|
|
@@ -14,23 +14,46 @@ import path from 'node:path'
|
|
|
14
14
|
import os from 'node:os'
|
|
15
15
|
import { spawn, spawnSync } from 'node:child_process'
|
|
16
16
|
import { parseFlags } from '../../cli/helpers/args.js'
|
|
17
|
+
import { JSON5 } from '@wwkit/shared'
|
|
17
18
|
import { getConfig, getSection, getUserConfigDir } from '../../config.js'
|
|
18
19
|
import { output } from '../../formatter.js'
|
|
19
|
-
import { getShareServer, pingServer, uploadShare, getShare,
|
|
20
|
+
import { getShareServer, pingServer, uploadShare, getShare, getHtmlUrl, listShares, deleteShare, clearShares } from './client.js'
|
|
20
21
|
import { startServer } from './server.js'
|
|
22
|
+
import { readClipboard, writeClipboard } from '../clipboard.js'
|
|
21
23
|
|
|
22
24
|
const DEFAULT_PASSWORD = '0000'
|
|
23
25
|
const ZIP_TMP_PREFIX = 'opm-share-'
|
|
26
|
+
const CLIPBOARD_ID = 'clipboard'
|
|
27
|
+
const DEFAULT_TEMP_ID = 'opm_share'
|
|
24
28
|
|
|
25
29
|
/**
|
|
26
|
-
*
|
|
30
|
+
* 将 content 美化为可读字符串:若为 JSON5 可解析文本则返回 2 空格缩进的 JSON,否则原样返回
|
|
31
|
+
* 用于默认输出(直接打印到 stdout,保留真实换行)
|
|
27
32
|
* @param {string} content
|
|
28
33
|
* @returns {string}
|
|
29
34
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
function formatContent(content) {
|
|
36
|
+
if (typeof content !== 'string' || !content.trim()) return content || ''
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(JSON5.parse(content), null, 2)
|
|
39
|
+
} catch {
|
|
40
|
+
return content
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 将 content 解析为结构化值:若为 JSON5 可解析则返回解析后的对象/数组,否则返回原字符串
|
|
46
|
+
* 用于 --detail 模式,使 JSON content 在 dict 中以嵌套结构显示而非转义字符串
|
|
47
|
+
* @param {string} content
|
|
48
|
+
* @returns {*}
|
|
49
|
+
*/
|
|
50
|
+
function parseContentJson(content) {
|
|
51
|
+
if (typeof content !== 'string' || !content.trim()) return content
|
|
52
|
+
try {
|
|
53
|
+
return JSON5.parse(content)
|
|
54
|
+
} catch {
|
|
55
|
+
return content
|
|
56
|
+
}
|
|
34
57
|
}
|
|
35
58
|
|
|
36
59
|
/**
|
|
@@ -45,6 +68,25 @@ function expandTilde(p) {
|
|
|
45
68
|
return p
|
|
46
69
|
}
|
|
47
70
|
|
|
71
|
+
/**
|
|
72
|
+
* 计算跨系统可还原的 sourcePath:保留 ~ 锚点
|
|
73
|
+
* - 入参以 ~/ 开头(用户引号保留)→ 原样
|
|
74
|
+
* - 解析后绝对路径落在 home 下 → 反推 ~
|
|
75
|
+
* - home 之外(绝对/相对)→ 存绝对路径(无 ~ 锚点,sync 回退默认目录)
|
|
76
|
+
* @param {string} inputPath - 原始入参
|
|
77
|
+
* @param {string} resolved - path.resolve 后的绝对路径
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function tildefy(inputPath, resolved) {
|
|
81
|
+
if (inputPath.startsWith('~/')) return inputPath
|
|
82
|
+
const home = os.homedir()
|
|
83
|
+
if (home && resolved.startsWith(home)) {
|
|
84
|
+
const rest = resolved.slice(home.length)
|
|
85
|
+
return '~' + rest
|
|
86
|
+
}
|
|
87
|
+
return resolved
|
|
88
|
+
}
|
|
89
|
+
|
|
48
90
|
/**
|
|
49
91
|
* 浏览器打开 URL
|
|
50
92
|
* @param {string} url
|
|
@@ -102,16 +144,19 @@ function isProcessAlive(pid) {
|
|
|
102
144
|
}
|
|
103
145
|
|
|
104
146
|
const ACTIONS = {
|
|
105
|
-
upload: { desc: 'Upload text
|
|
106
|
-
zip: { desc: 'Upload file/folder as
|
|
107
|
-
get: { desc: 'Get
|
|
108
|
-
|
|
109
|
-
|
|
147
|
+
upload: { desc: 'Upload text (positional string OR existing file path, auto-detected; -f file; stdin; -i for custom id; default id=opm_share)' },
|
|
148
|
+
zip: { desc: 'Upload file/folder as zip (preserves ~ -anchored sourcePath; -i for custom id; default id=opm_share)' },
|
|
149
|
+
get: { desc: 'Get text content (id optional; defaults to opm_share; --detail for full dict; -o file to save)' },
|
|
150
|
+
unzip: { desc: 'Download and extract a zip share <id> (dir optional, default ~/.config/opm/share/<id>/; --dry-run to preview)' },
|
|
151
|
+
sync: { desc: 'Sync a zip share <id> to its original ~ -anchored path (--dry-run to preview; fallback to default dir)' },
|
|
152
|
+
open: { desc: 'Open shared content in browser (id optional; defaults to opm_share)' },
|
|
110
153
|
list: { desc: 'List recent shares (-n count, default 10)' },
|
|
111
154
|
delete: { desc: 'Delete a share by id' },
|
|
112
|
-
clear: { desc: '
|
|
155
|
+
clear: { desc: 'Keep shares by days and/or count (--days n keeps last n days, --keep n keeps n most recent)' },
|
|
113
156
|
config: { desc: 'Show current share config section' },
|
|
114
157
|
server: { desc: 'Manage local server (start/stop/status)' },
|
|
158
|
+
copy: { desc: 'Copy system clipboard to server (id=clipboard; skips when clipboard is empty)' },
|
|
159
|
+
paste: { desc: 'Fetch server clipboard (id=clipboard) back into system clipboard and print' },
|
|
115
160
|
help: { desc: 'Show this help' },
|
|
116
161
|
}
|
|
117
162
|
|
|
@@ -129,7 +174,7 @@ export class ShareGroup {
|
|
|
129
174
|
return
|
|
130
175
|
}
|
|
131
176
|
|
|
132
|
-
const parsed = parseFlags(rest)
|
|
177
|
+
const parsed = parseFlags(rest, { booleans: ['detail'] })
|
|
133
178
|
|
|
134
179
|
switch (action) {
|
|
135
180
|
case 'upload':
|
|
@@ -138,8 +183,10 @@ export class ShareGroup {
|
|
|
138
183
|
return this._zip(parsed)
|
|
139
184
|
case 'get':
|
|
140
185
|
return this._get(parsed)
|
|
141
|
-
case '
|
|
142
|
-
return this.
|
|
186
|
+
case 'unzip':
|
|
187
|
+
return this._unzip(parsed)
|
|
188
|
+
case 'sync':
|
|
189
|
+
return this._sync(parsed)
|
|
143
190
|
case 'open':
|
|
144
191
|
return this._open(parsed)
|
|
145
192
|
case 'list':
|
|
@@ -152,6 +199,10 @@ export class ShareGroup {
|
|
|
152
199
|
return this._config(parsed)
|
|
153
200
|
case 'server':
|
|
154
201
|
return this._server(parsed)
|
|
202
|
+
case 'copy':
|
|
203
|
+
return this._copy(parsed)
|
|
204
|
+
case 'paste':
|
|
205
|
+
return this._paste(parsed)
|
|
155
206
|
default:
|
|
156
207
|
return this._upload(parseFlags([action, ...rest]))
|
|
157
208
|
}
|
|
@@ -182,17 +233,18 @@ export class ShareGroup {
|
|
|
182
233
|
}
|
|
183
234
|
|
|
184
235
|
/**
|
|
185
|
-
* 解析密码:-p/--password 参数 > 配置 share.password >
|
|
236
|
+
* 解析密码:-p/--password 参数 > 位置参数(get/open)> 配置 share.password > 默认 0000
|
|
186
237
|
* @param {object} parsed - parseFlags 结果
|
|
187
238
|
* @param {string} [positional] - 位置参数密码(get/open 的第二个位置参数)
|
|
188
239
|
* @returns {string}
|
|
189
240
|
*/
|
|
190
241
|
_resolvePassword(parsed, positional = '') {
|
|
191
|
-
const flag = parsed.flags.
|
|
242
|
+
const flag = parsed.flags.password || parsed.flags.p || ''
|
|
192
243
|
if (flag && flag !== 'true') return flag
|
|
244
|
+
if (positional) return positional
|
|
193
245
|
const cfgPassword = this._getShareConfig().password
|
|
194
246
|
if (cfgPassword) return cfgPassword
|
|
195
|
-
return
|
|
247
|
+
return DEFAULT_PASSWORD
|
|
196
248
|
}
|
|
197
249
|
|
|
198
250
|
/**
|
|
@@ -233,9 +285,8 @@ export class ShareGroup {
|
|
|
233
285
|
}
|
|
234
286
|
|
|
235
287
|
async _upload(parsed) {
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
const id = parsed.flags.i || parsed.flags.id || ''
|
|
288
|
+
const file = parsed.flags.file || parsed.flags.f || ''
|
|
289
|
+
const id = parsed.flags.i || parsed.flags.id || DEFAULT_TEMP_ID
|
|
239
290
|
const password = this._resolvePassword(parsed)
|
|
240
291
|
|
|
241
292
|
let content = ''
|
|
@@ -243,47 +294,60 @@ export class ShareGroup {
|
|
|
243
294
|
if (file) {
|
|
244
295
|
const resolved = path.resolve(file)
|
|
245
296
|
if (!fs.existsSync(resolved)) {
|
|
246
|
-
console.error(`File not found: ${resolved}`)
|
|
297
|
+
console.error(`[opm share] File not found: ${resolved}`)
|
|
247
298
|
process.exit(1)
|
|
248
299
|
}
|
|
249
300
|
content = fs.readFileSync(resolved, 'utf8')
|
|
250
301
|
} else if (parsed.positional.length > 0) {
|
|
251
|
-
|
|
302
|
+
const first = parsed.positional[0]
|
|
303
|
+
const expandedFirst = expandTilde(first)
|
|
304
|
+
if (parsed.positional.length === 1 && fs.existsSync(expandedFirst)) {
|
|
305
|
+
const stat = fs.statSync(expandedFirst)
|
|
306
|
+
if (stat.isFile()) {
|
|
307
|
+
content = fs.readFileSync(expandedFirst, 'utf8')
|
|
308
|
+
} else if (stat.isDirectory()) {
|
|
309
|
+
console.error(`[opm share] "${first}" is a directory. Use "opm share zip ${first}" to upload as zip.`)
|
|
310
|
+
process.exit(1)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (!content) {
|
|
314
|
+
content = parsed.positional.join(' ')
|
|
315
|
+
}
|
|
252
316
|
} else if (!process.stdin.isTTY) {
|
|
253
317
|
content = fs.readFileSync(0, 'utf8')
|
|
254
318
|
}
|
|
255
319
|
|
|
256
320
|
if (!content) {
|
|
257
|
-
console.error('Usage: opm share upload "<content>" [-
|
|
258
|
-
console.error(' Or: echo "..." | opm share upload [-
|
|
259
|
-
console.error(' Or: opm share "<content>" [-
|
|
321
|
+
console.error('Usage: opm share upload "<content>" [-f file] [-i id] [-p password]')
|
|
322
|
+
console.error(' Or: echo "..." | opm share upload [-i id] [-p password]')
|
|
323
|
+
console.error(' Or: opm share "<content>" [-i id] [-p password] (shorthand without "upload")')
|
|
324
|
+
console.error(' No -i → id defaults to "opm_share" (temporary slot, overwritten on next upload without -i)')
|
|
260
325
|
process.exit(1)
|
|
261
326
|
}
|
|
262
327
|
|
|
263
328
|
const baseUrl = await this._ensureReachable()
|
|
264
329
|
const result = await uploadShare(baseUrl, {
|
|
265
|
-
title: title || defaultTitle(content),
|
|
266
330
|
content,
|
|
267
331
|
password,
|
|
268
|
-
|
|
332
|
+
id,
|
|
269
333
|
})
|
|
270
334
|
output(result)
|
|
271
335
|
}
|
|
272
336
|
|
|
273
337
|
async _zip(parsed) {
|
|
274
|
-
const inputPath = parsed.positional[0] || parsed.flags.
|
|
275
|
-
const
|
|
276
|
-
const id = parsed.flags.i || parsed.flags.id || ''
|
|
338
|
+
const inputPath = parsed.positional[0] || parsed.flags.file || parsed.flags.f || ''
|
|
339
|
+
const id = parsed.flags.i || parsed.flags.id || DEFAULT_TEMP_ID
|
|
277
340
|
const password = this._resolvePassword(parsed)
|
|
278
341
|
|
|
279
342
|
if (!inputPath) {
|
|
280
|
-
console.error('Usage: opm share zip <path> [-
|
|
343
|
+
console.error('Usage: opm share zip <path> [-i id] [-p password]')
|
|
344
|
+
console.error(' No -i → id defaults to "opm_share" (temporary slot, overwritten on next upload without -i)')
|
|
281
345
|
process.exit(1)
|
|
282
346
|
}
|
|
283
347
|
|
|
284
|
-
const resolved = path.resolve(inputPath)
|
|
348
|
+
const resolved = path.resolve(expandTilde(inputPath))
|
|
285
349
|
if (!fs.existsSync(resolved)) {
|
|
286
|
-
console.error(`Path not found: ${resolved}`)
|
|
350
|
+
console.error(`[opm share] Path not found: ${resolved}`)
|
|
287
351
|
process.exit(1)
|
|
288
352
|
}
|
|
289
353
|
|
|
@@ -292,11 +356,13 @@ export class ShareGroup {
|
|
|
292
356
|
if (isDir) {
|
|
293
357
|
const entries = fs.readdirSync(resolved)
|
|
294
358
|
if (entries.length === 0) {
|
|
295
|
-
console.error(`Directory is empty: ${resolved}`)
|
|
359
|
+
console.error(`[opm share] Directory is empty: ${resolved}`)
|
|
296
360
|
process.exit(1)
|
|
297
361
|
}
|
|
298
362
|
}
|
|
299
363
|
|
|
364
|
+
const sourcePath = tildefy(inputPath, resolved)
|
|
365
|
+
|
|
300
366
|
const baseName = path.basename(resolved)
|
|
301
367
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), ZIP_TMP_PREFIX))
|
|
302
368
|
const zipPath = path.join(tmpDir, `${baseName}.zip`)
|
|
@@ -309,7 +375,7 @@ export class ShareGroup {
|
|
|
309
375
|
|
|
310
376
|
if (zipResult.status !== 0) {
|
|
311
377
|
const stderr = zipResult.stderr ? zipResult.stderr.toString().trim() : 'unknown error'
|
|
312
|
-
console.error(`Zip failed: ${stderr}`)
|
|
378
|
+
console.error(`[opm share] Zip failed: ${stderr}`)
|
|
313
379
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
314
380
|
process.exit(1)
|
|
315
381
|
}
|
|
@@ -321,64 +387,178 @@ export class ShareGroup {
|
|
|
321
387
|
|
|
322
388
|
const baseUrl = await this._ensureReachable()
|
|
323
389
|
const result = await uploadShare(baseUrl, {
|
|
324
|
-
title:
|
|
390
|
+
title: `${baseName}.zip`,
|
|
325
391
|
content,
|
|
326
392
|
password,
|
|
327
393
|
type: 'zip',
|
|
328
394
|
filename: `${baseName}.zip`,
|
|
329
|
-
|
|
395
|
+
sourcePath,
|
|
396
|
+
id,
|
|
330
397
|
})
|
|
331
398
|
output(result)
|
|
332
399
|
}
|
|
333
400
|
|
|
334
401
|
async _get(parsed) {
|
|
335
|
-
const id = parsed.positional[0] ||
|
|
402
|
+
const id = parsed.positional[0] || DEFAULT_TEMP_ID
|
|
336
403
|
const password = this._resolvePassword(parsed, parsed.positional[1] || '')
|
|
404
|
+
const detail = parsed.flags.detail === 'true' || !!parsed.flags.detail
|
|
405
|
+
const outputFile = parsed.flags.output || parsed.flags.o || ''
|
|
337
406
|
const extractDir = parsed.flags.d || parsed.flags.dir || ''
|
|
338
407
|
|
|
408
|
+
if (extractDir) {
|
|
409
|
+
console.error('[opm share] "get -d/--dir" is deprecated. Use "opm share unzip <id> <dir>" instead.')
|
|
410
|
+
process.exit(1)
|
|
411
|
+
}
|
|
412
|
+
|
|
339
413
|
const baseUrl = await this._ensureReachable()
|
|
340
|
-
const result = id
|
|
341
|
-
? await getShare(baseUrl, id, password)
|
|
342
|
-
: await getLatestShare(baseUrl, password)
|
|
414
|
+
const result = await getShare(baseUrl, id, password)
|
|
343
415
|
|
|
344
|
-
if (
|
|
345
|
-
|
|
416
|
+
if (detail) {
|
|
417
|
+
output({ ...result, content: parseContentJson(result.content) })
|
|
346
418
|
return
|
|
347
419
|
}
|
|
348
420
|
|
|
349
|
-
|
|
350
|
-
|
|
421
|
+
if ((result.type || 'text') === 'zip') {
|
|
422
|
+
console.error(`[opm share] This is a zip share. Use "opm share unzip ${id || result.id}" to extract, or "opm share get ${id || result.id} --detail" for metadata.`)
|
|
423
|
+
process.exit(1)
|
|
424
|
+
}
|
|
351
425
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
426
|
+
if (outputFile) {
|
|
427
|
+
const resolved = path.resolve(expandTilde(outputFile))
|
|
428
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
429
|
+
const buf = Buffer.from(formatContent(result.content || ''), 'utf8')
|
|
430
|
+
fs.writeFileSync(resolved, buf)
|
|
431
|
+
console.error(`[opm share] Wrote ${resolved} (${buf.length} bytes)`)
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const formatted = formatContent(result.content || '')
|
|
436
|
+
process.stdout.write(formatted)
|
|
437
|
+
if (!formatted.endsWith('\n')) {
|
|
438
|
+
process.stdout.write('\n')
|
|
439
|
+
}
|
|
440
|
+
}
|
|
355
441
|
|
|
442
|
+
/**
|
|
443
|
+
* 解码 zip content 并解压到目标目录(或 dry-run 时解压到临时目录仅列条目)
|
|
444
|
+
* @param {object} result - getShare 返回的 share 对象
|
|
445
|
+
* @param {string} targetDir - 解压目标目录(绝对路径)
|
|
446
|
+
* @param {boolean} dryRun - 仅预览,不落盘
|
|
447
|
+
* @returns {Promise<{files: Array, targetDir: string, extracted: boolean}>}
|
|
448
|
+
*/
|
|
449
|
+
async _extractZipTo(result, targetDir, dryRun) {
|
|
356
450
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), ZIP_TMP_PREFIX))
|
|
357
451
|
const zipPath = path.join(tmpDir, result.filename || `${result.id}.zip`)
|
|
452
|
+
try {
|
|
453
|
+
fs.writeFileSync(zipPath, Buffer.from(result.content, 'base64'))
|
|
454
|
+
const { default: extractZip } = await import('extract-zip')
|
|
455
|
+
const extractTarget = dryRun ? tmpDir : targetDir
|
|
456
|
+
if (!dryRun) fs.mkdirSync(targetDir, { recursive: true })
|
|
457
|
+
await extractZip(zipPath, { dir: extractTarget })
|
|
458
|
+
fs.unlinkSync(zipPath)
|
|
459
|
+
const files = this._listTopLevel(extractTarget)
|
|
460
|
+
return { files, targetDir, extracted: !dryRun }
|
|
461
|
+
} catch (err) {
|
|
462
|
+
throw new Error(`Extract failed: ${err.message}`)
|
|
463
|
+
} finally {
|
|
464
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async _unzip(parsed) {
|
|
469
|
+
const id = parsed.positional[0] || ''
|
|
470
|
+
const dir = parsed.positional[1] || ''
|
|
471
|
+
const password = this._resolvePassword(parsed)
|
|
472
|
+
const dryRun = parsed.flags['dry-run'] === 'true'
|
|
473
|
+
|
|
474
|
+
if (!id) {
|
|
475
|
+
console.error('Usage: opm share unzip <id> [dir] [--dry-run] [-p password]')
|
|
476
|
+
console.error(' <id> is required (zip share id to extract). Default dir: ~/.config/opm/share/<id>/')
|
|
477
|
+
process.exit(1)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const baseUrl = await this._ensureReachable()
|
|
481
|
+
const result = await getShare(baseUrl, id, password)
|
|
482
|
+
|
|
483
|
+
if ((result.type || 'text') !== 'zip') {
|
|
484
|
+
console.error(`[opm share] Not a zip share (type=${result.type || 'text'}). Use "opm share get ${id || result.id}" for text content.`)
|
|
485
|
+
process.exit(1)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const targetDir = dir
|
|
489
|
+
? path.resolve(expandTilde(dir))
|
|
490
|
+
: path.join(getUserConfigDir(), 'share', id || result.id)
|
|
358
491
|
|
|
359
492
|
try {
|
|
360
|
-
const
|
|
361
|
-
|
|
493
|
+
const { files, targetDir: target, extracted } = await this._extractZipTo(result, targetDir, dryRun)
|
|
494
|
+
output({
|
|
495
|
+
id: result.id,
|
|
496
|
+
title: result.title,
|
|
497
|
+
type: 'zip',
|
|
498
|
+
filename: result.filename,
|
|
499
|
+
sourcePath: result.sourcePath || '',
|
|
500
|
+
created: result.created,
|
|
501
|
+
dryRun,
|
|
502
|
+
extracted,
|
|
503
|
+
dir: target,
|
|
504
|
+
files,
|
|
505
|
+
})
|
|
506
|
+
} catch (err) {
|
|
507
|
+
console.error(`[opm share] ${err.message}`)
|
|
508
|
+
process.exit(1)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
362
511
|
|
|
363
|
-
|
|
364
|
-
|
|
512
|
+
async _sync(parsed) {
|
|
513
|
+
const id = parsed.positional[0] || ''
|
|
514
|
+
const password = this._resolvePassword(parsed)
|
|
515
|
+
const dryRun = parsed.flags['dry-run'] === 'true'
|
|
365
516
|
|
|
366
|
-
|
|
517
|
+
if (!id) {
|
|
518
|
+
console.error('Usage: opm share sync <id> [--dry-run] [-p password]')
|
|
519
|
+
console.error(' <id> is required (zip share id to sync to its original ~ -anchored path)')
|
|
520
|
+
process.exit(1)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const baseUrl = await this._ensureReachable()
|
|
524
|
+
const result = await getShare(baseUrl, id, password)
|
|
525
|
+
|
|
526
|
+
if ((result.type || 'text') !== 'zip') {
|
|
527
|
+
console.error(`[opm share] Not a zip share (type=${result.type || 'text'}). Use "opm share get ${id || result.id}" for text content.`)
|
|
528
|
+
process.exit(1)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const sourcePath = result.sourcePath || ''
|
|
532
|
+
const defaultDir = path.join(getUserConfigDir(), 'share', id || result.id)
|
|
533
|
+
let targetDir
|
|
534
|
+
let mode
|
|
535
|
+
if (sourcePath.startsWith('~/')) {
|
|
536
|
+
const expanded = expandTilde(sourcePath)
|
|
537
|
+
targetDir = path.dirname(expanded)
|
|
538
|
+
mode = `sync → ${sourcePath} (expanded: ${expanded})`
|
|
539
|
+
} else {
|
|
540
|
+
targetDir = defaultDir
|
|
541
|
+
mode = `fallback (no ~ anchor; sourcePath=${sourcePath || '<none>'}) → default dir`
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
const { files, targetDir: target, extracted } = await this._extractZipTo(result, targetDir, dryRun)
|
|
367
546
|
output({
|
|
368
547
|
id: result.id,
|
|
369
548
|
title: result.title,
|
|
370
549
|
type: 'zip',
|
|
371
550
|
filename: result.filename,
|
|
551
|
+
sourcePath,
|
|
372
552
|
created: result.created,
|
|
373
|
-
|
|
374
|
-
|
|
553
|
+
dryRun,
|
|
554
|
+
extracted,
|
|
555
|
+
mode,
|
|
556
|
+
dir: target,
|
|
375
557
|
files,
|
|
376
558
|
})
|
|
377
559
|
} catch (err) {
|
|
378
|
-
console.error(`
|
|
560
|
+
console.error(`[opm share] ${err.message}`)
|
|
379
561
|
process.exit(1)
|
|
380
|
-
} finally {
|
|
381
|
-
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
382
562
|
}
|
|
383
563
|
}
|
|
384
564
|
|
|
@@ -398,62 +578,14 @@ export class ShareGroup {
|
|
|
398
578
|
}
|
|
399
579
|
}
|
|
400
580
|
|
|
401
|
-
async _view(parsed) {
|
|
402
|
-
const id = parsed.positional[0] || ''
|
|
403
|
-
const password = this._resolvePassword(parsed, parsed.positional[1] || '')
|
|
404
|
-
|
|
405
|
-
const baseUrl = await this._ensureReachable()
|
|
406
|
-
const result = id
|
|
407
|
-
? await getShare(baseUrl, id, password)
|
|
408
|
-
: await getLatestShare(baseUrl, password)
|
|
409
|
-
|
|
410
|
-
if (result.type === 'zip') {
|
|
411
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), ZIP_TMP_PREFIX))
|
|
412
|
-
try {
|
|
413
|
-
const zipPath = path.join(tmpDir, result.filename || `${result.id}.zip`)
|
|
414
|
-
const buf = Buffer.from(result.content, 'base64')
|
|
415
|
-
fs.writeFileSync(zipPath, buf)
|
|
416
|
-
|
|
417
|
-
const { default: extractZip } = await import('extract-zip')
|
|
418
|
-
await extractZip(zipPath, { dir: tmpDir })
|
|
419
|
-
|
|
420
|
-
fs.unlinkSync(zipPath)
|
|
421
|
-
|
|
422
|
-
const files = this._listTopLevel(tmpDir)
|
|
423
|
-
for (const f of files) {
|
|
424
|
-
console.log(`${f.type === 'dir' ? 'd' : 'f'} ${f.name}`)
|
|
425
|
-
}
|
|
426
|
-
} catch (err) {
|
|
427
|
-
console.error(`View zip failed: ${err.message}`)
|
|
428
|
-
process.exit(1)
|
|
429
|
-
} finally {
|
|
430
|
-
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
431
|
-
}
|
|
432
|
-
return
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
process.stdout.write(result.content || '')
|
|
436
|
-
if (!(result.content || '').endsWith('\n')) {
|
|
437
|
-
process.stdout.write('\n')
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
581
|
async _open(parsed) {
|
|
442
|
-
const id = parsed.positional[0] ||
|
|
582
|
+
const id = parsed.positional[0] || DEFAULT_TEMP_ID
|
|
443
583
|
const password = this._resolvePassword(parsed, parsed.positional[1] || '')
|
|
444
584
|
|
|
445
585
|
const baseUrl = await this._ensureReachable()
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
openBrowser(htmlUrl)
|
|
450
|
-
output({ opened: true, url: htmlUrl })
|
|
451
|
-
} else {
|
|
452
|
-
const result = await getLatestShare(baseUrl, password)
|
|
453
|
-
const htmlUrl = getHtmlUrl(baseUrl, result.id, password)
|
|
454
|
-
openBrowser(htmlUrl)
|
|
455
|
-
output({ opened: true, id: result.id, url: htmlUrl })
|
|
456
|
-
}
|
|
586
|
+
const htmlUrl = getHtmlUrl(baseUrl, id, password)
|
|
587
|
+
openBrowser(htmlUrl)
|
|
588
|
+
output({ opened: true, id, url: htmlUrl })
|
|
457
589
|
}
|
|
458
590
|
|
|
459
591
|
async _list(parsed) {
|
|
@@ -477,12 +609,12 @@ export class ShareGroup {
|
|
|
477
609
|
}
|
|
478
610
|
|
|
479
611
|
async _clear(parsed) {
|
|
480
|
-
const days =
|
|
612
|
+
const days = parseFloat(parsed.flags.days || '0') || 0
|
|
481
613
|
const keep = parseInt(parsed.flags.keep || parsed.flags.n || '0', 10) || 0
|
|
482
614
|
|
|
483
615
|
if (!days && !keep) {
|
|
484
616
|
console.error('Usage: opm share clear [--days <n>] [--keep <n>]')
|
|
485
|
-
console.error(' --days <n>
|
|
617
|
+
console.error(' --days <n> Keep shares from the last n days (integer = aligned to local midnight, fraction = hour window; default keeps today)')
|
|
486
618
|
console.error(' --keep <n> Keep only the most recent n shares')
|
|
487
619
|
console.error(' Both can be combined: clear --days 7 --keep 50')
|
|
488
620
|
process.exit(1)
|
|
@@ -515,7 +647,7 @@ export class ShareGroup {
|
|
|
515
647
|
return
|
|
516
648
|
}
|
|
517
649
|
|
|
518
|
-
console.error(`Unknown server subcommand: ${sub}`)
|
|
650
|
+
console.error(`[opm share] Unknown server subcommand: ${sub}`)
|
|
519
651
|
console.error('Usage: opm share server [start|stop|status|help]')
|
|
520
652
|
process.exit(1)
|
|
521
653
|
}
|
|
@@ -667,6 +799,49 @@ Environment variables (for server.js direct execution):
|
|
|
667
799
|
output({ running: true, ...pidInfo })
|
|
668
800
|
}
|
|
669
801
|
|
|
802
|
+
/**
|
|
803
|
+
* opm copy — 将系统剪切板内容以 id=clipboard 上传到服务器
|
|
804
|
+
* 剪切板为空(trim 后)时提示并停止。
|
|
805
|
+
* @param {object} parsed
|
|
806
|
+
*/
|
|
807
|
+
async _copy(parsed) {
|
|
808
|
+
const content = readClipboard()
|
|
809
|
+
if (!content.trim()) {
|
|
810
|
+
console.error('[opm share] Clipboard is empty, nothing to upload.')
|
|
811
|
+
process.exit(1)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const password = this._resolvePassword(parsed)
|
|
815
|
+
const baseUrl = await this._ensureReachable()
|
|
816
|
+
const result = await uploadShare(baseUrl, {
|
|
817
|
+
content,
|
|
818
|
+
password,
|
|
819
|
+
id: CLIPBOARD_ID,
|
|
820
|
+
})
|
|
821
|
+
output({ ...result, copied: true })
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* opm paste — 取回服务器上 id=clipboard 的内容,写回系统剪切板并显示
|
|
826
|
+
* @param {object} parsed
|
|
827
|
+
*/
|
|
828
|
+
async _paste(parsed) {
|
|
829
|
+
const password = this._resolvePassword(parsed, parsed.positional[0] || '')
|
|
830
|
+
const baseUrl = await this._ensureReachable()
|
|
831
|
+
const result = await getShare(baseUrl, CLIPBOARD_ID, password)
|
|
832
|
+
|
|
833
|
+
const content = result.content || ''
|
|
834
|
+
writeClipboard(content)
|
|
835
|
+
output({
|
|
836
|
+
pasted: true,
|
|
837
|
+
id: result.id,
|
|
838
|
+
title: result.title,
|
|
839
|
+
created: result.created,
|
|
840
|
+
length: content.length,
|
|
841
|
+
content,
|
|
842
|
+
})
|
|
843
|
+
}
|
|
844
|
+
|
|
670
845
|
printHelp() {
|
|
671
846
|
const actionLines = Object.entries(ACTIONS)
|
|
672
847
|
.map(([sig, { desc }]) => ` ${sig.padEnd(12)} ${desc}`)
|
|
@@ -684,17 +859,22 @@ Server subcommands:
|
|
|
684
859
|
server status Check local server status
|
|
685
860
|
|
|
686
861
|
Shorthand:
|
|
687
|
-
opm share "<content>"
|
|
688
|
-
opm share
|
|
689
|
-
|
|
862
|
+
opm share "<content>" Upload text (without "upload" keyword)
|
|
863
|
+
opm share <file> Upload existing file content (auto-detected)
|
|
864
|
+
opm share -f <file> Upload file content (explicit)
|
|
865
|
+
echo "..." | opm share Upload from stdin
|
|
866
|
+
|
|
867
|
+
Without -i, id defaults to "opm_share" — a temporary slot overwritten on the next
|
|
868
|
+
upload/zip without -i. Use -i <id> for a persistent, named share.
|
|
690
869
|
|
|
691
870
|
Options:
|
|
692
|
-
-t, --title <title> Title (default: first 30 chars of content)
|
|
693
871
|
-f, --file <path> Read content from file
|
|
694
|
-
-i, --id <id> Custom share id (
|
|
872
|
+
-i, --id <id> Custom share id (default: opm_share, overwritten on next no-id upload)
|
|
695
873
|
-p, --password <password> Password (default: config share.password, fallback 0000)
|
|
696
874
|
-n, --count <n> Number of items (list, default: 10)
|
|
697
|
-
-
|
|
875
|
+
-o, --output <file> Write text content to file instead of stdout (get)
|
|
876
|
+
--detail Show full dict (get); default prints content only, JSON pretty-printed
|
|
877
|
+
--dry-run Preview only, do not write to disk (unzip/sync)
|
|
698
878
|
-h, --help Show this help
|
|
699
879
|
|
|
700
880
|
Server config (config.json5):
|
|
@@ -703,33 +883,41 @@ Server config (config.json5):
|
|
|
703
883
|
share.serve.host / .port / .dataDir Local serve parameters
|
|
704
884
|
|
|
705
885
|
Examples:
|
|
886
|
+
opm share copy Copy system clipboard to server (id=clipboard)
|
|
887
|
+
opm share paste Fetch server clipboard back to system clipboard
|
|
888
|
+
opm scopy Top-level shortcut for "opm share copy"
|
|
889
|
+
opm spaste Top-level shortcut for "opm share paste"
|
|
890
|
+
opm sget / szip / sunzip / ssync / sopen Other share top-level shortcuts
|
|
706
891
|
opm share config Show current share config
|
|
707
892
|
opm share server start Start local server (background)
|
|
708
893
|
opm share server status Check if server is running
|
|
709
894
|
opm share server stop Stop local server
|
|
710
|
-
opm share "hello world" -
|
|
711
|
-
opm share "hello world" -i myid Upload text with custom id "myid"
|
|
712
|
-
opm share "hello world"
|
|
713
|
-
opm share
|
|
714
|
-
opm share -f doc.md
|
|
895
|
+
opm share "hello world" Upload text (id=opm_share, overwritten on next no-id upload)
|
|
896
|
+
opm share "hello world" -i myid Upload text with custom id "myid" (persistent)
|
|
897
|
+
opm share "hello world" --password 1234 Upload text with password override
|
|
898
|
+
opm share doc.md Upload existing file content (auto-detected)
|
|
899
|
+
opm share -f doc.md Upload file content
|
|
715
900
|
echo "hello" | opm share Upload from stdin
|
|
716
|
-
opm share zip
|
|
717
|
-
opm share zip ./
|
|
718
|
-
opm share get Get
|
|
719
|
-
opm share get myid Get share by custom id
|
|
720
|
-
opm share get
|
|
721
|
-
opm share get
|
|
722
|
-
opm share get
|
|
723
|
-
opm share
|
|
724
|
-
opm share
|
|
725
|
-
opm share
|
|
901
|
+
opm share zip ~/Projects/foo Upload folder as zip (sourcePath preserved as ~/Projects/foo)
|
|
902
|
+
opm share zip './src' -i mysrc Upload folder; quote ~ to preserve literal, or rely on auto re-tilde-ify
|
|
903
|
+
opm share get Get text share id=opm_share (content only; JSON pretty-printed if parseable)
|
|
904
|
+
opm share get myid Get text share by custom id (content only)
|
|
905
|
+
opm share get myid --detail Get share showing the full dict (content parsed as JSON if parseable)
|
|
906
|
+
opm share get myid -o out.json Write text content to out.json (no stdout)
|
|
907
|
+
opm share get myid --password 1234 Get with password override
|
|
908
|
+
opm share unzip myid Extract zip share myid → ~/.config/opm/share/myid/
|
|
909
|
+
opm share unzip myid ./out Extract zip share myid → ./out/
|
|
910
|
+
opm share unzip myid --dry-run Preview zip entries without extracting
|
|
911
|
+
opm share sync myid Sync zip share to its original ~ -anchored path (e.g. ~/Projects/foo)
|
|
912
|
+
opm share sync myid --dry-run Preview sync target path without extracting
|
|
913
|
+
opm share open Open share id=opm_share in browser
|
|
726
914
|
opm share open myid Open share by custom id in browser
|
|
727
915
|
opm share list List recent 10 shares
|
|
728
916
|
opm share list -n 5 List recent 5 shares
|
|
729
917
|
opm share delete abc12345 Delete a share by id
|
|
730
|
-
opm share clear --days 7
|
|
918
|
+
opm share clear --days 7 Keep shares from the last 7 days
|
|
731
919
|
opm share clear --keep 50 Keep only the most recent 50 shares
|
|
732
920
|
opm share clear --days 7 --keep 50 Both combined
|
|
733
|
-
`)
|
|
921
|
+
`)
|
|
734
922
|
}
|
|
735
923
|
}
|