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,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
|
+
}
|