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.
Files changed (156) hide show
  1. package/LICENSE.md +18 -0
  2. package/README.md +314 -0
  3. package/bin/pal.js +230 -0
  4. package/extensions/@palexplorer/analytics/README.md +45 -0
  5. package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
  6. package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
  7. package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
  8. package/extensions/@palexplorer/analytics/extension.json +27 -0
  9. package/extensions/@palexplorer/analytics/index.js +186 -0
  10. package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
  11. package/extensions/@palexplorer/audit/extension.json +17 -0
  12. package/extensions/@palexplorer/audit/index.js +2 -0
  13. package/extensions/@palexplorer/auth-email/extension.json +17 -0
  14. package/extensions/@palexplorer/auth-email/index.js +102 -0
  15. package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
  16. package/extensions/@palexplorer/auth-oauth/index.js +199 -0
  17. package/extensions/@palexplorer/chat/extension.json +17 -0
  18. package/extensions/@palexplorer/chat/index.js +2 -0
  19. package/extensions/@palexplorer/discovery/extension.json +16 -0
  20. package/extensions/@palexplorer/discovery/index.js +111 -0
  21. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  22. package/extensions/@palexplorer/email-notifications/index.js +242 -0
  23. package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
  24. package/extensions/@palexplorer/explorer-integration/index.js +122 -0
  25. package/extensions/@palexplorer/groups/extension.json +17 -0
  26. package/extensions/@palexplorer/groups/index.js +2 -0
  27. package/extensions/@palexplorer/networks/extension.json +17 -0
  28. package/extensions/@palexplorer/networks/index.js +2 -0
  29. package/extensions/@palexplorer/share-links/extension.json +17 -0
  30. package/extensions/@palexplorer/share-links/index.js +2 -0
  31. package/extensions/@palexplorer/sync/extension.json +17 -0
  32. package/extensions/@palexplorer/sync/index.js +2 -0
  33. package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
  34. package/extensions/@palexplorer/user-mgmt/index.js +2 -0
  35. package/extensions/@palexplorer/vfs/extension.json +17 -0
  36. package/extensions/@palexplorer/vfs/index.js +167 -0
  37. package/lib/capabilities.js +263 -0
  38. package/lib/commands/analytics.js +175 -0
  39. package/lib/commands/api-keys.js +131 -0
  40. package/lib/commands/audit.js +235 -0
  41. package/lib/commands/auth.js +137 -0
  42. package/lib/commands/backup.js +76 -0
  43. package/lib/commands/billing.js +148 -0
  44. package/lib/commands/chat.js +217 -0
  45. package/lib/commands/cloud-backup.js +231 -0
  46. package/lib/commands/comment.js +99 -0
  47. package/lib/commands/completion.js +203 -0
  48. package/lib/commands/compliance.js +218 -0
  49. package/lib/commands/config.js +136 -0
  50. package/lib/commands/connect.js +44 -0
  51. package/lib/commands/dept.js +294 -0
  52. package/lib/commands/device.js +146 -0
  53. package/lib/commands/download.js +226 -0
  54. package/lib/commands/explorer.js +178 -0
  55. package/lib/commands/extension.js +970 -0
  56. package/lib/commands/favorite.js +90 -0
  57. package/lib/commands/federation.js +270 -0
  58. package/lib/commands/file.js +533 -0
  59. package/lib/commands/group.js +271 -0
  60. package/lib/commands/gui-share.js +29 -0
  61. package/lib/commands/init.js +61 -0
  62. package/lib/commands/invite.js +59 -0
  63. package/lib/commands/list.js +59 -0
  64. package/lib/commands/log.js +116 -0
  65. package/lib/commands/nearby.js +108 -0
  66. package/lib/commands/network.js +251 -0
  67. package/lib/commands/notify.js +198 -0
  68. package/lib/commands/org.js +273 -0
  69. package/lib/commands/pal.js +180 -0
  70. package/lib/commands/permissions.js +216 -0
  71. package/lib/commands/pin.js +97 -0
  72. package/lib/commands/protocol.js +357 -0
  73. package/lib/commands/rbac.js +147 -0
  74. package/lib/commands/recover.js +36 -0
  75. package/lib/commands/register.js +171 -0
  76. package/lib/commands/relay.js +131 -0
  77. package/lib/commands/remote.js +368 -0
  78. package/lib/commands/revoke.js +50 -0
  79. package/lib/commands/scanner.js +280 -0
  80. package/lib/commands/schedule.js +344 -0
  81. package/lib/commands/scim.js +203 -0
  82. package/lib/commands/search.js +181 -0
  83. package/lib/commands/serve.js +438 -0
  84. package/lib/commands/server.js +350 -0
  85. package/lib/commands/share-link.js +199 -0
  86. package/lib/commands/share.js +323 -0
  87. package/lib/commands/sso.js +200 -0
  88. package/lib/commands/status.js +136 -0
  89. package/lib/commands/stream.js +562 -0
  90. package/lib/commands/su.js +187 -0
  91. package/lib/commands/sync.js +827 -0
  92. package/lib/commands/transfers.js +152 -0
  93. package/lib/commands/uninstall.js +188 -0
  94. package/lib/commands/update.js +204 -0
  95. package/lib/commands/user.js +276 -0
  96. package/lib/commands/vfs.js +84 -0
  97. package/lib/commands/web.js +52 -0
  98. package/lib/commands/webhook.js +180 -0
  99. package/lib/commands/whoami.js +59 -0
  100. package/lib/commands/workspace.js +121 -0
  101. package/lib/core/accessLog.js +54 -0
  102. package/lib/core/analytics.js +99 -0
  103. package/lib/core/backup.js +84 -0
  104. package/lib/core/billing.js +336 -0
  105. package/lib/core/bitfieldStore.js +53 -0
  106. package/lib/core/connectionManager.js +182 -0
  107. package/lib/core/dhtDiscovery.js +148 -0
  108. package/lib/core/discoveryClient.js +408 -0
  109. package/lib/core/extensionAnalyzer.js +357 -0
  110. package/lib/core/extensionSandbox.js +250 -0
  111. package/lib/core/extensionWorkerHost.js +166 -0
  112. package/lib/core/extensions.js +1082 -0
  113. package/lib/core/fileDiff.js +69 -0
  114. package/lib/core/groups.js +119 -0
  115. package/lib/core/identity.js +340 -0
  116. package/lib/core/mdnsService.js +126 -0
  117. package/lib/core/networks.js +81 -0
  118. package/lib/core/permissions.js +109 -0
  119. package/lib/core/pro.js +27 -0
  120. package/lib/core/resolver.js +74 -0
  121. package/lib/core/serverList.js +224 -0
  122. package/lib/core/sharePolicy.js +69 -0
  123. package/lib/core/shares.js +325 -0
  124. package/lib/core/signalingServer.js +441 -0
  125. package/lib/core/streamTransport.js +106 -0
  126. package/lib/core/su.js +55 -0
  127. package/lib/core/syncEngine.js +264 -0
  128. package/lib/core/syncState.js +159 -0
  129. package/lib/core/transfers.js +259 -0
  130. package/lib/core/users.js +225 -0
  131. package/lib/core/vfs.js +216 -0
  132. package/lib/core/webServer.js +702 -0
  133. package/lib/core/webrtcStream.js +396 -0
  134. package/lib/crypto/chatEncryption.js +57 -0
  135. package/lib/crypto/shareEncryption.js +195 -0
  136. package/lib/crypto/sharePassword.js +35 -0
  137. package/lib/crypto/streamEncryption.js +189 -0
  138. package/lib/package.json +1 -0
  139. package/lib/protocol/envelope.js +271 -0
  140. package/lib/protocol/handler.js +191 -0
  141. package/lib/protocol/index.js +27 -0
  142. package/lib/protocol/messages.js +247 -0
  143. package/lib/protocol/negotiation.js +127 -0
  144. package/lib/protocol/policy.js +142 -0
  145. package/lib/protocol/router.js +86 -0
  146. package/lib/protocol/sync.js +122 -0
  147. package/lib/utils/cli.js +15 -0
  148. package/lib/utils/config.js +123 -0
  149. package/lib/utils/configIntegrity.js +87 -0
  150. package/lib/utils/downloadDir.js +9 -0
  151. package/lib/utils/explorer.js +83 -0
  152. package/lib/utils/format.js +12 -0
  153. package/lib/utils/help.js +357 -0
  154. package/lib/utils/logger.js +103 -0
  155. package/lib/utils/torrent.js +203 -0
  156. package/package.json +71 -0
