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 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('0.4.0')
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, share-rename, permissions
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) {
@@ -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.'));
@@ -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) {
@@ -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(` @${identity.handle}`) : ''}`);
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
- console.log(chalk.yellow('No handle registered. Provide one: pal device list <handle>'));
107
- process.exitCode = 1;
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 @${handle}.`));
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 @${handle}:`));
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)}`);
@@ -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
- startMdns(identity, { advertise: !daemonRunning });
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
- if (lanPeers.length > 0) injectLanPeers(t, lanPeers);
85
- if (opts.peer) {
86
- try { t.addPeer(opts.peer); } catch (e) { console.error(chalk.yellow(` Warning: Could not add peer ${opts.peer}: ${e.message}`)); }
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
- torrent.on('done', async () => {
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
- client.destroy();
212
- process.exit(0);
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 into the torrent right after adding
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
- if (lanPeers.length > 0) {
220
- const injected = injectLanPeers(currentTorrent, lanPeers);
221
- if (injected > 0) logger.info(`Injected ${injected} LAN peer(s) into torrent`);
222
- }
223
- if (opts.peer) {
224
- try {
225
- currentTorrent.addPeer(opts.peer);
226
- logger.info(`Injected manual peer ${opts.peer} into torrent`);
227
- } catch (e) {
228
- console.error(chalk.yellow(`Warning: Could not add peer ${opts.peer}: ${e.message}`));
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) {
@@ -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(` @${m.handle}`) : ''}`);
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 @${handle}`));
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 @${handle}`));
252
+ console.log(chalk.yellow(` Failed to send to ${handle.startsWith('@') ? handle : '@' + handle}`));
253
253
  failed++;
254
254
  }
255
255
  }
@@ -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
  }
@@ -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(` @${m.handle}`) : '';
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('');
@@ -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 = [
@@ -33,7 +33,7 @@ Examples:
33
33
  const summary = getShareSummary();
34
34
  const groups = getGroups();
35
35
 
36
- if (opts.json) {
36
+ if (opts.json || program.opts().json) {
37
37
  console.log(JSON.stringify(summary, null, 2));
38
38
  return;
39
39
  }
@@ -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)}`);
@@ -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 client = new WebTorrent();
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
@@ -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
- console.log(chalk.red('You must register a handle first: pal register'));
132
- process.exitCode = 1;
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
 
@@ -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(formatBytes(stats.avgSpeed) + '/s')}`);
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 === 0) return '0 B';
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
+ }
@@ -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) {
@@ -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 identity = await getIdentity();
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
- 'X-Public-Key': identity?.publicKey || '',
10
- 'X-Handle': identity?.handle || '',
12
+ 'Authorization': `Bearer ${token}`,
11
13
  };
12
14
  }
13
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pal-explorer-cli",
3
- "version": "0.4.14",
3
+ "version": "0.4.15",
4
4
  "description": "P2P encrypted file sharing CLI — share files directly with friends, not with the cloud",
5
5
  "main": "bin/pal.js",
6
6
  "bin": {