pal-explorer-cli 0.4.0
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/LICENSE.md +18 -0
- package/README.md +314 -0
- package/bin/pal.js +230 -0
- package/extensions/@palexplorer/analytics/README.md +45 -0
- package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
- package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
- package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
- package/extensions/@palexplorer/analytics/extension.json +27 -0
- package/extensions/@palexplorer/analytics/index.js +186 -0
- package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
- package/extensions/@palexplorer/audit/extension.json +17 -0
- package/extensions/@palexplorer/audit/index.js +2 -0
- package/extensions/@palexplorer/auth-email/extension.json +17 -0
- package/extensions/@palexplorer/auth-email/index.js +102 -0
- package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
- package/extensions/@palexplorer/auth-oauth/index.js +199 -0
- package/extensions/@palexplorer/chat/extension.json +17 -0
- package/extensions/@palexplorer/chat/index.js +2 -0
- package/extensions/@palexplorer/discovery/extension.json +16 -0
- package/extensions/@palexplorer/discovery/index.js +111 -0
- package/extensions/@palexplorer/email-notifications/extension.json +23 -0
- package/extensions/@palexplorer/email-notifications/index.js +242 -0
- package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
- package/extensions/@palexplorer/explorer-integration/index.js +122 -0
- package/extensions/@palexplorer/groups/extension.json +17 -0
- package/extensions/@palexplorer/groups/index.js +2 -0
- package/extensions/@palexplorer/networks/extension.json +17 -0
- package/extensions/@palexplorer/networks/index.js +2 -0
- package/extensions/@palexplorer/share-links/extension.json +17 -0
- package/extensions/@palexplorer/share-links/index.js +2 -0
- package/extensions/@palexplorer/sync/extension.json +17 -0
- package/extensions/@palexplorer/sync/index.js +2 -0
- package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
- package/extensions/@palexplorer/user-mgmt/index.js +2 -0
- package/extensions/@palexplorer/vfs/extension.json +17 -0
- package/extensions/@palexplorer/vfs/index.js +167 -0
- package/lib/capabilities.js +263 -0
- package/lib/commands/analytics.js +175 -0
- package/lib/commands/api-keys.js +131 -0
- package/lib/commands/audit.js +235 -0
- package/lib/commands/auth.js +137 -0
- package/lib/commands/backup.js +76 -0
- package/lib/commands/billing.js +148 -0
- package/lib/commands/chat.js +217 -0
- package/lib/commands/cloud-backup.js +231 -0
- package/lib/commands/comment.js +99 -0
- package/lib/commands/completion.js +203 -0
- package/lib/commands/compliance.js +218 -0
- package/lib/commands/config.js +136 -0
- package/lib/commands/connect.js +44 -0
- package/lib/commands/dept.js +294 -0
- package/lib/commands/device.js +146 -0
- package/lib/commands/download.js +226 -0
- package/lib/commands/explorer.js +178 -0
- package/lib/commands/extension.js +970 -0
- package/lib/commands/favorite.js +90 -0
- package/lib/commands/federation.js +270 -0
- package/lib/commands/file.js +533 -0
- package/lib/commands/group.js +271 -0
- package/lib/commands/gui-share.js +29 -0
- package/lib/commands/init.js +61 -0
- package/lib/commands/invite.js +59 -0
- package/lib/commands/list.js +59 -0
- package/lib/commands/log.js +116 -0
- package/lib/commands/nearby.js +108 -0
- package/lib/commands/network.js +251 -0
- package/lib/commands/notify.js +198 -0
- package/lib/commands/org.js +273 -0
- package/lib/commands/pal.js +180 -0
- package/lib/commands/permissions.js +216 -0
- package/lib/commands/pin.js +97 -0
- package/lib/commands/protocol.js +357 -0
- package/lib/commands/rbac.js +147 -0
- package/lib/commands/recover.js +36 -0
- package/lib/commands/register.js +171 -0
- package/lib/commands/relay.js +131 -0
- package/lib/commands/remote.js +368 -0
- package/lib/commands/revoke.js +50 -0
- package/lib/commands/scanner.js +280 -0
- package/lib/commands/schedule.js +344 -0
- package/lib/commands/scim.js +203 -0
- package/lib/commands/search.js +181 -0
- package/lib/commands/serve.js +438 -0
- package/lib/commands/server.js +350 -0
- package/lib/commands/share-link.js +199 -0
- package/lib/commands/share.js +323 -0
- package/lib/commands/sso.js +200 -0
- package/lib/commands/status.js +136 -0
- package/lib/commands/stream.js +562 -0
- package/lib/commands/su.js +187 -0
- package/lib/commands/sync.js +827 -0
- package/lib/commands/transfers.js +152 -0
- package/lib/commands/uninstall.js +188 -0
- package/lib/commands/update.js +204 -0
- package/lib/commands/user.js +276 -0
- package/lib/commands/vfs.js +84 -0
- package/lib/commands/web.js +52 -0
- package/lib/commands/webhook.js +180 -0
- package/lib/commands/whoami.js +59 -0
- package/lib/commands/workspace.js +121 -0
- package/lib/core/accessLog.js +54 -0
- package/lib/core/analytics.js +99 -0
- package/lib/core/backup.js +84 -0
- package/lib/core/billing.js +336 -0
- package/lib/core/bitfieldStore.js +53 -0
- package/lib/core/connectionManager.js +182 -0
- package/lib/core/dhtDiscovery.js +148 -0
- package/lib/core/discoveryClient.js +408 -0
- package/lib/core/extensionAnalyzer.js +357 -0
- package/lib/core/extensionSandbox.js +250 -0
- package/lib/core/extensionWorkerHost.js +166 -0
- package/lib/core/extensions.js +1082 -0
- package/lib/core/fileDiff.js +69 -0
- package/lib/core/groups.js +119 -0
- package/lib/core/identity.js +340 -0
- package/lib/core/mdnsService.js +126 -0
- package/lib/core/networks.js +81 -0
- package/lib/core/permissions.js +109 -0
- package/lib/core/pro.js +27 -0
- package/lib/core/resolver.js +74 -0
- package/lib/core/serverList.js +224 -0
- package/lib/core/sharePolicy.js +69 -0
- package/lib/core/shares.js +325 -0
- package/lib/core/signalingServer.js +441 -0
- package/lib/core/streamTransport.js +106 -0
- package/lib/core/su.js +55 -0
- package/lib/core/syncEngine.js +264 -0
- package/lib/core/syncState.js +159 -0
- package/lib/core/transfers.js +259 -0
- package/lib/core/users.js +225 -0
- package/lib/core/vfs.js +216 -0
- package/lib/core/webServer.js +702 -0
- package/lib/core/webrtcStream.js +396 -0
- package/lib/crypto/chatEncryption.js +57 -0
- package/lib/crypto/shareEncryption.js +195 -0
- package/lib/crypto/sharePassword.js +35 -0
- package/lib/crypto/streamEncryption.js +189 -0
- package/lib/package.json +1 -0
- package/lib/protocol/envelope.js +271 -0
- package/lib/protocol/handler.js +191 -0
- package/lib/protocol/index.js +27 -0
- package/lib/protocol/messages.js +247 -0
- package/lib/protocol/negotiation.js +127 -0
- package/lib/protocol/policy.js +142 -0
- package/lib/protocol/router.js +86 -0
- package/lib/protocol/sync.js +122 -0
- package/lib/utils/cli.js +15 -0
- package/lib/utils/config.js +123 -0
- package/lib/utils/configIntegrity.js +87 -0
- package/lib/utils/downloadDir.js +9 -0
- package/lib/utils/explorer.js +83 -0
- package/lib/utils/format.js +12 -0
- package/lib/utils/help.js +357 -0
- package/lib/utils/logger.js +103 -0
- package/lib/utils/torrent.js +203 -0
- package/package.json +71 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
export const HelpRegistry = {
|
|
2
|
+
general: {
|
|
3
|
+
title: "Palexplorer",
|
|
4
|
+
description: "Decentralized P2P file sharing from the command line. Share files, folders, and drives directly with your pals — no cloud required."
|
|
5
|
+
},
|
|
6
|
+
commands: {
|
|
7
|
+
init: {
|
|
8
|
+
summary: "Create a new identity",
|
|
9
|
+
details: "Generates an Ed25519 keypair and a 24-word recovery phrase. Required before any other command.",
|
|
10
|
+
subcommands: {},
|
|
11
|
+
examples: [
|
|
12
|
+
"pe init alice",
|
|
13
|
+
"pe init bob --force"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
whoami: {
|
|
17
|
+
summary: "Display your current identity",
|
|
18
|
+
details: "Shows your name, handle, public key, device ID, and creation date.",
|
|
19
|
+
subcommands: {},
|
|
20
|
+
examples: [
|
|
21
|
+
"pe whoami"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
register: {
|
|
25
|
+
summary: "Register a handle on the discovery network",
|
|
26
|
+
details: "Claims a unique @handle via direct signature or email OTP verification.",
|
|
27
|
+
subcommands: {},
|
|
28
|
+
examples: [
|
|
29
|
+
"pe register alice",
|
|
30
|
+
"pe register alice --email a@b.com"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
verify: {
|
|
34
|
+
summary: "Complete OTP email verification",
|
|
35
|
+
details: "Finishes the email-based handle registration started by `pe register --email`.",
|
|
36
|
+
subcommands: {},
|
|
37
|
+
examples: [
|
|
38
|
+
"pe verify 123456"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
recover: {
|
|
42
|
+
summary: "Recover identity from mnemonic phrase",
|
|
43
|
+
details: "Restores your Ed25519 keypair from the 24-word BIP-39 phrase shown during `pe init`.",
|
|
44
|
+
subcommands: {},
|
|
45
|
+
examples: [
|
|
46
|
+
"pe recover word1 word2 ... word24",
|
|
47
|
+
"pe recover word1 word2 ... word24 -n alice"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
share: {
|
|
51
|
+
summary: "Share a file, folder, or drive via P2P",
|
|
52
|
+
details: "Adds a resource to your share list. Use `pe serve` to start seeding. Supports public (global) and private (encrypted) shares.",
|
|
53
|
+
subcommands: {},
|
|
54
|
+
options: {
|
|
55
|
+
"-v, --visibility <type>": "Share visibility: global or private (default: global)",
|
|
56
|
+
"-w, --with <pals...>": "Specific pals to share with",
|
|
57
|
+
"--with-group <group>": "Share with all members of a group"
|
|
58
|
+
},
|
|
59
|
+
examples: [
|
|
60
|
+
"pe share ./photos",
|
|
61
|
+
"pe share ./docs -v private -w alice",
|
|
62
|
+
"pe share ./project --with-group team",
|
|
63
|
+
"pe share ./file.zip"
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
list: {
|
|
67
|
+
summary: "List shared resources",
|
|
68
|
+
details: "Shows all active shares with their visibility, ID, path, and type.",
|
|
69
|
+
subcommands: {},
|
|
70
|
+
options: {
|
|
71
|
+
"--by-drive": "Group shares by drive letter"
|
|
72
|
+
},
|
|
73
|
+
examples: [
|
|
74
|
+
"pe list",
|
|
75
|
+
"pe list --by-drive"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
revoke: {
|
|
79
|
+
summary: "Revoke access to a shared resource",
|
|
80
|
+
details: "Removes a share entirely, or removes a specific recipient and rotates encryption keys.",
|
|
81
|
+
subcommands: {},
|
|
82
|
+
options: {
|
|
83
|
+
"--pal <palId>": "Remove a specific pal (triggers key rotation)"
|
|
84
|
+
},
|
|
85
|
+
examples: [
|
|
86
|
+
"pe revoke abc123",
|
|
87
|
+
"pe revoke ./photos",
|
|
88
|
+
"pe revoke abc123 --pal alice"
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
serve: {
|
|
92
|
+
summary: "Start the sharing daemon",
|
|
93
|
+
details: "Seeds all active shares via WebTorrent. Supports foreground, daemon, and web dashboard modes. Auto-resumes incomplete downloads.",
|
|
94
|
+
subcommands: {},
|
|
95
|
+
options: {
|
|
96
|
+
"--daemon": "Run as background daemon",
|
|
97
|
+
"--stop": "Stop a running daemon",
|
|
98
|
+
"--web": "Start web dashboard alongside seeder",
|
|
99
|
+
"--web-port <port>": "Web dashboard port (default: 8585)"
|
|
100
|
+
},
|
|
101
|
+
examples: [
|
|
102
|
+
"pe serve",
|
|
103
|
+
"pe serve --daemon",
|
|
104
|
+
"pe serve --stop",
|
|
105
|
+
"pe serve --web",
|
|
106
|
+
"pe serve --web --web-port 9090"
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
download: {
|
|
110
|
+
summary: "Download a file from a magnet link",
|
|
111
|
+
details: "Downloads via WebTorrent. For private shares, provide the encrypted share key for automatic decryption.",
|
|
112
|
+
subcommands: {},
|
|
113
|
+
options: {
|
|
114
|
+
"--key <encryptedShareKey>": "Encrypted share key for private share decryption"
|
|
115
|
+
},
|
|
116
|
+
examples: [
|
|
117
|
+
'pe download "magnet:?xt=urn:btih:..."',
|
|
118
|
+
'pe download "magnet:?xt=..." --key abc123'
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
pal: {
|
|
122
|
+
summary: "Manage your list of pals (friends)",
|
|
123
|
+
details: "Add pals by @handle, invite link, or raw public key. Remove pals with automatic key rotation for affected shares.",
|
|
124
|
+
subcommands: {
|
|
125
|
+
add: "Add a pal by @handle, invite link (pal://...), or public key",
|
|
126
|
+
list: "List all your pals",
|
|
127
|
+
remove: "Remove a pal (rotates affected share keys)"
|
|
128
|
+
},
|
|
129
|
+
examples: [
|
|
130
|
+
"pe pal add @alice",
|
|
131
|
+
"pe pal add pal://... bob",
|
|
132
|
+
"pe pal add abc123def456 carol",
|
|
133
|
+
"pe pal list",
|
|
134
|
+
"pe pal remove @alice"
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
invite: {
|
|
138
|
+
summary: "Generate a magic invite link",
|
|
139
|
+
details: "Creates a pal:// link containing your public key, handle, and name. Share it with anyone to let them add you.",
|
|
140
|
+
subcommands: {},
|
|
141
|
+
options: {
|
|
142
|
+
"--qr": "Render the invite as a QR code in the terminal"
|
|
143
|
+
},
|
|
144
|
+
examples: [
|
|
145
|
+
"pe invite",
|
|
146
|
+
"pe invite --qr"
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
nearby: {
|
|
150
|
+
summary: "Discover nearby pals on the LAN",
|
|
151
|
+
details: "Uses mDNS (Bonjour) to find other Palexplorer users on the local network.",
|
|
152
|
+
subcommands: {},
|
|
153
|
+
options: {
|
|
154
|
+
"--add": "Auto-add discovered pals",
|
|
155
|
+
"--time <seconds>": "Scan duration (default: 10)"
|
|
156
|
+
},
|
|
157
|
+
examples: [
|
|
158
|
+
"pe nearby",
|
|
159
|
+
"pe nearby --add",
|
|
160
|
+
"pe nearby --time 30"
|
|
161
|
+
]
|
|
162
|
+
},
|
|
163
|
+
group: {
|
|
164
|
+
summary: "Manage sharing groups",
|
|
165
|
+
details: "Create named groups of pals for easy bulk sharing with `pe share --with-group`.",
|
|
166
|
+
subcommands: {
|
|
167
|
+
create: "Create a new group",
|
|
168
|
+
add: "Add a pal to a group",
|
|
169
|
+
remove: "Remove a pal from a group",
|
|
170
|
+
list: "List all groups and members",
|
|
171
|
+
delete: "Delete a group"
|
|
172
|
+
},
|
|
173
|
+
examples: [
|
|
174
|
+
"pe group create team",
|
|
175
|
+
"pe group add team @alice",
|
|
176
|
+
"pe group remove team @alice",
|
|
177
|
+
"pe group list",
|
|
178
|
+
"pe group delete team"
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
sync: {
|
|
182
|
+
summary: "Sync a directory with a pal",
|
|
183
|
+
details: "Builds a file manifest and sends it to a pal via the discovery server. Supports one-time and continuous (watch) modes.",
|
|
184
|
+
subcommands: {},
|
|
185
|
+
options: {
|
|
186
|
+
"--watch": "Watch for changes and continuously sync",
|
|
187
|
+
"--list": "List all sync pairs",
|
|
188
|
+
"--remove <id>": "Remove a sync pair"
|
|
189
|
+
},
|
|
190
|
+
examples: [
|
|
191
|
+
"pe sync ./project @alice",
|
|
192
|
+
"pe sync --watch ./docs @bob",
|
|
193
|
+
"pe sync --list",
|
|
194
|
+
"pe sync --remove <id>"
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
transfers: {
|
|
198
|
+
summary: "View and manage file transfers",
|
|
199
|
+
details: "Shows active and completed transfers. Supports pause, resume, and cancel operations.",
|
|
200
|
+
subcommands: {
|
|
201
|
+
cancel: "Remove a transfer from tracking",
|
|
202
|
+
pause: "Pause a transfer",
|
|
203
|
+
resume: "Resume a paused transfer"
|
|
204
|
+
},
|
|
205
|
+
options: {
|
|
206
|
+
"--history": "Show completed transfers"
|
|
207
|
+
},
|
|
208
|
+
examples: [
|
|
209
|
+
"pe transfers",
|
|
210
|
+
"pe transfers --history",
|
|
211
|
+
"pe transfers cancel <magnet>",
|
|
212
|
+
"pe transfers pause <magnet>",
|
|
213
|
+
"pe transfers resume <magnet>"
|
|
214
|
+
]
|
|
215
|
+
},
|
|
216
|
+
config: {
|
|
217
|
+
summary: "View and manage configuration",
|
|
218
|
+
details: "Get, set, list, and reset configuration values like port, storage path, bandwidth cap, and discovery URL.",
|
|
219
|
+
subcommands: {
|
|
220
|
+
list: "Show all current settings and known keys",
|
|
221
|
+
get: "Get a single config value",
|
|
222
|
+
set: "Set a config value",
|
|
223
|
+
reset: "Clear all custom settings"
|
|
224
|
+
},
|
|
225
|
+
examples: [
|
|
226
|
+
"pe config list",
|
|
227
|
+
"pe config get theme",
|
|
228
|
+
"pe config set theme dark",
|
|
229
|
+
"pe config reset"
|
|
230
|
+
]
|
|
231
|
+
},
|
|
232
|
+
device: {
|
|
233
|
+
summary: "View and manage this device",
|
|
234
|
+
details: "Rename your device, register it with the discovery server, or list all devices under a handle.",
|
|
235
|
+
subcommands: {
|
|
236
|
+
rename: "Set a unique name for this device",
|
|
237
|
+
register: "Register device with the discovery server",
|
|
238
|
+
list: "List all devices registered under a handle"
|
|
239
|
+
},
|
|
240
|
+
examples: [
|
|
241
|
+
"pe device",
|
|
242
|
+
"pe device list @alice",
|
|
243
|
+
'pe device rename "My Laptop"',
|
|
244
|
+
"pe device register alice"
|
|
245
|
+
]
|
|
246
|
+
},
|
|
247
|
+
server: {
|
|
248
|
+
summary: "Provision and manage server nodes",
|
|
249
|
+
details: "Install pe serve as a system service, configure server parameters, register with discovery, and check status. Supports Windows (schtasks), Linux (systemd), and macOS (launchd).",
|
|
250
|
+
subcommands: {
|
|
251
|
+
install: "Install as a background service (cross-platform)",
|
|
252
|
+
config: "Set server-specific operational parameters",
|
|
253
|
+
register: "Register this server node with the discovery network",
|
|
254
|
+
status: "Check server health and network connectivity"
|
|
255
|
+
},
|
|
256
|
+
examples: [
|
|
257
|
+
"pe server install",
|
|
258
|
+
"pe server status",
|
|
259
|
+
"pe server config port 8080",
|
|
260
|
+
"pe server register mynode"
|
|
261
|
+
]
|
|
262
|
+
},
|
|
263
|
+
explorer: {
|
|
264
|
+
summary: "Manage file manager integration",
|
|
265
|
+
details: "Adds or removes a 'Share with Pal' context menu entry in Windows Explorer, macOS Finder, or Linux Nautilus.",
|
|
266
|
+
subcommands: {
|
|
267
|
+
install: "Install context menu for your platform",
|
|
268
|
+
uninstall: "Remove context menu entry"
|
|
269
|
+
},
|
|
270
|
+
examples: [
|
|
271
|
+
"pe explorer install",
|
|
272
|
+
"pe explorer uninstall"
|
|
273
|
+
]
|
|
274
|
+
},
|
|
275
|
+
status: {
|
|
276
|
+
summary: "Show system health dashboard",
|
|
277
|
+
details: "Displays identity, daemon status, active shares, transfers, and discovery server connectivity at a glance.",
|
|
278
|
+
subcommands: {},
|
|
279
|
+
examples: [
|
|
280
|
+
"pe status"
|
|
281
|
+
]
|
|
282
|
+
},
|
|
283
|
+
log: {
|
|
284
|
+
summary: "View application logs",
|
|
285
|
+
details: "Shows recent log entries from ~/.pal/logs/pal.log. Supports filtering by level and real-time tailing.",
|
|
286
|
+
subcommands: {},
|
|
287
|
+
options: {
|
|
288
|
+
"--tail": "Follow new log entries in real-time",
|
|
289
|
+
"--level <level>": "Filter by level (debug/info/warn/error)",
|
|
290
|
+
"-n, --lines <count>": "Number of recent lines to show (default: 20)"
|
|
291
|
+
},
|
|
292
|
+
examples: [
|
|
293
|
+
"pe log",
|
|
294
|
+
"pe log --tail",
|
|
295
|
+
"pe log --level error",
|
|
296
|
+
"pe log -n 50"
|
|
297
|
+
]
|
|
298
|
+
},
|
|
299
|
+
completion: {
|
|
300
|
+
summary: "Generate shell completion script",
|
|
301
|
+
details: "Outputs a completion script for bash or zsh. Source it in your shell profile for tab completion.",
|
|
302
|
+
subcommands: {},
|
|
303
|
+
options: {
|
|
304
|
+
"--shell <shell>": "Shell type: bash or zsh (default: bash)"
|
|
305
|
+
},
|
|
306
|
+
examples: [
|
|
307
|
+
"pe completion",
|
|
308
|
+
"pe completion --shell zsh",
|
|
309
|
+
"source <(pe completion)"
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
web: {
|
|
313
|
+
summary: "Open the web dashboard",
|
|
314
|
+
details: "Opens the web dashboard in your default browser. Requires `pe serve --web` to be running first.",
|
|
315
|
+
subcommands: {},
|
|
316
|
+
options: {
|
|
317
|
+
"--port <port>": "Web dashboard port (default: 8585)",
|
|
318
|
+
"--url-only": "Just print the URL without opening browser"
|
|
319
|
+
},
|
|
320
|
+
examples: [
|
|
321
|
+
"pe web",
|
|
322
|
+
"pe web --url-only",
|
|
323
|
+
"pe web --port 9090"
|
|
324
|
+
]
|
|
325
|
+
},
|
|
326
|
+
vfs: {
|
|
327
|
+
summary: "Manage the virtual filesystem drive",
|
|
328
|
+
details: "Mounts your shared files as a virtual drive letter via WebDAV. Requires WinFSP on Windows or FUSE on Linux/macOS.",
|
|
329
|
+
subcommands: {
|
|
330
|
+
mount: "Mount virtual drive (default P:)",
|
|
331
|
+
unmount: "Unmount virtual drive",
|
|
332
|
+
status: "Show mount status and listed shares"
|
|
333
|
+
},
|
|
334
|
+
examples: [
|
|
335
|
+
"pe vfs mount",
|
|
336
|
+
"pe vfs mount S",
|
|
337
|
+
"pe vfs unmount",
|
|
338
|
+
"pe vfs status"
|
|
339
|
+
]
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
gui: {
|
|
343
|
+
transfers: "The Transfer Manager shows all active uploads (seeding) and downloads. You can pause or cancel transfers here.",
|
|
344
|
+
inbox: "View and accept share invitations from your pals. Clicking 'Download' will add the resource to your Transfers.",
|
|
345
|
+
pals: "Manage your social circle. Search for friends by their verified email handles."
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export const getHelp = (topic) => {
|
|
350
|
+
const parts = topic.split('.');
|
|
351
|
+
let current = HelpRegistry;
|
|
352
|
+
for (const part of parts) {
|
|
353
|
+
if (current[part]) current = current[part];
|
|
354
|
+
else return null;
|
|
355
|
+
}
|
|
356
|
+
return current;
|
|
357
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const LOG_DIR = path.join(os.homedir(), '.pal', 'logs');
|
|
6
|
+
const LOG_FILE = path.join(LOG_DIR, 'pal.log');
|
|
7
|
+
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
|
|
8
|
+
|
|
9
|
+
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 };
|
|
10
|
+
|
|
11
|
+
let _level = 'info';
|
|
12
|
+
|
|
13
|
+
const _originalConsole = {
|
|
14
|
+
log: console.log.bind(console),
|
|
15
|
+
warn: console.warn.bind(console),
|
|
16
|
+
error: console.error.bind(console),
|
|
17
|
+
debug: console.debug.bind(console),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function ensureLogDir() {
|
|
21
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
22
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function rotate() {
|
|
27
|
+
try {
|
|
28
|
+
if (!fs.existsSync(LOG_FILE)) return;
|
|
29
|
+
const stat = fs.statSync(LOG_FILE);
|
|
30
|
+
if (stat.size < MAX_SIZE) return;
|
|
31
|
+
const rotated = LOG_FILE + '.1';
|
|
32
|
+
if (fs.existsSync(rotated)) fs.unlinkSync(rotated);
|
|
33
|
+
fs.renameSync(LOG_FILE, rotated);
|
|
34
|
+
} catch {
|
|
35
|
+
// best effort
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeLog(level, message, meta) {
|
|
40
|
+
ensureLogDir();
|
|
41
|
+
rotate();
|
|
42
|
+
const entry = {
|
|
43
|
+
ts: new Date().toISOString(),
|
|
44
|
+
level,
|
|
45
|
+
msg: message,
|
|
46
|
+
...meta && { meta }
|
|
47
|
+
};
|
|
48
|
+
fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + '\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shouldLog(level) {
|
|
52
|
+
return LEVELS[level] >= LEVELS[_level];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const logger = {
|
|
56
|
+
LEVELS: Object.keys(LEVELS),
|
|
57
|
+
|
|
58
|
+
get level() { return _level; },
|
|
59
|
+
get logDir() { return LOG_DIR; },
|
|
60
|
+
get logFile() { return LOG_FILE; },
|
|
61
|
+
|
|
62
|
+
setLevel(level) {
|
|
63
|
+
if (LEVELS[level] !== undefined) _level = level;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
shouldLog(level) {
|
|
67
|
+
return shouldLog(level);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
debug(msg, meta) {
|
|
71
|
+
writeLog('debug', msg, meta);
|
|
72
|
+
if (shouldLog('debug')) _originalConsole.debug(`[debug] ${msg}`);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
info(msg, meta) {
|
|
76
|
+
writeLog('info', msg, meta);
|
|
77
|
+
if (shouldLog('info')) _originalConsole.log(msg);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
warn(msg, meta) {
|
|
81
|
+
writeLog('warn', msg, meta);
|
|
82
|
+
if (shouldLog('warn')) _originalConsole.warn(`[warn] ${msg}`);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
error(msg, meta) {
|
|
86
|
+
writeLog('error', msg, meta);
|
|
87
|
+
if (shouldLog('error')) _originalConsole.error(`[error] ${msg}`);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
applyGlobalOverride() {
|
|
91
|
+
console.warn = (...args) => {
|
|
92
|
+
if (shouldLog('warn')) _originalConsole.warn(...args);
|
|
93
|
+
};
|
|
94
|
+
console.error = (...args) => {
|
|
95
|
+
if (shouldLog('error')) _originalConsole.error(...args);
|
|
96
|
+
};
|
|
97
|
+
console.debug = (...args) => {
|
|
98
|
+
if (shouldLog('debug')) _originalConsole.debug(...args);
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export default logger;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { pipeline } from 'stream/promises';
|
|
5
|
+
import { Readable } from 'stream';
|
|
6
|
+
|
|
7
|
+
export const TORRENT_TIMEOUT_SHORT = 30_000;
|
|
8
|
+
export const TORRENT_TIMEOUT_LONG = 60_000;
|
|
9
|
+
export const TORRENT_TIMEOUT_METADATA = 60_000;
|
|
10
|
+
|
|
11
|
+
// Reliable public trackers to speed up discovery
|
|
12
|
+
export const DEFAULT_TRACKERS = [
|
|
13
|
+
'udp://tracker.opentrackr.org:1337/announce',
|
|
14
|
+
'udp://9.rarbg.com:2810/announce',
|
|
15
|
+
'udp://tracker.openbittorrent.com:6969/announce',
|
|
16
|
+
'udp://exodus.desync.com:6969/announce',
|
|
17
|
+
'udp://www.torrent.eu.org:451/announce',
|
|
18
|
+
'wss://tracker.btorrent.xyz',
|
|
19
|
+
'wss://tracker.openwebtorrent.com',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function injectLanPeers(torrent, lanPeers) {
|
|
23
|
+
if (!torrent || !lanPeers?.length) return 0;
|
|
24
|
+
let injected = 0;
|
|
25
|
+
for (const peer of lanPeers) {
|
|
26
|
+
if (!peer.ip || peer.ip === 'unknown') continue;
|
|
27
|
+
if (peer.torrentPort) {
|
|
28
|
+
try {
|
|
29
|
+
torrent.addPeer(`${peer.ip}:${peer.torrentPort}`);
|
|
30
|
+
injected++;
|
|
31
|
+
} catch {
|
|
32
|
+
// peer injection can fail if address is invalid
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return injected;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function tryLanHttpDownload(lanPeers, shareName, outputDir, opts = {}) {
|
|
40
|
+
const { onProgress, selectedFiles, timeout = 30_000 } = opts;
|
|
41
|
+
if (!lanPeers?.length || !shareName) return null;
|
|
42
|
+
|
|
43
|
+
for (const peer of lanPeers) {
|
|
44
|
+
if (!peer.ip || peer.ip === 'unknown' || !peer.webPort) continue;
|
|
45
|
+
const baseUrl = `http://${peer.ip}:${peer.webPort}`;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Check if peer has this share via /p2p/browse
|
|
49
|
+
const browseRes = await fetch(
|
|
50
|
+
`${baseUrl}/p2p/browse?share=${encodeURIComponent(shareName)}`,
|
|
51
|
+
{ signal: AbortSignal.timeout(5000) }
|
|
52
|
+
);
|
|
53
|
+
if (!browseRes.ok) continue;
|
|
54
|
+
|
|
55
|
+
const entries = await browseRes.json();
|
|
56
|
+
if (!entries?.length) continue;
|
|
57
|
+
|
|
58
|
+
// Collect all files recursively
|
|
59
|
+
const files = await collectLanFiles(baseUrl, shareName, entries, timeout);
|
|
60
|
+
if (!files.length) continue;
|
|
61
|
+
|
|
62
|
+
// Filter files if selective download requested
|
|
63
|
+
const toDownload = selectedFiles
|
|
64
|
+
? files.filter(f => selectedFiles.some(sf => f.name === sf || f.path.includes(sf)))
|
|
65
|
+
: files;
|
|
66
|
+
|
|
67
|
+
if (!toDownload.length) continue;
|
|
68
|
+
|
|
69
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
70
|
+
const downloaded = [];
|
|
71
|
+
let totalBytes = 0;
|
|
72
|
+
const totalSize = toDownload.reduce((sum, f) => sum + (f.size || 0), 0);
|
|
73
|
+
|
|
74
|
+
for (const file of toDownload) {
|
|
75
|
+
const filePath = path.join(outputDir, file.path);
|
|
76
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
77
|
+
|
|
78
|
+
const fileRes = await fetch(
|
|
79
|
+
`${baseUrl}/p2p/file?share=${encodeURIComponent(shareName)}&path=${encodeURIComponent(file.path)}`,
|
|
80
|
+
{ signal: AbortSignal.timeout(timeout) }
|
|
81
|
+
);
|
|
82
|
+
if (!fileRes.ok) continue;
|
|
83
|
+
|
|
84
|
+
await pipeline(Readable.fromWeb(fileRes.body), fs.createWriteStream(filePath));
|
|
85
|
+
totalBytes += file.size || 0;
|
|
86
|
+
downloaded.push(file);
|
|
87
|
+
|
|
88
|
+
if (onProgress && totalSize > 0) {
|
|
89
|
+
onProgress(totalBytes / totalSize, file.name);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (downloaded.length > 0) {
|
|
94
|
+
return { files: downloaded, peerIp: peer.ip, totalBytes };
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// This peer didn't work, try next
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function collectLanFiles(baseUrl, shareName, entries, timeout) {
|
|
106
|
+
const files = [];
|
|
107
|
+
|
|
108
|
+
async function walk(items) {
|
|
109
|
+
for (const entry of items) {
|
|
110
|
+
if (entry.isDir) {
|
|
111
|
+
try {
|
|
112
|
+
const res = await fetch(
|
|
113
|
+
`${baseUrl}/p2p/browse?share=${encodeURIComponent(shareName)}&path=${encodeURIComponent(entry.path)}`,
|
|
114
|
+
{ signal: AbortSignal.timeout(5000) }
|
|
115
|
+
);
|
|
116
|
+
if (res.ok) {
|
|
117
|
+
const subEntries = await res.json();
|
|
118
|
+
await walk(subEntries);
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// skip unreachable subdirs
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
files.push(entry);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await walk(entries);
|
|
130
|
+
return files;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function getTorrentMetadata(magnet, opts = {}) {
|
|
134
|
+
const { path: downloadPath = os.tmpdir(), timeout = TORRENT_TIMEOUT_METADATA, lanPeers } = opts;
|
|
135
|
+
const WebTorrent = (await import('webtorrent')).default;
|
|
136
|
+
const client = new WebTorrent();
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
// Proactively fetch seeders from discovery server if possible
|
|
140
|
+
const infohashMatch = magnet.match(/xt=urn:btih:([a-fA-F0-9]{40})/);
|
|
141
|
+
if (infohashMatch) {
|
|
142
|
+
const infohash = infohashMatch[1];
|
|
143
|
+
try {
|
|
144
|
+
const { fetchSeeders } = await import('../core/discoveryClient.js');
|
|
145
|
+
const seeders = await fetchSeeders(infohash);
|
|
146
|
+
if (seeders.length > 0) {
|
|
147
|
+
seeders.forEach(s => {
|
|
148
|
+
if (s.peerId) {
|
|
149
|
+
// Note: We don't have direct IP here, but we can announce to the tracker
|
|
150
|
+
// that we are looking for this peerId.
|
|
151
|
+
// Actually, seeder announce usually includes connection info if available.
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// ignore fetch errors
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const torrent = await new Promise((resolve, reject) => {
|
|
161
|
+
// Add trackers to magnet if they are missing
|
|
162
|
+
let magnetWithTrackers = magnet;
|
|
163
|
+
if (!magnet.includes('&tr=')) {
|
|
164
|
+
DEFAULT_TRACKERS.forEach(tr => {
|
|
165
|
+
magnetWithTrackers += `&tr=${encodeURIComponent(tr)}`;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const t = client.add(magnetWithTrackers, { path: downloadPath });
|
|
170
|
+
|
|
171
|
+
// Inject LAN peers for faster discovery
|
|
172
|
+
if (lanPeers?.length) {
|
|
173
|
+
injectLanPeers(t, lanPeers);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const timer = setTimeout(() => {
|
|
177
|
+
const err = new Error(`Timed out waiting for torrent metadata (${Math.round(timeout/1000)}s)`);
|
|
178
|
+
err.code = 'ETIMEDOUT';
|
|
179
|
+
reject(err);
|
|
180
|
+
}, timeout);
|
|
181
|
+
|
|
182
|
+
t.on('metadata', () => {
|
|
183
|
+
// Metadata is ready (file list is known)
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
resolve(t);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
t.on('error', (err) => {
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
reject(err);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return { torrent, client };
|
|
195
|
+
} catch (err) {
|
|
196
|
+
await destroyClient(client);
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function destroyClient(client) {
|
|
202
|
+
return new Promise(resolve => client.destroy(resolve));
|
|
203
|
+
}
|