pal-explorer-cli 0.4.11 → 0.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +149 -149
  2. package/bin/pal.js +63 -2
  3. package/extensions/@palexplorer/analytics/extension.json +20 -1
  4. package/extensions/@palexplorer/analytics/index.js +19 -9
  5. package/extensions/@palexplorer/audit/extension.json +14 -0
  6. package/extensions/@palexplorer/auth-email/extension.json +15 -0
  7. package/extensions/@palexplorer/auth-oauth/extension.json +15 -0
  8. package/extensions/@palexplorer/chat/extension.json +14 -0
  9. package/extensions/@palexplorer/discovery/extension.json +17 -0
  10. package/extensions/@palexplorer/discovery/index.js +1 -1
  11. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  12. package/extensions/@palexplorer/groups/extension.json +15 -0
  13. package/extensions/@palexplorer/share-links/extension.json +15 -0
  14. package/extensions/@palexplorer/sync/extension.json +16 -0
  15. package/extensions/@palexplorer/user-mgmt/extension.json +15 -0
  16. package/lib/capabilities.js +24 -24
  17. package/lib/commands/analytics.js +175 -175
  18. package/lib/commands/api-keys.js +131 -131
  19. package/lib/commands/audit.js +235 -235
  20. package/lib/commands/auth.js +137 -137
  21. package/lib/commands/backup.js +76 -76
  22. package/lib/commands/billing.js +148 -148
  23. package/lib/commands/chat.js +217 -217
  24. package/lib/commands/cloud-backup.js +231 -231
  25. package/lib/commands/comment.js +99 -99
  26. package/lib/commands/completion.js +203 -203
  27. package/lib/commands/compliance.js +218 -218
  28. package/lib/commands/config.js +136 -136
  29. package/lib/commands/connect.js +44 -44
  30. package/lib/commands/dept.js +294 -294
  31. package/lib/commands/device.js +146 -146
  32. package/lib/commands/download.js +240 -226
  33. package/lib/commands/explorer.js +178 -178
  34. package/lib/commands/extension.js +1060 -970
  35. package/lib/commands/favorite.js +90 -90
  36. package/lib/commands/federation.js +270 -270
  37. package/lib/commands/file.js +533 -533
  38. package/lib/commands/group.js +271 -271
  39. package/lib/commands/gui-share.js +29 -29
  40. package/lib/commands/init.js +61 -61
  41. package/lib/commands/invite.js +59 -59
  42. package/lib/commands/list.js +58 -58
  43. package/lib/commands/log.js +116 -116
  44. package/lib/commands/nearby.js +108 -108
  45. package/lib/commands/network.js +251 -251
  46. package/lib/commands/notify.js +198 -198
  47. package/lib/commands/org.js +273 -273
  48. package/lib/commands/pal.js +403 -180
  49. package/lib/commands/permissions.js +216 -216
  50. package/lib/commands/pin.js +97 -97
  51. package/lib/commands/protocol.js +357 -357
  52. package/lib/commands/rbac.js +147 -147
  53. package/lib/commands/recover.js +36 -36
  54. package/lib/commands/register.js +171 -171
  55. package/lib/commands/relay.js +131 -131
  56. package/lib/commands/remote.js +368 -368
  57. package/lib/commands/revoke.js +50 -50
  58. package/lib/commands/scanner.js +280 -280
  59. package/lib/commands/schedule.js +344 -344
  60. package/lib/commands/scim.js +203 -203
  61. package/lib/commands/search.js +181 -181
  62. package/lib/commands/serve.js +438 -438
  63. package/lib/commands/server.js +350 -350
  64. package/lib/commands/share-link.js +199 -199
  65. package/lib/commands/share.js +336 -323
  66. package/lib/commands/sso.js +200 -200
  67. package/lib/commands/status.js +145 -145
  68. package/lib/commands/stream.js +562 -562
  69. package/lib/commands/su.js +187 -187
  70. package/lib/commands/sync.js +979 -979
  71. package/lib/commands/transfers.js +152 -152
  72. package/lib/commands/uninstall.js +188 -188
  73. package/lib/commands/update.js +204 -204
  74. package/lib/commands/user.js +276 -276
  75. package/lib/commands/vfs.js +84 -84
  76. package/lib/commands/web-login.js +79 -79
  77. package/lib/commands/web.js +52 -52
  78. package/lib/commands/webhook.js +180 -180
  79. package/lib/commands/whoami.js +59 -59
  80. package/lib/commands/workspace.js +121 -121
  81. package/lib/core/billing.js +16 -5
  82. package/lib/core/dhtDiscovery.js +9 -2
  83. package/lib/core/discoveryClient.js +13 -7
  84. package/lib/core/extensions.js +142 -1
  85. package/lib/core/identity.js +33 -2
  86. package/lib/core/imageProcessor.js +109 -0
  87. package/lib/core/imageTorrent.js +167 -0
  88. package/lib/core/permissions.js +1 -1
  89. package/lib/core/pro.js +11 -4
  90. package/lib/core/serverList.js +4 -1
  91. package/lib/core/shares.js +12 -1
  92. package/lib/core/signalingServer.js +14 -2
  93. package/lib/core/su.js +1 -1
  94. package/lib/core/users.js +1 -1
  95. package/lib/protocol/messages.js +12 -3
  96. package/lib/utils/explorer.js +1 -1
  97. package/lib/utils/help.js +357 -357
  98. package/lib/utils/torrent.js +1 -0
  99. package/package.json +4 -3
