pal-explorer-cli 0.4.14 → 0.4.15
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/bin/pal.js +11 -3
- package/lib/commands/backup.js +6 -0
- package/lib/commands/chat.js +25 -0
- package/lib/commands/device.js +14 -5
- package/lib/commands/download.js +47 -24
- package/lib/commands/extension.js +20 -4
- package/lib/commands/group.js +4 -4
- package/lib/commands/log.js +8 -0
- package/lib/commands/network.js +1 -1
- package/lib/commands/pal.js +9 -0
- package/lib/commands/permissions.js +1 -1
- package/lib/commands/protocol.js +17 -0
- package/lib/commands/serve.js +5 -1
- package/lib/commands/server.js +18 -1
- package/lib/commands/share-link.js +14 -2
- package/lib/commands/sync.js +12 -3
- package/lib/commands/transfers.js +13 -3
- package/lib/commands/user.js +16 -0
- package/lib/commands/workspace.js +13 -3
- package/lib/core/networks.js +5 -3
- package/package.json +1 -1
package/bin/pal.js
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
// Enable V8 code caching for faster subsequent boots
|
|
4
4
|
import 'v8-compile-cache-lib';
|
|
5
5
|
|
|
6
|
+
import { createRequire } from 'module';
|
|
6
7
|
import { program } from 'commander';
|
|
7
8
|
import chalk from 'chalk';
|
|
8
9
|
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const { version } = require('../package.json');
|
|
12
|
+
|
|
9
13
|
// Utility for lazy loading commands
|
|
10
14
|
const lazy = (path) => async (program) => {
|
|
11
15
|
const mod = await import(path);
|
|
@@ -49,7 +53,7 @@ logger.applyGlobalOverride();
|
|
|
49
53
|
program
|
|
50
54
|
.name('pal')
|
|
51
55
|
.description('p2p file sharing for friends')
|
|
52
|
-
.version(
|
|
56
|
+
.version(version)
|
|
53
57
|
.option('--json', 'output in JSON format')
|
|
54
58
|
.option('--log-level <level>', 'set log level (debug, info, warn, error, silent)');
|
|
55
59
|
|
|
@@ -98,6 +102,10 @@ const commands = [
|
|
|
98
102
|
['network', '../lib/commands/network.js'],
|
|
99
103
|
['search', '../lib/commands/search.js'],
|
|
100
104
|
['connect', '../lib/commands/connect.js'],
|
|
105
|
+
['disconnect', '../lib/commands/connect.js'],
|
|
106
|
+
['login', '../lib/commands/user.js'],
|
|
107
|
+
['logout', '../lib/commands/user.js'],
|
|
108
|
+
['verify', '../lib/commands/register.js'],
|
|
101
109
|
['protocol', '../lib/commands/protocol.js'],
|
|
102
110
|
['workspace', '../lib/commands/workspace.js'],
|
|
103
111
|
['favorite', '../lib/commands/favorite.js'],
|
|
@@ -170,7 +178,7 @@ ${chalk.cyan.bold('Command Groups:')}
|
|
|
170
178
|
|
|
171
179
|
${chalk.yellow('Identity:')} init, whoami, register, verify, recover
|
|
172
180
|
${chalk.yellow('Users:')} user list/add/remove/promote, login
|
|
173
|
-
${chalk.yellow('Sharing:')} share, list, revoke, serve, download, share-link,
|
|
181
|
+
${chalk.yellow('Sharing:')} share, list, revoke, serve, download, share-link, permissions
|
|
174
182
|
${chalk.yellow('Sync:')} sync push/pull/status/watch/list/remove/diff
|
|
175
183
|
${chalk.yellow('Files:')} file ls/tree/info/copy/move/rename/mkdir/delete/search/open/reveal/audit
|
|
176
184
|
${chalk.yellow('Remote:')} remote browse/files/download
|
|
@@ -291,7 +299,7 @@ if (!process.argv.slice(2).length) {
|
|
|
291
299
|
}
|
|
292
300
|
|
|
293
301
|
// keytar's native D-Bus handles block the event loop on headless Linux
|
|
294
|
-
const longRunning = new Set(['serve', 'nearby', 'stream']);
|
|
302
|
+
const longRunning = new Set(['serve', 'nearby', 'stream', 'download']);
|
|
295
303
|
if (!longRunning.has(currentCmd)) {
|
|
296
304
|
// Flush analytics and run shutdown hooks before exiting
|
|
297
305
|
if (extensionsLoaded) {
|
package/lib/commands/backup.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createBackup, restoreBackup } from '../core/backup.js';
|
|
5
|
+
import { isPro } from '../core/pro.js';
|
|
5
6
|
|
|
6
7
|
export default function backupCommand(program) {
|
|
7
8
|
const cmd = program
|
|
@@ -25,6 +26,11 @@ Examples:
|
|
|
25
26
|
.option('-p, --password <password>', 'Encryption password')
|
|
26
27
|
.action(async (opts) => {
|
|
27
28
|
try {
|
|
29
|
+
if (!isPro()) {
|
|
30
|
+
console.log(chalk.red('Backup requires a Pro plan. Upgrade with `pal upgrade`.'));
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
28
34
|
const password = opts.password || process.env.PAL_BACKUP_PASSWORD;
|
|
29
35
|
if (!password) {
|
|
30
36
|
console.log(chalk.red('Password required. Use --password or PAL_BACKUP_PASSWORD env var.'));
|
package/lib/commands/chat.js
CHANGED
|
@@ -19,9 +19,24 @@ Examples:
|
|
|
19
19
|
const chatStore = config.get('chatHistory') || {};
|
|
20
20
|
const keys = Object.keys(chatStore);
|
|
21
21
|
if (keys.length === 0) {
|
|
22
|
+
if (program.opts().json) {
|
|
23
|
+
console.log(JSON.stringify([], null, 2));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
22
26
|
console.log(chalk.gray('No conversations yet. Use `pal chat send <handle> "message"` to start one.'));
|
|
23
27
|
return;
|
|
24
28
|
}
|
|
29
|
+
|
|
30
|
+
if (program.opts().json) {
|
|
31
|
+
const conversations = keys.map(key => {
|
|
32
|
+
const msgs = chatStore[key] || [];
|
|
33
|
+
const last = msgs[msgs.length - 1];
|
|
34
|
+
return { handle: key, messageCount: msgs.length, lastMessage: last || null };
|
|
35
|
+
});
|
|
36
|
+
console.log(JSON.stringify(conversations, null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
console.log('');
|
|
26
41
|
console.log(chalk.cyan('Conversations:'));
|
|
27
42
|
for (const key of keys) {
|
|
@@ -108,11 +123,21 @@ Examples:
|
|
|
108
123
|
const chatStore = config.get('chatHistory') || {};
|
|
109
124
|
const msgs = chatStore[handle] || [];
|
|
110
125
|
if (msgs.length === 0) {
|
|
126
|
+
if (program.opts().json) {
|
|
127
|
+
console.log(JSON.stringify([], null, 2));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
111
130
|
console.log(chalk.gray(`No messages with ${handle}.`));
|
|
112
131
|
return;
|
|
113
132
|
}
|
|
114
133
|
const limit = parseInt(opts.limit) || 20;
|
|
115
134
|
const shown = msgs.slice(-limit);
|
|
135
|
+
|
|
136
|
+
if (program.opts().json) {
|
|
137
|
+
console.log(JSON.stringify(shown, null, 2));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
116
141
|
console.log('');
|
|
117
142
|
console.log(chalk.cyan(`Chat with ${handle} (${shown.length}/${msgs.length}):`));
|
|
118
143
|
for (const m of shown) {
|
package/lib/commands/device.js
CHANGED
|
@@ -31,7 +31,7 @@ Examples:
|
|
|
31
31
|
console.log(chalk.cyan('This Device:'));
|
|
32
32
|
console.log(` Device Name: ${chalk.white(device.name)}`);
|
|
33
33
|
console.log(` Device ID: ${chalk.gray(device.id)}`);
|
|
34
|
-
console.log(` Identity: ${chalk.white(identity.name)}${identity.handle ? chalk.cyan(`
|
|
34
|
+
console.log(` Identity: ${chalk.white(identity.name)}${identity.handle ? chalk.cyan(` ${identity.handle.startsWith('@') ? identity.handle : '@' + identity.handle}`) : ''}`);
|
|
35
35
|
console.log(` Public Key: ${chalk.gray(identity.publicKey)}`);
|
|
36
36
|
console.log(` Created At: ${chalk.gray(device.createdAt)}`);
|
|
37
37
|
console.log('');
|
|
@@ -103,8 +103,17 @@ Examples:
|
|
|
103
103
|
const id = config.get('identity');
|
|
104
104
|
handle = id?.handle;
|
|
105
105
|
if (!handle) {
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
const device = getDeviceInfo();
|
|
107
|
+
if (program.opts().json) {
|
|
108
|
+
console.log(JSON.stringify({ devices: [{ deviceName: device.name, deviceId: device.id, status: 'local', updatedAt: device.createdAt }] }, null, 2));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(chalk.cyan('Local device (no handle registered):'));
|
|
113
|
+
console.log(` ${chalk.white(device.name)} [${chalk.green('local')}] ${chalk.gray(device.id)}`);
|
|
114
|
+
console.log(` Created: ${chalk.gray(device.createdAt)}`);
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log(chalk.gray('Register a handle to see remote devices: pal device register <handle>'));
|
|
108
117
|
return;
|
|
109
118
|
}
|
|
110
119
|
}
|
|
@@ -124,7 +133,7 @@ Examples:
|
|
|
124
133
|
console.log(JSON.stringify({ handle, devices: [] }, null, 2));
|
|
125
134
|
return;
|
|
126
135
|
}
|
|
127
|
-
console.log(chalk.gray(`No devices found for
|
|
136
|
+
console.log(chalk.gray(`No devices found for ${handle.startsWith('@') ? handle : '@' + handle}.`));
|
|
128
137
|
return;
|
|
129
138
|
}
|
|
130
139
|
if (program.opts().json) {
|
|
@@ -132,7 +141,7 @@ Examples:
|
|
|
132
141
|
return;
|
|
133
142
|
}
|
|
134
143
|
console.log('');
|
|
135
|
-
console.log(chalk.cyan(`Devices for
|
|
144
|
+
console.log(chalk.cyan(`Devices for ${handle.startsWith('@') ? handle : '@' + handle}:`));
|
|
136
145
|
devices.forEach(d => {
|
|
137
146
|
const status = d.status === 'online' ? chalk.green('online') : chalk.gray(d.status || 'offline');
|
|
138
147
|
console.log(` ${chalk.white(d.deviceName)} [${status}] ${chalk.gray(d.deviceId)}`);
|
package/lib/commands/download.js
CHANGED
|
@@ -18,7 +18,11 @@ function startMdnsIfNeeded() {
|
|
|
18
18
|
if (identity?.publicKey) {
|
|
19
19
|
const pidPath = path.join(path.dirname(config.path), 'serve.pid');
|
|
20
20
|
const daemonRunning = fs.existsSync(pidPath);
|
|
21
|
-
|
|
21
|
+
try {
|
|
22
|
+
startMdns(identity, { advertise: !daemonRunning });
|
|
23
|
+
} catch (e) {
|
|
24
|
+
logger.warn(`mDNS start failed (port in use?): ${e.message}`);
|
|
25
|
+
}
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
|
|
@@ -51,7 +55,7 @@ Examples:
|
|
|
51
55
|
|
|
52
56
|
if (allMagnets.length > 1) {
|
|
53
57
|
console.log(chalk.blue(`Batch downloading ${allMagnets.length} items...`));
|
|
54
|
-
const client = new WebTorrent();
|
|
58
|
+
const client = new WebTorrent({ utp: false, lsd: false });
|
|
55
59
|
let completed = 0;
|
|
56
60
|
let failed = 0;
|
|
57
61
|
|
|
@@ -80,11 +84,14 @@ Examples:
|
|
|
80
84
|
});
|
|
81
85
|
});
|
|
82
86
|
|
|
83
|
-
// Inject LAN peers and manual peer
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
// Inject LAN peers and manual peer after infoHash resolves
|
|
88
|
+
const injectBatchPeers = () => {
|
|
89
|
+
if (lanPeers.length > 0) injectLanPeers(t, lanPeers);
|
|
90
|
+
if (opts.peer) {
|
|
91
|
+
try { t.addPeer(opts.peer); } catch (e) { console.error(chalk.yellow(` Warning: Could not add peer ${opts.peer}: ${e.message}`)); }
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
if (t.infoHash) { injectBatchPeers(); } else { t.once('infoHash', injectBatchPeers); }
|
|
88
95
|
|
|
89
96
|
client.on('error', (err) => { clearTimeout(timeout); reject(err); });
|
|
90
97
|
});
|
|
@@ -151,7 +158,7 @@ Examples:
|
|
|
151
158
|
}
|
|
152
159
|
const spinner = ora('Finding peers...').start();
|
|
153
160
|
|
|
154
|
-
const client = new WebTorrent();
|
|
161
|
+
const client = new WebTorrent({ utp: false, lsd: false });
|
|
155
162
|
|
|
156
163
|
const peerTimeout = setTimeout(() => {
|
|
157
164
|
const torrents = client.torrents;
|
|
@@ -165,21 +172,23 @@ Examples:
|
|
|
165
172
|
const addOpts = { path: dlDir };
|
|
166
173
|
const torrentSrc = opts.lanOnly ? magnet : magnet;
|
|
167
174
|
|
|
175
|
+
const pendingPeer = opts.peer || null;
|
|
168
176
|
client.add(torrentSrc, addOpts, (torrent) => {
|
|
169
177
|
clearTimeout(peerTimeout);
|
|
170
178
|
spinner.succeed(chalk.green(`Connected! Downloading: ${torrent.name}`));
|
|
171
|
-
|
|
172
179
|
// Track transfer, storing the encrypted key if provided
|
|
173
180
|
trackTransfer(magnet, torrent.name, dlDir, opts.key || null);
|
|
174
181
|
|
|
175
182
|
const progressSpinner = ora('Downloading... 0%').start();
|
|
183
|
+
// Keep event loop alive until download completes (Node may exit early otherwise)
|
|
184
|
+
const keepAlive = setInterval(() => {}, 1000);
|
|
176
185
|
|
|
177
186
|
torrent.on('download', (bytes) => {
|
|
178
187
|
const progress = (torrent.progress * 100).toFixed(1);
|
|
179
188
|
progressSpinner.text = `Downloading... ${progress}% (${(torrent.downloaded / 1024 / 1024).toFixed(2)} MB / ${(torrent.length / 1024 / 1024).toFixed(2)} MB)`;
|
|
180
189
|
});
|
|
181
190
|
|
|
182
|
-
|
|
191
|
+
const onDone = async () => {
|
|
183
192
|
logger.info(`Download complete: ${torrent.name}`, { size: torrent.length });
|
|
184
193
|
progressSpinner.succeed(chalk.green('Download Complete!'));
|
|
185
194
|
const downloadedDir = path.join(dlDir, torrent.name);
|
|
@@ -208,25 +217,39 @@ Examples:
|
|
|
208
217
|
}
|
|
209
218
|
|
|
210
219
|
completeTransfer(magnet);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
clearInterval(keepAlive);
|
|
221
|
+
// Wait for file system writes to flush before exiting
|
|
222
|
+
setTimeout(() => {
|
|
223
|
+
client.destroy();
|
|
224
|
+
setTimeout(() => process.exit(0), 500);
|
|
225
|
+
}, 1000);
|
|
226
|
+
};
|
|
227
|
+
torrent.on('done', onDone);
|
|
228
|
+
// For small files, done may fire before handler is registered
|
|
229
|
+
if (torrent.progress === 1) onDone();
|
|
214
230
|
});
|
|
215
231
|
|
|
216
|
-
// Inject LAN peers
|
|
232
|
+
// Inject LAN peers and manual peer after infoHash is resolved
|
|
217
233
|
const currentTorrent = client.torrents[client.torrents.length - 1];
|
|
218
234
|
if (currentTorrent) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
235
|
+
const injectPeers = () => {
|
|
236
|
+
if (lanPeers.length > 0) {
|
|
237
|
+
const injected = injectLanPeers(currentTorrent, lanPeers);
|
|
238
|
+
if (injected > 0) logger.info(`Injected ${injected} LAN peer(s) into torrent`);
|
|
239
|
+
}
|
|
240
|
+
if (pendingPeer) {
|
|
241
|
+
try {
|
|
242
|
+
currentTorrent.addPeer(pendingPeer);
|
|
243
|
+
logger.info(`Injected manual peer ${pendingPeer} into torrent`);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
console.error(chalk.yellow(`Warning: Could not add peer ${pendingPeer}: ${e.message}`));
|
|
246
|
+
}
|
|
229
247
|
}
|
|
248
|
+
};
|
|
249
|
+
if (currentTorrent.infoHash) {
|
|
250
|
+
injectPeers();
|
|
251
|
+
} else {
|
|
252
|
+
currentTorrent.once('infoHash', injectPeers);
|
|
230
253
|
}
|
|
231
254
|
}
|
|
232
255
|
|
|
@@ -46,13 +46,13 @@ Examples:
|
|
|
46
46
|
$ pal ext audit Security audit all extensions
|
|
47
47
|
`)
|
|
48
48
|
.action(async () => {
|
|
49
|
-
await listExtensions();
|
|
49
|
+
await listExtensions(program);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
cmd
|
|
53
53
|
.command('list')
|
|
54
54
|
.description('list installed extensions')
|
|
55
|
-
.action(async () => { await listExtensions(); });
|
|
55
|
+
.action(async () => { await listExtensions(program); });
|
|
56
56
|
|
|
57
57
|
cmd
|
|
58
58
|
.command('install <source>')
|
|
@@ -426,7 +426,7 @@ Examples:
|
|
|
426
426
|
const stars = '\u2605'.repeat(Math.round(ext.rating || 0)) + '\u2606'.repeat(5 - Math.round(ext.rating || 0));
|
|
427
427
|
console.log(` ${chalk.white(ext.name)} v${ext.version}${verified}${pro}`);
|
|
428
428
|
console.log(` ${chalk.gray(ext.description || '')}`);
|
|
429
|
-
console.log(` ${chalk.yellow(stars)} (${ext.reviewCount || 0}) \u00b7 ${chalk.cyan(ext.downloads || 0)} downloads \u00b7 by ${chalk.blue('@' + (ext.authorHandle || 'unknown'))}`);
|
|
429
|
+
console.log(` ${chalk.yellow(stars)} (${ext.reviewCount || 0}) \u00b7 ${chalk.cyan(ext.downloads || 0)} downloads \u00b7 by ${chalk.blue((ext.authorHandle?.startsWith('@') ? ext.authorHandle : '@' + (ext.authorHandle || 'unknown')))}`);
|
|
430
430
|
console.log('');
|
|
431
431
|
}
|
|
432
432
|
console.log(chalk.gray('Install: pal ext install-remote <name>'));
|
|
@@ -1035,16 +1035,32 @@ Why this tier?
|
|
|
1035
1035
|
});
|
|
1036
1036
|
}
|
|
1037
1037
|
|
|
1038
|
-
async function listExtensions() {
|
|
1038
|
+
async function listExtensions(program) {
|
|
1039
1039
|
const { getInstalledExtensions } = await import('../core/extensions.js');
|
|
1040
1040
|
const extensions = getInstalledExtensions();
|
|
1041
1041
|
if (extensions.length === 0) {
|
|
1042
|
+
if (program?.opts().json) {
|
|
1043
|
+
console.log(JSON.stringify([], null, 2));
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1042
1046
|
console.log(chalk.gray('No extensions installed.'));
|
|
1043
1047
|
console.log(chalk.gray(' pal ext install <path|git-url>'));
|
|
1044
1048
|
console.log(chalk.gray(' pal ext create <name>'));
|
|
1045
1049
|
return;
|
|
1046
1050
|
}
|
|
1047
1051
|
|
|
1052
|
+
if (program?.opts().json) {
|
|
1053
|
+
console.log(JSON.stringify(extensions.map(ext => ({
|
|
1054
|
+
name: ext.name,
|
|
1055
|
+
version: ext.version,
|
|
1056
|
+
enabled: ext.enabled,
|
|
1057
|
+
bundled: !!ext.bundled,
|
|
1058
|
+
tier: ext.tier || (ext.pro ? 'pro' : 'free'),
|
|
1059
|
+
description: ext.description || null,
|
|
1060
|
+
})), null, 2));
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1048
1064
|
console.log('');
|
|
1049
1065
|
console.log(chalk.cyan('Extensions:'));
|
|
1050
1066
|
for (const ext of extensions) {
|
package/lib/commands/group.js
CHANGED
|
@@ -135,7 +135,7 @@ Examples:
|
|
|
135
135
|
const members = getGroupMembers(groupName);
|
|
136
136
|
checkLimit('maxGroupMembers', members.length);
|
|
137
137
|
addMemberToGroup(groupName, pal);
|
|
138
|
-
console.log(chalk.green(`Added '${pal.name}' to '${groupName}'.`));
|
|
138
|
+
console.log(chalk.green(`Added '${pal.name || pal.publicKey?.substring(0, 8) + '...' || palName}' to '${groupName}'.`));
|
|
139
139
|
} catch (err) {
|
|
140
140
|
console.log(chalk.red(err.message));
|
|
141
141
|
process.exitCode = 1;
|
|
@@ -188,7 +188,7 @@ Examples:
|
|
|
188
188
|
for (const g of groups) {
|
|
189
189
|
console.log(` ${chalk.white(g.name)} ${chalk.gray(`(${g.members.length} members)`)}`);
|
|
190
190
|
for (const m of g.members) {
|
|
191
|
-
console.log(` - ${m.name}${m.handle ? chalk.gray(`
|
|
191
|
+
console.log(` - ${m.name || m.publicKey?.substring(0, 8) + '...' || 'unknown'}${m.handle ? chalk.gray(` ${m.handle.startsWith('@') ? m.handle : '@' + m.handle}`) : ''}`);
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
console.log('');
|
|
@@ -245,11 +245,11 @@ Examples:
|
|
|
245
245
|
if (res.ok) {
|
|
246
246
|
sent++;
|
|
247
247
|
} else {
|
|
248
|
-
console.log(chalk.yellow(` Failed to send to
|
|
248
|
+
console.log(chalk.yellow(` Failed to send to ${handle.startsWith('@') ? handle : '@' + handle}`));
|
|
249
249
|
failed++;
|
|
250
250
|
}
|
|
251
251
|
} catch {
|
|
252
|
-
console.log(chalk.yellow(` Failed to send to
|
|
252
|
+
console.log(chalk.yellow(` Failed to send to ${handle.startsWith('@') ? handle : '@' + handle}`));
|
|
253
253
|
failed++;
|
|
254
254
|
}
|
|
255
255
|
}
|
package/lib/commands/log.js
CHANGED
|
@@ -78,6 +78,14 @@ Examples:
|
|
|
78
78
|
const count = parseInt(opts.lines, 10) || 20;
|
|
79
79
|
lines = lines.slice(-count);
|
|
80
80
|
|
|
81
|
+
if (program.opts().json) {
|
|
82
|
+
const entries = lines.map(line => {
|
|
83
|
+
try { return JSON.parse(line); } catch { return { raw: line }; }
|
|
84
|
+
});
|
|
85
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
81
89
|
for (const line of lines) {
|
|
82
90
|
console.log(formatEntry(line));
|
|
83
91
|
}
|
package/lib/commands/network.js
CHANGED
|
@@ -152,7 +152,7 @@ Examples:
|
|
|
152
152
|
console.log(chalk.cyan('Members:'));
|
|
153
153
|
for (const m of members) {
|
|
154
154
|
const role = m.role ? chalk.yellow(` [${m.role}]`) : '';
|
|
155
|
-
const handle = m.handle ? chalk.gray(`
|
|
155
|
+
const handle = m.handle ? chalk.gray(` ${m.handle.startsWith('@') ? m.handle : '@' + m.handle}`) : '';
|
|
156
156
|
console.log(` ${chalk.white(m.name || m.userId)}${handle}${role}`);
|
|
157
157
|
}
|
|
158
158
|
console.log('');
|
package/lib/commands/pal.js
CHANGED
|
@@ -375,10 +375,19 @@ Examples:
|
|
|
375
375
|
|
|
376
376
|
const profile = getProfile();
|
|
377
377
|
if (!profile) {
|
|
378
|
+
if (program.opts().json) {
|
|
379
|
+
console.log(JSON.stringify(null, null, 2));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
378
382
|
console.log(chalk.yellow('No identity found. Run `pal init` first.'));
|
|
379
383
|
return;
|
|
380
384
|
}
|
|
381
385
|
|
|
386
|
+
if (program.opts().json) {
|
|
387
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
382
391
|
console.log('');
|
|
383
392
|
console.log(chalk.cyan('Your Profile:'));
|
|
384
393
|
const rows = [
|
package/lib/commands/protocol.js
CHANGED
|
@@ -31,6 +31,23 @@ Examples:
|
|
|
31
31
|
const { getRelayLimits } = await import('../protocol/router.js');
|
|
32
32
|
const { FREE_POLICY_LIMITS, PRO_POLICY_LIMITS } = await import('../protocol/policy.js');
|
|
33
33
|
|
|
34
|
+
if (program.opts().json) {
|
|
35
|
+
const caps = ['share', 'sync', 'chat', 'relay'];
|
|
36
|
+
if (isPro()) caps.push('delta-sync', 'receipts');
|
|
37
|
+
const limits = getRelayLimits();
|
|
38
|
+
const pl = isPro() ? PRO_POLICY_LIMITS : FREE_POLICY_LIMITS;
|
|
39
|
+
console.log(JSON.stringify({
|
|
40
|
+
protocol: PROTOCOL_NAME,
|
|
41
|
+
version: PROTOCOL_VERSION,
|
|
42
|
+
tier: isPro() ? 'pro' : 'free',
|
|
43
|
+
capabilities: caps,
|
|
44
|
+
relayLimits: limits,
|
|
45
|
+
policyLimits: pl,
|
|
46
|
+
identity: identity ? { publicKey: identity.publicKey, handle: identity.handle || null } : null,
|
|
47
|
+
}, null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
console.log('');
|
|
35
52
|
console.log(chalk.cyan.bold('PAL Protocol'));
|
|
36
53
|
console.log(` Protocol: ${chalk.white(PROTOCOL_NAME)}`);
|
package/lib/commands/serve.js
CHANGED
|
@@ -47,6 +47,7 @@ export default function serveCommand(program) {
|
|
|
47
47
|
.option('--web-port <port>', 'Web dashboard port (default: 8585)', '8585')
|
|
48
48
|
.option('--lan', 'Bind web server to 0.0.0.0 for LAN access')
|
|
49
49
|
.option('--public-ip <ip>', 'Public IP/hostname for serveUrl announcement (for VPS/cloud)')
|
|
50
|
+
.option('--torrent-port <port>', 'BitTorrent listening port (default: random)')
|
|
50
51
|
.addHelpText('after', `
|
|
51
52
|
Examples:
|
|
52
53
|
$ pal serve Start seeding in foreground
|
|
@@ -95,6 +96,7 @@ Examples:
|
|
|
95
96
|
if (opts.lan) args.push('--lan');
|
|
96
97
|
if (opts.publicIp) args.push('--public-ip', opts.publicIp);
|
|
97
98
|
}
|
|
99
|
+
if (opts.torrentPort) args.push('--torrent-port', opts.torrentPort);
|
|
98
100
|
const child = spawn(process.execPath, args, {
|
|
99
101
|
detached: true,
|
|
100
102
|
stdio: 'ignore'
|
|
@@ -207,7 +209,9 @@ Examples:
|
|
|
207
209
|
console.log(chalk.gray('No active shares to seed. Signaling server still running.'));
|
|
208
210
|
}
|
|
209
211
|
|
|
210
|
-
const
|
|
212
|
+
const wtOpts = {};
|
|
213
|
+
if (opts.torrentPort) wtOpts.torrentPort = parseInt(opts.torrentPort, 10);
|
|
214
|
+
const client = new WebTorrent(wtOpts);
|
|
211
215
|
client.setMaxListeners(Math.max(shares.length, 10) + 20);
|
|
212
216
|
|
|
213
217
|
// Auto-expire old shares before seeding
|
package/lib/commands/server.js
CHANGED
|
@@ -232,9 +232,26 @@ Examples:
|
|
|
232
232
|
.description('list all configured discovery servers with health status and roles')
|
|
233
233
|
.action(async () => {
|
|
234
234
|
const servers = getServers();
|
|
235
|
+
const checks = await Promise.all(servers.map(s => checkServer(s)));
|
|
236
|
+
|
|
237
|
+
if (program.opts().json) {
|
|
238
|
+
const result = checks.map((c, i) => {
|
|
239
|
+
const meta = getServerRoles(c.url);
|
|
240
|
+
return {
|
|
241
|
+
url: c.url,
|
|
242
|
+
reachable: c.reachable,
|
|
243
|
+
primary: i === 0,
|
|
244
|
+
roles: meta.roles,
|
|
245
|
+
addedBy: meta.addedBy,
|
|
246
|
+
writeTrusted: isWriteTrusted(c.url),
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
console.log(JSON.stringify(result, null, 2));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
235
253
|
console.log('');
|
|
236
254
|
console.log(chalk.cyan('Discovery Servers:'));
|
|
237
|
-
const checks = await Promise.all(servers.map(s => checkServer(s)));
|
|
238
255
|
for (let i = 0; i < checks.length; i++) {
|
|
239
256
|
const c = checks[i];
|
|
240
257
|
const primary = i === 0 ? chalk.cyan(' (primary)') : '';
|
|
@@ -128,8 +128,20 @@ Examples:
|
|
|
128
128
|
try {
|
|
129
129
|
const identity = await getIdentity();
|
|
130
130
|
if (!identity?.handle) {
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
const shares = listShares();
|
|
132
|
+
const sharedLinks = shares.filter(s => s.magnet);
|
|
133
|
+
if (sharedLinks.length === 0) {
|
|
134
|
+
console.log(chalk.gray('No share links. Register a handle to manage server-side links: pal register'));
|
|
135
|
+
} else {
|
|
136
|
+
console.log('');
|
|
137
|
+
console.log(chalk.cyan('Local shares with magnets:'));
|
|
138
|
+
for (const s of sharedLinks) {
|
|
139
|
+
console.log(` ${chalk.white(s.name || s.id)}`);
|
|
140
|
+
console.log(` Magnet: ${chalk.gray(s.magnet.substring(0, 60) + '...')}`);
|
|
141
|
+
}
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log(chalk.gray('Register a handle to create web share links: pal register'));
|
|
144
|
+
}
|
|
133
145
|
return;
|
|
134
146
|
}
|
|
135
147
|
|
package/lib/commands/sync.js
CHANGED
|
@@ -480,14 +480,23 @@ async function watchSync(dirPath, palName, opts) {
|
|
|
480
480
|
|
|
481
481
|
// --- pal sync status <path> ---
|
|
482
482
|
|
|
483
|
-
async function syncStatus(dirPath) {
|
|
483
|
+
async function syncStatus(dirPath, program) {
|
|
484
484
|
if (!dirPath) {
|
|
485
485
|
const pairs = getSyncPairs();
|
|
486
486
|
if (pairs.length === 0) {
|
|
487
|
+
if (program?.opts().json) {
|
|
488
|
+
console.log(JSON.stringify([], null, 2));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
487
491
|
console.log(chalk.gray('No sync pairs configured. Use `pal sync <path> <pal>` to start.'));
|
|
488
492
|
return;
|
|
489
493
|
}
|
|
490
494
|
|
|
495
|
+
if (program?.opts().json) {
|
|
496
|
+
console.log(JSON.stringify(pairs, null, 2));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
491
500
|
const friends = getFriends();
|
|
492
501
|
console.log('');
|
|
493
502
|
console.log(chalk.cyan.bold('Sync Pairs'));
|
|
@@ -674,14 +683,14 @@ export default function syncCommand(program) {
|
|
|
674
683
|
.command('status [path]')
|
|
675
684
|
.description('show sync status and changes since last sync')
|
|
676
685
|
.action((syncPath) => {
|
|
677
|
-
syncStatus(syncPath);
|
|
686
|
+
syncStatus(syncPath, program);
|
|
678
687
|
});
|
|
679
688
|
|
|
680
689
|
cmd
|
|
681
690
|
.command('list')
|
|
682
691
|
.description('list all sync pairs')
|
|
683
692
|
.action(() => {
|
|
684
|
-
syncStatus();
|
|
693
|
+
syncStatus(undefined, program);
|
|
685
694
|
});
|
|
686
695
|
|
|
687
696
|
cmd
|
|
@@ -129,11 +129,17 @@ Examples:
|
|
|
129
129
|
const days = parseInt(opts.days, 10) || 90;
|
|
130
130
|
pruneOldHistory(days);
|
|
131
131
|
const stats = getTransferStats(days);
|
|
132
|
+
|
|
133
|
+
if (program.opts().json) {
|
|
134
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
132
138
|
console.log('');
|
|
133
139
|
console.log(chalk.cyan(`Transfer Stats (last ${days} days):`));
|
|
134
140
|
console.log(` Total transfers: ${chalk.white(stats.totalTransfers)}`);
|
|
135
141
|
console.log(` Total data: ${chalk.white(formatBytes(stats.totalBytes))}`);
|
|
136
|
-
console.log(` Avg speed: ${chalk.white(
|
|
142
|
+
console.log(` Avg speed: ${chalk.white(formatSpeed(stats.avgSpeed))}`);
|
|
137
143
|
if (Object.keys(stats.perDay).length > 0) {
|
|
138
144
|
console.log('');
|
|
139
145
|
console.log(chalk.cyan(' Per day:'));
|
|
@@ -145,8 +151,12 @@ Examples:
|
|
|
145
151
|
}
|
|
146
152
|
|
|
147
153
|
function formatBytes(bytes) {
|
|
148
|
-
if (bytes
|
|
154
|
+
if (!bytes || !isFinite(bytes) || bytes <= 0) return '0 B';
|
|
149
155
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
150
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
156
|
+
const i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1));
|
|
151
157
|
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
|
152
158
|
}
|
|
159
|
+
|
|
160
|
+
function formatSpeed(bytesPerSec) {
|
|
161
|
+
return formatBytes(bytesPerSec) + '/s';
|
|
162
|
+
}
|
package/lib/commands/user.js
CHANGED
|
@@ -28,10 +28,26 @@ export default function userCommand(program) {
|
|
|
28
28
|
const active = getActiveUser();
|
|
29
29
|
|
|
30
30
|
if (profiles.length === 0) {
|
|
31
|
+
if (program.opts().json) {
|
|
32
|
+
console.log(JSON.stringify([], null, 2));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
31
35
|
console.log(chalk.gray('No profiles. Run `pal init <name>` to create one.'));
|
|
32
36
|
return;
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
if (program.opts().json) {
|
|
40
|
+
console.log(JSON.stringify(profiles.map(p => ({
|
|
41
|
+
name: p.name,
|
|
42
|
+
handle: p.handle || null,
|
|
43
|
+
role: p.role,
|
|
44
|
+
publicKey: p.publicKey,
|
|
45
|
+
lastLogin: p.lastLogin || null,
|
|
46
|
+
active: active?.publicKey === p.publicKey,
|
|
47
|
+
})), null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
35
51
|
console.log('');
|
|
36
52
|
console.log(chalk.cyan.bold('User Profiles'));
|
|
37
53
|
console.log('');
|
|
@@ -19,14 +19,14 @@ Examples:
|
|
|
19
19
|
$ pal workspace remove <wsId> <shareId> Remove share from workspace
|
|
20
20
|
`)
|
|
21
21
|
.action(() => {
|
|
22
|
-
printWorkspaces();
|
|
22
|
+
printWorkspaces(program);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
cmd
|
|
26
26
|
.command('list')
|
|
27
27
|
.description('list all workspaces')
|
|
28
28
|
.action(() => {
|
|
29
|
-
printWorkspaces();
|
|
29
|
+
printWorkspaces(program);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
cmd
|
|
@@ -105,12 +105,22 @@ Examples:
|
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
function printWorkspaces() {
|
|
108
|
+
function printWorkspaces(program) {
|
|
109
109
|
const workspaces = config.get('workspaces') || [];
|
|
110
110
|
if (workspaces.length === 0) {
|
|
111
|
+
if (program?.opts().json) {
|
|
112
|
+
console.log(JSON.stringify([], null, 2));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
111
115
|
console.log(chalk.gray('No workspaces. Use `pal workspace create <name>` to create one.'));
|
|
112
116
|
return;
|
|
113
117
|
}
|
|
118
|
+
|
|
119
|
+
if (program?.opts().json) {
|
|
120
|
+
console.log(JSON.stringify(workspaces, null, 2));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
console.log('');
|
|
115
125
|
console.log(chalk.cyan('Workspaces:'));
|
|
116
126
|
for (const ws of workspaces) {
|
package/lib/core/networks.js
CHANGED
|
@@ -3,11 +3,13 @@ import { getIdentity } from './identity.js';
|
|
|
3
3
|
import { getPrimaryServer } from './discoveryClient.js';
|
|
4
4
|
|
|
5
5
|
async function authHeaders() {
|
|
6
|
-
const
|
|
6
|
+
const token = config.get('ext.@palexplorer/auth-email')?.verifiedToken;
|
|
7
|
+
if (!token) {
|
|
8
|
+
throw new Error('Not authenticated. Run `pal auth login` first to link your account.');
|
|
9
|
+
}
|
|
7
10
|
return {
|
|
8
11
|
'Content-Type': 'application/json',
|
|
9
|
-
'
|
|
10
|
-
'X-Handle': identity?.handle || '',
|
|
12
|
+
'Authorization': `Bearer ${token}`,
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
15
|
|