pal-explorer-cli 0.4.13 → 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 +25 -5
- package/lib/commands/backup.js +6 -0
- package/lib/commands/chat.js +25 -0
- package/lib/commands/completion.js +65 -3
- 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/lib/utils/fuzzy.js +47 -0
- package/package.json +2 -2
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'],
|
|
@@ -146,9 +154,21 @@ if (cmdMatch) {
|
|
|
146
154
|
} else if (isVersionOrHelp) {
|
|
147
155
|
// Help/version — load all commands for full help text, skip migrations
|
|
148
156
|
await Promise.all(commands.map(c => lazy(c[1])(program)));
|
|
157
|
+
} else if (currentCmd && !currentCmd.startsWith('-')) {
|
|
158
|
+
// Unknown command — suggest similar commands
|
|
159
|
+
const { suggest } = await import('../lib/utils/fuzzy.js');
|
|
160
|
+
const allNames = commands.map(c => c[0]);
|
|
161
|
+
const suggestions = suggest(currentCmd, allNames);
|
|
162
|
+
if (suggestions.length) {
|
|
163
|
+
console.error(chalk.red(`Unknown command: ${currentCmd}`));
|
|
164
|
+
console.error(chalk.yellow(`\nDid you mean?\n${suggestions.map(s => ` pal ${s}`).join('\n')}`));
|
|
165
|
+
} else {
|
|
166
|
+
console.error(chalk.red(`Unknown command: ${currentCmd}`));
|
|
167
|
+
console.error(chalk.gray('Run "pal --help" to see available commands.'));
|
|
168
|
+
}
|
|
169
|
+
process.exitCode = 1;
|
|
149
170
|
} else {
|
|
150
|
-
//
|
|
151
|
-
await runMigrations();
|
|
171
|
+
// Flag-only args (--help, --version handled above) — load all
|
|
152
172
|
await Promise.all(commands.map(c => lazy(c[1])(program)));
|
|
153
173
|
}
|
|
154
174
|
|
|
@@ -158,7 +178,7 @@ ${chalk.cyan.bold('Command Groups:')}
|
|
|
158
178
|
|
|
159
179
|
${chalk.yellow('Identity:')} init, whoami, register, verify, recover
|
|
160
180
|
${chalk.yellow('Users:')} user list/add/remove/promote, login
|
|
161
|
-
${chalk.yellow('Sharing:')} share, list, revoke, serve, download, share-link,
|
|
181
|
+
${chalk.yellow('Sharing:')} share, list, revoke, serve, download, share-link, permissions
|
|
162
182
|
${chalk.yellow('Sync:')} sync push/pull/status/watch/list/remove/diff
|
|
163
183
|
${chalk.yellow('Files:')} file ls/tree/info/copy/move/rename/mkdir/delete/search/open/reveal/audit
|
|
164
184
|
${chalk.yellow('Remote:')} remote browse/files/download
|
|
@@ -279,7 +299,7 @@ if (!process.argv.slice(2).length) {
|
|
|
279
299
|
}
|
|
280
300
|
|
|
281
301
|
// keytar's native D-Bus handles block the event loop on headless Linux
|
|
282
|
-
const longRunning = new Set(['serve', 'nearby', 'stream']);
|
|
302
|
+
const longRunning = new Set(['serve', 'nearby', 'stream', 'download']);
|
|
283
303
|
if (!longRunning.has(currentCmd)) {
|
|
284
304
|
// Flush analytics and run shutdown hooks before exiting
|
|
285
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) {
|
|
@@ -8,10 +8,21 @@ _pal_completions() {
|
|
|
8
8
|
|
|
9
9
|
local commands="init whoami register recover share download list revoke serve pal invite nearby group sync transfers config device server explorer status log completion web gui-share vfs file remote user schedule chat backup api-keys share-link comment update auth network search connect protocol workspace favorite pin stream uninstall extension ext billing permissions org su federation analytics webhook relay compliance sso rbac scim audit scanner notify cloud-backup dept"
|
|
10
10
|
|
|
11
|
+
local global_flags="--help --version --json --log-level"
|
|
12
|
+
|
|
13
|
+
# Complete flags anywhere
|
|
14
|
+
if [[ "\$cur" == -* ]]; then
|
|
15
|
+
COMPREPLY=( $(compgen -W "\$global_flags" -- "\$cur") )
|
|
16
|
+
return
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Handle top-level vs subcommand completion
|
|
20
|
+
if [[ \$COMP_CWORD -eq 1 ]]; then
|
|
21
|
+
COMPREPLY=( $(compgen -W "\$commands" -- "\$cur") )
|
|
22
|
+
return
|
|
23
|
+
fi
|
|
24
|
+
|
|
11
25
|
case "\$prev" in
|
|
12
|
-
pal)
|
|
13
|
-
COMPREPLY=( $(compgen -W "\$commands" -- "\$cur") )
|
|
14
|
-
;;
|
|
15
26
|
pal)
|
|
16
27
|
COMPREPLY=( $(compgen -W "add list remove" -- "\$cur") )
|
|
17
28
|
;;
|
|
@@ -66,6 +77,39 @@ _pal_completions() {
|
|
|
66
77
|
explorer)
|
|
67
78
|
COMPREPLY=( $(compgen -W "install uninstall" -- "\$cur") )
|
|
68
79
|
;;
|
|
80
|
+
network)
|
|
81
|
+
COMPREPLY=( $(compgen -W "create list info invite join members groups connect disconnect" -- "\$cur") )
|
|
82
|
+
;;
|
|
83
|
+
stream)
|
|
84
|
+
COMPREPLY=( $(compgen -W "local remote stop status broadcast join transport" -- "\$cur") )
|
|
85
|
+
;;
|
|
86
|
+
extension|ext)
|
|
87
|
+
COMPREPLY=( $(compgen -W "list install remove enable disable info config create" -- "\$cur") )
|
|
88
|
+
;;
|
|
89
|
+
billing)
|
|
90
|
+
COMPREPLY=( $(compgen -W "status plans activate deactivate checkout" -- "\$cur") )
|
|
91
|
+
;;
|
|
92
|
+
org)
|
|
93
|
+
COMPREPLY=( $(compgen -W "create list info invite remove subscribe unsubscribe billing" -- "\$cur") )
|
|
94
|
+
;;
|
|
95
|
+
protocol)
|
|
96
|
+
COMPREPLY=( $(compgen -W "info policy route envelope keys" -- "\$cur") )
|
|
97
|
+
;;
|
|
98
|
+
workspace)
|
|
99
|
+
COMPREPLY=( $(compgen -W "list create delete add remove" -- "\$cur") )
|
|
100
|
+
;;
|
|
101
|
+
pin)
|
|
102
|
+
COMPREPLY=( $(compgen -W "set remove status" -- "\$cur") )
|
|
103
|
+
;;
|
|
104
|
+
auth)
|
|
105
|
+
COMPREPLY=( $(compgen -W "login logout status" -- "\$cur") )
|
|
106
|
+
;;
|
|
107
|
+
cloud-backup)
|
|
108
|
+
COMPREPLY=( $(compgen -W "create restore list status" -- "\$cur") )
|
|
109
|
+
;;
|
|
110
|
+
--log-level)
|
|
111
|
+
COMPREPLY=( $(compgen -W "debug info warn error silent" -- "\$cur") )
|
|
112
|
+
;;
|
|
69
113
|
*)
|
|
70
114
|
COMPREPLY=()
|
|
71
115
|
;;
|
|
@@ -148,6 +192,14 @@ _pe() {
|
|
|
148
192
|
'dept:Department management'
|
|
149
193
|
)
|
|
150
194
|
|
|
195
|
+
# Complete flags
|
|
196
|
+
if [[ "\$words[CURRENT]" == -* ]]; then
|
|
197
|
+
local -a flags
|
|
198
|
+
flags=('--help:Show help' '--version:Show version' '--json:Output as JSON' '--log-level:Set log level')
|
|
199
|
+
_describe 'flag' flags
|
|
200
|
+
return
|
|
201
|
+
fi
|
|
202
|
+
|
|
151
203
|
if (( CURRENT == 2 )); then
|
|
152
204
|
_describe 'command' commands
|
|
153
205
|
elif (( CURRENT == 3 )); then
|
|
@@ -171,6 +223,16 @@ _pe() {
|
|
|
171
223
|
api-keys) subcommands=('create:Create key' 'revoke:Revoke key'); _describe 'subcommand' subcommands ;;
|
|
172
224
|
share-link) subcommands=('create:Create link' 'list:List links'); _describe 'subcommand' subcommands ;;
|
|
173
225
|
update) subcommands=('check:Check for updates'); _describe 'subcommand' subcommands ;;
|
|
226
|
+
network) subcommands=('create:Create network' 'list:List networks' 'info:Network info' 'invite:Invite to network' 'join:Join network' 'members:List members' 'groups:List groups' 'connect:Connect' 'disconnect:Disconnect'); _describe 'subcommand' subcommands ;;
|
|
227
|
+
stream) subcommands=('local:Stream local file' 'remote:Stream remote file' 'stop:Stop stream' 'status:Stream status' 'broadcast:Broadcast stream' 'join:Join broadcast' 'transport:Transport settings'); _describe 'subcommand' subcommands ;;
|
|
228
|
+
extension|ext) subcommands=('list:List extensions' 'install:Install extension' 'remove:Remove extension' 'enable:Enable extension' 'disable:Disable extension' 'info:Extension info' 'config:Extension config' 'create:Create extension'); _describe 'subcommand' subcommands ;;
|
|
229
|
+
billing) subcommands=('status:Billing status' 'plans:View plans' 'activate:Activate plan' 'deactivate:Cancel plan' 'checkout:Open checkout'); _describe 'subcommand' subcommands ;;
|
|
230
|
+
org) subcommands=('create:Create org' 'list:List orgs' 'info:Org info' 'invite:Invite member' 'remove:Remove member' 'subscribe:Subscribe' 'unsubscribe:Unsubscribe' 'billing:Org billing'); _describe 'subcommand' subcommands ;;
|
|
231
|
+
protocol) subcommands=('info:Protocol info' 'policy:Policy rules' 'route:Routing table' 'envelope:Envelope tools' 'keys:Key management'); _describe 'subcommand' subcommands ;;
|
|
232
|
+
workspace) subcommands=('list:List workspaces' 'create:Create workspace' 'delete:Delete workspace' 'add:Add to workspace' 'remove:Remove from workspace'); _describe 'subcommand' subcommands ;;
|
|
233
|
+
pin) subcommands=('set:Pin a share' 'remove:Unpin a share' 'status:Pin status'); _describe 'subcommand' subcommands ;;
|
|
234
|
+
auth) subcommands=('login:Log in' 'logout:Log out' 'status:Auth status'); _describe 'subcommand' subcommands ;;
|
|
235
|
+
cloud-backup) subcommands=('create:Create backup' 'restore:Restore backup' 'list:List backups' 'status:Backup status'); _describe 'subcommand' subcommands ;;
|
|
174
236
|
esac
|
|
175
237
|
fi
|
|
176
238
|
}
|
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
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
function levenshtein(a, b) {
|
|
2
|
+
const m = a.length, n = b.length;
|
|
3
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => {
|
|
4
|
+
const row = new Array(n + 1);
|
|
5
|
+
row[0] = i;
|
|
6
|
+
return row;
|
|
7
|
+
});
|
|
8
|
+
for (let j = 1; j <= n; j++) dp[0][j] = j;
|
|
9
|
+
|
|
10
|
+
for (let i = 1; i <= m; i++) {
|
|
11
|
+
for (let j = 1; j <= n; j++) {
|
|
12
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
13
|
+
? dp[i - 1][j - 1]
|
|
14
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return dp[m][n];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function suggest(input, candidates, { maxDistance = 3, maxResults = 3 } = {}) {
|
|
21
|
+
if (!input) return [];
|
|
22
|
+
|
|
23
|
+
const scored = candidates
|
|
24
|
+
.map(cmd => ({ cmd, dist: levenshtein(input.toLowerCase(), cmd.toLowerCase()) }))
|
|
25
|
+
.filter(({ dist, cmd }) => {
|
|
26
|
+
if (dist > maxDistance) return false;
|
|
27
|
+
// Also allow prefix matches (e.g., "ser" → "serve", "server", "search")
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Also add prefix matches not already included
|
|
32
|
+
const prefixMatches = candidates
|
|
33
|
+
.filter(cmd => cmd.toLowerCase().startsWith(input.toLowerCase()))
|
|
34
|
+
.map(cmd => ({ cmd, dist: 0.5 })); // give prefix matches high priority
|
|
35
|
+
|
|
36
|
+
const merged = new Map();
|
|
37
|
+
for (const entry of [...prefixMatches, ...scored]) {
|
|
38
|
+
if (!merged.has(entry.cmd) || entry.dist < merged.get(entry.cmd)) {
|
|
39
|
+
merged.set(entry.cmd, entry.dist);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [...merged.entries()]
|
|
44
|
+
.sort((a, b) => a[1] - b[1])
|
|
45
|
+
.slice(0, maxResults)
|
|
46
|
+
.map(([cmd]) => cmd);
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pal-explorer-cli",
|
|
3
|
-
"version": "0.4.
|
|
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": {
|
|
7
|
-
"pal": "
|
|
7
|
+
"pal": "bin/pal.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|