@@ -1,180 +1,403 @@
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
- }
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
+ import { updateProfile, getProfile, PRESENCE_STATUSES } from '../core/identity.js';
8
+
9
+ async function resolveHandle(handle) {
10
+ const data = await resolveHandleFull(handle);
11
+ if (!data) return null;
12
+ return data.publicKey || data.publicKeys?.[0] || null;
13
+ }
14
+
15
+ async function addPal(id, resolvedName, handle) {
16
+ const friends = getFriends();
17
+
18
+ const existing = friends.find(f => f.id === id);
19
+ if (existing) {
20
+ console.log(chalk.yellow(`This ID is already in your list as '${existing.name}'.`));
21
+ return false;
22
+ }
23
+
24
+ const palName = resolvedName || null;
25
+ if (palName && friends.find(f => f.name === palName)) {
26
+ console.log(chalk.red(`Error: The name '${palName}' is already taken. Please provide a unique nickname.`));
27
+ return false;
28
+ }
29
+
30
+ const newPal = { id, name: palName, handle: handle || null, addedAt: new Date().toISOString() };
31
+ friends.push(newPal);
32
+ saveFriends(friends);
33
+ return newPal;
34
+ }
35
+
36
+ export default function palCommand(program) {
37
+ const pal = program.command('pal').description('manage your list of pals (friends)')
38
+ .addHelpText('after', `
39
+ Examples:
40
+ $ pal pal add @alice Add pal by handle (requires discovery server)
41
+ $ pal pal add pal://... bob Add pal from invite link
42
+ $ pal pal add abc123def456 carol Add pal by public key (pure P2P)
43
+ $ pal pal add abc123def456 Add pal without nickname (shows as key prefix)
44
+ $ pal pal list Show all pals
45
+ $ pal pal name abc123de "Alice" Set or change a pal's nickname
46
+ $ pal pal unname Alice Remove a pal's nickname
47
+ $ pal pal remove Alice Remove a pal (rotates affected share keys)
48
+ `);
49
+
50
+ pal
51
+ .command('add <target> [name]')
52
+ .description('add a pal by @handle, invite link (pal://...), or raw public key')
53
+ .action(async (target, name) => {
54
+ // --- Mode 1: Magic invite link ---
55
+ if (target.startsWith('pal://')) {
56
+ const decoded = decodeInvite(target);
57
+ if (!decoded) {
58
+ console.log(chalk.red('Invalid invite link.'));
59
+ process.exitCode = 1;
60
+ return;
61
+ }
62
+ const palName = name || decoded.n || null;
63
+ const result = await addPal(decoded.pk, palName, decoded.h);
64
+ if (result) {
65
+ console.log(chalk.green(`✔ Added pal: ${result.name}${decoded.h ? ` (@${decoded.h})` : ''}`));
66
+ }
67
+ return;
68
+ }
69
+
70
+ // --- Mode 2: Handle lookup (@handle or plain handle string) ---
71
+ const isHandle = target.startsWith('@') || (!target.match(/^[0-9a-fA-F]{20,}$/));
72
+ if (isHandle) {
73
+ const handle = target.startsWith('@') ? target.slice(1) : target;
74
+ process.stdout.write(chalk.gray(`Resolving @${handle}... `));
75
+ let publicKey;
76
+ try {
77
+ publicKey = await resolveHandle(handle);
78
+ } catch {
79
+ console.log(chalk.red(`\nCould not reach discovery server(s)`));
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+ if (!publicKey) {
84
+ console.log(chalk.red(`\nHandle @${handle} not found.`));
85
+ process.exitCode = 1;
86
+ return;
87
+ }
88
+ console.log(chalk.green('found.'));
89
+ const palName = name || handle;
90
+ const result = await addPal(publicKey, palName, handle);
91
+ if (result) {
92
+ console.log(chalk.green(`✔ Added pal: ${result.name} (@${handle})`));
93
+ }
94
+ return;
95
+ }
96
+
97
+ // --- Mode 3: Raw public key ---
98
+ const palName = name || null;
99
+ const result = await addPal(target, palName, null);
100
+ if (result) {
101
+ const display = result.name || target.slice(0, 8) + '...' + target.slice(-8);
102
+ console.log(chalk.green(`✔ Added pal: ${display} (${target})`));
103
+ }
104
+ });
105
+
106
+ pal
107
+ .command('list')
108
+ .description('list all your pals with online status')
109
+ .action(async () => {
110
+ const friends = getFriends();
111
+ if (friends.length === 0) {
112
+ if (program.opts().json) {
113
+ console.log(JSON.stringify({ pals: [] }, null, 2));
114
+ return;
115
+ }
116
+ console.log(chalk.gray('Your pal list is empty. Use `pal pal add @handle` or `pal invite` to add friends.'));
117
+ return;
118
+ }
119
+
120
+ const discoveryUrl = getPrimaryServer();
121
+ for (const f of friends) {
122
+ if (f.handle) {
123
+ try {
124
+ const res = await fetch(`${discoveryUrl}/api/v1/presence/${encodeURIComponent(f.handle)}`, {
125
+ signal: AbortSignal.timeout(3000),
126
+ });
127
+ if (res.ok) {
128
+ const p = await res.json();
129
+ f._online = p.online;
130
+ f._lastSeen = p.devices?.[0]?.lastSeen;
131
+ }
132
+ } catch {}
133
+ }
134
+ }
135
+
136
+ if (program.opts().json) {
137
+ const pals = friends.map(f => ({ id: f.id, name: f.name || null, handle: f.handle, online: !!f._online, lastSeen: f._lastSeen || null }));
138
+ console.log(JSON.stringify({ pals }, null, 2));
139
+ return;
140
+ }
141
+
142
+ console.log('');
143
+ console.log(chalk.cyan('Your Pals:'));
144
+ friends.forEach(f => {
145
+ const displayName = f.name || (f.id.slice(0, 8) + '...' + f.id.slice(-8));
146
+ const handle = f.handle ? chalk.cyan(` @${f.handle}`) : '';
147
+ const statusIcon = f._online ? chalk.green('\u25cf') : chalk.gray('\u25cb');
148
+ const statusText = f._online ? chalk.green('online') : chalk.gray('offline');
149
+ console.log(`${statusIcon} ${chalk.white(displayName)}${handle} ${statusText} [${chalk.gray(f.id)}]`);
150
+ });
151
+ });
152
+
153
+ pal
154
+ .command('info <pal>')
155
+ .description('show detailed profile info for a pal')
156
+ .action(async (palId) => {
157
+ const friends = getFriends();
158
+ const target = palId.startsWith('@') ? palId.slice(1) : palId;
159
+ const pal = friends.find(f => f.id === target || f.handle === target || f.name === target);
160
+ if (!pal) {
161
+ console.log(chalk.red('Pal not found. Use `pal pal list` to see your pals.'));
162
+ process.exitCode = 1;
163
+ return;
164
+ }
165
+
166
+ const displayName = pal.name || (pal.id.slice(0, 8) + '...' + pal.id.slice(-8));
167
+ console.log('');
168
+ console.log(chalk.cyan(`Pal: ${displayName}`));
169
+ console.log(` ${'Public Key'.padEnd(14)} ${chalk.gray(pal.id)}`);
170
+ if (pal.handle) console.log(` ${'Handle'.padEnd(14)} ${chalk.cyan('@' + pal.handle)}`);
171
+ console.log(` ${'Added'.padEnd(14)} ${pal.addedAt || chalk.gray('unknown')}`);
172
+
173
+ // Fetch presence + profile from discovery server
174
+ if (pal.handle) {
175
+ const discoveryUrl = getPrimaryServer();
176
+ process.stdout.write(chalk.gray('\n Fetching presence... '));
177
+ try {
178
+ const res = await fetch(`${discoveryUrl}/api/v1/presence/${encodeURIComponent(pal.handle)}`, {
179
+ signal: AbortSignal.timeout(5000),
180
+ });
181
+ if (res.ok) {
182
+ const p = await res.json();
183
+ const profile = p.profile || {};
184
+ const online = p.online;
185
+ const presenceStatus = profile.presenceStatus || (online ? 'online' : 'offline');
186
+ const presenceColors = { online: 'green', away: 'yellow', dnd: 'red', invisible: 'gray', offline: 'gray' };
187
+ const colorFn = chalk[presenceColors[presenceStatus] || 'gray'];
188
+
189
+ console.log(colorFn(presenceStatus));
190
+ console.log('');
191
+
192
+ const rows = [
193
+ ['Display Name', profile.displayName],
194
+ ['Avatar', profile.avatar],
195
+ ['Bio', profile.bio],
196
+ ['Presence', presenceStatus],
197
+ ['Status', profile.statusText || profile.status],
198
+ ['Age', profile.age != null ? String(profile.age) : null],
199
+ ['Gender', profile.gender],
200
+ ['Country', profile.country],
201
+ ['Language', profile.language],
202
+ ['Interests', profile.interests?.length ? profile.interests.join(', ') : null],
203
+ ['Joined', profile.joinedAt],
204
+ ];
205
+
206
+ for (const [label, value] of rows) {
207
+ if (value) console.log(` ${chalk.white(label.padEnd(14))} ${value}`);
208
+ }
209
+
210
+ // Devices
211
+ const devices = p.devices || [];
212
+ if (devices.length > 0) {
213
+ console.log('');
214
+ console.log(chalk.cyan(' Devices:'));
215
+ for (const d of devices) {
216
+ const dStatus = d.status === 'online' ? chalk.green('online') : chalk.gray('offline');
217
+ console.log(` ${d.deviceName || d.deviceId} — ${dStatus}`);
218
+ }
219
+ }
220
+
221
+ // Shared with me
222
+ const { getSharesForPal } = await import('../core/shares.js');
223
+ const sharedShares = getSharesForPal(pal.id);
224
+ if (sharedShares.length > 0) {
225
+ console.log('');
226
+ console.log(chalk.cyan(` Shares with this pal (${sharedShares.length}):`));
227
+ for (const s of sharedShares) {
228
+ console.log(` ${s.name || s.id} ${s.category ? chalk.gray(`[${s.category}]`) : ''}`);
229
+ }
230
+ }
231
+ } else {
232
+ console.log(chalk.yellow('unavailable'));
233
+ }
234
+ } catch {
235
+ console.log(chalk.yellow('server unreachable (pure P2P mode)'));
236
+ }
237
+ }
238
+ console.log('');
239
+ });
240
+
241
+ pal
242
+ .command('remove <id>')
243
+ .description('remove a pal from your list by their public key or @handle')
244
+ .action(async (id) => {
245
+ const friends = getFriends();
246
+ const target = id.startsWith('@') ? id.slice(1) : id;
247
+ const removed = friends.find(f => f.id === target || f.handle === target || f.name === target);
248
+ const filtered = friends.filter(f => f !== removed);
249
+ if (!removed) {
250
+ console.log(chalk.red('Pal not found.'));
251
+ return;
252
+ }
253
+ saveFriends(filtered);
254
+ console.log(chalk.green('✔ Pal removed.'));
255
+
256
+ // Rotate keys for any shares this pal was a recipient of
257
+ const affectedShares = getSharesForPal(removed.id);
258
+ if (affectedShares.length > 0) {
259
+ console.log(chalk.blue(`Rotating keys for ${affectedShares.length} affected share(s)...`));
260
+ for (const share of affectedShares) {
261
+ try {
262
+ removeRecipientFromShare(share.id, removed.id);
263
+ if (share.visibility === 'private') {
264
+ await rotateShareKey(share.id);
265
+ console.log(chalk.gray(` ✔ Rotated key for "${share.name || share.id}"`));
266
+ }
267
+ } catch (err) {
268
+ console.log(chalk.yellow(` Warning: Could not rotate key for ${share.id}: ${err.message}`));
269
+ }
270
+ }
271
+ console.log(chalk.green('Key rotation complete. Re-run `pal serve` to seed with new keys.'));
272
+ }
273
+ });
274
+
275
+ pal
276
+ .command('name <pal> <nickname>')
277
+ .description('set or change a pal\'s nickname')
278
+ .action(async (palId, nickname) => {
279
+ const friends = getFriends();
280
+ const target = palId.startsWith('@') ? palId.slice(1) : palId;
281
+ const pal = friends.find(f => f.id === target || f.handle === target || f.name === target);
282
+ if (!pal) {
283
+ console.log(chalk.red('Pal not found.'));
284
+ process.exitCode = 1;
285
+ return;
286
+ }
287
+ if (friends.find(f => f !== pal && f.name === nickname)) {
288
+ console.log(chalk.red(`The name '${nickname}' is already taken by another pal.`));
289
+ process.exitCode = 1;
290
+ return;
291
+ }
292
+ const oldName = pal.name || pal.id.slice(0, 8) + '...' + pal.id.slice(-8);
293
+ pal.name = nickname;
294
+ saveFriends(friends);
295
+ console.log(chalk.green(`✔ Renamed ${oldName} → ${nickname}`));
296
+ });
297
+
298
+ pal
299
+ .command('unname <pal>')
300
+ .description('remove a pal\'s nickname (will show as truncated public key)')
301
+ .action(async (palId) => {
302
+ const friends = getFriends();
303
+ const target = palId.startsWith('@') ? palId.slice(1) : palId;
304
+ const pal = friends.find(f => f.id === target || f.handle === target || f.name === target);
305
+ if (!pal) {
306
+ console.log(chalk.red('Pal not found.'));
307
+ process.exitCode = 1;
308
+ return;
309
+ }
310
+ if (!pal.name) {
311
+ console.log(chalk.yellow('This pal has no nickname.'));
312
+ return;
313
+ }
314
+ const oldName = pal.name;
315
+ pal.name = null;
316
+ saveFriends(friends);
317
+ console.log(chalk.green(`✔ Removed nickname '${oldName}' — pal will show as ${pal.id.slice(0, 8)}...${pal.id.slice(-8)}`));
318
+ });
319
+
320
+ // ── Profile management ──
321
+
322
+ pal
323
+ .command('profile')
324
+ .description('view or update your profile')
325
+ .option('--display-name <name>', 'set display name (max 50 chars)')
326
+ .option('--bio <text>', 'set bio (max 200 chars)')
327
+ .option('--status <text>', 'set status text (max 100 chars)')
328
+ .option('--presence <status>', `set presence: ${PRESENCE_STATUSES.join(', ')}`)
329
+ .option('--age <n>', 'set age (or "clear" to remove)')
330
+ .option('--gender <value>', 'set gender (or "clear" to remove)')
331
+ .option('--country <code>', 'set country ISO code, e.g. US, IL (or "clear")')
332
+ .option('--language <code>', 'set language code, e.g. en, he (or "clear")')
333
+ .option('--interests <tags>', 'comma-separated interests, e.g. "p2p,crypto,gaming"')
334
+ .option('--avatar <emoji>', 'set emoji avatar')
335
+ .action(async (opts) => {
336
+ const updates = {};
337
+ let hasUpdates = false;
338
+
339
+ if (opts.displayName !== undefined) { updates.displayName = opts.displayName.slice(0, 50); hasUpdates = true; }
340
+ if (opts.bio !== undefined) { updates.bio = opts.bio.slice(0, 200); hasUpdates = true; }
341
+ if (opts.status !== undefined) { updates.statusText = opts.status.slice(0, 100); hasUpdates = true; }
342
+ if (opts.presence !== undefined) { updates.presenceStatus = opts.presence; hasUpdates = true; }
343
+ if (opts.avatar !== undefined) { updates.avatar = opts.avatar; hasUpdates = true; }
344
+ if (opts.age !== undefined) {
345
+ updates.age = opts.age === 'clear' ? null : opts.age;
346
+ hasUpdates = true;
347
+ }
348
+ if (opts.gender !== undefined) {
349
+ updates.gender = opts.gender === 'clear' ? undefined : opts.gender.slice(0, 20);
350
+ hasUpdates = true;
351
+ }
352
+ if (opts.country !== undefined) {
353
+ updates.country = opts.country === 'clear' ? undefined : opts.country.toUpperCase().slice(0, 2);
354
+ hasUpdates = true;
355
+ }
356
+ if (opts.language !== undefined) {
357
+ updates.language = opts.language === 'clear' ? undefined : opts.language.toLowerCase().slice(0, 5);
358
+ hasUpdates = true;
359
+ }
360
+ if (opts.interests !== undefined) {
361
+ updates.interests = opts.interests.split(',').map(s => s.trim()).filter(Boolean);
362
+ hasUpdates = true;
363
+ }
364
+
365
+ if (hasUpdates) {
366
+ try {
367
+ updateProfile(updates);
368
+ console.log(chalk.green('✔ Profile updated.'));
369
+ } catch (err) {
370
+ console.log(chalk.red(`Error: ${err.message}`));
371
+ process.exitCode = 1;
372
+ return;
373
+ }
374
+ }
375
+
376
+ const profile = getProfile();
377
+ if (!profile) {
378
+ console.log(chalk.yellow('No identity found. Run `pal init` first.'));
379
+ return;
380
+ }
381
+
382
+ console.log('');
383
+ console.log(chalk.cyan('Your Profile:'));
384
+ const rows = [
385
+ ['Display Name', profile.displayName || chalk.gray('(not set)')],
386
+ ['Avatar', profile.avatar || chalk.gray('(not set)')],
387
+ ['Bio', profile.bio || chalk.gray('(not set)')],
388
+ ['Presence', profile.presenceStatus || 'online'],
389
+ ['Status', profile.statusText || chalk.gray('(not set)')],
390
+ ['Age', profile.age != null ? String(profile.age) : chalk.gray('(not set)')],
391
+ ['Gender', profile.gender || chalk.gray('(not set)')],
392
+ ['Country', profile.country || chalk.gray('(not set)')],
393
+ ['Language', profile.language || chalk.gray('(not set)')],
394
+ ['Interests', profile.interests?.length ? profile.interests.join(', ') : chalk.gray('(none)')],
395
+ ['Joined', profile.joinedAt || chalk.gray('unknown')],
396
+ ];
397
+ if (profile.avatarHash) rows.splice(2, 0, ['Avatar Image', chalk.gray(profile.avatarHash.slice(0, 16) + '...')]);
398
+
399
+ for (const [label, value] of rows) {
400
+ console.log(` ${chalk.white(label.padEnd(14))} ${value}`);
401
+ }
402
+ });
403
+ }