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.
Files changed (156) hide show
  1. package/LICENSE.md +18 -0
  2. package/README.md +314 -0
  3. package/bin/pal.js +230 -0
  4. package/extensions/@palexplorer/analytics/README.md +45 -0
  5. package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
  6. package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
  7. package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
  8. package/extensions/@palexplorer/analytics/extension.json +27 -0
  9. package/extensions/@palexplorer/analytics/index.js +186 -0
  10. package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
  11. package/extensions/@palexplorer/audit/extension.json +17 -0
  12. package/extensions/@palexplorer/audit/index.js +2 -0
  13. package/extensions/@palexplorer/auth-email/extension.json +17 -0
  14. package/extensions/@palexplorer/auth-email/index.js +102 -0
  15. package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
  16. package/extensions/@palexplorer/auth-oauth/index.js +199 -0
  17. package/extensions/@palexplorer/chat/extension.json +17 -0
  18. package/extensions/@palexplorer/chat/index.js +2 -0
  19. package/extensions/@palexplorer/discovery/extension.json +16 -0
  20. package/extensions/@palexplorer/discovery/index.js +111 -0
  21. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  22. package/extensions/@palexplorer/email-notifications/index.js +242 -0
  23. package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
  24. package/extensions/@palexplorer/explorer-integration/index.js +122 -0
  25. package/extensions/@palexplorer/groups/extension.json +17 -0
  26. package/extensions/@palexplorer/groups/index.js +2 -0
  27. package/extensions/@palexplorer/networks/extension.json +17 -0
  28. package/extensions/@palexplorer/networks/index.js +2 -0
  29. package/extensions/@palexplorer/share-links/extension.json +17 -0
  30. package/extensions/@palexplorer/share-links/index.js +2 -0
  31. package/extensions/@palexplorer/sync/extension.json +17 -0
  32. package/extensions/@palexplorer/sync/index.js +2 -0
  33. package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
  34. package/extensions/@palexplorer/user-mgmt/index.js +2 -0
  35. package/extensions/@palexplorer/vfs/extension.json +17 -0
  36. package/extensions/@palexplorer/vfs/index.js +167 -0
  37. package/lib/capabilities.js +263 -0
  38. package/lib/commands/analytics.js +175 -0
  39. package/lib/commands/api-keys.js +131 -0
  40. package/lib/commands/audit.js +235 -0
  41. package/lib/commands/auth.js +137 -0
  42. package/lib/commands/backup.js +76 -0
  43. package/lib/commands/billing.js +148 -0
  44. package/lib/commands/chat.js +217 -0
  45. package/lib/commands/cloud-backup.js +231 -0
  46. package/lib/commands/comment.js +99 -0
  47. package/lib/commands/completion.js +203 -0
  48. package/lib/commands/compliance.js +218 -0
  49. package/lib/commands/config.js +136 -0
  50. package/lib/commands/connect.js +44 -0
  51. package/lib/commands/dept.js +294 -0
  52. package/lib/commands/device.js +146 -0
  53. package/lib/commands/download.js +226 -0
  54. package/lib/commands/explorer.js +178 -0
  55. package/lib/commands/extension.js +970 -0
  56. package/lib/commands/favorite.js +90 -0
  57. package/lib/commands/federation.js +270 -0
  58. package/lib/commands/file.js +533 -0
  59. package/lib/commands/group.js +271 -0
  60. package/lib/commands/gui-share.js +29 -0
  61. package/lib/commands/init.js +61 -0
  62. package/lib/commands/invite.js +59 -0
  63. package/lib/commands/list.js +59 -0
  64. package/lib/commands/log.js +116 -0
  65. package/lib/commands/nearby.js +108 -0
  66. package/lib/commands/network.js +251 -0
  67. package/lib/commands/notify.js +198 -0
  68. package/lib/commands/org.js +273 -0
  69. package/lib/commands/pal.js +180 -0
  70. package/lib/commands/permissions.js +216 -0
  71. package/lib/commands/pin.js +97 -0
  72. package/lib/commands/protocol.js +357 -0
  73. package/lib/commands/rbac.js +147 -0
  74. package/lib/commands/recover.js +36 -0
  75. package/lib/commands/register.js +171 -0
  76. package/lib/commands/relay.js +131 -0
  77. package/lib/commands/remote.js +368 -0
  78. package/lib/commands/revoke.js +50 -0
  79. package/lib/commands/scanner.js +280 -0
  80. package/lib/commands/schedule.js +344 -0
  81. package/lib/commands/scim.js +203 -0
  82. package/lib/commands/search.js +181 -0
  83. package/lib/commands/serve.js +438 -0
  84. package/lib/commands/server.js +350 -0
  85. package/lib/commands/share-link.js +199 -0
  86. package/lib/commands/share.js +323 -0
  87. package/lib/commands/sso.js +200 -0
  88. package/lib/commands/status.js +136 -0
  89. package/lib/commands/stream.js +562 -0
  90. package/lib/commands/su.js +187 -0
  91. package/lib/commands/sync.js +827 -0
  92. package/lib/commands/transfers.js +152 -0
  93. package/lib/commands/uninstall.js +188 -0
  94. package/lib/commands/update.js +204 -0
  95. package/lib/commands/user.js +276 -0
  96. package/lib/commands/vfs.js +84 -0
  97. package/lib/commands/web.js +52 -0
  98. package/lib/commands/webhook.js +180 -0
  99. package/lib/commands/whoami.js +59 -0
  100. package/lib/commands/workspace.js +121 -0
  101. package/lib/core/accessLog.js +54 -0
  102. package/lib/core/analytics.js +99 -0
  103. package/lib/core/backup.js +84 -0
  104. package/lib/core/billing.js +336 -0
  105. package/lib/core/bitfieldStore.js +53 -0
  106. package/lib/core/connectionManager.js +182 -0
  107. package/lib/core/dhtDiscovery.js +148 -0
  108. package/lib/core/discoveryClient.js +408 -0
  109. package/lib/core/extensionAnalyzer.js +357 -0
  110. package/lib/core/extensionSandbox.js +250 -0
  111. package/lib/core/extensionWorkerHost.js +166 -0
  112. package/lib/core/extensions.js +1082 -0
  113. package/lib/core/fileDiff.js +69 -0
  114. package/lib/core/groups.js +119 -0
  115. package/lib/core/identity.js +340 -0
  116. package/lib/core/mdnsService.js +126 -0
  117. package/lib/core/networks.js +81 -0
  118. package/lib/core/permissions.js +109 -0
  119. package/lib/core/pro.js +27 -0
  120. package/lib/core/resolver.js +74 -0
  121. package/lib/core/serverList.js +224 -0
  122. package/lib/core/sharePolicy.js +69 -0
  123. package/lib/core/shares.js +325 -0
  124. package/lib/core/signalingServer.js +441 -0
  125. package/lib/core/streamTransport.js +106 -0
  126. package/lib/core/su.js +55 -0
  127. package/lib/core/syncEngine.js +264 -0
  128. package/lib/core/syncState.js +159 -0
  129. package/lib/core/transfers.js +259 -0
  130. package/lib/core/users.js +225 -0
  131. package/lib/core/vfs.js +216 -0
  132. package/lib/core/webServer.js +702 -0
  133. package/lib/core/webrtcStream.js +396 -0
  134. package/lib/crypto/chatEncryption.js +57 -0
  135. package/lib/crypto/shareEncryption.js +195 -0
  136. package/lib/crypto/sharePassword.js +35 -0
  137. package/lib/crypto/streamEncryption.js +189 -0
  138. package/lib/package.json +1 -0
  139. package/lib/protocol/envelope.js +271 -0
  140. package/lib/protocol/handler.js +191 -0
  141. package/lib/protocol/index.js +27 -0
  142. package/lib/protocol/messages.js +247 -0
  143. package/lib/protocol/negotiation.js +127 -0
  144. package/lib/protocol/policy.js +142 -0
  145. package/lib/protocol/router.js +86 -0
  146. package/lib/protocol/sync.js +122 -0
  147. package/lib/utils/cli.js +15 -0
  148. package/lib/utils/config.js +123 -0
  149. package/lib/utils/configIntegrity.js +87 -0
  150. package/lib/utils/downloadDir.js +9 -0
  151. package/lib/utils/explorer.js +83 -0
  152. package/lib/utils/format.js +12 -0
  153. package/lib/utils/help.js +357 -0
  154. package/lib/utils/logger.js +103 -0
  155. package/lib/utils/torrent.js +203 -0
  156. 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
+ }