@wwkit/opm 1.0.19 → 1.0.21
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/package.json +2 -2
- package/src/cli/groups/nvm.js +1 -1
- package/src/cli/groups/phpenv.js +2 -2
- package/src/cli/groups/ping.js +2 -2
- package/src/cli/index.js +19 -5
- package/src/managers/node.js +1 -1
- package/src/managers/php.js +2 -2
- package/src/managers/runtime.js +1 -1
- package/src/tools/share/index.js +271 -139
- package/src/tools/share/server.js +5 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwkit/opm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"author": "bluesliu <langcai163@163.com>",
|
|
5
5
|
"description": "Unified CLI — package management (npm/pip/dnf/apt) + config view + opencode maintenance",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"basic-ftp": "^6.2.1",
|
|
36
36
|
"extract-zip": "^2.0.1",
|
|
37
37
|
"systeminformation": "^5.23.0",
|
|
38
|
-
"@wwkit/shared": "1.0.
|
|
38
|
+
"@wwkit/shared": "1.0.26"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"jest": "^29.7.0"
|
package/src/cli/groups/nvm.js
CHANGED
|
@@ -44,7 +44,7 @@ export class NvmGroup extends ToolManagerGroup {
|
|
|
44
44
|
throw new Error(`${nvmDir} is not a git repository (nvm was not installed via git clone). Run \`opm nvm uninstall -y\` then \`opm nvm install\` to reinstall.`)
|
|
45
45
|
}
|
|
46
46
|
execSync(`cd "${nvmDir}" && git remote set-url origin "${getActiveRegistry('nvm')}" && git pull --tags`, {
|
|
47
|
-
stdio:
|
|
47
|
+
stdio: shell.stdioForExec(true),
|
|
48
48
|
shell: shell.resolveBash(),
|
|
49
49
|
})
|
|
50
50
|
return { action: 'upgraded' }
|
package/src/cli/groups/phpenv.js
CHANGED
|
@@ -52,12 +52,12 @@ export class PhpenvGroup extends ToolManagerGroup {
|
|
|
52
52
|
install: () => installPhpenv(),
|
|
53
53
|
upgrade: () => {
|
|
54
54
|
execSync(`cd "${getPhpenvRoot()}" && git remote set-url origin "${getActiveRegistry('phpenv')}" && git pull --tags`, {
|
|
55
|
-
stdio:
|
|
55
|
+
stdio: shell.stdioForExec(true),
|
|
56
56
|
shell: shell.resolveBash(),
|
|
57
57
|
})
|
|
58
58
|
const pluginsDir = path.join(getPhpenvRoot(), 'plugins', 'php-build')
|
|
59
59
|
if (fs.existsSync(pluginsDir)) {
|
|
60
|
-
execSync(`cd "${pluginsDir}" && git pull`, { stdio:
|
|
60
|
+
execSync(`cd "${pluginsDir}" && git pull`, { stdio: shell.stdioForExec(true), shell: shell.resolveBash() })
|
|
61
61
|
}
|
|
62
62
|
return { action: 'upgraded' }
|
|
63
63
|
},
|
package/src/cli/groups/ping.js
CHANGED
|
@@ -51,8 +51,8 @@ function isUrl(addr) {
|
|
|
51
51
|
|
|
52
52
|
// HTTP 状态码 → 可达性判断:2xx/3xx 视为可达,allowlist 中的状态码
|
|
53
53
|
// 虽非成功响应但证明服务器已响应(如 404 表示主机/代理存活,
|
|
54
|
-
// 429
|
|
55
|
-
const REACHABLE_ALLOWLIST = [404, 429]
|
|
54
|
+
// 405 表示 git 仓库不允许 GET 但服务器存活,429 表示临时限流)
|
|
55
|
+
const REACHABLE_ALLOWLIST = [404, 405, 429]
|
|
56
56
|
|
|
57
57
|
function httpReachable(httpCode) {
|
|
58
58
|
return (httpCode >= 200 && httpCode < 400) || REACHABLE_ALLOWLIST.includes(httpCode)
|
package/src/cli/index.js
CHANGED
|
@@ -70,6 +70,18 @@ const GROUPS = {
|
|
|
70
70
|
env: new EnvGroup(),
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// share 快捷命令:s<action> → share <action>(前缀 s,业界惯例同 git gco/kubectl kgp)
|
|
74
|
+
const SHARE_SHORTCUTS = {
|
|
75
|
+
supload: 'upload',
|
|
76
|
+
szip: 'zip',
|
|
77
|
+
sget: 'get',
|
|
78
|
+
sunzip: 'unzip',
|
|
79
|
+
ssync: 'sync',
|
|
80
|
+
sopen: 'open',
|
|
81
|
+
scopy: 'copy',
|
|
82
|
+
spaste: 'paste',
|
|
83
|
+
}
|
|
84
|
+
|
|
73
85
|
class CLI {
|
|
74
86
|
async run(argv) {
|
|
75
87
|
const [groupName, ...rest] = argv
|
|
@@ -101,9 +113,10 @@ if (groupName === 'version' || groupName === '-v' || groupName === '--version')
|
|
|
101
113
|
return
|
|
102
114
|
}
|
|
103
115
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
// share 快捷命令 → share 组对应 action
|
|
117
|
+
const shortcutAction = SHARE_SHORTCUTS[groupName]
|
|
118
|
+
if (shortcutAction) {
|
|
119
|
+
return GROUPS.share.run([shortcutAction, ...rest])
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
const group = GROUPS[groupName]
|
|
@@ -131,8 +144,9 @@ Usage: opm <command> [args] [options]
|
|
|
131
144
|
|
|
132
145
|
Commands:
|
|
133
146
|
${groupLines}
|
|
134
|
-
|
|
135
|
-
|
|
147
|
+
supload/szip/sget Share shortcuts: upload / zip / get
|
|
148
|
+
sunzip/ssync/sopen Share shortcuts: unzip / sync / open
|
|
149
|
+
scopy/spaste Share shortcuts: copy / paste (clipboard)
|
|
136
150
|
version Show opm version
|
|
137
151
|
upgrade Upgrade opm itself (npm install -g @wwkit/opm@latest)
|
|
138
152
|
doc Build docs HTML and open in browser
|
package/src/managers/node.js
CHANGED
|
@@ -55,7 +55,7 @@ export function installNvm(proxy) {
|
|
|
55
55
|
env.http_proxy = proxy
|
|
56
56
|
env.https_proxy = proxy
|
|
57
57
|
}
|
|
58
|
-
execSync(`git clone "${mirror}" "${nvmDir}"`, { stdio:
|
|
58
|
+
execSync(`git clone "${mirror}" "${nvmDir}"`, { stdio: shell.stdioForExec(true), shell: shell.resolveBash(), env })
|
|
59
59
|
const lines = [
|
|
60
60
|
`export NVM_DIR="${nvmDir}"`,
|
|
61
61
|
`[ -s "\\$NVM_DIR/nvm.sh" ] && \\. "\\$NVM_DIR/nvm.sh"`,
|
package/src/managers/php.js
CHANGED
|
@@ -51,9 +51,9 @@ export function isPhpenvInstalled() {
|
|
|
51
51
|
export function installPhpenv() {
|
|
52
52
|
const mirror = getActiveRegistry('phpenv')
|
|
53
53
|
const root = getPhpenvRoot()
|
|
54
|
-
execSync(`git clone "${mirror}" "${root}"`, { stdio:
|
|
54
|
+
execSync(`git clone "${mirror}" "${root}"`, { stdio: shell.stdioForExec(true), shell: shell.resolveBash() })
|
|
55
55
|
execSync(`git clone "${PHP_BUILD_GIT}" "${path.join(root, 'plugins', 'php-build')}"`, {
|
|
56
|
-
stdio:
|
|
56
|
+
stdio: shell.stdioForExec(true),
|
|
57
57
|
shell: shell.resolveBash(),
|
|
58
58
|
})
|
|
59
59
|
const lines = [
|
package/src/managers/runtime.js
CHANGED
|
@@ -127,7 +127,7 @@ export class RuntimeManager extends PackageManager {
|
|
|
127
127
|
throw new Error(this.cfg.toolMissingMsg)
|
|
128
128
|
}
|
|
129
129
|
const result = spawnSync(this._cmd(args), {
|
|
130
|
-
stdio:
|
|
130
|
+
stdio: shell.stdioForExec(this.cfg.needsBash),
|
|
131
131
|
shell: shell.shellForExec(this.cfg.needsBash),
|
|
132
132
|
env: this._buildEnv(opts.proxy),
|
|
133
133
|
})
|
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,25 +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'
|
|
21
22
|
import { readClipboard, writeClipboard } from '../clipboard.js'
|
|
22
23
|
|
|
23
24
|
const DEFAULT_PASSWORD = '0000'
|
|
24
25
|
const ZIP_TMP_PREFIX = 'opm-share-'
|
|
25
26
|
const CLIPBOARD_ID = 'clipboard'
|
|
27
|
+
const DEFAULT_TEMP_ID = 'opm_share'
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
|
-
*
|
|
30
|
+
* 将 content 美化为可读字符串:若为 JSON5 可解析文本则返回 2 空格缩进的 JSON,否则原样返回
|
|
31
|
+
* 用于默认输出(直接打印到 stdout,保留真实换行)
|
|
29
32
|
* @param {string} content
|
|
30
33
|
* @returns {string}
|
|
31
34
|
*/
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
}
|
|
36
57
|
}
|
|
37
58
|
|
|
38
59
|
/**
|
|
@@ -47,6 +68,25 @@ function expandTilde(p) {
|
|
|
47
68
|
return p
|
|
48
69
|
}
|
|
49
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
|
+
|
|
50
90
|
/**
|
|
51
91
|
* 浏览器打开 URL
|
|
52
92
|
* @param {string} url
|
|
@@ -104,11 +144,12 @@ function isProcessAlive(pid) {
|
|
|
104
144
|
}
|
|
105
145
|
|
|
106
146
|
const ACTIONS = {
|
|
107
|
-
upload: { desc: 'Upload text
|
|
108
|
-
zip: { desc: 'Upload file/folder as
|
|
109
|
-
get: { desc: 'Get
|
|
110
|
-
|
|
111
|
-
|
|
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)' },
|
|
112
153
|
list: { desc: 'List recent shares (-n count, default 10)' },
|
|
113
154
|
delete: { desc: 'Delete a share by id' },
|
|
114
155
|
clear: { desc: 'Keep shares by days and/or count (--days n keeps last n days, --keep n keeps n most recent)' },
|
|
@@ -133,7 +174,7 @@ export class ShareGroup {
|
|
|
133
174
|
return
|
|
134
175
|
}
|
|
135
176
|
|
|
136
|
-
const parsed = parseFlags(rest)
|
|
177
|
+
const parsed = parseFlags(rest, { booleans: ['detail'] })
|
|
137
178
|
|
|
138
179
|
switch (action) {
|
|
139
180
|
case 'upload':
|
|
@@ -142,8 +183,10 @@ export class ShareGroup {
|
|
|
142
183
|
return this._zip(parsed)
|
|
143
184
|
case 'get':
|
|
144
185
|
return this._get(parsed)
|
|
145
|
-
case '
|
|
146
|
-
return this.
|
|
186
|
+
case 'unzip':
|
|
187
|
+
return this._unzip(parsed)
|
|
188
|
+
case 'sync':
|
|
189
|
+
return this._sync(parsed)
|
|
147
190
|
case 'open':
|
|
148
191
|
return this._open(parsed)
|
|
149
192
|
case 'list':
|
|
@@ -190,17 +233,18 @@ export class ShareGroup {
|
|
|
190
233
|
}
|
|
191
234
|
|
|
192
235
|
/**
|
|
193
|
-
* 解析密码:-p/--password 参数 > 配置 share.password >
|
|
236
|
+
* 解析密码:-p/--password 参数 > 位置参数(get/open)> 配置 share.password > 默认 0000
|
|
194
237
|
* @param {object} parsed - parseFlags 结果
|
|
195
238
|
* @param {string} [positional] - 位置参数密码(get/open 的第二个位置参数)
|
|
196
239
|
* @returns {string}
|
|
197
240
|
*/
|
|
198
241
|
_resolvePassword(parsed, positional = '') {
|
|
199
|
-
const flag = parsed.flags.password || ''
|
|
242
|
+
const flag = parsed.flags.password || parsed.flags.p || ''
|
|
200
243
|
if (flag && flag !== 'true') return flag
|
|
244
|
+
if (positional) return positional
|
|
201
245
|
const cfgPassword = this._getShareConfig().password
|
|
202
246
|
if (cfgPassword) return cfgPassword
|
|
203
|
-
return
|
|
247
|
+
return DEFAULT_PASSWORD
|
|
204
248
|
}
|
|
205
249
|
|
|
206
250
|
/**
|
|
@@ -241,9 +285,8 @@ export class ShareGroup {
|
|
|
241
285
|
}
|
|
242
286
|
|
|
243
287
|
async _upload(parsed) {
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
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
|
|
247
290
|
const password = this._resolvePassword(parsed)
|
|
248
291
|
|
|
249
292
|
let content = ''
|
|
@@ -251,47 +294,60 @@ export class ShareGroup {
|
|
|
251
294
|
if (file) {
|
|
252
295
|
const resolved = path.resolve(file)
|
|
253
296
|
if (!fs.existsSync(resolved)) {
|
|
254
|
-
console.error(`File not found: ${resolved}`)
|
|
297
|
+
console.error(`[opm share] File not found: ${resolved}`)
|
|
255
298
|
process.exit(1)
|
|
256
299
|
}
|
|
257
300
|
content = fs.readFileSync(resolved, 'utf8')
|
|
258
301
|
} else if (parsed.positional.length > 0) {
|
|
259
|
-
|
|
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
|
+
}
|
|
260
316
|
} else if (!process.stdin.isTTY) {
|
|
261
317
|
content = fs.readFileSync(0, 'utf8')
|
|
262
318
|
}
|
|
263
319
|
|
|
264
320
|
if (!content) {
|
|
265
|
-
console.error('Usage: opm share upload "<content>" [-
|
|
266
|
-
console.error(' Or: echo "..." | opm share upload [-
|
|
267
|
-
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)')
|
|
268
325
|
process.exit(1)
|
|
269
326
|
}
|
|
270
327
|
|
|
271
328
|
const baseUrl = await this._ensureReachable()
|
|
272
329
|
const result = await uploadShare(baseUrl, {
|
|
273
|
-
title: title || defaultTitle(content),
|
|
274
330
|
content,
|
|
275
331
|
password,
|
|
276
|
-
|
|
332
|
+
id,
|
|
277
333
|
})
|
|
278
334
|
output(result)
|
|
279
335
|
}
|
|
280
336
|
|
|
281
337
|
async _zip(parsed) {
|
|
282
|
-
const inputPath = parsed.positional[0] || parsed.flags.file || ''
|
|
283
|
-
const
|
|
284
|
-
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
|
|
285
340
|
const password = this._resolvePassword(parsed)
|
|
286
341
|
|
|
287
342
|
if (!inputPath) {
|
|
288
|
-
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)')
|
|
289
345
|
process.exit(1)
|
|
290
346
|
}
|
|
291
347
|
|
|
292
|
-
const resolved = path.resolve(inputPath)
|
|
348
|
+
const resolved = path.resolve(expandTilde(inputPath))
|
|
293
349
|
if (!fs.existsSync(resolved)) {
|
|
294
|
-
console.error(`Path not found: ${resolved}`)
|
|
350
|
+
console.error(`[opm share] Path not found: ${resolved}`)
|
|
295
351
|
process.exit(1)
|
|
296
352
|
}
|
|
297
353
|
|
|
@@ -300,11 +356,13 @@ export class ShareGroup {
|
|
|
300
356
|
if (isDir) {
|
|
301
357
|
const entries = fs.readdirSync(resolved)
|
|
302
358
|
if (entries.length === 0) {
|
|
303
|
-
console.error(`Directory is empty: ${resolved}`)
|
|
359
|
+
console.error(`[opm share] Directory is empty: ${resolved}`)
|
|
304
360
|
process.exit(1)
|
|
305
361
|
}
|
|
306
362
|
}
|
|
307
363
|
|
|
364
|
+
const sourcePath = tildefy(inputPath, resolved)
|
|
365
|
+
|
|
308
366
|
const baseName = path.basename(resolved)
|
|
309
367
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), ZIP_TMP_PREFIX))
|
|
310
368
|
const zipPath = path.join(tmpDir, `${baseName}.zip`)
|
|
@@ -317,7 +375,7 @@ export class ShareGroup {
|
|
|
317
375
|
|
|
318
376
|
if (zipResult.status !== 0) {
|
|
319
377
|
const stderr = zipResult.stderr ? zipResult.stderr.toString().trim() : 'unknown error'
|
|
320
|
-
console.error(`Zip failed: ${stderr}`)
|
|
378
|
+
console.error(`[opm share] Zip failed: ${stderr}`)
|
|
321
379
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
322
380
|
process.exit(1)
|
|
323
381
|
}
|
|
@@ -329,64 +387,178 @@ export class ShareGroup {
|
|
|
329
387
|
|
|
330
388
|
const baseUrl = await this._ensureReachable()
|
|
331
389
|
const result = await uploadShare(baseUrl, {
|
|
332
|
-
title:
|
|
390
|
+
title: `${baseName}.zip`,
|
|
333
391
|
content,
|
|
334
392
|
password,
|
|
335
393
|
type: 'zip',
|
|
336
394
|
filename: `${baseName}.zip`,
|
|
337
|
-
|
|
395
|
+
sourcePath,
|
|
396
|
+
id,
|
|
338
397
|
})
|
|
339
398
|
output(result)
|
|
340
399
|
}
|
|
341
400
|
|
|
342
401
|
async _get(parsed) {
|
|
343
|
-
const id = parsed.positional[0] ||
|
|
402
|
+
const id = parsed.positional[0] || DEFAULT_TEMP_ID
|
|
344
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 || ''
|
|
345
406
|
const extractDir = parsed.flags.d || parsed.flags.dir || ''
|
|
346
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
|
+
|
|
347
413
|
const baseUrl = await this._ensureReachable()
|
|
348
|
-
const result = id
|
|
349
|
-
? await getShare(baseUrl, id, password)
|
|
350
|
-
: await getLatestShare(baseUrl, password)
|
|
414
|
+
const result = await getShare(baseUrl, id, password)
|
|
351
415
|
|
|
352
|
-
if (
|
|
353
|
-
|
|
416
|
+
if (detail) {
|
|
417
|
+
output({ ...result, content: parseContentJson(result.content) })
|
|
354
418
|
return
|
|
355
419
|
}
|
|
356
420
|
|
|
357
|
-
|
|
358
|
-
|
|
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
|
+
}
|
|
359
425
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
+
}
|
|
363
434
|
|
|
435
|
+
const formatted = formatContent(result.content || '')
|
|
436
|
+
process.stdout.write(formatted)
|
|
437
|
+
if (!formatted.endsWith('\n')) {
|
|
438
|
+
process.stdout.write('\n')
|
|
439
|
+
}
|
|
440
|
+
}
|
|
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) {
|
|
364
450
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), ZIP_TMP_PREFIX))
|
|
365
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)
|
|
366
491
|
|
|
367
492
|
try {
|
|
368
|
-
const
|
|
369
|
-
|
|
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
|
+
}
|
|
370
511
|
|
|
371
|
-
|
|
372
|
-
|
|
512
|
+
async _sync(parsed) {
|
|
513
|
+
const id = parsed.positional[0] || ''
|
|
514
|
+
const password = this._resolvePassword(parsed)
|
|
515
|
+
const dryRun = parsed.flags['dry-run'] === 'true'
|
|
516
|
+
|
|
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
|
+
}
|
|
373
543
|
|
|
374
|
-
|
|
544
|
+
try {
|
|
545
|
+
const { files, targetDir: target, extracted } = await this._extractZipTo(result, targetDir, dryRun)
|
|
375
546
|
output({
|
|
376
547
|
id: result.id,
|
|
377
548
|
title: result.title,
|
|
378
549
|
type: 'zip',
|
|
379
550
|
filename: result.filename,
|
|
551
|
+
sourcePath,
|
|
380
552
|
created: result.created,
|
|
381
|
-
|
|
382
|
-
|
|
553
|
+
dryRun,
|
|
554
|
+
extracted,
|
|
555
|
+
mode,
|
|
556
|
+
dir: target,
|
|
383
557
|
files,
|
|
384
558
|
})
|
|
385
559
|
} catch (err) {
|
|
386
|
-
console.error(`
|
|
560
|
+
console.error(`[opm share] ${err.message}`)
|
|
387
561
|
process.exit(1)
|
|
388
|
-
} finally {
|
|
389
|
-
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
390
562
|
}
|
|
391
563
|
}
|
|
392
564
|
|
|
@@ -406,62 +578,14 @@ export class ShareGroup {
|
|
|
406
578
|
}
|
|
407
579
|
}
|
|
408
580
|
|
|
409
|
-
async _view(parsed) {
|
|
410
|
-
const id = parsed.positional[0] || ''
|
|
411
|
-
const password = this._resolvePassword(parsed, parsed.positional[1] || '')
|
|
412
|
-
|
|
413
|
-
const baseUrl = await this._ensureReachable()
|
|
414
|
-
const result = id
|
|
415
|
-
? await getShare(baseUrl, id, password)
|
|
416
|
-
: await getLatestShare(baseUrl, password)
|
|
417
|
-
|
|
418
|
-
if (result.type === 'zip') {
|
|
419
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), ZIP_TMP_PREFIX))
|
|
420
|
-
try {
|
|
421
|
-
const zipPath = path.join(tmpDir, result.filename || `${result.id}.zip`)
|
|
422
|
-
const buf = Buffer.from(result.content, 'base64')
|
|
423
|
-
fs.writeFileSync(zipPath, buf)
|
|
424
|
-
|
|
425
|
-
const { default: extractZip } = await import('extract-zip')
|
|
426
|
-
await extractZip(zipPath, { dir: tmpDir })
|
|
427
|
-
|
|
428
|
-
fs.unlinkSync(zipPath)
|
|
429
|
-
|
|
430
|
-
const files = this._listTopLevel(tmpDir)
|
|
431
|
-
for (const f of files) {
|
|
432
|
-
console.log(`${f.type === 'dir' ? 'd' : 'f'} ${f.name}`)
|
|
433
|
-
}
|
|
434
|
-
} catch (err) {
|
|
435
|
-
console.error(`View zip failed: ${err.message}`)
|
|
436
|
-
process.exit(1)
|
|
437
|
-
} finally {
|
|
438
|
-
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
439
|
-
}
|
|
440
|
-
return
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
process.stdout.write(result.content || '')
|
|
444
|
-
if (!(result.content || '').endsWith('\n')) {
|
|
445
|
-
process.stdout.write('\n')
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
581
|
async _open(parsed) {
|
|
450
|
-
const id = parsed.positional[0] ||
|
|
582
|
+
const id = parsed.positional[0] || DEFAULT_TEMP_ID
|
|
451
583
|
const password = this._resolvePassword(parsed, parsed.positional[1] || '')
|
|
452
584
|
|
|
453
585
|
const baseUrl = await this._ensureReachable()
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
openBrowser(htmlUrl)
|
|
458
|
-
output({ opened: true, url: htmlUrl })
|
|
459
|
-
} else {
|
|
460
|
-
const result = await getLatestShare(baseUrl, password)
|
|
461
|
-
const htmlUrl = getHtmlUrl(baseUrl, result.id, password)
|
|
462
|
-
openBrowser(htmlUrl)
|
|
463
|
-
output({ opened: true, id: result.id, url: htmlUrl })
|
|
464
|
-
}
|
|
586
|
+
const htmlUrl = getHtmlUrl(baseUrl, id, password)
|
|
587
|
+
openBrowser(htmlUrl)
|
|
588
|
+
output({ opened: true, id, url: htmlUrl })
|
|
465
589
|
}
|
|
466
590
|
|
|
467
591
|
async _list(parsed) {
|
|
@@ -523,7 +647,7 @@ export class ShareGroup {
|
|
|
523
647
|
return
|
|
524
648
|
}
|
|
525
649
|
|
|
526
|
-
console.error(`Unknown server subcommand: ${sub}`)
|
|
650
|
+
console.error(`[opm share] Unknown server subcommand: ${sub}`)
|
|
527
651
|
console.error('Usage: opm share server [start|stop|status|help]')
|
|
528
652
|
process.exit(1)
|
|
529
653
|
}
|
|
@@ -690,7 +814,6 @@ Environment variables (for server.js direct execution):
|
|
|
690
814
|
const password = this._resolvePassword(parsed)
|
|
691
815
|
const baseUrl = await this._ensureReachable()
|
|
692
816
|
const result = await uploadShare(baseUrl, {
|
|
693
|
-
title: defaultTitle(content),
|
|
694
817
|
content,
|
|
695
818
|
password,
|
|
696
819
|
id: CLIPBOARD_ID,
|
|
@@ -736,17 +859,22 @@ Server subcommands:
|
|
|
736
859
|
server status Check local server status
|
|
737
860
|
|
|
738
861
|
Shorthand:
|
|
739
|
-
opm share "<content>"
|
|
740
|
-
opm share
|
|
741
|
-
|
|
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.
|
|
742
869
|
|
|
743
870
|
Options:
|
|
744
|
-
-
|
|
745
|
-
--
|
|
746
|
-
-
|
|
747
|
-
--password <password> Password (default: config share.password, fallback 0000)
|
|
871
|
+
-f, --file <path> Read content from file
|
|
872
|
+
-i, --id <id> Custom share id (default: opm_share, overwritten on next no-id upload)
|
|
873
|
+
-p, --password <password> Password (default: config share.password, fallback 0000)
|
|
748
874
|
-n, --count <n> Number of items (list, default: 10)
|
|
749
|
-
-
|
|
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)
|
|
750
878
|
-h, --help Show this help
|
|
751
879
|
|
|
752
880
|
Server config (config.json5):
|
|
@@ -757,28 +885,32 @@ Server config (config.json5):
|
|
|
757
885
|
Examples:
|
|
758
886
|
opm share copy Copy system clipboard to server (id=clipboard)
|
|
759
887
|
opm share paste Fetch server clipboard back to system clipboard
|
|
760
|
-
opm
|
|
761
|
-
opm
|
|
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
|
|
762
891
|
opm share config Show current share config
|
|
763
892
|
opm share server start Start local server (background)
|
|
764
893
|
opm share server status Check if server is running
|
|
765
894
|
opm share server stop Stop local server
|
|
766
|
-
opm share "hello world" -
|
|
767
|
-
opm share "hello world" -i myid Upload text with custom id "myid"
|
|
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)
|
|
768
897
|
opm share "hello world" --password 1234 Upload text with password override
|
|
769
|
-
opm share
|
|
770
|
-
opm share -f doc.md
|
|
898
|
+
opm share doc.md Upload existing file content (auto-detected)
|
|
899
|
+
opm share -f doc.md Upload file content
|
|
771
900
|
echo "hello" | opm share Upload from stdin
|
|
772
|
-
opm share zip
|
|
773
|
-
opm share zip ./
|
|
774
|
-
opm share get Get
|
|
775
|
-
opm share get myid Get share by custom id
|
|
776
|
-
opm share get
|
|
777
|
-
opm share get
|
|
778
|
-
opm share get
|
|
779
|
-
opm share
|
|
780
|
-
opm share
|
|
781
|
-
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
|
|
782
914
|
opm share open myid Open share by custom id in browser
|
|
783
915
|
opm share list List recent 10 shares
|
|
784
916
|
opm share list -n 5 List recent 5 shares
|
|
@@ -786,6 +918,6 @@ Examples:
|
|
|
786
918
|
opm share clear --days 7 Keep shares from the last 7 days
|
|
787
919
|
opm share clear --keep 50 Keep only the most recent 50 shares
|
|
788
920
|
opm share clear --days 7 --keep 50 Both combined
|
|
789
|
-
`)
|
|
921
|
+
`)
|
|
790
922
|
}
|
|
791
923
|
}
|
|
@@ -219,6 +219,7 @@ async function handleUpload(req, res, dataDir, baseUrl) {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
const id = parsed.id || genId()
|
|
222
|
+
const existed = fs.existsSync(path.join(dataDir, `${id}.json`))
|
|
222
223
|
const item = {
|
|
223
224
|
id,
|
|
224
225
|
title: parsed.title || defaultTitle(content),
|
|
@@ -226,11 +227,12 @@ async function handleUpload(req, res, dataDir, baseUrl) {
|
|
|
226
227
|
password: parsed.password || DEFAULT_PASSWORD,
|
|
227
228
|
type: parsed.type || 'text',
|
|
228
229
|
filename: parsed.filename || '',
|
|
230
|
+
sourcePath: parsed.sourcePath || '',
|
|
229
231
|
created: new Date().toISOString(),
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
writeShare(dataDir, item)
|
|
233
|
-
sendJson(res, 200, { id, url: `${baseUrl}/share/${id}`, htmlUrl: `${baseUrl}/share/html/${id}
|
|
235
|
+
sendJson(res, 200, { id, url: `${baseUrl}/share/${id}`, htmlUrl: `${baseUrl}/share/html/${id}`, overwritten: existed })
|
|
234
236
|
}
|
|
235
237
|
|
|
236
238
|
function handleList(req, res, dataDir, url) {
|
|
@@ -269,7 +271,7 @@ function handleLatest(req, res, dataDir, pw) {
|
|
|
269
271
|
}
|
|
270
272
|
sendByAccept(
|
|
271
273
|
req, res, 200,
|
|
272
|
-
{ id: item.id, title: item.title, content: item.content, type: item.type || 'text', filename: item.filename || '', created: item.created },
|
|
274
|
+
{ id: item.id, title: item.title, content: item.content, type: item.type || 'text', filename: item.filename || '', sourcePath: item.sourcePath || '', created: item.created },
|
|
273
275
|
renderHtml(item),
|
|
274
276
|
)
|
|
275
277
|
}
|
|
@@ -286,7 +288,7 @@ function handleGet(req, res, dataDir, id, pw) {
|
|
|
286
288
|
}
|
|
287
289
|
sendByAccept(
|
|
288
290
|
req, res, 200,
|
|
289
|
-
{ id: item.id, title: item.title, content: item.content, type: item.type || 'text', filename: item.filename || '', created: item.created },
|
|
291
|
+
{ id: item.id, title: item.title, content: item.content, type: item.type || 'text', filename: item.filename || '', sourcePath: item.sourcePath || '', created: item.created },
|
|
290
292
|
renderHtml(item),
|
|
291
293
|
)
|
|
292
294
|
}
|