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,273 @@
1
+ import chalk from 'chalk';
2
+
3
+ async function getAuthHeaders() {
4
+ const { fetchFirst } = await import('../core/discoveryClient.js');
5
+ const { getIdentity } = await import('../core/identity.js');
6
+
7
+ const identity = await getIdentity();
8
+ if (!identity?.publicKey) {
9
+ console.error(chalk.red('No identity. Run pe init first.'));
10
+ process.exitCode = 1;
11
+ return null;
12
+ }
13
+
14
+ const keysRes = await fetchFirst(`/api/v1/keys/${encodeURIComponent(identity.publicKey)}`);
15
+ const keys = keysRes ? await keysRes.json() : [];
16
+ if (!keys?.length) {
17
+ console.error(chalk.red('No API key. Create one: pe api-keys create'));
18
+ process.exitCode = 1;
19
+ return null;
20
+ }
21
+
22
+ return { 'Content-Type': 'application/json', 'x-api-key': keys[0].key };
23
+ }
24
+
25
+ async function apiRequest(method, path, body) {
26
+ const { getPrimaryServer } = await import('../core/discoveryClient.js');
27
+ const headers = await getAuthHeaders();
28
+ if (!headers) return null;
29
+
30
+ const baseUrl = getPrimaryServer();
31
+ const opts = { method, headers, signal: AbortSignal.timeout(10000) };
32
+ if (body) opts.body = JSON.stringify(body);
33
+
34
+ const res = await fetch(`${baseUrl}${path}`, opts);
35
+ return res;
36
+ }
37
+
38
+ export default function orgCommand(program) {
39
+ const cmd = program
40
+ .command('org')
41
+ .description('manage organizations for enterprise extension management')
42
+ .addHelpText('after', `
43
+ Examples:
44
+ $ pe org create "Acme Corp" Create an organization
45
+ $ pe org list List your organizations
46
+ $ pe org info abc123 Show org details and members
47
+ $ pe org invite abc123 alice Add a member by handle
48
+ $ pe org remove abc123 alice Remove a member
49
+ $ pe org subscribe abc123 analytics Subscribe org to an extension
50
+ $ pe org unsubscribe abc123 analytics Cancel extension subscription
51
+ $ pe org billing abc123 Show billing summary
52
+ `);
53
+
54
+ cmd
55
+ .command('create <name>')
56
+ .description('create an organization')
57
+ .action(async (name) => {
58
+ try {
59
+ const res = await apiRequest('POST', '/api/v1/orgs', { name });
60
+ if (!res) return;
61
+ const data = await res.json();
62
+ if (res.ok && data?.id) {
63
+ console.log(chalk.green(`\u2714 Created organization "${name}"`));
64
+ console.log(` ID: ${chalk.white(data.id)}`);
65
+ } else {
66
+ console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
67
+ process.exitCode = 1;
68
+ }
69
+ } catch (err) {
70
+ console.error(chalk.red(`Create failed: ${err.message}`));
71
+ process.exitCode = 1;
72
+ }
73
+ });
74
+
75
+ cmd
76
+ .command('list')
77
+ .description('list your organizations')
78
+ .action(async () => {
79
+ try {
80
+ const res = await apiRequest('GET', '/api/v1/orgs');
81
+ if (!res) return;
82
+ const data = await res.json();
83
+ if (!data?.orgs?.length) {
84
+ console.log(chalk.gray('No organizations.'));
85
+ console.log(chalk.gray(' pe org create <name>'));
86
+ return;
87
+ }
88
+ console.log('');
89
+ console.log(chalk.cyan('Organizations:'));
90
+ console.log(chalk.gray('\u2500'.repeat(50)));
91
+ for (const org of data.orgs) {
92
+ const role = org.role === 'owner' ? chalk.yellow('owner') : chalk.gray(org.role);
93
+ console.log(` ${chalk.white(org.name)} ${chalk.gray(`(${org.id})`)}`);
94
+ console.log(` Role: ${role} Members: ${chalk.cyan(org.memberCount || 0)} Subscriptions: ${chalk.cyan(org.subscriptionCount || 0)}`);
95
+ }
96
+ console.log('');
97
+ } catch (err) {
98
+ console.error(chalk.red(`List failed: ${err.message}`));
99
+ process.exitCode = 1;
100
+ }
101
+ });
102
+
103
+ cmd
104
+ .command('info <id>')
105
+ .description('show org details, members, and subscriptions')
106
+ .action(async (id) => {
107
+ try {
108
+ const res = await apiRequest('GET', `/api/v1/orgs/${encodeURIComponent(id)}`);
109
+ if (!res) return;
110
+ if (!res.ok) {
111
+ const err = await res.json().catch(() => ({}));
112
+ console.error(chalk.red(`Not found: ${err?.error || 'Organization not found'}`));
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+ const data = await res.json();
117
+ console.log('');
118
+ console.log(chalk.cyan.bold(data.name));
119
+ console.log(chalk.gray('\u2500'.repeat(50)));
120
+ console.log(` ID: ${chalk.white(data.id)}`);
121
+ console.log(` Created: ${chalk.gray(new Date(data.createdAt).toLocaleDateString())}`);
122
+ console.log(` Plan: ${chalk.yellow(data.plan || 'free')}`);
123
+
124
+ if (data.members?.length) {
125
+ console.log('');
126
+ console.log(chalk.cyan(' Members:'));
127
+ for (const m of data.members) {
128
+ const roleColor = m.role === 'owner' ? chalk.yellow : m.role === 'admin' ? chalk.cyan : chalk.gray;
129
+ console.log(` ${chalk.white('@' + m.handle)} ${roleColor(m.role)}`);
130
+ }
131
+ }
132
+
133
+ if (data.subscriptions?.length) {
134
+ console.log('');
135
+ console.log(chalk.cyan(' Subscriptions:'));
136
+ for (const sub of data.subscriptions) {
137
+ const status = sub.active ? chalk.green('active') : chalk.red('cancelled');
138
+ console.log(` ${chalk.white(sub.extName)} ${status} ${chalk.gray('$' + (sub.price || 0).toFixed(2) + '/mo')}`);
139
+ }
140
+ }
141
+ console.log('');
142
+ } catch (err) {
143
+ console.error(chalk.red(`Info failed: ${err.message}`));
144
+ process.exitCode = 1;
145
+ }
146
+ });
147
+
148
+ cmd
149
+ .command('invite <id> <handle>')
150
+ .description('add a member to an organization')
151
+ .action(async (id, handle) => {
152
+ try {
153
+ const res = await apiRequest('POST', `/api/v1/orgs/${encodeURIComponent(id)}/members`, { handle });
154
+ if (!res) return;
155
+ const data = await res.json();
156
+ if (res.ok && data?.success) {
157
+ console.log(chalk.green(`\u2714 Invited @${handle} to organization`));
158
+ } else {
159
+ console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
160
+ process.exitCode = 1;
161
+ }
162
+ } catch (err) {
163
+ console.error(chalk.red(`Invite failed: ${err.message}`));
164
+ process.exitCode = 1;
165
+ }
166
+ });
167
+
168
+ cmd
169
+ .command('remove <id> <handle>')
170
+ .description('remove a member from an organization')
171
+ .action(async (id, handle) => {
172
+ try {
173
+ const res = await apiRequest('DELETE', `/api/v1/orgs/${encodeURIComponent(id)}/members/${encodeURIComponent(handle)}`);
174
+ if (!res) return;
175
+ const data = await res.json();
176
+ if (res.ok && data?.success) {
177
+ console.log(chalk.green(`\u2714 Removed @${handle} from organization`));
178
+ } else {
179
+ console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
180
+ process.exitCode = 1;
181
+ }
182
+ } catch (err) {
183
+ console.error(chalk.red(`Remove failed: ${err.message}`));
184
+ process.exitCode = 1;
185
+ }
186
+ });
187
+
188
+ cmd
189
+ .command('subscribe <id> <ext-name>')
190
+ .description('subscribe organization to an enterprise extension')
191
+ .action(async (id, extName) => {
192
+ try {
193
+ const res = await apiRequest('POST', `/api/v1/orgs/${encodeURIComponent(id)}/subscriptions`, { extName });
194
+ if (!res) return;
195
+ const data = await res.json();
196
+ if (res.ok && data?.success) {
197
+ console.log(chalk.green(`\u2714 Subscribed to "${extName}"`));
198
+ if (data.price) console.log(` Price: ${chalk.yellow('$' + data.price.toFixed(2) + '/mo')}`);
199
+ } else {
200
+ console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
201
+ process.exitCode = 1;
202
+ }
203
+ } catch (err) {
204
+ console.error(chalk.red(`Subscribe failed: ${err.message}`));
205
+ process.exitCode = 1;
206
+ }
207
+ });
208
+
209
+ cmd
210
+ .command('unsubscribe <id> <ext-name>')
211
+ .description('cancel an extension subscription')
212
+ .action(async (id, extName) => {
213
+ try {
214
+ const res = await apiRequest('DELETE', `/api/v1/orgs/${encodeURIComponent(id)}/subscriptions/${encodeURIComponent(extName)}`);
215
+ if (!res) return;
216
+ const data = await res.json();
217
+ if (res.ok && data?.success) {
218
+ console.log(chalk.green(`\u2714 Unsubscribed from "${extName}"`));
219
+ } else {
220
+ console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
221
+ process.exitCode = 1;
222
+ }
223
+ } catch (err) {
224
+ console.error(chalk.red(`Unsubscribe failed: ${err.message}`));
225
+ process.exitCode = 1;
226
+ }
227
+ });
228
+
229
+ cmd
230
+ .command('billing <id>')
231
+ .description('show billing summary for an organization')
232
+ .action(async (id) => {
233
+ try {
234
+ const res = await apiRequest('GET', `/api/v1/orgs/${encodeURIComponent(id)}/billing`);
235
+ if (!res) return;
236
+ if (!res.ok) {
237
+ const err = await res.json().catch(() => ({}));
238
+ console.error(chalk.red(`Failed: ${err?.error || 'Unknown error'}`));
239
+ process.exitCode = 1;
240
+ return;
241
+ }
242
+ const data = await res.json();
243
+ console.log('');
244
+ console.log(chalk.cyan.bold('Organization Billing'));
245
+ console.log(chalk.gray('\u2500'.repeat(40)));
246
+ console.log(` Plan: ${chalk.yellow(data.plan || 'free')}`);
247
+ console.log(` Monthly Total: ${chalk.white('$' + (data.monthlyTotal || 0).toFixed(2))}`);
248
+ console.log(` Active Seats: ${chalk.cyan(data.activeSeats || 0)}`);
249
+ console.log(` Subscriptions: ${chalk.cyan(data.subscriptionCount || 0)}`);
250
+
251
+ if (data.subscriptions?.length) {
252
+ console.log('');
253
+ console.log(chalk.cyan(' Active Subscriptions:'));
254
+ for (const sub of data.subscriptions) {
255
+ console.log(` ${chalk.white(sub.extName)} ${chalk.gray('$' + (sub.price || 0).toFixed(2) + '/mo')} ${chalk.gray('since ' + new Date(sub.startDate).toLocaleDateString())}`);
256
+ }
257
+ }
258
+
259
+ if (data.invoices?.length) {
260
+ console.log('');
261
+ console.log(chalk.cyan(' Recent Invoices:'));
262
+ for (const inv of data.invoices) {
263
+ const statusColor = inv.status === 'paid' ? chalk.green : inv.status === 'pending' ? chalk.yellow : chalk.red;
264
+ console.log(` ${chalk.gray(new Date(inv.date).toLocaleDateString())} $${inv.amount.toFixed(2)} ${statusColor(inv.status)}`);
265
+ }
266
+ }
267
+ console.log('');
268
+ } catch (err) {
269
+ console.error(chalk.red(`Billing failed: ${err.message}`));
270
+ process.exitCode = 1;
271
+ }
272
+ });
273
+ }
@@ -0,0 +1,180 @@
1
+ import chalk from 'chalk';
2
+ import { getFriends, saveFriends } from '../core/users.js';
3
+ import { decodeInvite } from './invite.js';
4
+ import { getSharesForPal, removeRecipientFromShare, rotateShareKey } from '../core/shares.js';
5
+ import { resolveHandle as resolveHandleFull } from '../core/resolver.js';
6
+ import { getPrimaryServer } from '../core/discoveryClient.js';
7
+
8
+ async function resolveHandle(handle) {
9
+ const data = await resolveHandleFull(handle);
10
+ if (!data) return null;
11
+ return data.publicKey || data.publicKeys?.[0] || null;
12
+ }
13
+
14
+ async function addPal(id, resolvedName, handle) {
15
+ const friends = getFriends();
16
+
17
+ const existing = friends.find(f => f.id === id);
18
+ if (existing) {
19
+ console.log(chalk.yellow(`This ID is already in your list as '${existing.name}'.`));
20
+ return false;
21
+ }
22
+
23
+ const palName = resolvedName || 'Unnamed Pal';
24
+ if (friends.find(f => f.name === palName)) {
25
+ console.log(chalk.red(`Error: The name '${palName}' is already taken. Please provide a unique nickname.`));
26
+ return false;
27
+ }
28
+
29
+ const newPal = { id, name: palName, handle: handle || null, addedAt: new Date().toISOString() };
30
+ friends.push(newPal);
31
+ saveFriends(friends);
32
+ return newPal;
33
+ }
34
+
35
+ export default function palCommand(program) {
36
+ const pal = program.command('pal').description('manage your list of pals (friends)')
37
+ .addHelpText('after', `
38
+ Examples:
39
+ $ pe pal add @alice Add pal by handle
40
+ $ pe pal add pal://... bob Add pal from invite link
41
+ $ pe pal add abc123def456 carol Add pal by public key
42
+ $ pe pal list Show all pals
43
+ $ pe pal remove @alice Remove a pal (rotates affected share keys)
44
+ `);
45
+
46
+ pal
47
+ .command('add <target> [name]')
48
+ .description('add a pal by @handle, invite link (pal://...), or raw public key')
49
+ .action(async (target, name) => {
50
+ // --- Mode 1: Magic invite link ---
51
+ if (target.startsWith('pal://')) {
52
+ const decoded = decodeInvite(target);
53
+ if (!decoded) {
54
+ console.log(chalk.red('Invalid invite link.'));
55
+ process.exitCode = 1;
56
+ return;
57
+ }
58
+ const palName = name || decoded.n || 'Unnamed Pal';
59
+ const result = await addPal(decoded.pk, palName, decoded.h);
60
+ if (result) {
61
+ console.log(chalk.green(`✔ Added pal: ${result.name}${decoded.h ? ` (@${decoded.h})` : ''}`));
62
+ }
63
+ return;
64
+ }
65
+
66
+ // --- Mode 2: Handle lookup (@handle or plain handle string) ---
67
+ const isHandle = target.startsWith('@') || (!target.match(/^[0-9a-fA-F]{20,}$/));
68
+ if (isHandle) {
69
+ const handle = target.startsWith('@') ? target.slice(1) : target;
70
+ process.stdout.write(chalk.gray(`Resolving @${handle}... `));
71
+ let publicKey;
72
+ try {
73
+ publicKey = await resolveHandle(handle);
74
+ } catch {
75
+ console.log(chalk.red(`\nCould not reach discovery server(s)`));
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+ if (!publicKey) {
80
+ console.log(chalk.red(`\nHandle @${handle} not found.`));
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+ console.log(chalk.green('found.'));
85
+ const palName = name || handle;
86
+ const result = await addPal(publicKey, palName, handle);
87
+ if (result) {
88
+ console.log(chalk.green(`✔ Added pal: ${result.name} (@${handle})`));
89
+ }
90
+ return;
91
+ }
92
+
93
+ // --- Mode 3: Raw public key ---
94
+ const palName = name || 'Unnamed Pal';
95
+ const result = await addPal(target, palName, null);
96
+ if (result) {
97
+ console.log(chalk.green(`✔ Added pal: ${result.name} (${target})`));
98
+ }
99
+ });
100
+
101
+ pal
102
+ .command('list')
103
+ .description('list all your pals with online status')
104
+ .action(async () => {
105
+ const friends = getFriends();
106
+ if (friends.length === 0) {
107
+ if (program.opts().json) {
108
+ console.log(JSON.stringify({ pals: [] }, null, 2));
109
+ return;
110
+ }
111
+ console.log(chalk.gray('Your pal list is empty. Use `pe pal add @handle` or `pe invite` to add friends.'));
112
+ return;
113
+ }
114
+
115
+ const discoveryUrl = getPrimaryServer();
116
+ for (const f of friends) {
117
+ if (f.handle) {
118
+ try {
119
+ const res = await fetch(`${discoveryUrl}/api/v1/presence/${encodeURIComponent(f.handle)}`, {
120
+ signal: AbortSignal.timeout(3000),
121
+ });
122
+ if (res.ok) {
123
+ const p = await res.json();
124
+ f._online = p.online;
125
+ f._lastSeen = p.devices?.[0]?.lastSeen;
126
+ }
127
+ } catch {}
128
+ }
129
+ }
130
+
131
+ if (program.opts().json) {
132
+ const pals = friends.map(f => ({ id: f.id, name: f.name, handle: f.handle, online: !!f._online, lastSeen: f._lastSeen || null }));
133
+ console.log(JSON.stringify({ pals }, null, 2));
134
+ return;
135
+ }
136
+
137
+ console.log('');
138
+ console.log(chalk.cyan('Your Pals:'));
139
+ friends.forEach(f => {
140
+ const handle = f.handle ? chalk.cyan(` @${f.handle}`) : '';
141
+ const statusIcon = f._online ? chalk.green('\u25cf') : chalk.gray('\u25cb');
142
+ const statusText = f._online ? chalk.green('online') : chalk.gray('offline');
143
+ console.log(`${statusIcon} ${chalk.white(f.name)}${handle} ${statusText} [${chalk.gray(f.id)}]`);
144
+ });
145
+ });
146
+
147
+ pal
148
+ .command('remove <id>')
149
+ .description('remove a pal from your list by their public key or @handle')
150
+ .action(async (id) => {
151
+ const friends = getFriends();
152
+ const target = id.startsWith('@') ? id.slice(1) : id;
153
+ const removed = friends.find(f => f.id === target || f.handle === target);
154
+ const filtered = friends.filter(f => f.id !== target && f.handle !== target);
155
+ if (!removed) {
156
+ console.log(chalk.red('Pal not found.'));
157
+ return;
158
+ }
159
+ saveFriends(filtered);
160
+ console.log(chalk.green('✔ Pal removed.'));
161
+
162
+ // Rotate keys for any shares this pal was a recipient of
163
+ const affectedShares = getSharesForPal(removed.id);
164
+ if (affectedShares.length > 0) {
165
+ console.log(chalk.blue(`Rotating keys for ${affectedShares.length} affected share(s)...`));
166
+ for (const share of affectedShares) {
167
+ try {
168
+ removeRecipientFromShare(share.id, removed.id);
169
+ if (share.visibility === 'private') {
170
+ await rotateShareKey(share.id);
171
+ console.log(chalk.gray(` ✔ Rotated key for "${share.name || share.id}"`));
172
+ }
173
+ } catch (err) {
174
+ console.log(chalk.yellow(` Warning: Could not rotate key for ${share.id}: ${err.message}`));
175
+ }
176
+ }
177
+ console.log(chalk.green('Key rotation complete. Re-run `pe serve` to seed with new keys.'));
178
+ }
179
+ });
180
+ }
@@ -0,0 +1,216 @@
1
+ import chalk from 'chalk';
2
+ import { listShares, updateShare, getShareSummary, VISIBILITY_LEVELS } from '../core/shares.js';
3
+ import { getGroups } from '../core/groups.js';
4
+ import config from '../utils/config.js';
5
+ import path from 'path';
6
+
7
+ const VIS_COLORS = {
8
+ public: 'green', global: 'green', private: 'red',
9
+ group: 'blue', network: 'cyan', 'link-only': 'yellow',
10
+ };
11
+
12
+ function visLabel(v) {
13
+ const color = VIS_COLORS[v] || 'white';
14
+ return chalk[color](v.toUpperCase());
15
+ }
16
+
17
+ export default function permissionsCommand(program) {
18
+ const cmd = program
19
+ .command('permissions')
20
+ .description('unified view of all shares and their permissions')
21
+ .addHelpText('after', `
22
+ Examples:
23
+ $ pe permissions Show all shares with permissions
24
+ $ pe permissions --compact Compact one-line-per-share view
25
+ $ pe permissions set <id> --visibility group --group team
26
+ $ pe permissions set <id> --streamable Mark share as streamable media
27
+ $ pe permissions set <id> --add-pal alice
28
+ $ pe permissions set <id> --remove-pal bob
29
+ `)
30
+ .option('--compact', 'One-line-per-share view')
31
+ .option('--json', 'Output as JSON')
32
+ .action((opts) => {
33
+ const summary = getShareSummary();
34
+ const groups = getGroups();
35
+
36
+ if (opts.json) {
37
+ console.log(JSON.stringify(summary, null, 2));
38
+ return;
39
+ }
40
+
41
+ if (summary.length === 0) {
42
+ console.log(chalk.gray('No active shares.'));
43
+ return;
44
+ }
45
+
46
+ console.log('');
47
+ console.log(chalk.bold(` Shares & Permissions (${summary.length} total)`));
48
+ console.log(chalk.gray(' ─'.repeat(30)));
49
+ console.log('');
50
+
51
+ if (opts.compact) {
52
+ // Compact table view
53
+ const maxName = Math.max(12, ...summary.map(s => s.name.length));
54
+ const header = ` ${'NAME'.padEnd(maxName)} ${'VISIBILITY'.padEnd(12)} ${'STREAM'.padEnd(6)} ${'RECIPIENTS'.padEnd(24)} ID`;
55
+ console.log(chalk.gray(header));
56
+ console.log(chalk.gray(' ' + '─'.repeat(header.length)));
57
+
58
+ for (const s of summary) {
59
+ const recips = [];
60
+ if (s.recipients.length) recips.push(s.recipients.join(', '));
61
+ if (s.groups.length) {
62
+ const gNames = s.groups.map(gId => groups.find(g => g.id === gId)?.name || gId);
63
+ recips.push(`[${gNames.join(', ')}]`);
64
+ }
65
+ if (s.networks.length) recips.push(`{${s.networks.join(', ')}}`);
66
+ const recipStr = recips.join(', ') || chalk.gray('everyone');
67
+
68
+ console.log(` ${chalk.white(s.name.padEnd(maxName))} ${visLabel(s.visibility).padEnd(12 + 10)} ${s.streamable ? chalk.magenta('yes') : chalk.gray('no ').padEnd(6)} ${recipStr.substring(0, 24).padEnd(24)} ${chalk.gray(s.id)}`);
69
+ }
70
+ } else {
71
+ // Detailed view
72
+ for (const s of summary) {
73
+ const gNames = s.groups.map(gId => groups.find(g => g.id === gId)?.name || gId);
74
+
75
+ console.log(` ${chalk.bold.white(s.name)} ${chalk.gray(`(${s.id})`)}`);
76
+ console.log(` Path: ${chalk.gray(s.path)}`);
77
+ console.log(` Visibility: ${visLabel(s.visibility)}`);
78
+ console.log(` Streamable: ${s.streamable ? chalk.magenta('Yes — media streaming enabled') : chalk.gray('No')}`);
79
+
80
+ if (s.visibility === 'public' || s.visibility === 'global') {
81
+ console.log(` Access: ${chalk.green('Everyone')}`);
82
+ } else if (s.visibility === 'group' && gNames.length) {
83
+ console.log(` Groups: ${chalk.blue(gNames.join(', '))}`);
84
+ } else if (s.visibility === 'network' && s.networks.length) {
85
+ console.log(` Networks: ${chalk.cyan(s.networks.join(', '))}`);
86
+ } else if (s.visibility === 'link-only') {
87
+ console.log(` Access: ${chalk.yellow('Link-only (anyone with the link)')}`);
88
+ }
89
+
90
+ if (s.recipients.length) {
91
+ console.log(` Recipients: ${chalk.white(s.recipients.join(', '))}`);
92
+ }
93
+
94
+ console.log(` Recursive: ${s.recursive ? 'Yes' : 'No'}`);
95
+ if (s.hasMagnet) console.log(` Magnet: ${chalk.green('Active')}`);
96
+ if (s.hasPassword) console.log(` Password: ${chalk.yellow('Protected')}`);
97
+ console.log('');
98
+ }
99
+ }
100
+ });
101
+
102
+ cmd
103
+ .command('set <id>')
104
+ .description('edit share permissions')
105
+ .option('--visibility <type>', `Set visibility: ${VISIBILITY_LEVELS.join(', ')}`)
106
+ .option('--streamable', 'Enable media streaming for this share')
107
+ .option('--no-streamable', 'Disable media streaming')
108
+ .option('--add-pal <name>', 'Add a pal as recipient')
109
+ .option('--remove-pal <name>', 'Remove a pal from recipients')
110
+ .option('--add-group <name>', 'Add a group')
111
+ .option('--remove-group <name>', 'Remove a group')
112
+ .option('--add-network <id>', 'Add a network')
113
+ .option('--remove-network <id>', 'Remove a network')
114
+ .option('--recursive', 'Enable recursive sharing')
115
+ .option('--no-recursive', 'Disable recursive sharing')
116
+ .option('--writable', 'Allow recipients to write/modify files')
117
+ .option('--read-only', 'Restrict recipients to read-only access')
118
+ .action(async (id, opts) => {
119
+ try {
120
+ const shares = listShares();
121
+ const share = shares.find(s => s.id === id || s.path === path.resolve(id));
122
+ if (!share) {
123
+ console.log(chalk.red('Share not found.'));
124
+ process.exitCode = 1;
125
+ return;
126
+ }
127
+
128
+ const updates = {};
129
+
130
+ if (opts.visibility) {
131
+ if (!VISIBILITY_LEVELS.includes(opts.visibility)) {
132
+ console.log(chalk.red(`Invalid visibility. Options: ${VISIBILITY_LEVELS.join(', ')}`));
133
+ process.exitCode = 1;
134
+ return;
135
+ }
136
+ updates.visibility = opts.visibility;
137
+ }
138
+
139
+ if (opts.streamable !== undefined) {
140
+ updates.streamable = opts.streamable;
141
+ }
142
+
143
+ if (opts.recursive !== undefined) {
144
+ updates.recursive = opts.recursive;
145
+ }
146
+
147
+ if (opts.writable) {
148
+ updates.permissions = { ...share.permissions, write: true };
149
+ }
150
+ if (opts.readOnly) {
151
+ updates.permissions = { ...share.permissions, write: false };
152
+ }
153
+
154
+ if (opts.addPal) {
155
+ const { getFriends } = await import('../core/users.js');
156
+ const friends = getFriends();
157
+ const pal = friends.find(f => f.name === opts.addPal || f.handle === opts.addPal);
158
+ if (!pal) { console.log(chalk.red(`Pal '${opts.addPal}' not found.`)); process.exitCode = 1; return; }
159
+ const recipients = share.recipients || [];
160
+ if (!recipients.find(r => r.id === pal.id)) {
161
+ recipients.push({ id: pal.id, name: pal.name, handle: pal.handle });
162
+ updates.recipients = recipients;
163
+ }
164
+ }
165
+
166
+ if (opts.removePal) {
167
+ const recipients = (share.recipients || []).filter(
168
+ r => r.name !== opts.removePal && r.handle !== opts.removePal
169
+ );
170
+ updates.recipients = recipients;
171
+ }
172
+
173
+ if (opts.addGroup) {
174
+ const { getGroup } = await import('../core/groups.js');
175
+ const group = getGroup(opts.addGroup);
176
+ if (!group) { console.log(chalk.red(`Group '${opts.addGroup}' not found.`)); process.exitCode = 1; return; }
177
+ const groups = share.sharedWithGroups || [];
178
+ if (!groups.includes(group.id)) {
179
+ groups.push(group.id);
180
+ updates.sharedWithGroups = groups;
181
+ }
182
+ }
183
+
184
+ if (opts.removeGroup) {
185
+ const { getGroup } = await import('../core/groups.js');
186
+ const group = getGroup(opts.removeGroup);
187
+ updates.sharedWithGroups = (share.sharedWithGroups || []).filter(gId => gId !== group?.id);
188
+ }
189
+
190
+ if (opts.addNetwork) {
191
+ const networks = share.sharedWithNetworks || [];
192
+ if (!networks.includes(opts.addNetwork)) networks.push(opts.addNetwork);
193
+ updates.sharedWithNetworks = networks;
194
+ }
195
+
196
+ if (opts.removeNetwork) {
197
+ updates.sharedWithNetworks = (share.sharedWithNetworks || []).filter(n => n !== opts.removeNetwork);
198
+ }
199
+
200
+ if (Object.keys(updates).length === 0) {
201
+ console.log(chalk.yellow('No changes specified.'));
202
+ return;
203
+ }
204
+
205
+ const updated = updateShare(id, updates);
206
+ console.log(chalk.green(`Share updated: ${updated.name || path.basename(updated.path)}`));
207
+
208
+ for (const [key, value] of Object.entries(updates)) {
209
+ console.log(chalk.gray(` ${key}: ${JSON.stringify(value)}`));
210
+ }
211
+ } catch (err) {
212
+ console.log(chalk.red(`Error: ${err.message}`));
213
+ process.exitCode = 1;
214
+ }
215
+ });
216
+ }