@@ -0,0 +1,294 @@
1
+ import chalk from 'chalk';
2
+
3
+ export default function deptCommand(program) {
4
+ const cmd = program
5
+ .command('dept')
6
+ .description('department management within organizations (Enterprise)')
7
+ .addHelpText('after', `
8
+ Examples:
9
+ $ pe dept create org123 "Engineering" Create a department
10
+ $ pe dept list org123 List departments
11
+ $ pe dept info org123 Engineering Show department details
12
+ $ pe dept add org123 Engineering <pubkey> Add a member
13
+ $ pe dept remove org123 Engineering <pubkey>
14
+ $ pe dept delete org123 Engineering Delete a department
15
+ $ pe dept shares org123 Engineering List department shares
16
+ $ pe dept policy org123 Engineering Show department policies
17
+ $ pe dept policy org123 Engineering --set maxFileSize 100MB
18
+ `)
19
+ .action(() => { cmd.outputHelp(); });
20
+
21
+ cmd
22
+ .command('create <orgId> <name>')
23
+ .description('create a department in an organization')
24
+ .action(async (orgId, name) => {
25
+ try {
26
+ const extConfig = (await import('../utils/config.js')).default;
27
+ const key = `departments:${orgId}`;
28
+ const departments = extConfig.get(key) || [];
29
+ if (departments.find(d => d.name === name)) {
30
+ console.log(chalk.yellow(`Department already exists: ${name}`));
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ departments.push({
35
+ name,
36
+ createdAt: new Date().toISOString(),
37
+ members: [],
38
+ policies: {},
39
+ });
40
+ extConfig.set(key, departments);
41
+ console.log(chalk.green(`✔ Department created: ${name}`));
42
+ console.log(` Organization: ${chalk.white(orgId)}`);
43
+ } catch (err) {
44
+ console.log(chalk.red(`Create failed: ${err.message}`));
45
+ process.exitCode = 1;
46
+ }
47
+ });
48
+
49
+ cmd
50
+ .command('list <orgId>')
51
+ .description('list all departments in an organization')
52
+ .action(async (orgId) => {
53
+ try {
54
+ const extConfig = (await import('../utils/config.js')).default;
55
+ const key = `departments:${orgId}`;
56
+ const departments = extConfig.get(key) || [];
57
+ if (departments.length === 0) {
58
+ console.log(chalk.dim('No departments.'));
59
+ console.log(chalk.dim(` pe dept create ${orgId} <name>`));
60
+ return;
61
+ }
62
+ console.log(chalk.bold(`Departments in ${orgId} (${departments.length})\n`));
63
+ for (const d of departments) {
64
+ console.log(` ${chalk.cyan(d.name)}`);
65
+ console.log(` Members: ${chalk.white(d.members.length)} Created: ${chalk.dim(d.createdAt)}`);
66
+ }
67
+ } catch (err) {
68
+ console.log(chalk.red(`List failed: ${err.message}`));
69
+ process.exitCode = 1;
70
+ }
71
+ });
72
+
73
+ cmd
74
+ .command('info <orgId> <deptName>')
75
+ .description('show department details and members')
76
+ .action(async (orgId, deptName) => {
77
+ try {
78
+ const extConfig = (await import('../utils/config.js')).default;
79
+ const key = `departments:${orgId}`;
80
+ const departments = extConfig.get(key) || [];
81
+ const dept = departments.find(d => d.name === deptName);
82
+ if (!dept) {
83
+ console.log(chalk.red(`Department not found: ${deptName}`));
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+ console.log('');
88
+ console.log(chalk.bold.cyan(dept.name));
89
+ console.log(chalk.gray('─'.repeat(50)));
90
+ console.log(` Organization: ${chalk.white(orgId)}`);
91
+ console.log(` Created: ${chalk.dim(dept.createdAt)}`);
92
+ console.log(` Members: ${chalk.white(dept.members.length)}`);
93
+
94
+ if (dept.members.length > 0) {
95
+ console.log('');
96
+ console.log(chalk.cyan(' Members:'));
97
+ for (const m of dept.members) {
98
+ const roleColor = m.role === 'dept-admin' ? chalk.yellow : chalk.gray;
99
+ const pubShort = m.publicKey.slice(0, 16) + '...';
100
+ console.log(` ${chalk.white(pubShort)} ${roleColor(m.role)} ${chalk.dim('joined ' + m.joinedAt)}`);
101
+ }
102
+ }
103
+
104
+ const policyKeys = Object.keys(dept.policies || {});
105
+ if (policyKeys.length > 0) {
106
+ console.log('');
107
+ console.log(chalk.cyan(' Policies:'));
108
+ for (const k of policyKeys) {
109
+ console.log(` ${chalk.white(k)}: ${chalk.dim(dept.policies[k])}`);
110
+ }
111
+ }
112
+ console.log('');
113
+ } catch (err) {
114
+ console.log(chalk.red(`Info failed: ${err.message}`));
115
+ process.exitCode = 1;
116
+ }
117
+ });
118
+
119
+ cmd
120
+ .command('add <orgId> <deptName> <publicKey>')
121
+ .description('add a member to a department (requires dept-admin or org-admin)')
122
+ .option('--role <role>', 'member role', 'member')
123
+ .action(async (orgId, deptName, publicKey, opts) => {
124
+ try {
125
+ const extConfig = (await import('../utils/config.js')).default;
126
+ const key = `departments:${orgId}`;
127
+ const departments = extConfig.get(key) || [];
128
+ const dept = departments.find(d => d.name === deptName);
129
+ if (!dept) {
130
+ console.log(chalk.red(`Department not found: ${deptName}`));
131
+ process.exitCode = 1;
132
+ return;
133
+ }
134
+ const VALID_ROLES = ['member', 'dept-admin'];
135
+ if (opts.role && !VALID_ROLES.includes(opts.role)) {
136
+ console.error(chalk.red(`Invalid role "${opts.role}". Allowed: ${VALID_ROLES.join(', ')}`));
137
+ process.exitCode = 1;
138
+ return;
139
+ }
140
+ if (dept.members.find(m => m.publicKey === publicKey)) {
141
+ console.log(chalk.yellow('Member already in department.'));
142
+ process.exitCode = 1;
143
+ return;
144
+ }
145
+ dept.members.push({
146
+ publicKey,
147
+ role: opts.role,
148
+ joinedAt: new Date().toISOString(),
149
+ });
150
+ extConfig.set(key, departments);
151
+ console.log(chalk.green(`✔ Member added to ${deptName}`));
152
+ console.log(` Key: ${chalk.dim(publicKey.slice(0, 16) + '...')}`);
153
+ console.log(` Role: ${chalk.white(opts.role)}`);
154
+ } catch (err) {
155
+ console.log(chalk.red(`Add failed: ${err.message}`));
156
+ process.exitCode = 1;
157
+ }
158
+ });
159
+
160
+ cmd
161
+ .command('remove <orgId> <deptName> <publicKey>')
162
+ .description('remove a member from a department')
163
+ .action(async (orgId, deptName, publicKey) => {
164
+ try {
165
+ const extConfig = (await import('../utils/config.js')).default;
166
+ const key = `departments:${orgId}`;
167
+ const departments = extConfig.get(key) || [];
168
+ const dept = departments.find(d => d.name === deptName);
169
+ if (!dept) {
170
+ console.log(chalk.red(`Department not found: ${deptName}`));
171
+ process.exitCode = 1;
172
+ return;
173
+ }
174
+ const idx = dept.members.findIndex(m => m.publicKey === publicKey);
175
+ if (idx === -1) {
176
+ console.log(chalk.yellow('Member not found in department.'));
177
+ process.exitCode = 1;
178
+ return;
179
+ }
180
+ dept.members.splice(idx, 1);
181
+ extConfig.set(key, departments);
182
+ console.log(chalk.green(`✔ Member removed from ${deptName}`));
183
+ } catch (err) {
184
+ console.log(chalk.red(`Remove failed: ${err.message}`));
185
+ process.exitCode = 1;
186
+ }
187
+ });
188
+
189
+ cmd
190
+ .command('delete <orgId> <name>')
191
+ .description('delete a department')
192
+ .action(async (orgId, name) => {
193
+ try {
194
+ const extConfig = (await import('../utils/config.js')).default;
195
+ const key = `departments:${orgId}`;
196
+ const departments = extConfig.get(key) || [];
197
+ const idx = departments.findIndex(d => d.name === name);
198
+ if (idx === -1) {
199
+ console.log(chalk.red(`Department not found: ${name}`));
200
+ process.exitCode = 1;
201
+ return;
202
+ }
203
+ const removed = departments.splice(idx, 1)[0];
204
+ extConfig.set(key, departments);
205
+ console.log(chalk.green(`✔ Department deleted: ${name}`));
206
+ console.log(` Had ${chalk.white(removed.members.length)} member(s)`);
207
+ } catch (err) {
208
+ console.log(chalk.red(`Delete failed: ${err.message}`));
209
+ process.exitCode = 1;
210
+ }
211
+ });
212
+
213
+ cmd
214
+ .command('shares <orgId> <deptName>')
215
+ .description('list shares scoped to a department')
216
+ .action(async (orgId, deptName) => {
217
+ try {
218
+ const extConfig = (await import('../utils/config.js')).default;
219
+ const key = `departments:${orgId}`;
220
+ const departments = extConfig.get(key) || [];
221
+ const dept = departments.find(d => d.name === deptName);
222
+ if (!dept) {
223
+ console.log(chalk.red(`Department not found: ${deptName}`));
224
+ process.exitCode = 1;
225
+ return;
226
+ }
227
+
228
+ const config = (await import('../utils/config.js')).default;
229
+ const shares = config.get('shares') || [];
230
+ const deptShares = shares.filter(s => s.department === deptName && s.orgId === orgId);
231
+
232
+ if (deptShares.length === 0) {
233
+ console.log(chalk.dim(`No shares scoped to department ${deptName}.`));
234
+ return;
235
+ }
236
+ console.log(chalk.bold(`Shares for ${deptName} (${deptShares.length})\n`));
237
+ for (const s of deptShares) {
238
+ console.log(` ${chalk.cyan(s.path || s.name)}`);
239
+ console.log(` Visibility: ${chalk.dim(s.visibility || 'private')} Created: ${chalk.dim(s.createdAt || 'unknown')}`);
240
+ }
241
+ } catch (err) {
242
+ console.log(chalk.red(`Shares failed: ${err.message}`));
243
+ process.exitCode = 1;
244
+ }
245
+ });
246
+
247
+ cmd
248
+ .command('policy <orgId> <deptName>')
249
+ .description('show/set department policies')
250
+ .option('--set <key>', 'set a policy value (followed by value as argument)')
251
+ .action(async (orgId, deptName, opts, command) => {
252
+ try {
253
+ const extConfig = (await import('../utils/config.js')).default;
254
+ const key = `departments:${orgId}`;
255
+ const departments = extConfig.get(key) || [];
256
+ const dept = departments.find(d => d.name === deptName);
257
+ if (!dept) {
258
+ console.log(chalk.red(`Department not found: ${deptName}`));
259
+ process.exitCode = 1;
260
+ return;
261
+ }
262
+
263
+ if (opts.set) {
264
+ const value = command.args[0];
265
+ if (!value) {
266
+ console.log(chalk.red('Usage: pe dept policy <orgId> <deptName> --set <key> <value>'));
267
+ process.exitCode = 1;
268
+ return;
269
+ }
270
+ if (!dept.policies) dept.policies = {};
271
+ dept.policies[opts.set] = value;
272
+ extConfig.set(key, departments);
273
+ console.log(chalk.green(`✔ Policy set: ${opts.set} = ${value}`));
274
+ return;
275
+ }
276
+
277
+ // Show policies
278
+ const policies = dept.policies || {};
279
+ const policyKeys = Object.keys(policies);
280
+ if (policyKeys.length === 0) {
281
+ console.log(chalk.dim(`No policies set for ${deptName}.`));
282
+ console.log(chalk.dim(` pe dept policy ${orgId} ${deptName} --set <key> <value>`));
283
+ return;
284
+ }
285
+ console.log(chalk.bold(`Policies for ${deptName}\n`));
286
+ for (const k of policyKeys) {
287
+ console.log(` ${chalk.white(k)}: ${chalk.cyan(policies[k])}`);
288
+ }
289
+ } catch (err) {
290
+ console.log(chalk.red(`Policy failed: ${err.message}`));
291
+ process.exitCode = 1;
292
+ }
293
+ });
294
+ }
@@ -0,0 +1,146 @@
1
+ import chalk from 'chalk';
2
+ import { getIdentity, getDeviceInfo, setDeviceName } from '../core/identity.js';
3
+ import config from '../utils/config.js';
4
+ import { getPrimaryServer } from '../core/discoveryClient.js';
5
+
6
+ export default function deviceCommand(program) {
7
+ const cmd = program
8
+ .command('device')
9
+ .description('view and manage this device\'s identity')
10
+ .addHelpText('after', `
11
+ Examples:
12
+ $ pe device list List linked devices
13
+ $ pe device rename "My Laptop" Rename this device
14
+ $ pe device register Register device on discovery server
15
+ `);
16
+
17
+ // Default: show device info
18
+ cmd.action(async () => {
19
+ const identity = await getIdentity();
20
+ const device = getDeviceInfo();
21
+ if (!identity) {
22
+ console.log(chalk.red('No identity found. Run `pe init <name>` first.'));
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+ if (program.opts().json) {
27
+ console.log(JSON.stringify({ device, identity: { name: identity.name, handle: identity.handle, publicKey: identity.publicKey } }, null, 2));
28
+ return;
29
+ }
30
+ console.log('');
31
+ console.log(chalk.cyan('This Device:'));
32
+ console.log(` Device Name: ${chalk.white(device.name)}`);
33
+ console.log(` Device ID: ${chalk.gray(device.id)}`);
34
+ console.log(` Identity: ${chalk.white(identity.name)}${identity.handle ? chalk.cyan(` @${identity.handle}`) : ''}`);
35
+ console.log(` Public Key: ${chalk.gray(identity.publicKey)}`);
36
+ console.log(` Created At: ${chalk.gray(device.createdAt)}`);
37
+ console.log('');
38
+ });
39
+
40
+ cmd
41
+ .command('rename <name>')
42
+ .description('set a unique name for this device')
43
+ .action((name) => {
44
+ const updated = setDeviceName(name);
45
+ console.log(chalk.green(`✔ Device renamed to: ${updated.name}`));
46
+ });
47
+
48
+ cmd
49
+ .command('register <handle>')
50
+ .description('register this device with the discovery server under a handle')
51
+ .action(async (handle) => {
52
+ handle = handle.replace(/^@/, '');
53
+ const identity = await getIdentity();
54
+ const device = getDeviceInfo();
55
+
56
+ if (!identity || !identity.privateKey) {
57
+ console.log(chalk.red('No identity found. Run `pe init <name>` first.'));
58
+ process.exitCode = 1;
59
+ return;
60
+ }
61
+
62
+ const sodium = (await import('sodium-native')).default;
63
+ const privateKey = Buffer.from(identity.privateKey, 'hex');
64
+ const now = Date.now();
65
+ const message = `${handle}:${device.id}:${device.name}:${now}`;
66
+ const sig = Buffer.alloc(sodium.crypto_sign_BYTES);
67
+ sodium.crypto_sign_detached(sig, Buffer.from(message), privateKey);
68
+
69
+ const url = getPrimaryServer();
70
+ try {
71
+ process.stdout.write(chalk.gray(`Registering device '${device.name}' under @${handle}... `));
72
+ const res = await fetch(`${url}/devices/register`, {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify({
76
+ handle,
77
+ deviceId: device.id,
78
+ deviceName: device.name,
79
+ publicKey: identity.publicKey,
80
+ timestamp: now,
81
+ signature: sig.toString('hex')
82
+ })
83
+ });
84
+ const result = await res.json();
85
+ if (result.success) {
86
+ console.log(chalk.green('done.'));
87
+ console.log(chalk.green(`✔ Device registered. Total devices for @${handle}: ${result.devices?.length || 1}`));
88
+ } else {
89
+ console.log(chalk.red(`\nFailed: ${result.error}`));
90
+ process.exitCode = 1;
91
+ }
92
+ } catch {
93
+ console.log(chalk.red(`\nCould not reach discovery server at ${url}`));
94
+ process.exitCode = 1;
95
+ }
96
+ });
97
+
98
+ cmd
99
+ .command('list [handle]')
100
+ .description('list all devices registered under a handle (defaults to your own)')
101
+ .action(async (handle) => {
102
+ if (!handle) {
103
+ const id = config.get('identity');
104
+ handle = id?.handle;
105
+ if (!handle) {
106
+ console.log(chalk.yellow('No handle registered. Provide one: pe device list <handle>'));
107
+ process.exitCode = 1;
108
+ return;
109
+ }
110
+ }
111
+ const url = getPrimaryServer();
112
+ try {
113
+ const res = await fetch(`${url}/devices/${encodeURIComponent(handle)}`);
114
+ if (!res.ok) {
115
+ const err = await res.json().catch(() => ({}));
116
+ console.log(chalk.red(`Failed: ${err.error || res.statusText}`));
117
+ process.exitCode = 1;
118
+ return;
119
+ }
120
+ const data = await res.json();
121
+ const devices = data.devices || [];
122
+ if (devices.length === 0) {
123
+ if (program.opts().json) {
124
+ console.log(JSON.stringify({ handle, devices: [] }, null, 2));
125
+ return;
126
+ }
127
+ console.log(chalk.gray(`No devices found for @${handle}.`));
128
+ return;
129
+ }
130
+ if (program.opts().json) {
131
+ console.log(JSON.stringify({ handle, devices }, null, 2));
132
+ return;
133
+ }
134
+ console.log('');
135
+ console.log(chalk.cyan(`Devices for @${handle}:`));
136
+ devices.forEach(d => {
137
+ const status = d.status === 'online' ? chalk.green('online') : chalk.gray(d.status || 'offline');
138
+ console.log(` ${chalk.white(d.deviceName)} [${status}] ${chalk.gray(d.deviceId)}`);
139
+ console.log(` Last seen: ${chalk.gray(d.updatedAt || 'unknown')}`);
140
+ });
141
+ } catch {
142
+ console.log(chalk.red(`Could not reach discovery server at ${url}`));
143
+ process.exitCode = 1;
144
+ }
145
+ });
146
+ }
@@ -0,0 +1,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
+ .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
+ }