pal-explorer-cli 0.4.12 → 0.4.13

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.
Files changed (99) hide show
  1. package/README.md +149 -149
  2. package/bin/pal.js +63 -2
  3. package/extensions/@palexplorer/analytics/extension.json +20 -1
  4. package/extensions/@palexplorer/analytics/index.js +19 -9
  5. package/extensions/@palexplorer/audit/extension.json +14 -0
  6. package/extensions/@palexplorer/auth-email/extension.json +15 -0
  7. package/extensions/@palexplorer/auth-oauth/extension.json +15 -0
  8. package/extensions/@palexplorer/chat/extension.json +14 -0
  9. package/extensions/@palexplorer/discovery/extension.json +17 -0
  10. package/extensions/@palexplorer/discovery/index.js +1 -1
  11. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  12. package/extensions/@palexplorer/groups/extension.json +15 -0
  13. package/extensions/@palexplorer/share-links/extension.json +15 -0
  14. package/extensions/@palexplorer/sync/extension.json +16 -0
  15. package/extensions/@palexplorer/user-mgmt/extension.json +15 -0
  16. package/lib/capabilities.js +24 -24
  17. package/lib/commands/analytics.js +175 -175
  18. package/lib/commands/api-keys.js +131 -131
  19. package/lib/commands/audit.js +235 -235
  20. package/lib/commands/auth.js +137 -137
  21. package/lib/commands/backup.js +76 -76
  22. package/lib/commands/billing.js +148 -148
  23. package/lib/commands/chat.js +217 -217
  24. package/lib/commands/cloud-backup.js +231 -231
  25. package/lib/commands/comment.js +99 -99
  26. package/lib/commands/completion.js +203 -203
  27. package/lib/commands/compliance.js +218 -218
  28. package/lib/commands/config.js +136 -136
  29. package/lib/commands/connect.js +44 -44
  30. package/lib/commands/dept.js +294 -294
  31. package/lib/commands/device.js +146 -146
  32. package/lib/commands/download.js +240 -226
  33. package/lib/commands/explorer.js +178 -178
  34. package/lib/commands/extension.js +1060 -970
  35. package/lib/commands/favorite.js +90 -90
  36. package/lib/commands/federation.js +270 -270
  37. package/lib/commands/file.js +533 -533
  38. package/lib/commands/group.js +271 -271
  39. package/lib/commands/gui-share.js +29 -29
  40. package/lib/commands/init.js +61 -61
  41. package/lib/commands/invite.js +59 -59
  42. package/lib/commands/list.js +58 -58
  43. package/lib/commands/log.js +116 -116
  44. package/lib/commands/nearby.js +108 -108
  45. package/lib/commands/network.js +251 -251
  46. package/lib/commands/notify.js +198 -198
  47. package/lib/commands/org.js +273 -273
  48. package/lib/commands/pal.js +403 -180
  49. package/lib/commands/permissions.js +216 -216
  50. package/lib/commands/pin.js +97 -97
  51. package/lib/commands/protocol.js +357 -357
  52. package/lib/commands/rbac.js +147 -147
  53. package/lib/commands/recover.js +36 -36
  54. package/lib/commands/register.js +171 -171
  55. package/lib/commands/relay.js +131 -131
  56. package/lib/commands/remote.js +368 -368
  57. package/lib/commands/revoke.js +50 -50
  58. package/lib/commands/scanner.js +280 -280
  59. package/lib/commands/schedule.js +344 -344
  60. package/lib/commands/scim.js +203 -203
  61. package/lib/commands/search.js +181 -181
  62. package/lib/commands/serve.js +438 -438
  63. package/lib/commands/server.js +350 -350
  64. package/lib/commands/share-link.js +199 -199
  65. package/lib/commands/share.js +336 -323
  66. package/lib/commands/sso.js +200 -200
  67. package/lib/commands/status.js +145 -145
  68. package/lib/commands/stream.js +562 -562
  69. package/lib/commands/su.js +187 -187
  70. package/lib/commands/sync.js +979 -979
  71. package/lib/commands/transfers.js +152 -152
  72. package/lib/commands/uninstall.js +188 -188
  73. package/lib/commands/update.js +204 -204
  74. package/lib/commands/user.js +276 -276
  75. package/lib/commands/vfs.js +84 -84
  76. package/lib/commands/web-login.js +79 -79
  77. package/lib/commands/web.js +52 -52
  78. package/lib/commands/webhook.js +180 -180
  79. package/lib/commands/whoami.js +59 -59
  80. package/lib/commands/workspace.js +121 -121
  81. package/lib/core/billing.js +16 -5
  82. package/lib/core/dhtDiscovery.js +9 -2
  83. package/lib/core/discoveryClient.js +13 -7
  84. package/lib/core/extensions.js +142 -1
  85. package/lib/core/identity.js +33 -2
  86. package/lib/core/imageProcessor.js +109 -0
  87. package/lib/core/imageTorrent.js +167 -0
  88. package/lib/core/permissions.js +1 -1
  89. package/lib/core/pro.js +11 -4
  90. package/lib/core/serverList.js +4 -1
  91. package/lib/core/shares.js +12 -1
  92. package/lib/core/signalingServer.js +14 -2
  93. package/lib/core/su.js +1 -1
  94. package/lib/core/users.js +1 -1
  95. package/lib/protocol/messages.js +12 -3
  96. package/lib/utils/explorer.js +1 -1
  97. package/lib/utils/help.js +357 -357
  98. package/lib/utils/torrent.js +1 -0
  99. package/package.json +4 -3
