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