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.
- package/README.md +149 -149
- package/bin/pal.js +63 -2
- package/extensions/@palexplorer/analytics/extension.json +20 -1
- package/extensions/@palexplorer/analytics/index.js +19 -9
- package/extensions/@palexplorer/audit/extension.json +14 -0
- package/extensions/@palexplorer/auth-email/extension.json +15 -0
- package/extensions/@palexplorer/auth-oauth/extension.json +15 -0
- package/extensions/@palexplorer/chat/extension.json +14 -0
- package/extensions/@palexplorer/discovery/extension.json +17 -0
- package/extensions/@palexplorer/discovery/index.js +1 -1
- package/extensions/@palexplorer/email-notifications/extension.json +23 -0
- package/extensions/@palexplorer/groups/extension.json +15 -0
- package/extensions/@palexplorer/share-links/extension.json +15 -0
- package/extensions/@palexplorer/sync/extension.json +16 -0
- package/extensions/@palexplorer/user-mgmt/extension.json +15 -0
- package/lib/capabilities.js +24 -24
- package/lib/commands/analytics.js +175 -175
- package/lib/commands/api-keys.js +131 -131
- package/lib/commands/audit.js +235 -235
- package/lib/commands/auth.js +137 -137
- package/lib/commands/backup.js +76 -76
- package/lib/commands/billing.js +148 -148
- package/lib/commands/chat.js +217 -217
- package/lib/commands/cloud-backup.js +231 -231
- package/lib/commands/comment.js +99 -99
- package/lib/commands/completion.js +203 -203
- package/lib/commands/compliance.js +218 -218
- package/lib/commands/config.js +136 -136
- package/lib/commands/connect.js +44 -44
- package/lib/commands/dept.js +294 -294
- package/lib/commands/device.js +146 -146
- package/lib/commands/download.js +240 -226
- package/lib/commands/explorer.js +178 -178
- package/lib/commands/extension.js +1060 -970
- package/lib/commands/favorite.js +90 -90
- package/lib/commands/federation.js +270 -270
- package/lib/commands/file.js +533 -533
- package/lib/commands/group.js +271 -271
- package/lib/commands/gui-share.js +29 -29
- package/lib/commands/init.js +61 -61
- package/lib/commands/invite.js +59 -59
- package/lib/commands/list.js +58 -58
- package/lib/commands/log.js +116 -116
- package/lib/commands/nearby.js +108 -108
- package/lib/commands/network.js +251 -251
- package/lib/commands/notify.js +198 -198
- package/lib/commands/org.js +273 -273
- package/lib/commands/pal.js +403 -180
- package/lib/commands/permissions.js +216 -216
- package/lib/commands/pin.js +97 -97
- package/lib/commands/protocol.js +357 -357
- package/lib/commands/rbac.js +147 -147
- package/lib/commands/recover.js +36 -36
- package/lib/commands/register.js +171 -171
- package/lib/commands/relay.js +131 -131
- package/lib/commands/remote.js +368 -368
- package/lib/commands/revoke.js +50 -50
- package/lib/commands/scanner.js +280 -280
- package/lib/commands/schedule.js +344 -344
- package/lib/commands/scim.js +203 -203
- package/lib/commands/search.js +181 -181
- package/lib/commands/serve.js +438 -438
- package/lib/commands/server.js +350 -350
- package/lib/commands/share-link.js +199 -199
- package/lib/commands/share.js +336 -323
- package/lib/commands/sso.js +200 -200
- package/lib/commands/status.js +145 -145
- package/lib/commands/stream.js +562 -562
- package/lib/commands/su.js +187 -187
- package/lib/commands/sync.js +979 -979
- package/lib/commands/transfers.js +152 -152
- package/lib/commands/uninstall.js +188 -188
- package/lib/commands/update.js +204 -204
- package/lib/commands/user.js +276 -276
- package/lib/commands/vfs.js +84 -84
- package/lib/commands/web-login.js +79 -79
- package/lib/commands/web.js +52 -52
- package/lib/commands/webhook.js +180 -180
- package/lib/commands/whoami.js +59 -59
- package/lib/commands/workspace.js +121 -121
- package/lib/core/billing.js +16 -5
- package/lib/core/dhtDiscovery.js +9 -2
- package/lib/core/discoveryClient.js +13 -7
- package/lib/core/extensions.js +142 -1
- package/lib/core/identity.js +33 -2
- package/lib/core/imageProcessor.js +109 -0
- package/lib/core/imageTorrent.js +167 -0
- package/lib/core/permissions.js +1 -1
- package/lib/core/pro.js +11 -4
- package/lib/core/serverList.js +4 -1
- package/lib/core/shares.js +12 -1
- package/lib/core/signalingServer.js +14 -2
- package/lib/core/su.js +1 -1
- package/lib/core/users.js +1 -1
- package/lib/protocol/messages.js +12 -3
- package/lib/utils/explorer.js +1 -1
- package/lib/utils/help.js +357 -357
- package/lib/utils/torrent.js +1 -0
- package/package.json +4 -3
package/lib/commands/pal.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
$
|
|
41
|
-
$
|
|
42
|
-
$
|
|
43
|
-
$
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
pal
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
}
|