@@ -1,226 +1,240 @@
1
- import WebTorrent from 'webtorrent';
2
- import chalk from 'chalk';
3
- import ora from 'ora';
4
- import path from 'path';
5
- import { trackTransfer, getTransfers, completeTransfer } from '../core/transfers.js';
6
- import { getIdentity } from '../core/identity.js';
7
- import { decryptFromDownload, getDecryptedDownloadDir } from '../crypto/streamEncryption.js';
8
- import logger from '../utils/logger.js';
9
- import { getNearbyPeers, startMdns, isRunning as isMdnsRunning } from '../core/mdnsService.js';
10
- import { injectLanPeers, tryLanHttpDownload } from '../utils/torrent.js';
11
- import config from '../utils/config.js';
12
- import { getDownloadDir } from '../utils/downloadDir.js';
13
- import fs from 'fs';
14
-
15
- function startMdnsIfNeeded() {
16
- if (isMdnsRunning()) return;
17
- const identity = config.get('identity');
18
- if (identity?.publicKey) {
19
- const pidPath = path.join(path.dirname(config.path), 'serve.pid');
20
- const daemonRunning = fs.existsSync(pidPath);
21
- startMdns(identity, { advertise: !daemonRunning });
22
- }
23
- }
24
-
25
- export default function downloadCommand(program) {
26
- program
27
- .command('download <magnet> [moreMagnets...]')
28
- .description('download file(s) from Magnet Link(s)')
29
- .option('--key <encryptedShareKey>', 'Encrypted share key for decryption (for private shares)')
30
- .option('--lan-only', 'Only download from LAN peers (skip public trackers)')
31
- .option('--share-name <name>', 'Share name for LAN HTTP download')
32
- .addHelpText('after', `
33
- Examples:
34
- $ pe download "magnet:?xt=urn:btih:..." Download a public share
35
- $ pe download "magnet:?xt=..." --key abc123 Download encrypted share
36
- $ pe download "magnet:?xt=..." "magnet:?xt=..." Batch download multiple
37
- $ pe download "magnet:?xt=..." --lan-only LAN peers only
38
- $ pe download "magnet:?xt=..." --share-name Photos Try direct HTTP from LAN first
39
- `)
40
- .action(async (magnet, moreMagnets, opts) => {
41
- // Start mDNS to discover LAN peers
42
- startMdnsIfNeeded();
43
- const lanPeers = getNearbyPeers();
44
- if (lanPeers.length > 0) {
45
- logger.info(`Found ${lanPeers.length} LAN peer(s) via mDNS`);
46
- }
47
-
48
- const allMagnets = [magnet, ...(moreMagnets || [])];
49
-
50
- if (allMagnets.length > 1) {
51
- console.log(chalk.blue(`Batch downloading ${allMagnets.length} items...`));
52
- const client = new WebTorrent();
53
- let completed = 0;
54
- let failed = 0;
55
-
56
- for (const m of allMagnets) {
57
- if (!m.startsWith('magnet:?') && !/^[0-9a-fA-F]{40}$/.test(m)) {
58
- console.error(chalk.red(`Skipping invalid magnet: ${m.slice(0, 50)}...`));
59
- failed++;
60
- continue;
61
- }
62
-
63
- try {
64
- const dlDir = getDownloadDir();
65
- if (!fs.existsSync(dlDir)) fs.mkdirSync(dlDir, { recursive: true });
66
- await new Promise((resolve, reject) => {
67
- const timeout = setTimeout(() => reject(new Error('Peer timeout (60s)')), 60000);
68
- const addOpts = { path: dlDir };
69
- const t = client.add(opts.lanOnly ? m : m, addOpts, (torrent) => {
70
- clearTimeout(timeout);
71
- console.log(chalk.cyan(` Downloading: ${torrent.name}`));
72
- trackTransfer(m, torrent.name, dlDir, null);
73
- torrent.on('done', () => {
74
- console.log(chalk.green(` ✔ ${torrent.name}`));
75
- completeTransfer(m);
76
- completed++;
77
- resolve();
78
- });
79
- });
80
-
81
- // Inject LAN peers into each torrent
82
- if (lanPeers.length > 0) {
83
- injectLanPeers(t, lanPeers);
84
- }
85
-
86
- client.on('error', (err) => { clearTimeout(timeout); reject(err); });
87
- });
88
- } catch (err) {
89
- console.error(chalk.red(` ✖ Failed: ${err.message}`));
90
- failed++;
91
- }
92
- }
93
-
94
- console.log('');
95
- console.log(chalk.green(`Completed: ${completed}`) + (failed > 0 ? chalk.red(` Failed: ${failed}`) : ''));
96
- client.destroy();
97
- process.exit(failed > 0 ? 1 : 0);
98
- return;
99
- }
100
-
101
- // Single download
102
- if (!magnet.startsWith('magnet:?') && !/^[0-9a-fA-F]{40}$/.test(magnet)) {
103
- console.error(chalk.red('Invalid magnet URI. Must start with "magnet:?" or be a 40-character hex infohash.'));
104
- process.exitCode = 1;
105
- return;
106
- }
107
-
108
- const dlDir = getDownloadDir();
109
- if (!fs.existsSync(dlDir)) fs.mkdirSync(dlDir, { recursive: true });
110
-
111
- // Strategy 1: Try direct HTTP download from LAN peer (fastest)
112
- if (opts.shareName && lanPeers.length > 0) {
113
- console.log(chalk.blue(`Trying direct LAN download from ${lanPeers.length} nearby peer(s)...`));
114
- const spinner = ora('Downloading via LAN HTTP...').start();
115
- try {
116
- const result = await tryLanHttpDownload(lanPeers, opts.shareName, dlDir, {
117
- onProgress: (pct, fileName) => {
118
- spinner.text = `LAN download... ${(pct * 100).toFixed(1)}% (${fileName})`;
119
- },
120
- });
121
- if (result) {
122
- spinner.succeed(chalk.green(`LAN download complete from ${result.peerIp} (${result.files.length} files, ${(result.totalBytes / 1024 / 1024).toFixed(2)} MB)`));
123
- trackTransfer(magnet, opts.shareName, dlDir, opts.key || null);
124
- completeTransfer(magnet);
125
- process.exit(0);
126
- return;
127
- }
128
- spinner.info(chalk.dim('No LAN peer serving this share. Falling back to BitTorrent...'));
129
- } catch (err) {
130
- spinner.info(chalk.dim(`LAN download failed: ${err.message}. Falling back to BitTorrent...`));
131
- }
132
- }
133
-
134
- if (opts.lanOnly && lanPeers.length === 0) {
135
- console.error(chalk.red('No LAN peers found. Remove --lan-only to use public trackers.'));
136
- process.exitCode = 1;
137
- return;
138
- }
139
-
140
- // Strategy 2: WebTorrent with LAN peer injection
141
- logger.info('Starting download', { magnet: magnet.slice(0, 40) });
142
- console.log(chalk.blue('Connecting to peers...'));
143
- if (lanPeers.length > 0) {
144
- console.log(chalk.cyan(` ${lanPeers.length} LAN peer(s) will be injected for faster discovery`));
145
- }
146
- const spinner = ora('Finding peers...').start();
147
-
148
- const client = new WebTorrent();
149
-
150
- const peerTimeout = setTimeout(() => {
151
- const torrents = client.torrents;
152
- if (torrents.length > 0 && torrents[0].numPeers === 0) {
153
- spinner.warn(chalk.yellow('⚠ No peers found after 120 seconds. Check that the share is being seeded.'));
154
- client.destroy();
155
- process.exitCode = 1;
156
- }
157
- }, 120000);
158
-
159
- const addOpts = { path: dlDir };
160
- const torrentSrc = opts.lanOnly ? magnet : magnet;
161
-
162
- client.add(torrentSrc, addOpts, (torrent) => {
163
- clearTimeout(peerTimeout);
164
- spinner.succeed(chalk.green(`Connected! Downloading: ${torrent.name}`));
165
-
166
- // Track transfer, storing the encrypted key if provided
167
- trackTransfer(magnet, torrent.name, dlDir, opts.key || null);
168
-
169
- const progressSpinner = ora('Downloading... 0%').start();
170
-
171
- torrent.on('download', (bytes) => {
172
- const progress = (torrent.progress * 100).toFixed(1);
173
- progressSpinner.text = `Downloading... ${progress}% (${(torrent.downloaded / 1024 / 1024).toFixed(2)} MB / ${(torrent.length / 1024 / 1024).toFixed(2)} MB)`;
174
- });
175
-
176
- torrent.on('done', async () => {
177
- logger.info(`Download complete: ${torrent.name}`, { size: torrent.length });
178
- progressSpinner.succeed(chalk.green('Download Complete!'));
179
- const downloadedDir = path.join(dlDir, torrent.name);
180
- console.log(chalk.cyan(`File saved to: ${downloadedDir}`));
181
-
182
- // Attempt decryption if an encrypted share key was provided
183
- const transfers = getTransfers();
184
- const thisTransfer = transfers.find(t => t.magnet === magnet);
185
- if (thisTransfer?.encryptedShareKey) {
186
- const identity = await getIdentity();
187
- if (identity?.publicKey && identity?.privateKey) {
188
- try {
189
- const { decryptShareKey } = await import('../crypto/shareEncryption.js');
190
- const shareKey = decryptShareKey(thisTransfer.encryptedShareKey, identity.publicKey, identity.privateKey);
191
- const shareName = thisTransfer.name || 'download';
192
- const outDir = getDecryptedDownloadDir(shareName);
193
- console.log(chalk.blue('Decrypting E2EE files...'));
194
- decryptFromDownload(downloadedDir, outDir, shareKey);
195
- console.log(chalk.green(`Decrypted files saved to: ${outDir}`));
196
- } catch (e) {
197
- console.log(chalk.yellow(`Warning: Could not decrypt files — ${e.message}`));
198
- }
199
- } else {
200
- console.log(chalk.yellow('Warning: No identity found — skipping decryption. Run `pe identity` to set up.'));
201
- }
202
- }
203
-
204
- completeTransfer(magnet);
205
- client.destroy();
206
- process.exit(0);
207
- });
208
- });
209
-
210
- // Inject LAN peers into the torrent right after adding
211
- const currentTorrent = client.torrents[client.torrents.length - 1];
212
- if (currentTorrent && lanPeers.length > 0) {
213
- const injected = injectLanPeers(currentTorrent, lanPeers);
214
- if (injected > 0) {
215
- logger.info(`Injected ${injected} LAN peer(s) into torrent`);
216
- }
217
- }
218
-
219
- client.on('error', (err) => {
220
- logger.error(`Download failed: ${err.message}`, { magnet: magnet.slice(0, 40) });
221
- spinner.fail(chalk.red('Download Failed'));
222
- console.error(chalk.red(`Error: ${err.message}`));
223
- process.exit(1);
224
- });
225
- });
226
- }
1
+ import WebTorrent from 'webtorrent';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import path from 'path';
5
+ import { trackTransfer, getTransfers, completeTransfer } from '../core/transfers.js';
6
+ import { getIdentity } from '../core/identity.js';
7
+ import { decryptFromDownload, getDecryptedDownloadDir } from '../crypto/streamEncryption.js';
8
+ import logger from '../utils/logger.js';
9
+ import { getNearbyPeers, startMdns, isRunning as isMdnsRunning } from '../core/mdnsService.js';
10
+ import { injectLanPeers, tryLanHttpDownload } from '../utils/torrent.js';
11
+ import config from '../utils/config.js';
12
+ import { getDownloadDir } from '../utils/downloadDir.js';
13
+ import fs from 'fs';
14
+
15
+ function startMdnsIfNeeded() {
16
+ if (isMdnsRunning()) return;
17
+ const identity = config.get('identity');
18
+ if (identity?.publicKey) {
19
+ const pidPath = path.join(path.dirname(config.path), 'serve.pid');
20
+ const daemonRunning = fs.existsSync(pidPath);
21
+ startMdns(identity, { advertise: !daemonRunning });
22
+ }
23
+ }
24
+
25
+ export default function downloadCommand(program) {
26
+ program
27
+ .command('download <magnet> [moreMagnets...]')
28
+ .description('download file(s) from Magnet Link(s)')
29
+ .option('--key <encryptedShareKey>', 'Encrypted share key for decryption (for private shares)')
30
+ .option('--lan-only', 'Only download from LAN peers (skip public trackers)')
31
+ .option('--share-name <name>', 'Share name for LAN HTTP download')
32
+ .option('-o, --output <dir>', 'Output directory (overrides default)')
33
+ .option('-p, --peer <addr>', 'Direct peer address (ip:port) to add')
34
+ .addHelpText('after', `
35
+ Examples:
36
+ $ pal download "magnet:?xt=urn:btih:..." Download a public share
37
+ $ pal download "magnet:?xt=..." --key abc123 Download encrypted share
38
+ $ pal download "magnet:?xt=..." -p 1.2.3.4:6881 Direct connection to peer
39
+ $ pal download "magnet:?xt=..." --lan-only LAN peers only
40
+ $ pal download "magnet:?xt=..." --share-name Photos Try direct HTTP from LAN first
41
+ `)
42
+ .action(async (magnet, moreMagnets, opts) => {
43
+ // Start mDNS to discover LAN peers
44
+ startMdnsIfNeeded();
45
+ const lanPeers = getNearbyPeers();
46
+ if (lanPeers.length > 0) {
47
+ logger.info(`Found ${lanPeers.length} LAN peer(s) via mDNS`);
48
+ }
49
+
50
+ const allMagnets = [magnet, ...(moreMagnets || [])];
51
+
52
+ if (allMagnets.length > 1) {
53
+ console.log(chalk.blue(`Batch downloading ${allMagnets.length} items...`));
54
+ const client = new WebTorrent();
55
+ let completed = 0;
56
+ let failed = 0;
57
+
58
+ for (const m of allMagnets) {
59
+ if (!m.startsWith('magnet:?') && !/^[0-9a-fA-F]{40}$/.test(m)) {
60
+ console.error(chalk.red(`Skipping invalid magnet: ${m.slice(0, 50)}...`));
61
+ failed++;
62
+ continue;
63
+ }
64
+
65
+ try {
66
+ const dlDir = opts.output || getDownloadDir();
67
+ if (!fs.existsSync(dlDir)) fs.mkdirSync(dlDir, { recursive: true });
68
+ await new Promise((resolve, reject) => {
69
+ const timeout = setTimeout(() => reject(new Error('Peer timeout (60s)')), 60000);
70
+ const addOpts = { path: dlDir };
71
+ const t = client.add(opts.lanOnly ? m : m, addOpts, (torrent) => {
72
+ clearTimeout(timeout);
73
+ console.log(chalk.cyan(` Downloading: ${torrent.name}`));
74
+ trackTransfer(m, torrent.name, dlDir, null);
75
+ torrent.on('done', () => {
76
+ console.log(chalk.green(` ✔ ${torrent.name}`));
77
+ completeTransfer(m);
78
+ completed++;
79
+ resolve();
80
+ });
81
+ });
82
+
83
+ // Inject LAN peers and manual peer
84
+ if (lanPeers.length > 0) injectLanPeers(t, lanPeers);
85
+ if (opts.peer) {
86
+ try { t.addPeer(opts.peer); } catch (e) { console.error(chalk.yellow(` Warning: Could not add peer ${opts.peer}: ${e.message}`)); }
87
+ }
88
+
89
+ client.on('error', (err) => { clearTimeout(timeout); reject(err); });
90
+ });
91
+ } catch (err) {
92
+ console.error(chalk.red(` ✖ Failed: ${err.message}`));
93
+ failed++;
94
+ }
95
+ }
96
+
97
+ console.log('');
98
+ console.log(chalk.green(`Completed: ${completed}`) + (failed > 0 ? chalk.red(` Failed: ${failed}`) : ''));
99
+ client.destroy();
100
+ process.exit(failed > 0 ? 1 : 0);
101
+ return;
102
+ }
103
+
104
+ // Single download
105
+ if (!magnet.startsWith('magnet:?') && !/^[0-9a-fA-F]{40}$/.test(magnet)) {
106
+ console.error(chalk.red('Invalid magnet URI. Must start with "magnet:?" or be a 40-character hex infohash.'));
107
+ process.exitCode = 1;
108
+ return;
109
+ }
110
+
111
+ const dlDir = opts.output || getDownloadDir();
112
+ if (!fs.existsSync(dlDir)) fs.mkdirSync(dlDir, { recursive: true });
113
+
114
+ // Strategy 1: Try direct HTTP download from LAN peer (fastest)
115
+ if (opts.shareName && lanPeers.length > 0) {
116
+ console.log(chalk.blue(`Trying direct LAN download from ${lanPeers.length} nearby peer(s)...`));
117
+ const spinner = ora('Downloading via LAN HTTP...').start();
118
+ try {
119
+ const result = await tryLanHttpDownload(lanPeers, opts.shareName, dlDir, {
120
+ onProgress: (pct, fileName) => {
121
+ spinner.text = `LAN download... ${(pct * 100).toFixed(1)}% (${fileName})`;
122
+ },
123
+ });
124
+ if (result) {
125
+ spinner.succeed(chalk.green(`LAN download complete from ${result.peerIp} (${result.files.length} files, ${(result.totalBytes / 1024 / 1024).toFixed(2)} MB)`));
126
+ trackTransfer(magnet, opts.shareName, dlDir, opts.key || null);
127
+ completeTransfer(magnet);
128
+ process.exit(0);
129
+ return;
130
+ }
131
+ spinner.info(chalk.dim('No LAN peer serving this share. Falling back to BitTorrent...'));
132
+ } catch (err) {
133
+ spinner.info(chalk.dim(`LAN download failed: ${err.message}. Falling back to BitTorrent...`));
134
+ }
135
+ }
136
+
137
+ if (opts.lanOnly && lanPeers.length === 0) {
138
+ console.error(chalk.red('No LAN peers found. Remove --lan-only to use public trackers.'));
139
+ process.exitCode = 1;
140
+ return;
141
+ }
142
+
143
+ // Strategy 2: WebTorrent with LAN peer injection
144
+ logger.info('Starting download', { magnet: magnet.slice(0, 40) });
145
+ console.log(chalk.blue('Connecting to peers...'));
146
+ if (lanPeers.length > 0) {
147
+ console.log(chalk.cyan(` ${lanPeers.length} LAN peer(s) will be injected for faster discovery`));
148
+ }
149
+ if (opts.peer) {
150
+ console.log(chalk.cyan(` Manual peer ${opts.peer} will be injected`));
151
+ }
152
+ const spinner = ora('Finding peers...').start();
153
+
154
+ const client = new WebTorrent();
155
+
156
+ const peerTimeout = setTimeout(() => {
157
+ const torrents = client.torrents;
158
+ if (torrents.length > 0 && torrents[0].numPeers === 0) {
159
+ spinner.warn(chalk.yellow('⚠ No peers found after 120 seconds. Check that the share is being seeded.'));
160
+ client.destroy();
161
+ process.exitCode = 1;
162
+ }
163
+ }, 120000);
164
+
165
+ const addOpts = { path: dlDir };
166
+ const torrentSrc = opts.lanOnly ? magnet : magnet;
167
+
168
+ client.add(torrentSrc, addOpts, (torrent) => {
169
+ clearTimeout(peerTimeout);
170
+ spinner.succeed(chalk.green(`Connected! Downloading: ${torrent.name}`));
171
+
172
+ // Track transfer, storing the encrypted key if provided
173
+ trackTransfer(magnet, torrent.name, dlDir, opts.key || null);
174
+
175
+ const progressSpinner = ora('Downloading... 0%').start();
176
+
177
+ torrent.on('download', (bytes) => {
178
+ const progress = (torrent.progress * 100).toFixed(1);
179
+ progressSpinner.text = `Downloading... ${progress}% (${(torrent.downloaded / 1024 / 1024).toFixed(2)} MB / ${(torrent.length / 1024 / 1024).toFixed(2)} MB)`;
180
+ });
181
+
182
+ torrent.on('done', async () => {
183
+ logger.info(`Download complete: ${torrent.name}`, { size: torrent.length });
184
+ progressSpinner.succeed(chalk.green('Download Complete!'));
185
+ const downloadedDir = path.join(dlDir, torrent.name);
186
+ console.log(chalk.cyan(`File saved to: ${downloadedDir}`));
187
+
188
+ // Attempt decryption if an encrypted share key was provided
189
+ const transfers = getTransfers();
190
+ const thisTransfer = transfers.find(t => t.magnet === magnet);
191
+ if (thisTransfer?.encryptedShareKey) {
192
+ const identity = await getIdentity();
193
+ if (identity?.publicKey && identity?.privateKey) {
194
+ try {
195
+ const { decryptShareKey } = await import('../crypto/shareEncryption.js');
196
+ const shareKey = decryptShareKey(thisTransfer.encryptedShareKey, identity.publicKey, identity.privateKey);
197
+ const shareName = thisTransfer.name || 'download';
198
+ const outDir = getDecryptedDownloadDir(shareName);
199
+ console.log(chalk.blue('Decrypting E2EE files...'));
200
+ decryptFromDownload(downloadedDir, outDir, shareKey);
201
+ console.log(chalk.green(`Decrypted files saved to: ${outDir}`));
202
+ } catch (e) {
203
+ console.log(chalk.yellow(`Warning: Could not decrypt files — ${e.message}`));
204
+ }
205
+ } else {
206
+ console.log(chalk.yellow('Warning: No identity found — skipping decryption. Run `pal identity` to set up.'));
207
+ }
208
+ }
209
+
210
+ completeTransfer(magnet);
211
+ client.destroy();
212
+ process.exit(0);
213
+ });
214
+ });
215
+
216
+ // Inject LAN peers into the torrent right after adding
217
+ const currentTorrent = client.torrents[client.torrents.length - 1];
218
+ if (currentTorrent) {
219
+ if (lanPeers.length > 0) {
220
+ const injected = injectLanPeers(currentTorrent, lanPeers);
221
+ if (injected > 0) logger.info(`Injected ${injected} LAN peer(s) into torrent`);
222
+ }
223
+ if (opts.peer) {
224
+ try {
225
+ currentTorrent.addPeer(opts.peer);
226
+ logger.info(`Injected manual peer ${opts.peer} into torrent`);
227
+ } catch (e) {
228
+ console.error(chalk.yellow(`Warning: Could not add peer ${opts.peer}: ${e.message}`));
229
+ }
230
+ }
231
+ }
232
+
233
+ client.on('error', (err) => {
234
+ logger.error(`Download failed: ${err.message}`, { magnet: magnet.slice(0, 40) });
235
+ spinner.fail(chalk.red('Download Failed'));
236
+ console.error(chalk.red(`Error: ${err.message}`));
237
+ process.exit(1);
238
+ });
239
+ });
240
+ }