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,271 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getFriends } from '../core/users.js';
|
|
3
|
+
import config from '../utils/config.js';
|
|
4
|
+
import { getIdentity } from '../core/identity.js';
|
|
5
|
+
import {
|
|
6
|
+
getGroups,
|
|
7
|
+
getGroup,
|
|
8
|
+
getGroupMembers,
|
|
9
|
+
createGroup,
|
|
10
|
+
updateGroup,
|
|
11
|
+
addMemberToGroup,
|
|
12
|
+
removeMemberFromGroup,
|
|
13
|
+
deleteGroup
|
|
14
|
+
} from '../core/groups.js';
|
|
15
|
+
import { checkLimit } from '../core/pro.js';
|
|
16
|
+
import { announceGroups } from '../core/discoveryClient.js';
|
|
17
|
+
|
|
18
|
+
export default function groupCommand(program) {
|
|
19
|
+
const cmd = program
|
|
20
|
+
.command('group')
|
|
21
|
+
.description('manage sharing groups')
|
|
22
|
+
.addHelpText('after', `
|
|
23
|
+
Examples:
|
|
24
|
+
$ pe group create team --public Create a public group called "team"
|
|
25
|
+
$ pe group edit team --desc "info" Edit group description
|
|
26
|
+
$ pe group add team @alice Add alice to the group
|
|
27
|
+
$ pe group remove team @alice Remove alice from the group
|
|
28
|
+
$ pe group list List all groups
|
|
29
|
+
$ pe group announce Announce public groups to discovery server
|
|
30
|
+
$ pe group delete team Delete a group
|
|
31
|
+
`);
|
|
32
|
+
|
|
33
|
+
cmd
|
|
34
|
+
.command('create <name>')
|
|
35
|
+
.description('create a new group')
|
|
36
|
+
.option('--public', 'Make group discoverable by others')
|
|
37
|
+
.option('--desc <description>', 'Group description')
|
|
38
|
+
.action((name, opts) => {
|
|
39
|
+
if (!name || !name.trim()) {
|
|
40
|
+
console.log(chalk.red('Group name cannot be empty.'));
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const groups = getGroups();
|
|
46
|
+
checkLimit('maxGroups', groups.length);
|
|
47
|
+
const identity = config.get('identity');
|
|
48
|
+
const group = createGroup(name, identity?.publicKey, {
|
|
49
|
+
visibility: opts.public ? 'public' : 'private',
|
|
50
|
+
description: opts.desc || null
|
|
51
|
+
});
|
|
52
|
+
console.log(chalk.green(`Group '${name}' created.`));
|
|
53
|
+
if (opts.public) {
|
|
54
|
+
console.log(chalk.yellow('Note: Public groups are discoverable. Use `pe group announce` to publish.'));
|
|
55
|
+
}
|
|
56
|
+
console.log(chalk.gray(`ID: ${group.id}`));
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.log(chalk.red(err.message));
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
cmd
|
|
64
|
+
.command('edit <group>')
|
|
65
|
+
.description('edit a group (rename, description, visibility)')
|
|
66
|
+
.option('--name <name>', 'New group name')
|
|
67
|
+
.option('--desc <description>', 'Group description')
|
|
68
|
+
.option('--public', 'Make group public')
|
|
69
|
+
.option('--private', 'Make group private')
|
|
70
|
+
.action((groupName, opts) => {
|
|
71
|
+
try {
|
|
72
|
+
if (!opts.name && !opts.desc && !opts.public && !opts.private) {
|
|
73
|
+
console.log(chalk.yellow('Specify at least one option to update.'));
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const identity = config.get('identity');
|
|
78
|
+
const updates = {};
|
|
79
|
+
if (opts.name) updates.name = opts.name;
|
|
80
|
+
if (opts.desc) updates.description = opts.desc;
|
|
81
|
+
if (opts.public) updates.visibility = 'public';
|
|
82
|
+
if (opts.private) updates.visibility = 'private';
|
|
83
|
+
const group = updateGroup(groupName, updates, identity?.publicKey);
|
|
84
|
+
console.log(chalk.green(`Group '${group.name}' updated.`));
|
|
85
|
+
if (group.visibility === 'public') {
|
|
86
|
+
console.log(chalk.yellow('Remember to run `pe group announce` to sync changes to discovery server.'));
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.log(chalk.red(err.message));
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
cmd
|
|
95
|
+
.command('announce')
|
|
96
|
+
.description('announce public groups to the primary discovery server')
|
|
97
|
+
.action(async () => {
|
|
98
|
+
try {
|
|
99
|
+
const groups = getGroups();
|
|
100
|
+
const publicGroups = groups.filter(g => g.visibility === 'public');
|
|
101
|
+
if (publicGroups.length === 0) {
|
|
102
|
+
console.log(chalk.yellow('No public groups to announce.'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(chalk.gray(`Announcing ${publicGroups.length} public group(s)... `));
|
|
106
|
+
const res = await announceGroups(groups);
|
|
107
|
+
if (res && res.success) {
|
|
108
|
+
console.log(chalk.green('done.'));
|
|
109
|
+
} else {
|
|
110
|
+
console.log(chalk.red('failed.'));
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.log(chalk.red(`\nError: ${err.message}`));
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
cmd
|
|
120
|
+
.command('add <group> <pal>')
|
|
121
|
+
.description('add a pal to a group')
|
|
122
|
+
.action((groupName, palName) => {
|
|
123
|
+
const friends = getFriends();
|
|
124
|
+
const stripped = palName.replace(/^@/, '');
|
|
125
|
+
const pal = friends.find(f =>
|
|
126
|
+
f.name === palName || f.id === palName || f.handle === palName || f.publicKey === palName ||
|
|
127
|
+
f.name === stripped || f.id === stripped || f.handle === stripped || f.publicKey === stripped
|
|
128
|
+
);
|
|
129
|
+
if (!pal) {
|
|
130
|
+
console.log(chalk.red(`Pal '${palName}' not found. Add them first with \`pe pal add\`.`));
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const members = getGroupMembers(groupName);
|
|
136
|
+
checkLimit('maxGroupMembers', members.length);
|
|
137
|
+
addMemberToGroup(groupName, pal);
|
|
138
|
+
console.log(chalk.green(`Added '${pal.name}' to '${groupName}'.`));
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.log(chalk.red(err.message));
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
cmd
|
|
146
|
+
.command('remove <group> <pal>')
|
|
147
|
+
.description('remove a pal from a group')
|
|
148
|
+
.action((groupName, palName) => {
|
|
149
|
+
try {
|
|
150
|
+
removeMemberFromGroup(groupName, palName);
|
|
151
|
+
console.log(chalk.green(`Removed '${palName}' from '${groupName}'.`));
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.log(chalk.red(err.message));
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
cmd
|
|
159
|
+
.command('delete <group>')
|
|
160
|
+
.description('delete a group')
|
|
161
|
+
.action((groupName) => {
|
|
162
|
+
const group = getGroup(groupName);
|
|
163
|
+
if (!group) {
|
|
164
|
+
console.log(chalk.red(`Group '${groupName}' not found.`));
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const identity = config.get('identity');
|
|
169
|
+
deleteGroup(groupName, identity?.publicKey);
|
|
170
|
+
console.log(chalk.green(`Group '${group.name}' deleted.`));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
cmd
|
|
174
|
+
.command('list')
|
|
175
|
+
.description('list all groups')
|
|
176
|
+
.action(() => {
|
|
177
|
+
const groups = getGroups();
|
|
178
|
+
if (program.opts().json) {
|
|
179
|
+
console.log(JSON.stringify({ groups }, null, 2));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (groups.length === 0) {
|
|
183
|
+
console.log(chalk.gray('No groups. Create one with `pe group create <name>`.'));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log(chalk.cyan('Groups:'));
|
|
188
|
+
for (const g of groups) {
|
|
189
|
+
console.log(` ${chalk.white(g.name)} ${chalk.gray(`(${g.members.length} members)`)}`);
|
|
190
|
+
for (const m of g.members) {
|
|
191
|
+
console.log(` - ${m.name}${m.handle ? chalk.gray(` @${m.handle}`) : ''}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
console.log('');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
cmd
|
|
198
|
+
.command('broadcast <group> <message>')
|
|
199
|
+
.description('send a message to all group members')
|
|
200
|
+
.action(async (groupName, message) => {
|
|
201
|
+
try {
|
|
202
|
+
const group = getGroup(groupName);
|
|
203
|
+
if (!group) {
|
|
204
|
+
console.log(chalk.red(`Group '${groupName}' not found.`));
|
|
205
|
+
process.exitCode = 1;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (group.members.length === 0) {
|
|
209
|
+
console.log(chalk.yellow(`Group '${group.name}' has no members.`));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const identity = await getIdentity();
|
|
213
|
+
if (!identity?.handle) {
|
|
214
|
+
console.log(chalk.red('You must register a handle first: pe register'));
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const { getPrimaryServer } = await import('../core/discoveryClient.js');
|
|
219
|
+
const discoveryUrl = config.get('settings')?.discovery_servers?.[0]
|
|
220
|
+
|| config.get('settings')?.discovery_url
|
|
221
|
+
|| process.env.PAL_DISCOVERY_URL
|
|
222
|
+
|| getPrimaryServer();
|
|
223
|
+
|
|
224
|
+
let sent = 0;
|
|
225
|
+
let failed = 0;
|
|
226
|
+
for (const member of group.members) {
|
|
227
|
+
const handle = member.handle;
|
|
228
|
+
if (!handle) {
|
|
229
|
+
console.log(chalk.yellow(` Skipping ${member.name} (no handle)`));
|
|
230
|
+
failed++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const res = await fetch(`${discoveryUrl}/api/v1/messages`, {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: { 'Content-Type': 'application/json' },
|
|
237
|
+
body: JSON.stringify({
|
|
238
|
+
toHandle: handle,
|
|
239
|
+
fromHandle: identity.handle,
|
|
240
|
+
fromDeviceId: identity.deviceId || null,
|
|
241
|
+
payload: { type: 'chat', text: message, timestamp: Date.now() },
|
|
242
|
+
}),
|
|
243
|
+
signal: AbortSignal.timeout(10000),
|
|
244
|
+
});
|
|
245
|
+
if (res.ok) {
|
|
246
|
+
sent++;
|
|
247
|
+
} else {
|
|
248
|
+
console.log(chalk.yellow(` Failed to send to @${handle}`));
|
|
249
|
+
failed++;
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
console.log(chalk.yellow(` Failed to send to @${handle}`));
|
|
253
|
+
failed++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
console.log(chalk.green(`Broadcast to '${group.name}': ${sent} sent, ${failed} failed.`));
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.log(chalk.red(err.message));
|
|
259
|
+
process.exitCode = 1;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
cmd.action(() => {
|
|
264
|
+
const groups = getGroups();
|
|
265
|
+
if (groups.length === 0) {
|
|
266
|
+
console.log(chalk.gray('No groups. Create one with `pe group create <name>`.'));
|
|
267
|
+
} else {
|
|
268
|
+
console.log(chalk.cyan(`${groups.length} group(s). Use \`pe group list\` for details.`));
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export default function guiShareCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('gui-share <path>')
|
|
11
|
+
.description('internal: Open GUI to share a specific file/folder')
|
|
12
|
+
.action((filePath) => {
|
|
13
|
+
const fullPath = path.resolve(filePath);
|
|
14
|
+
console.log(chalk.cyan(`Opening Palexplorer to share: ${fullPath}`));
|
|
15
|
+
|
|
16
|
+
// In a real app, we'd check if Electron is already running and send an IPC message
|
|
17
|
+
// For now, we'll just launch the GUI with an environment variable or argument
|
|
18
|
+
const guiPath = path.resolve(__dirname, '../../gui');
|
|
19
|
+
|
|
20
|
+
const child = spawn('npm', ['run', 'electron', '--', `--share=${fullPath}`], {
|
|
21
|
+
cwd: guiPath,
|
|
22
|
+
detached: true,
|
|
23
|
+
stdio: 'ignore',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
child.unref();
|
|
27
|
+
process.exit(0);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createIdentity } from '../core/identity.js';
|
|
2
|
+
import { addProfile, getProfiles, migrateToMultiUser } from '../core/users.js';
|
|
3
|
+
import config from '../utils/config.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
|
|
7
|
+
export default function initCommand(program) {
|
|
8
|
+
program
|
|
9
|
+
.command('init <name>')
|
|
10
|
+
.description('create a new Pal Explorer identity with a name')
|
|
11
|
+
.option('-f, --force', 'Overwrite existing identity')
|
|
12
|
+
.addHelpText('after', `
|
|
13
|
+
Examples:
|
|
14
|
+
$ pe init alice Create identity as "alice"
|
|
15
|
+
$ pe init bob --force Overwrite existing identity
|
|
16
|
+
`)
|
|
17
|
+
.action(async (name, options) => {
|
|
18
|
+
const spinner = ora('Initializing Identity...').start();
|
|
19
|
+
try {
|
|
20
|
+
const id = await createIdentity(name, options);
|
|
21
|
+
|
|
22
|
+
// Create user profile (owner if first user, otherwise member)
|
|
23
|
+
migrateToMultiUser();
|
|
24
|
+
const profiles = getProfiles();
|
|
25
|
+
const isFirst = profiles.length === 0 || (profiles.length === 1 && profiles[0].publicKey === id.publicKey);
|
|
26
|
+
const role = isFirst ? 'owner' : 'user';
|
|
27
|
+
try {
|
|
28
|
+
addProfile(id.publicKey, null, name, role);
|
|
29
|
+
} catch {
|
|
30
|
+
// Profile may already exist from migration
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
spinner.succeed(chalk.green(`Identity Created for '${name}'!`));
|
|
34
|
+
console.log(chalk.cyan(`Your Public Key (ID): ${id.publicKey}`));
|
|
35
|
+
if (role === 'owner') {
|
|
36
|
+
console.log(chalk.yellow(`Role: Owner (full control)`));
|
|
37
|
+
}
|
|
38
|
+
console.log(chalk.gray('Next: Run `pe register <handle>` to claim your handle.'));
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log(chalk.bgYellow.black(' RECOVERY PHRASE — WRITE THIS DOWN AND KEEP IT SAFE '));
|
|
41
|
+
console.log(chalk.yellow(id.mnemonic));
|
|
42
|
+
console.log(chalk.red('This will NOT be shown again. Use `pe recover` to restore your identity.'));
|
|
43
|
+
|
|
44
|
+
// Opt-in to error reporting (first-time setup)
|
|
45
|
+
const settings = config.get('settings') || {};
|
|
46
|
+
if (settings.errorReportingEnabled === undefined) {
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log(chalk.cyan('Help improve Palexplorer?'));
|
|
49
|
+
console.log(chalk.gray('Automatically send anonymous crash reports and errors to help us fix bugs.'));
|
|
50
|
+
console.log(chalk.gray('No personal data, files, or encryption keys are ever sent.'));
|
|
51
|
+
console.log(chalk.gray('You can change this anytime: pe config set errorReportingEnabled false'));
|
|
52
|
+
settings.errorReportingEnabled = true;
|
|
53
|
+
config.set('settings', settings);
|
|
54
|
+
console.log(chalk.green('✔ Error reporting enabled (opt-out: pe config set errorReportingEnabled false)'));
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
spinner.fail(chalk.red('Initialization Failed'));
|
|
58
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import qrcode from 'qrcode-terminal';
|
|
3
|
+
import { getIdentity } from '../core/identity.js';
|
|
4
|
+
|
|
5
|
+
export function encodeInvite(identity) {
|
|
6
|
+
const payload = JSON.stringify({
|
|
7
|
+
pk: identity.publicKey,
|
|
8
|
+
h: identity.handle || null,
|
|
9
|
+
n: identity.name || null
|
|
10
|
+
});
|
|
11
|
+
return 'pal://' + Buffer.from(payload).toString('base64url');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function decodeInvite(url) {
|
|
15
|
+
if (!url.startsWith('pal://')) return null;
|
|
16
|
+
try {
|
|
17
|
+
const raw = Buffer.from(url.slice(6), 'base64url').toString('utf8');
|
|
18
|
+
const parsed = JSON.parse(raw);
|
|
19
|
+
if (!parsed.pk || typeof parsed.pk !== 'string') return null;
|
|
20
|
+
return parsed;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function inviteCommand(program) {
|
|
27
|
+
program
|
|
28
|
+
.command('invite')
|
|
29
|
+
.description('generate a magic invite link so others can add you as a pal')
|
|
30
|
+
.option('--qr', 'Also render the invite as a QR code in the terminal')
|
|
31
|
+
.addHelpText('after', `
|
|
32
|
+
Examples:
|
|
33
|
+
$ pe invite Generate invite link
|
|
34
|
+
$ pe invite --qr Show invite as QR code
|
|
35
|
+
`)
|
|
36
|
+
.action(async (opts) => {
|
|
37
|
+
const identity = await getIdentity();
|
|
38
|
+
if (!identity) {
|
|
39
|
+
console.log(chalk.red('No identity found. Run `pe init <name>` first.'));
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const url = encodeInvite(identity);
|
|
45
|
+
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(chalk.cyan('Your invite link:'));
|
|
48
|
+
console.log(chalk.white(url));
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log(chalk.gray('Share this over any channel. Recipient runs:'));
|
|
51
|
+
console.log(chalk.white(` pe pal add ${url}`));
|
|
52
|
+
|
|
53
|
+
if (opts.qr) {
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(chalk.cyan('QR Code:'));
|
|
56
|
+
qrcode.generate(url, { small: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { listShares, getSharesByDrive } from '../core/shares.js';
|
|
3
|
+
|
|
4
|
+
export default function listCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command('list')
|
|
7
|
+
.description('list shared resources')
|
|
8
|
+
.option('--by-drive', 'Group shares by drive')
|
|
9
|
+
.addHelpText('after', `
|
|
10
|
+
Examples:
|
|
11
|
+
$ pe list List all shared resources
|
|
12
|
+
`)
|
|
13
|
+
.action((options) => {
|
|
14
|
+
const shares = listShares();
|
|
15
|
+
if (program.opts().json) {
|
|
16
|
+
const safe = shares.map(s => {
|
|
17
|
+
const { password, encryptedShareKeys, ...rest } = s;
|
|
18
|
+
return { ...rest, password: password ? '***' : undefined };
|
|
19
|
+
});
|
|
20
|
+
console.log(JSON.stringify(safe, null, 2));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (shares.length === 0) {
|
|
24
|
+
console.log(chalk.gray('No shared resources found. Use `pe share <path>` to add one.'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (options.byDrive) {
|
|
29
|
+
const byDrive = getSharesByDrive();
|
|
30
|
+
Object.keys(byDrive).forEach(drive => {
|
|
31
|
+
console.log(''); // New line
|
|
32
|
+
console.log(chalk.yellow(`Drive: ${drive}`));
|
|
33
|
+
byDrive[drive].forEach(share => {
|
|
34
|
+
const vis = share.visibility || 'global';
|
|
35
|
+
const visColors = { global: 'green', public: 'green', private: 'red', group: 'blue', network: 'cyan', 'link-only': 'yellow' };
|
|
36
|
+
const visibilityStr = chalk[visColors[vis] || 'white'](`[${vis.toUpperCase()}]`);
|
|
37
|
+
const streamTag = share.streamable ? chalk.magenta(' [STREAM]') : '';
|
|
38
|
+
const expiredTag = share.expired ? chalk.red(' [EXPIRED]') : '';
|
|
39
|
+
console.log(` - ${visibilityStr}${streamTag}${expiredTag} [${chalk.cyan(share.id)}] ${share.path} (${chalk.gray(share.type)})`);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
console.log(''); // New line
|
|
44
|
+
console.log(chalk.green('Active Shares:'));
|
|
45
|
+
shares.forEach(share => {
|
|
46
|
+
const vis = share.visibility || 'global';
|
|
47
|
+
const visColors = { global: 'green', public: 'green', private: 'red', group: 'blue', network: 'cyan', 'link-only': 'yellow' };
|
|
48
|
+
const visibilityStr = chalk[visColors[vis] || 'white'](`[${vis.toUpperCase()}]`);
|
|
49
|
+
const streamTag = share.streamable ? chalk.magenta(' [STREAM]') : '';
|
|
50
|
+
const recipCount = (share.recipients || []).length;
|
|
51
|
+
const recipStr = recipCount > 0 ? chalk.gray(` → ${recipCount} recipient${recipCount > 1 ? 's' : ''}`) : '';
|
|
52
|
+
const expiredTag = share.expired ? chalk.red(' [EXPIRED]') : '';
|
|
53
|
+
const expiryStr = share.expiresAt && !share.expired ? chalk.gray(` expires ${new Date(share.expiresAt).toLocaleDateString()}`) : '';
|
|
54
|
+
const dlStr = share.maxDownloads ? chalk.gray(` ${share.downloads || 0}/${share.maxDownloads} downloads`) : '';
|
|
55
|
+
console.log(`- ${visibilityStr}${streamTag}${expiredTag} [${chalk.cyan(share.id)}] ${share.path} (${chalk.gray(share.type)})${recipStr}${expiryStr}${dlStr}`);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
const LOG_DIR = path.join(os.homedir(), '.pal', 'logs');
|
|
7
|
+
const LOG_FILE = path.join(LOG_DIR, 'pal.log');
|
|
8
|
+
|
|
9
|
+
function colorLevel(level) {
|
|
10
|
+
switch (level) {
|
|
11
|
+
case 'error': return chalk.red(level);
|
|
12
|
+
case 'warn': return chalk.yellow(level);
|
|
13
|
+
case 'info': return chalk.blue(level);
|
|
14
|
+
case 'debug': return chalk.gray(level);
|
|
15
|
+
default: return level;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatEntry(line) {
|
|
20
|
+
try {
|
|
21
|
+
const entry = JSON.parse(line);
|
|
22
|
+
const ts = entry.ts?.slice(11, 19) || '';
|
|
23
|
+
return `${chalk.gray(ts)} ${colorLevel(entry.level).padEnd(15)} ${entry.msg}`;
|
|
24
|
+
} catch {
|
|
25
|
+
return line;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function logCommand(program) {
|
|
30
|
+
program
|
|
31
|
+
.command('log')
|
|
32
|
+
.description('view application logs')
|
|
33
|
+
.option('--tail', 'Follow new log entries')
|
|
34
|
+
.option('--level <level>', 'Filter by level (debug/info/warn/error)')
|
|
35
|
+
.option('-n, --lines <count>', 'Number of recent lines to show', '20')
|
|
36
|
+
.option('--clear', 'Clear the log file')
|
|
37
|
+
.addHelpText('after', `
|
|
38
|
+
Examples:
|
|
39
|
+
$ pe log Show recent log entries
|
|
40
|
+
$ pe log --tail Follow log in real-time
|
|
41
|
+
$ pe log --level error Filter by log level
|
|
42
|
+
$ pe log -n 50 Show last 50 entries
|
|
43
|
+
`)
|
|
44
|
+
.action(async (opts) => {
|
|
45
|
+
try {
|
|
46
|
+
if (opts.clear) {
|
|
47
|
+
if (fs.existsSync(LOG_FILE)) {
|
|
48
|
+
fs.writeFileSync(LOG_FILE, '');
|
|
49
|
+
console.log(chalk.green('✔ Log file cleared.'));
|
|
50
|
+
} else {
|
|
51
|
+
console.log(chalk.gray('No log file to clear.'));
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!fs.existsSync(LOG_FILE)) {
|
|
57
|
+
console.log(chalk.gray('No log file found. Logs will appear after using commands.'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const content = fs.readFileSync(LOG_FILE, 'utf8').trim();
|
|
62
|
+
if (!content) {
|
|
63
|
+
console.log(chalk.gray('Log file is empty.'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let lines = content.split('\n');
|
|
68
|
+
|
|
69
|
+
if (opts.level) {
|
|
70
|
+
lines = lines.filter(line => {
|
|
71
|
+
try {
|
|
72
|
+
const entry = JSON.parse(line);
|
|
73
|
+
return entry.level === opts.level;
|
|
74
|
+
} catch { return true; }
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const count = parseInt(opts.lines, 10) || 20;
|
|
79
|
+
lines = lines.slice(-count);
|
|
80
|
+
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
console.log(formatEntry(line));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (opts.tail) {
|
|
86
|
+
console.log(chalk.gray('--- tailing (Ctrl+C to stop) ---'));
|
|
87
|
+
let size = fs.statSync(LOG_FILE).size;
|
|
88
|
+
const watcher = fs.watch(LOG_FILE, () => {
|
|
89
|
+
try {
|
|
90
|
+
const newSize = fs.statSync(LOG_FILE).size;
|
|
91
|
+
if (newSize <= size) { size = newSize; return; }
|
|
92
|
+
const fd = fs.openSync(LOG_FILE, 'r');
|
|
93
|
+
const buf = Buffer.alloc(newSize - size);
|
|
94
|
+
fs.readSync(fd, buf, 0, buf.length, size);
|
|
95
|
+
fs.closeSync(fd);
|
|
96
|
+
size = newSize;
|
|
97
|
+
const newLines = buf.toString().trim().split('\n');
|
|
98
|
+
for (const line of newLines) {
|
|
99
|
+
if (opts.level) {
|
|
100
|
+
try {
|
|
101
|
+
const entry = JSON.parse(line);
|
|
102
|
+
if (entry.level !== opts.level) continue;
|
|
103
|
+
} catch { /* show anyway */ }
|
|
104
|
+
}
|
|
105
|
+
console.log(formatEntry(line));
|
|
106
|
+
}
|
|
107
|
+
} catch { /* ignore */ }
|
|
108
|
+
});
|
|
109
|
+
await new Promise(() => {}); // block forever
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(chalk.red('Log viewer error:'), err.message);
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|