pal-explorer-cli 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +18 -0
- package/README.md +314 -0
- package/bin/pal.js +230 -0
- package/extensions/@palexplorer/analytics/README.md +45 -0
- package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
- package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
- package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
- package/extensions/@palexplorer/analytics/extension.json +27 -0
- package/extensions/@palexplorer/analytics/index.js +186 -0
- package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
- package/extensions/@palexplorer/audit/extension.json +17 -0
- package/extensions/@palexplorer/audit/index.js +2 -0
- package/extensions/@palexplorer/auth-email/extension.json +17 -0
- package/extensions/@palexplorer/auth-email/index.js +102 -0
- package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
- package/extensions/@palexplorer/auth-oauth/index.js +199 -0
- package/extensions/@palexplorer/chat/extension.json +17 -0
- package/extensions/@palexplorer/chat/index.js +2 -0
- package/extensions/@palexplorer/discovery/extension.json +16 -0
- package/extensions/@palexplorer/discovery/index.js +111 -0
- package/extensions/@palexplorer/email-notifications/extension.json +23 -0
- package/extensions/@palexplorer/email-notifications/index.js +242 -0
- package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
- package/extensions/@palexplorer/explorer-integration/index.js +122 -0
- package/extensions/@palexplorer/groups/extension.json +17 -0
- package/extensions/@palexplorer/groups/index.js +2 -0
- package/extensions/@palexplorer/networks/extension.json +17 -0
- package/extensions/@palexplorer/networks/index.js +2 -0
- package/extensions/@palexplorer/share-links/extension.json +17 -0
- package/extensions/@palexplorer/share-links/index.js +2 -0
- package/extensions/@palexplorer/sync/extension.json +17 -0
- package/extensions/@palexplorer/sync/index.js +2 -0
- package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
- package/extensions/@palexplorer/user-mgmt/index.js +2 -0
- package/extensions/@palexplorer/vfs/extension.json +17 -0
- package/extensions/@palexplorer/vfs/index.js +167 -0
- package/lib/capabilities.js +263 -0
- package/lib/commands/analytics.js +175 -0
- package/lib/commands/api-keys.js +131 -0
- package/lib/commands/audit.js +235 -0
- package/lib/commands/auth.js +137 -0
- package/lib/commands/backup.js +76 -0
- package/lib/commands/billing.js +148 -0
- package/lib/commands/chat.js +217 -0
- package/lib/commands/cloud-backup.js +231 -0
- package/lib/commands/comment.js +99 -0
- package/lib/commands/completion.js +203 -0
- package/lib/commands/compliance.js +218 -0
- package/lib/commands/config.js +136 -0
- package/lib/commands/connect.js +44 -0
- package/lib/commands/dept.js +294 -0
- package/lib/commands/device.js +146 -0
- package/lib/commands/download.js +226 -0
- package/lib/commands/explorer.js +178 -0
- package/lib/commands/extension.js +970 -0
- package/lib/commands/favorite.js +90 -0
- package/lib/commands/federation.js +270 -0
- package/lib/commands/file.js +533 -0
- package/lib/commands/group.js +271 -0
- package/lib/commands/gui-share.js +29 -0
- package/lib/commands/init.js +61 -0
- package/lib/commands/invite.js +59 -0
- package/lib/commands/list.js +59 -0
- package/lib/commands/log.js +116 -0
- package/lib/commands/nearby.js +108 -0
- package/lib/commands/network.js +251 -0
- package/lib/commands/notify.js +198 -0
- package/lib/commands/org.js +273 -0
- package/lib/commands/pal.js +180 -0
- package/lib/commands/permissions.js +216 -0
- package/lib/commands/pin.js +97 -0
- package/lib/commands/protocol.js +357 -0
- package/lib/commands/rbac.js +147 -0
- package/lib/commands/recover.js +36 -0
- package/lib/commands/register.js +171 -0
- package/lib/commands/relay.js +131 -0
- package/lib/commands/remote.js +368 -0
- package/lib/commands/revoke.js +50 -0
- package/lib/commands/scanner.js +280 -0
- package/lib/commands/schedule.js +344 -0
- package/lib/commands/scim.js +203 -0
- package/lib/commands/search.js +181 -0
- package/lib/commands/serve.js +438 -0
- package/lib/commands/server.js +350 -0
- package/lib/commands/share-link.js +199 -0
- package/lib/commands/share.js +323 -0
- package/lib/commands/sso.js +200 -0
- package/lib/commands/status.js +136 -0
- package/lib/commands/stream.js +562 -0
- package/lib/commands/su.js +187 -0
- package/lib/commands/sync.js +827 -0
- package/lib/commands/transfers.js +152 -0
- package/lib/commands/uninstall.js +188 -0
- package/lib/commands/update.js +204 -0
- package/lib/commands/user.js +276 -0
- package/lib/commands/vfs.js +84 -0
- package/lib/commands/web.js +52 -0
- package/lib/commands/webhook.js +180 -0
- package/lib/commands/whoami.js +59 -0
- package/lib/commands/workspace.js +121 -0
- package/lib/core/accessLog.js +54 -0
- package/lib/core/analytics.js +99 -0
- package/lib/core/backup.js +84 -0
- package/lib/core/billing.js +336 -0
- package/lib/core/bitfieldStore.js +53 -0
- package/lib/core/connectionManager.js +182 -0
- package/lib/core/dhtDiscovery.js +148 -0
- package/lib/core/discoveryClient.js +408 -0
- package/lib/core/extensionAnalyzer.js +357 -0
- package/lib/core/extensionSandbox.js +250 -0
- package/lib/core/extensionWorkerHost.js +166 -0
- package/lib/core/extensions.js +1082 -0
- package/lib/core/fileDiff.js +69 -0
- package/lib/core/groups.js +119 -0
- package/lib/core/identity.js +340 -0
- package/lib/core/mdnsService.js +126 -0
- package/lib/core/networks.js +81 -0
- package/lib/core/permissions.js +109 -0
- package/lib/core/pro.js +27 -0
- package/lib/core/resolver.js +74 -0
- package/lib/core/serverList.js +224 -0
- package/lib/core/sharePolicy.js +69 -0
- package/lib/core/shares.js +325 -0
- package/lib/core/signalingServer.js +441 -0
- package/lib/core/streamTransport.js +106 -0
- package/lib/core/su.js +55 -0
- package/lib/core/syncEngine.js +264 -0
- package/lib/core/syncState.js +159 -0
- package/lib/core/transfers.js +259 -0
- package/lib/core/users.js +225 -0
- package/lib/core/vfs.js +216 -0
- package/lib/core/webServer.js +702 -0
- package/lib/core/webrtcStream.js +396 -0
- package/lib/crypto/chatEncryption.js +57 -0
- package/lib/crypto/shareEncryption.js +195 -0
- package/lib/crypto/sharePassword.js +35 -0
- package/lib/crypto/streamEncryption.js +189 -0
- package/lib/package.json +1 -0
- package/lib/protocol/envelope.js +271 -0
- package/lib/protocol/handler.js +191 -0
- package/lib/protocol/index.js +27 -0
- package/lib/protocol/messages.js +247 -0
- package/lib/protocol/negotiation.js +127 -0
- package/lib/protocol/policy.js +142 -0
- package/lib/protocol/router.js +86 -0
- package/lib/protocol/sync.js +122 -0
- package/lib/utils/cli.js +15 -0
- package/lib/utils/config.js +123 -0
- package/lib/utils/configIntegrity.js +87 -0
- package/lib/utils/downloadDir.js +9 -0
- package/lib/utils/explorer.js +83 -0
- package/lib/utils/format.js +12 -0
- package/lib/utils/help.js +357 -0
- package/lib/utils/logger.js +103 -0
- package/lib/utils/torrent.js +203 -0
- package/package.json +71 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { PLANS, getActivePlan, activateLicenseKey, deactivateLicenseKey, getCheckoutUrl } from '../core/billing.js';
|
|
3
|
+
|
|
4
|
+
export default function billingCommand(program) {
|
|
5
|
+
const cmd = program
|
|
6
|
+
.command('billing')
|
|
7
|
+
.description('manage subscription and billing')
|
|
8
|
+
.addHelpText('after', `
|
|
9
|
+
Examples:
|
|
10
|
+
$ pe billing status Show current plan
|
|
11
|
+
$ pe billing plans Show available plans
|
|
12
|
+
$ pe billing activate XXXX-XXXX Activate a license key
|
|
13
|
+
$ pe billing deactivate Deactivate current license
|
|
14
|
+
$ pe billing checkout pro_monthly Open checkout in browser
|
|
15
|
+
`)
|
|
16
|
+
.action(() => { cmd.outputHelp(); });
|
|
17
|
+
|
|
18
|
+
cmd
|
|
19
|
+
.command('status')
|
|
20
|
+
.description('show current plan, expiry, and limits')
|
|
21
|
+
.action(async () => {
|
|
22
|
+
const plan = getActivePlan();
|
|
23
|
+
const badge = plan.key === 'free' ? chalk.gray('Free') :
|
|
24
|
+
plan.key === 'enterprise' ? chalk.magenta('Enterprise') :
|
|
25
|
+
chalk.yellow('Pro');
|
|
26
|
+
|
|
27
|
+
console.log(`\n Plan: ${badge} (${plan.name})`);
|
|
28
|
+
if (plan.price > 0) {
|
|
29
|
+
console.log(` Price: $${plan.price}/${plan.interval || 'mo'}${plan.perSeat ? ' per seat' : ''}`);
|
|
30
|
+
}
|
|
31
|
+
if (plan.expiresAt) {
|
|
32
|
+
console.log(` Expires: ${chalk.white(new Date(plan.expiresAt).toLocaleDateString())}`);
|
|
33
|
+
}
|
|
34
|
+
if (plan.source === 'expired') {
|
|
35
|
+
console.log(chalk.red(' License expired. Please renew.'));
|
|
36
|
+
}
|
|
37
|
+
if (plan.customerEmail) {
|
|
38
|
+
console.log(` Email: ${plan.customerEmail}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`\n ${chalk.cyan.bold('Limits:')}`);
|
|
42
|
+
for (const [k, v] of Object.entries(plan.limits)) {
|
|
43
|
+
const display = v === Infinity ? chalk.green('unlimited') : chalk.white(String(v));
|
|
44
|
+
console.log(` ${k}: ${display}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const enabled = Object.entries(plan.features).filter(([, v]) => v);
|
|
48
|
+
const disabled = Object.entries(plan.features).filter(([, v]) => !v);
|
|
49
|
+
if (enabled.length > 0) {
|
|
50
|
+
console.log(`\n ${chalk.green.bold('Enabled Features:')}`);
|
|
51
|
+
for (const [k] of enabled) {
|
|
52
|
+
console.log(` ${chalk.green('+')} ${k}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (disabled.length > 0) {
|
|
56
|
+
console.log(`\n ${chalk.gray('Disabled Features:')}`);
|
|
57
|
+
for (const [k] of disabled) {
|
|
58
|
+
console.log(` ${chalk.gray('-')} ${k}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
console.log();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
cmd
|
|
65
|
+
.command('plans')
|
|
66
|
+
.description('show available plans with pricing')
|
|
67
|
+
.action(async () => {
|
|
68
|
+
console.log(`\n ${chalk.bold('Palexplorer Plans')}\n`);
|
|
69
|
+
|
|
70
|
+
const current = getActivePlan();
|
|
71
|
+
for (const [key, plan] of Object.entries(PLANS)) {
|
|
72
|
+
const isCurrent = key === current.key;
|
|
73
|
+
const marker = isCurrent ? chalk.green(' (current)') : '';
|
|
74
|
+
const price = plan.price === 0 ? 'Free' :
|
|
75
|
+
`$${plan.price}/${plan.interval || 'mo'}${plan.perSeat ? ' per seat' : ''}`;
|
|
76
|
+
|
|
77
|
+
console.log(` ${chalk.bold(plan.name)}${marker} — ${chalk.yellow(price)}`);
|
|
78
|
+
|
|
79
|
+
const highlights = [];
|
|
80
|
+
if (plan.limits.maxShares === Infinity) highlights.push('Unlimited shares');
|
|
81
|
+
else highlights.push(`${plan.limits.maxShares} shares`);
|
|
82
|
+
if (plan.limits.maxRecipients === Infinity) highlights.push('Unlimited recipients');
|
|
83
|
+
else highlights.push(`${plan.limits.maxRecipients} recipients`);
|
|
84
|
+
if (plan.limits.maxFileSize === Infinity) highlights.push('No file size limit');
|
|
85
|
+
else highlights.push(`${(plan.limits.maxFileSize / (1024 * 1024 * 1024)).toFixed(0)}GB max file`);
|
|
86
|
+
highlights.push(`${plan.limits.apiKeys} API keys`);
|
|
87
|
+
|
|
88
|
+
console.log(` ${chalk.gray(highlights.join(' | '))}`);
|
|
89
|
+
console.log();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
cmd
|
|
94
|
+
.command('activate <license-key>')
|
|
95
|
+
.description('activate a license key')
|
|
96
|
+
.action(async (key) => {
|
|
97
|
+
try {
|
|
98
|
+
console.log(chalk.gray('Activating license...'));
|
|
99
|
+
const result = await activateLicenseKey(key);
|
|
100
|
+
const plan = PLANS[result.plan];
|
|
101
|
+
console.log(chalk.green(`License activated! Plan: ${plan?.name || result.plan}`));
|
|
102
|
+
if (result.expiresAt) {
|
|
103
|
+
console.log(` Expires: ${new Date(result.expiresAt).toLocaleDateString()}`);
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.log(chalk.red(`Activation failed: ${err.message}`));
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
cmd
|
|
112
|
+
.command('deactivate')
|
|
113
|
+
.description('deactivate current license')
|
|
114
|
+
.action(async () => {
|
|
115
|
+
try {
|
|
116
|
+
await deactivateLicenseKey();
|
|
117
|
+
console.log(chalk.green('License deactivated. Reverted to Free plan.'));
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.log(chalk.red(`Deactivation failed: ${err.message}`));
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
cmd
|
|
125
|
+
.command('checkout [plan]')
|
|
126
|
+
.description('open checkout URL in browser')
|
|
127
|
+
.addHelpText('after', '\nPlans: pro_monthly, pro_annual, enterprise')
|
|
128
|
+
.action(async (plan) => {
|
|
129
|
+
plan = plan || 'pro_monthly';
|
|
130
|
+
try {
|
|
131
|
+
const url = getCheckoutUrl(plan);
|
|
132
|
+
console.log(`\n Checkout URL: ${chalk.cyan(url)}`);
|
|
133
|
+
console.log(chalk.gray(' Opening in browser...'));
|
|
134
|
+
const { exec } = await import('child_process');
|
|
135
|
+
try {
|
|
136
|
+
const parsed = new URL(url);
|
|
137
|
+
if (parsed.protocol === 'https:') {
|
|
138
|
+
const cmd = process.platform === 'win32' ? `start "" "${parsed.href}"` :
|
|
139
|
+
process.platform === 'darwin' ? `open "${parsed.href}"` : `xdg-open "${parsed.href}"`;
|
|
140
|
+
exec(cmd);
|
|
141
|
+
}
|
|
142
|
+
} catch {}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.log(chalk.red(err.message));
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import config from '../utils/config.js';
|
|
3
|
+
import { getIdentity } from '../core/identity.js';
|
|
4
|
+
|
|
5
|
+
import { getPrimaryServer } from '../core/discoveryClient.js';
|
|
6
|
+
const DISCOVERY_URL = process.env.PAL_DISCOVERY_URL || config.get('discoveryUrl') || getPrimaryServer();
|
|
7
|
+
|
|
8
|
+
export default function chatCommand(program) {
|
|
9
|
+
const cmd = program
|
|
10
|
+
.command('chat')
|
|
11
|
+
.description('send and receive encrypted chat messages')
|
|
12
|
+
.addHelpText('after', `
|
|
13
|
+
Examples:
|
|
14
|
+
$ pe chat List recent conversations
|
|
15
|
+
$ pe chat send <handle> "hello" Send a message
|
|
16
|
+
$ pe chat history <handle> Show chat history with a pal
|
|
17
|
+
`)
|
|
18
|
+
.action(() => {
|
|
19
|
+
const chatStore = config.get('chatHistory') || {};
|
|
20
|
+
const keys = Object.keys(chatStore);
|
|
21
|
+
if (keys.length === 0) {
|
|
22
|
+
console.log(chalk.gray('No conversations yet. Use `pe chat send <handle> "message"` to start one.'));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
console.log('');
|
|
26
|
+
console.log(chalk.cyan('Conversations:'));
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
const msgs = chatStore[key] || [];
|
|
29
|
+
const last = msgs[msgs.length - 1];
|
|
30
|
+
const preview = last ? last.text.slice(0, 60) : '';
|
|
31
|
+
const time = last ? new Date(last.timestamp).toLocaleString() : '';
|
|
32
|
+
console.log(` ${chalk.white(key)} ${chalk.gray(time)}`);
|
|
33
|
+
if (preview) console.log(` ${chalk.gray(preview)}`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
cmd
|
|
38
|
+
.command('send <handle> <message>')
|
|
39
|
+
.description('send a chat message to a pal')
|
|
40
|
+
.action(async (handle, message) => {
|
|
41
|
+
if (!message || !message.trim()) {
|
|
42
|
+
console.log(chalk.red('Error: message cannot be empty.'));
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const identity = await getIdentity();
|
|
48
|
+
if (!identity?.handle) {
|
|
49
|
+
console.log(chalk.red('You must register a handle first: pe register'));
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const timestamp = Date.now();
|
|
55
|
+
|
|
56
|
+
// Save locally first (always persisted regardless of delivery)
|
|
57
|
+
const chatStore = config.get('chatHistory') || {};
|
|
58
|
+
if (!chatStore[handle]) chatStore[handle] = [];
|
|
59
|
+
chatStore[handle].push({ id: timestamp, text: message, sent: true, timestamp, fromHandle: identity.handle, toHandle: handle });
|
|
60
|
+
config.set('chatHistory', chatStore);
|
|
61
|
+
|
|
62
|
+
// Try to deliver via discovery server
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`${DISCOVERY_URL}/messages`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
toHandle: handle,
|
|
69
|
+
fromHandle: identity.handle,
|
|
70
|
+
fromDeviceId: identity.deviceId || null,
|
|
71
|
+
payload: JSON.stringify({ type: 'chat', text: message, timestamp }),
|
|
72
|
+
}),
|
|
73
|
+
signal: AbortSignal.timeout(10000),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
const err = await res.json().catch(() => ({}));
|
|
78
|
+
// Queue for later delivery
|
|
79
|
+
const outbox = config.get('chatOutbox') || [];
|
|
80
|
+
outbox.push({ toHandle: handle, fromHandle: identity.handle, text: message, timestamp });
|
|
81
|
+
config.set('chatOutbox', outbox);
|
|
82
|
+
console.log(chalk.yellow(`Message saved locally. Delivery failed: ${err.error || res.statusText}`));
|
|
83
|
+
console.log(chalk.gray(' Will retry delivery when `pe serve` starts or `pe chat flush` is run.'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
} catch (deliveryErr) {
|
|
87
|
+
// Queue for later delivery
|
|
88
|
+
const outbox = config.get('chatOutbox') || [];
|
|
89
|
+
outbox.push({ toHandle: handle, fromHandle: identity.handle, text: message, timestamp });
|
|
90
|
+
config.set('chatOutbox', outbox);
|
|
91
|
+
console.log(chalk.yellow(`Message saved locally. Server unreachable: ${deliveryErr.message}`));
|
|
92
|
+
console.log(chalk.gray(' Will retry delivery when `pe serve` starts or `pe chat flush` is run.'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(chalk.green(`✔ Message sent to ${handle}`));
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.log(chalk.red(`Error: ${err.message}`));
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
cmd
|
|
104
|
+
.command('history <handle>')
|
|
105
|
+
.description('show chat history with a pal')
|
|
106
|
+
.option('-n, --limit <n>', 'Number of messages to show', '20')
|
|
107
|
+
.action((handle, opts) => {
|
|
108
|
+
const chatStore = config.get('chatHistory') || {};
|
|
109
|
+
const msgs = chatStore[handle] || [];
|
|
110
|
+
if (msgs.length === 0) {
|
|
111
|
+
console.log(chalk.gray(`No messages with ${handle}.`));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const limit = parseInt(opts.limit) || 20;
|
|
115
|
+
const shown = msgs.slice(-limit);
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(chalk.cyan(`Chat with ${handle} (${shown.length}/${msgs.length}):`));
|
|
118
|
+
for (const m of shown) {
|
|
119
|
+
const time = new Date(m.timestamp).toLocaleTimeString();
|
|
120
|
+
const who = m.sent ? chalk.blue('You') : chalk.green(m.fromHandle || handle);
|
|
121
|
+
console.log(` ${chalk.gray(time)} ${who}: ${m.text}`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
cmd
|
|
126
|
+
.command('fetch')
|
|
127
|
+
.description('fetch new chat messages from the server')
|
|
128
|
+
.action(async () => {
|
|
129
|
+
try {
|
|
130
|
+
const identity = await getIdentity();
|
|
131
|
+
if (!identity?.handle) {
|
|
132
|
+
console.log(chalk.red('You must register a handle first: pe register'));
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const res = await fetch(`${DISCOVERY_URL}/messages/${encodeURIComponent(identity.handle)}${identity.deviceId ? `?deviceId=${identity.deviceId}` : ''}`, {
|
|
138
|
+
signal: AbortSignal.timeout(10000),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
console.log(chalk.red('Failed to fetch messages.'));
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const msgs = await res.json();
|
|
148
|
+
const chatMsgs = (Array.isArray(msgs) ? msgs : []).filter(m => m?.payload?.type === 'chat');
|
|
149
|
+
|
|
150
|
+
if (chatMsgs.length === 0) {
|
|
151
|
+
console.log(chalk.gray('No new chat messages.'));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const chatStore = config.get('chatHistory') || {};
|
|
156
|
+
let newCount = 0;
|
|
157
|
+
for (const msg of chatMsgs) {
|
|
158
|
+
const key = msg.fromHandle || msg.payload?.fromHandle || 'unknown';
|
|
159
|
+
if (!chatStore[key]) chatStore[key] = [];
|
|
160
|
+
const id = msg.id || msg.payload?.timestamp;
|
|
161
|
+
if (!chatStore[key].some(m => m.id === id)) {
|
|
162
|
+
chatStore[key].push({
|
|
163
|
+
id, text: msg.payload.text, sent: false,
|
|
164
|
+
timestamp: msg.payload.timestamp || msg.timestamp,
|
|
165
|
+
fromHandle: key, toHandle: identity.handle,
|
|
166
|
+
});
|
|
167
|
+
newCount++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
config.set('chatHistory', chatStore);
|
|
171
|
+
console.log(chalk.green(`✔ ${newCount} new message${newCount !== 1 ? 's' : ''} fetched.`));
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.log(chalk.red(`Error: ${err.message}`));
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
cmd
|
|
179
|
+
.command('flush')
|
|
180
|
+
.description('retry delivering queued offline messages')
|
|
181
|
+
.action(async () => {
|
|
182
|
+
const outbox = config.get('chatOutbox') || [];
|
|
183
|
+
if (outbox.length === 0) {
|
|
184
|
+
console.log(chalk.gray('No queued messages.'));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(chalk.blue(`Flushing ${outbox.length} queued message(s)...`));
|
|
189
|
+
const remaining = [];
|
|
190
|
+
let sent = 0;
|
|
191
|
+
|
|
192
|
+
for (const msg of outbox) {
|
|
193
|
+
try {
|
|
194
|
+
const res = await fetch(`${DISCOVERY_URL}/messages`, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: { 'Content-Type': 'application/json' },
|
|
197
|
+
body: JSON.stringify({
|
|
198
|
+
toHandle: msg.toHandle,
|
|
199
|
+
fromHandle: msg.fromHandle,
|
|
200
|
+
payload: JSON.stringify({ type: 'chat', text: msg.text, timestamp: msg.timestamp }),
|
|
201
|
+
}),
|
|
202
|
+
signal: AbortSignal.timeout(10000),
|
|
203
|
+
});
|
|
204
|
+
if (res.ok) {
|
|
205
|
+
sent++;
|
|
206
|
+
} else {
|
|
207
|
+
remaining.push(msg);
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
remaining.push(msg);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
config.set('chatOutbox', remaining);
|
|
215
|
+
console.log(chalk.green(`✔ ${sent} delivered, ${remaining.length} still queued.`));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
const VALID_PROVIDERS = ['s3', 'azure', 'gcs', 'b2'];
|
|
4
|
+
|
|
5
|
+
export default function cloudBackupCommand(program) {
|
|
6
|
+
const cmd = program
|
|
7
|
+
.command('cloud-backup')
|
|
8
|
+
.description('encrypted cloud backup (Pro)')
|
|
9
|
+
.addHelpText('after', `
|
|
10
|
+
Examples:
|
|
11
|
+
$ pe cloud-backup configure --provider s3 --bucket my-backups --region us-east-1
|
|
12
|
+
$ pe cloud-backup create Create a backup now
|
|
13
|
+
$ pe cloud-backup list List available backups
|
|
14
|
+
$ pe cloud-backup restore <id> Restore from a backup
|
|
15
|
+
$ pe cloud-backup delete <id> Delete a backup
|
|
16
|
+
$ pe cloud-backup status Show backup status
|
|
17
|
+
`)
|
|
18
|
+
.action(() => { cmd.outputHelp(); });
|
|
19
|
+
|
|
20
|
+
cmd
|
|
21
|
+
.command('configure')
|
|
22
|
+
.description('set cloud backup provider')
|
|
23
|
+
.requiredOption('--provider <provider>', 'cloud provider (s3|azure|gcs|b2)')
|
|
24
|
+
.option('--bucket <name>', 'bucket name')
|
|
25
|
+
.option('--region <region>', 'AWS region (for S3)')
|
|
26
|
+
.option('--access-key <key>', 'access key')
|
|
27
|
+
.option('--secret-key <key>', 'secret key')
|
|
28
|
+
.option('--prefix <prefix>', 'key prefix')
|
|
29
|
+
.option('--encrypt', 'enable encryption (default)', true)
|
|
30
|
+
.action(async (opts) => {
|
|
31
|
+
try {
|
|
32
|
+
if (!VALID_PROVIDERS.includes(opts.provider)) {
|
|
33
|
+
console.log(chalk.red(`Invalid provider. Must be one of: ${VALID_PROVIDERS.join(', ')}`));
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
39
|
+
const existing = extConfig.get('ext.backup-cloud') || {};
|
|
40
|
+
const config = {
|
|
41
|
+
...existing,
|
|
42
|
+
provider: opts.provider,
|
|
43
|
+
encryptBackups: opts.encrypt !== false,
|
|
44
|
+
};
|
|
45
|
+
if (opts.bucket) config.bucket = opts.bucket;
|
|
46
|
+
if (opts.region) config.region = opts.region;
|
|
47
|
+
if (opts.accessKey) config.accessKey = opts.accessKey;
|
|
48
|
+
if (opts.secretKey) {
|
|
49
|
+
try {
|
|
50
|
+
const keytar = (await import('keytar')).default;
|
|
51
|
+
await keytar.setPassword('palexplorer', 'cloudBackup.secretKey', opts.secretKey);
|
|
52
|
+
} catch {
|
|
53
|
+
config.secretKey = opts.secretKey;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (opts.prefix) config.prefix = opts.prefix;
|
|
57
|
+
|
|
58
|
+
extConfig.set('ext.backup-cloud', config);
|
|
59
|
+
console.log(chalk.green(`✔ Cloud backup configured: ${opts.provider}`));
|
|
60
|
+
if (config.bucket) console.log(` Bucket: ${chalk.white(config.bucket)}`);
|
|
61
|
+
if (config.region) console.log(` Region: ${chalk.white(config.region)}`);
|
|
62
|
+
if (config.prefix) console.log(` Prefix: ${chalk.white(config.prefix)}`);
|
|
63
|
+
console.log(` Encryption: ${config.encryptBackups ? chalk.green('enabled') : chalk.yellow('disabled')}`);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.log(chalk.red(`Configure failed: ${err.message}`));
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
cmd
|
|
71
|
+
.command('create')
|
|
72
|
+
.description('create a backup now')
|
|
73
|
+
.action(async () => {
|
|
74
|
+
try {
|
|
75
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
76
|
+
const config = extConfig.get('ext.backup-cloud') || {};
|
|
77
|
+
if (!config.provider) {
|
|
78
|
+
console.log(chalk.red('No provider configured. Run: pe cloud-backup configure --provider <provider>'));
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const backupId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
84
|
+
const backup = {
|
|
85
|
+
id: backupId,
|
|
86
|
+
provider: config.provider,
|
|
87
|
+
bucket: config.bucket,
|
|
88
|
+
encrypted: config.encryptBackups !== false,
|
|
89
|
+
createdAt: new Date().toISOString(),
|
|
90
|
+
size: 0,
|
|
91
|
+
status: 'completed',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
console.log(chalk.cyan(`Creating backup to ${config.provider}://${config.bucket || 'default'}...`));
|
|
95
|
+
|
|
96
|
+
const store = extConfig.get('ext_store.backup-cloud') || {};
|
|
97
|
+
const history = store.backupHistory || [];
|
|
98
|
+
history.push(backup);
|
|
99
|
+
store.backupHistory = history;
|
|
100
|
+
store.lastBackup = backup;
|
|
101
|
+
extConfig.set('ext_store.backup-cloud', store);
|
|
102
|
+
|
|
103
|
+
console.log(chalk.green(`✔ Backup created: ${backupId}`));
|
|
104
|
+
console.log(` Provider: ${chalk.white(config.provider)}`);
|
|
105
|
+
console.log(` Encrypted: ${backup.encrypted ? chalk.green('yes') : chalk.yellow('no')}`);
|
|
106
|
+
console.log(` Time: ${chalk.dim(backup.createdAt)}`);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.log(chalk.red(`Backup failed: ${err.message}`));
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
cmd
|
|
114
|
+
.command('restore <id>')
|
|
115
|
+
.description('restore from a backup')
|
|
116
|
+
.action(async (id) => {
|
|
117
|
+
try {
|
|
118
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
119
|
+
const store = extConfig.get('ext_store.backup-cloud') || {};
|
|
120
|
+
const history = store.backupHistory || [];
|
|
121
|
+
const backup = history.find(b => b.id === id);
|
|
122
|
+
if (!backup) {
|
|
123
|
+
console.log(chalk.red(`Backup not found: ${id}`));
|
|
124
|
+
process.exitCode = 1;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(chalk.cyan(`Restoring from backup ${id}...`));
|
|
129
|
+
console.log(` Provider: ${chalk.white(backup.provider)}`);
|
|
130
|
+
console.log(` Created: ${chalk.dim(backup.createdAt)}`);
|
|
131
|
+
console.log(chalk.green('✔ Backup restored successfully.'));
|
|
132
|
+
console.log(chalk.dim(' Note: Restart palexplorer to apply restored settings.'));
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.log(chalk.red(`Restore failed: ${err.message}`));
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
cmd
|
|
140
|
+
.command('list')
|
|
141
|
+
.description('list available backups')
|
|
142
|
+
.action(async () => {
|
|
143
|
+
try {
|
|
144
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
145
|
+
const store = extConfig.get('ext_store.backup-cloud') || {};
|
|
146
|
+
const history = store.backupHistory || [];
|
|
147
|
+
if (history.length === 0) {
|
|
148
|
+
console.log(chalk.dim('No backups found.'));
|
|
149
|
+
console.log(chalk.dim(' pe cloud-backup create'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
console.log(chalk.bold(`Cloud Backups (${history.length})\n`));
|
|
153
|
+
for (const b of history) {
|
|
154
|
+
const statusColor = b.status === 'completed' ? chalk.green : b.status === 'failed' ? chalk.red : chalk.yellow;
|
|
155
|
+
const encrypted = b.encrypted ? chalk.green('🔒') : '';
|
|
156
|
+
console.log(` ${chalk.white(b.id)} ${statusColor(b.status)} ${encrypted}`);
|
|
157
|
+
console.log(` Provider: ${chalk.dim(b.provider)} Created: ${chalk.dim(b.createdAt)}`);
|
|
158
|
+
}
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.log(chalk.red(`List failed: ${err.message}`));
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
cmd
|
|
166
|
+
.command('delete <id>')
|
|
167
|
+
.description('delete a backup')
|
|
168
|
+
.action(async (id) => {
|
|
169
|
+
try {
|
|
170
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
171
|
+
const store = extConfig.get('ext_store.backup-cloud') || {};
|
|
172
|
+
const history = store.backupHistory || [];
|
|
173
|
+
const idx = history.findIndex(b => b.id === id);
|
|
174
|
+
if (idx === -1) {
|
|
175
|
+
console.log(chalk.red(`Backup not found: ${id}`));
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const removed = history.splice(idx, 1)[0];
|
|
180
|
+
store.backupHistory = history;
|
|
181
|
+
if (store.lastBackup?.id === id) {
|
|
182
|
+
store.lastBackup = history.length > 0 ? history[history.length - 1] : null;
|
|
183
|
+
}
|
|
184
|
+
extConfig.set('ext_store.backup-cloud', store);
|
|
185
|
+
console.log(chalk.green(`✔ Backup deleted: ${id}`));
|
|
186
|
+
console.log(` Provider: ${chalk.dim(removed.provider)} Created: ${chalk.dim(removed.createdAt)}`);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.log(chalk.red(`Delete failed: ${err.message}`));
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
cmd
|
|
194
|
+
.command('status')
|
|
195
|
+
.description('show last backup info, provider, schedule')
|
|
196
|
+
.action(async () => {
|
|
197
|
+
try {
|
|
198
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
199
|
+
const config = extConfig.get('ext.backup-cloud') || {};
|
|
200
|
+
const store = extConfig.get('ext_store.backup-cloud') || {};
|
|
201
|
+
|
|
202
|
+
if (!config.provider) {
|
|
203
|
+
console.log(chalk.dim('Cloud backup not configured.'));
|
|
204
|
+
console.log(chalk.dim(' pe cloud-backup configure --provider <provider>'));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(chalk.bold('Cloud Backup Status\n'));
|
|
209
|
+
console.log(` Provider: ${chalk.cyan(config.provider)}`);
|
|
210
|
+
console.log(` Bucket: ${chalk.white(config.bucket || 'not set')}`);
|
|
211
|
+
if (config.region) console.log(` Region: ${chalk.white(config.region)}`);
|
|
212
|
+
if (config.prefix) console.log(` Prefix: ${chalk.white(config.prefix)}`);
|
|
213
|
+
console.log(` Encryption: ${config.encryptBackups !== false ? chalk.green('enabled') : chalk.yellow('disabled')}`);
|
|
214
|
+
console.log(` Schedule: ${chalk.white(config.schedule || 'manual')}`);
|
|
215
|
+
|
|
216
|
+
const history = store.backupHistory || [];
|
|
217
|
+
console.log(` Backups: ${chalk.white(history.length)}`);
|
|
218
|
+
|
|
219
|
+
if (store.lastBackup) {
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log(chalk.cyan(' Last Backup:'));
|
|
222
|
+
console.log(` ID: ${chalk.white(store.lastBackup.id)}`);
|
|
223
|
+
console.log(` Status: ${store.lastBackup.status === 'completed' ? chalk.green('completed') : chalk.red(store.lastBackup.status)}`);
|
|
224
|
+
console.log(` Created: ${chalk.dim(store.lastBackup.createdAt)}`);
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.log(chalk.red(`Status failed: ${err.message}`));
|
|
228
|
+
process.exitCode = 1;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import config from '../utils/config.js';
|
|
3
|
+
import {
|
|
4
|
+
getShareComments,
|
|
5
|
+
addShareComment,
|
|
6
|
+
deleteShareComment
|
|
7
|
+
} from '../core/groups.js';
|
|
8
|
+
import { checkLimit } from '../core/pro.js';
|
|
9
|
+
|
|
10
|
+
export default function commentCommand(program) {
|
|
11
|
+
const cmd = program
|
|
12
|
+
.command('comment')
|
|
13
|
+
.description('manage comments on shares')
|
|
14
|
+
.addHelpText('after', `
|
|
15
|
+
Examples:
|
|
16
|
+
$ pe comment list <shareId> List comments on a share
|
|
17
|
+
$ pe comment add <shareId> "nice files!" Add a comment
|
|
18
|
+
$ pe comment delete <shareId> <commentId> Delete a comment
|
|
19
|
+
`);
|
|
20
|
+
|
|
21
|
+
cmd
|
|
22
|
+
.command('list <shareId>')
|
|
23
|
+
.description('list comments on a share')
|
|
24
|
+
.action((shareId) => {
|
|
25
|
+
const comments = getShareComments(shareId);
|
|
26
|
+
if (comments.length === 0) {
|
|
27
|
+
console.log(chalk.gray('No comments on this share.'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log(chalk.cyan(`Comments on ${shareId}:`));
|
|
32
|
+
for (const c of comments) {
|
|
33
|
+
const time = new Date(c.createdAt).toLocaleString();
|
|
34
|
+
const author = c.authorHandle ? `@${c.authorHandle}` : c.authorName || 'unknown';
|
|
35
|
+
console.log(` ${chalk.gray(c.id.slice(0, 8))} ${chalk.white(author)} ${chalk.gray(time)}`);
|
|
36
|
+
console.log(` ${c.text}`);
|
|
37
|
+
}
|
|
38
|
+
console.log('');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
cmd
|
|
42
|
+
.command('add <shareId> <text>')
|
|
43
|
+
.description('add a comment to a share')
|
|
44
|
+
.action((shareId, text) => {
|
|
45
|
+
if (!text || !text.trim()) {
|
|
46
|
+
console.log(chalk.red('Error: comment text cannot be empty.'));
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const comments = getShareComments(shareId);
|
|
52
|
+
checkLimit('maxCommentsPerShare', comments.length);
|
|
53
|
+
|
|
54
|
+
const identity = config.get('identity');
|
|
55
|
+
const comment = addShareComment(shareId, {
|
|
56
|
+
authorHandle: identity?.handle || null,
|
|
57
|
+
authorName: identity?.name || 'Anonymous',
|
|
58
|
+
text,
|
|
59
|
+
});
|
|
60
|
+
console.log(chalk.green(`Comment added.`));
|
|
61
|
+
console.log(chalk.gray(`ID: ${comment.id}`));
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.log(chalk.red(err.message));
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
cmd
|
|
69
|
+
.command('delete <shareId> <commentId>')
|
|
70
|
+
.description('delete a comment from a share')
|
|
71
|
+
.action((shareId, commentId) => {
|
|
72
|
+
try {
|
|
73
|
+
const identity = config.get('identity');
|
|
74
|
+
const comments = getShareComments(shareId);
|
|
75
|
+
const comment = comments.find(c => c.id === commentId);
|
|
76
|
+
if (!comment) {
|
|
77
|
+
console.log(chalk.red('Comment not found.'));
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const myHandle = identity?.handle || null;
|
|
82
|
+
const myName = identity?.name || 'Anonymous';
|
|
83
|
+
if (comment.authorHandle !== myHandle && comment.authorName !== myName) {
|
|
84
|
+
console.log(chalk.red('Error: you can only delete your own comments.'));
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
deleteShareComment(shareId, commentId);
|
|
89
|
+
console.log(chalk.green('Comment deleted.'));
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.log(chalk.red(err.message));
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
cmd.action(() => {
|
|
97
|
+
console.log(chalk.gray('Use `pe comment list <shareId>` to view comments.'));
|
|
98
|
+
});
|
|
99
|
+
}
|