pal-explorer-cli 0.4.11 → 0.4.13
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/README.md +149 -149
- package/bin/pal.js +63 -2
- package/extensions/@palexplorer/analytics/extension.json +20 -1
- package/extensions/@palexplorer/analytics/index.js +19 -9
- package/extensions/@palexplorer/audit/extension.json +14 -0
- package/extensions/@palexplorer/auth-email/extension.json +15 -0
- package/extensions/@palexplorer/auth-oauth/extension.json +15 -0
- package/extensions/@palexplorer/chat/extension.json +14 -0
- package/extensions/@palexplorer/discovery/extension.json +17 -0
- package/extensions/@palexplorer/discovery/index.js +1 -1
- package/extensions/@palexplorer/email-notifications/extension.json +23 -0
- package/extensions/@palexplorer/groups/extension.json +15 -0
- package/extensions/@palexplorer/share-links/extension.json +15 -0
- package/extensions/@palexplorer/sync/extension.json +16 -0
- package/extensions/@palexplorer/user-mgmt/extension.json +15 -0
- package/lib/capabilities.js +24 -24
- package/lib/commands/analytics.js +175 -175
- package/lib/commands/api-keys.js +131 -131
- package/lib/commands/audit.js +235 -235
- package/lib/commands/auth.js +137 -137
- package/lib/commands/backup.js +76 -76
- package/lib/commands/billing.js +148 -148
- package/lib/commands/chat.js +217 -217
- package/lib/commands/cloud-backup.js +231 -231
- package/lib/commands/comment.js +99 -99
- package/lib/commands/completion.js +203 -203
- package/lib/commands/compliance.js +218 -218
- package/lib/commands/config.js +136 -136
- package/lib/commands/connect.js +44 -44
- package/lib/commands/dept.js +294 -294
- package/lib/commands/device.js +146 -146
- package/lib/commands/download.js +240 -226
- package/lib/commands/explorer.js +178 -178
- package/lib/commands/extension.js +1060 -970
- package/lib/commands/favorite.js +90 -90
- package/lib/commands/federation.js +270 -270
- package/lib/commands/file.js +533 -533
- package/lib/commands/group.js +271 -271
- package/lib/commands/gui-share.js +29 -29
- package/lib/commands/init.js +61 -61
- package/lib/commands/invite.js +59 -59
- package/lib/commands/list.js +58 -58
- package/lib/commands/log.js +116 -116
- package/lib/commands/nearby.js +108 -108
- package/lib/commands/network.js +251 -251
- package/lib/commands/notify.js +198 -198
- package/lib/commands/org.js +273 -273
- package/lib/commands/pal.js +403 -180
- package/lib/commands/permissions.js +216 -216
- package/lib/commands/pin.js +97 -97
- package/lib/commands/protocol.js +357 -357
- package/lib/commands/rbac.js +147 -147
- package/lib/commands/recover.js +36 -36
- package/lib/commands/register.js +171 -171
- package/lib/commands/relay.js +131 -131
- package/lib/commands/remote.js +368 -368
- package/lib/commands/revoke.js +50 -50
- package/lib/commands/scanner.js +280 -280
- package/lib/commands/schedule.js +344 -344
- package/lib/commands/scim.js +203 -203
- package/lib/commands/search.js +181 -181
- package/lib/commands/serve.js +438 -438
- package/lib/commands/server.js +350 -350
- package/lib/commands/share-link.js +199 -199
- package/lib/commands/share.js +336 -323
- package/lib/commands/sso.js +200 -200
- package/lib/commands/status.js +145 -145
- package/lib/commands/stream.js +562 -562
- package/lib/commands/su.js +187 -187
- package/lib/commands/sync.js +979 -979
- package/lib/commands/transfers.js +152 -152
- package/lib/commands/uninstall.js +188 -188
- package/lib/commands/update.js +204 -204
- package/lib/commands/user.js +276 -276
- package/lib/commands/vfs.js +84 -84
- package/lib/commands/web-login.js +79 -79
- package/lib/commands/web.js +52 -52
- package/lib/commands/webhook.js +180 -180
- package/lib/commands/whoami.js +59 -59
- package/lib/commands/workspace.js +121 -121
- package/lib/core/billing.js +16 -5
- package/lib/core/dhtDiscovery.js +9 -2
- package/lib/core/discoveryClient.js +13 -7
- package/lib/core/extensions.js +142 -1
- package/lib/core/identity.js +33 -2
- package/lib/core/imageProcessor.js +109 -0
- package/lib/core/imageTorrent.js +167 -0
- package/lib/core/permissions.js +1 -1
- package/lib/core/pro.js +11 -4
- package/lib/core/serverList.js +4 -1
- package/lib/core/shares.js +12 -1
- package/lib/core/signalingServer.js +14 -2
- package/lib/core/su.js +1 -1
- package/lib/core/users.js +1 -1
- package/lib/protocol/messages.js +12 -3
- package/lib/utils/explorer.js +1 -1
- package/lib/utils/help.js +357 -357
- package/lib/utils/torrent.js +1 -0
- package/package.json +4 -3
|
@@ -13,5 +13,20 @@
|
|
|
13
13
|
]
|
|
14
14
|
},
|
|
15
15
|
"config": { "enabled": { "type": "boolean", "default": false } },
|
|
16
|
+
"help": {
|
|
17
|
+
"summary": "Generate shareable download links for your shares. Links can be password-protected, set to expire after a date, or limited by download count (Pro).",
|
|
18
|
+
"usage": "pal share-link create <share-id>",
|
|
19
|
+
"examples": [
|
|
20
|
+
"pal share-link create abc123",
|
|
21
|
+
"pal share-link create abc123 --password secret --expires 2026-04-01",
|
|
22
|
+
"pal share-link create abc123 --max-downloads 10",
|
|
23
|
+
"pal share-link list abc123",
|
|
24
|
+
"pal share-link revoke <link-id>"
|
|
25
|
+
],
|
|
26
|
+
"configReference": {
|
|
27
|
+
"enabled": "Enable or disable the share links feature (true/false)"
|
|
28
|
+
},
|
|
29
|
+
"links": {}
|
|
30
|
+
},
|
|
16
31
|
"tier": "free"
|
|
17
32
|
}
|
|
@@ -13,5 +13,21 @@
|
|
|
13
13
|
]
|
|
14
14
|
},
|
|
15
15
|
"config": { "enabled": { "type": "boolean", "default": false } },
|
|
16
|
+
"help": {
|
|
17
|
+
"summary": "Manifest-based delta sync — only changed files are transferred. Push local changes, pull remote changes, or watch for auto-sync in real-time.",
|
|
18
|
+
"usage": "pal sync push <folder> <share-id>",
|
|
19
|
+
"examples": [
|
|
20
|
+
"pal sync push ./my-project abc123",
|
|
21
|
+
"pal sync pull abc123 ./local-copy",
|
|
22
|
+
"pal sync watch ./my-project abc123",
|
|
23
|
+
"pal sync diff abc123",
|
|
24
|
+
"pal sync list",
|
|
25
|
+
"pal sync resolve abc123 conflicted-file.txt keep_local"
|
|
26
|
+
],
|
|
27
|
+
"configReference": {
|
|
28
|
+
"enabled": "Enable or disable the sync feature (true/false)"
|
|
29
|
+
},
|
|
30
|
+
"links": {}
|
|
31
|
+
},
|
|
16
32
|
"tier": "free"
|
|
17
33
|
}
|
|
@@ -13,5 +13,20 @@
|
|
|
13
13
|
]
|
|
14
14
|
},
|
|
15
15
|
"config": { "enabled": { "type": "boolean", "default": false } },
|
|
16
|
+
"help": {
|
|
17
|
+
"summary": "Add multiple users to a single Palexplorer instance. Assign roles (owner/user/guest) to control who can create shares, manage settings, and view activity.",
|
|
18
|
+
"usage": "pal user add <username>",
|
|
19
|
+
"examples": [
|
|
20
|
+
"pal user add alice",
|
|
21
|
+
"pal user list",
|
|
22
|
+
"pal user promote alice owner",
|
|
23
|
+
"pal user remove bob",
|
|
24
|
+
"pal su alice"
|
|
25
|
+
],
|
|
26
|
+
"configReference": {
|
|
27
|
+
"enabled": "Enable or disable multi-user management (true/false)"
|
|
28
|
+
},
|
|
29
|
+
"links": {}
|
|
30
|
+
},
|
|
16
31
|
"tier": "free"
|
|
17
32
|
}
|
package/lib/capabilities.js
CHANGED
|
@@ -16,7 +16,7 @@ export const CAPABILITIES = {
|
|
|
16
16
|
maxDownloads: { type: 'number', label: 'Max downloads', min: 1 },
|
|
17
17
|
nonRecursive: { type: 'boolean', label: 'Top-level files only', default: false },
|
|
18
18
|
},
|
|
19
|
-
cli: '
|
|
19
|
+
cli: 'pal share <path>',
|
|
20
20
|
gui: 'ShareModal',
|
|
21
21
|
tier: 'free',
|
|
22
22
|
},
|
|
@@ -24,7 +24,7 @@ export const CAPABILITIES = {
|
|
|
24
24
|
label: 'Revoke Share',
|
|
25
25
|
description: 'Remove access to a shared file',
|
|
26
26
|
category: 'shares',
|
|
27
|
-
cli: '
|
|
27
|
+
cli: 'pal revoke <shareId>',
|
|
28
28
|
gui: 'ShareExplorer (context menu)',
|
|
29
29
|
tier: 'free',
|
|
30
30
|
},
|
|
@@ -32,7 +32,7 @@ export const CAPABILITIES = {
|
|
|
32
32
|
label: 'List Shares',
|
|
33
33
|
description: 'View all active shares',
|
|
34
34
|
category: 'shares',
|
|
35
|
-
cli: '
|
|
35
|
+
cli: 'pal list',
|
|
36
36
|
gui: 'ShareExplorer',
|
|
37
37
|
tier: 'free',
|
|
38
38
|
},
|
|
@@ -45,7 +45,7 @@ export const CAPABILITIES = {
|
|
|
45
45
|
password: { type: 'string', label: 'Link password' },
|
|
46
46
|
maxDownloads: { type: 'number', label: 'Max downloads', min: 1 },
|
|
47
47
|
},
|
|
48
|
-
cli: '
|
|
48
|
+
cli: 'pal share-link create',
|
|
49
49
|
gui: 'ShareLinksPage',
|
|
50
50
|
tier: 'free',
|
|
51
51
|
},
|
|
@@ -55,7 +55,7 @@ export const CAPABILITIES = {
|
|
|
55
55
|
label: 'Download',
|
|
56
56
|
description: 'Download from a magnet link or share',
|
|
57
57
|
category: 'transfers',
|
|
58
|
-
cli: '
|
|
58
|
+
cli: 'pal download <magnet>',
|
|
59
59
|
gui: 'TransfersPage (drag & drop or command palette)',
|
|
60
60
|
tier: 'free',
|
|
61
61
|
},
|
|
@@ -68,7 +68,7 @@ export const CAPABILITIES = {
|
|
|
68
68
|
options: {
|
|
69
69
|
method: { type: 'enum', values: ['handle', 'publicKey', 'inviteLink'], label: 'Add method' },
|
|
70
70
|
},
|
|
71
|
-
cli: '
|
|
71
|
+
cli: 'pal pal add <identifier>',
|
|
72
72
|
gui: 'AddPalModal',
|
|
73
73
|
tier: 'free',
|
|
74
74
|
gaps: ['GUI does not support invite link method'],
|
|
@@ -76,14 +76,14 @@ export const CAPABILITIES = {
|
|
|
76
76
|
'pal.remove': {
|
|
77
77
|
label: 'Remove Pal',
|
|
78
78
|
category: 'people',
|
|
79
|
-
cli: '
|
|
79
|
+
cli: 'pal pal remove <handle>',
|
|
80
80
|
gui: 'PalsPage (context menu)',
|
|
81
81
|
tier: 'free',
|
|
82
82
|
},
|
|
83
83
|
'pal.list': {
|
|
84
84
|
label: 'List Pals',
|
|
85
85
|
category: 'people',
|
|
86
|
-
cli: '
|
|
86
|
+
cli: 'pal pal list',
|
|
87
87
|
gui: 'PalsPage',
|
|
88
88
|
tier: 'free',
|
|
89
89
|
},
|
|
@@ -92,14 +92,14 @@ export const CAPABILITIES = {
|
|
|
92
92
|
'group.create': {
|
|
93
93
|
label: 'Create Group',
|
|
94
94
|
category: 'people',
|
|
95
|
-
cli: '
|
|
95
|
+
cli: 'pal group create <name>',
|
|
96
96
|
gui: 'GroupModal',
|
|
97
97
|
tier: 'free',
|
|
98
98
|
},
|
|
99
99
|
'group.list': {
|
|
100
100
|
label: 'List Groups',
|
|
101
101
|
category: 'people',
|
|
102
|
-
cli: '
|
|
102
|
+
cli: 'pal group list',
|
|
103
103
|
gui: 'GroupsPage',
|
|
104
104
|
tier: 'free',
|
|
105
105
|
},
|
|
@@ -112,7 +112,7 @@ export const CAPABILITIES = {
|
|
|
112
112
|
options: {
|
|
113
113
|
watch: { type: 'boolean', label: 'Watch for changes', default: false },
|
|
114
114
|
},
|
|
115
|
-
cli: '
|
|
115
|
+
cli: 'pal sync push <dir> <pal>',
|
|
116
116
|
gui: 'SyncPage',
|
|
117
117
|
tier: 'free',
|
|
118
118
|
},
|
|
@@ -120,7 +120,7 @@ export const CAPABILITIES = {
|
|
|
120
120
|
label: 'Pull Sync',
|
|
121
121
|
description: 'Pull a directory from a pal',
|
|
122
122
|
category: 'transfers',
|
|
123
|
-
cli: '
|
|
123
|
+
cli: 'pal sync pull <dir> <pal>',
|
|
124
124
|
gui: 'SyncPage',
|
|
125
125
|
tier: 'free',
|
|
126
126
|
},
|
|
@@ -129,7 +129,7 @@ export const CAPABILITIES = {
|
|
|
129
129
|
'chat.send': {
|
|
130
130
|
label: 'Send Message',
|
|
131
131
|
category: 'social',
|
|
132
|
-
cli: '
|
|
132
|
+
cli: 'pal chat send <pal> <message>',
|
|
133
133
|
gui: 'ChatPage',
|
|
134
134
|
tier: 'free',
|
|
135
135
|
},
|
|
@@ -142,7 +142,7 @@ export const CAPABILITIES = {
|
|
|
142
142
|
options: {
|
|
143
143
|
qr: { type: 'boolean', label: 'Generate QR code' },
|
|
144
144
|
},
|
|
145
|
-
cli: '
|
|
145
|
+
cli: 'pal invite --qr',
|
|
146
146
|
gui: null,
|
|
147
147
|
tier: 'free',
|
|
148
148
|
gaps: ['No QR generation in GUI'],
|
|
@@ -153,7 +153,7 @@ export const CAPABILITIES = {
|
|
|
153
153
|
label: 'Browse Remote Pal',
|
|
154
154
|
description: 'Browse files on a remote pal',
|
|
155
155
|
category: 'shares',
|
|
156
|
-
cli: '
|
|
156
|
+
cli: 'pal remote browse <pal>',
|
|
157
157
|
gui: 'RemoteBrowser (not linked from nav)',
|
|
158
158
|
tier: 'free',
|
|
159
159
|
gaps: ['RemoteBrowser exists but not accessible from nav'],
|
|
@@ -164,7 +164,7 @@ export const CAPABILITIES = {
|
|
|
164
164
|
label: 'Start Stream',
|
|
165
165
|
description: 'Stream media from a share',
|
|
166
166
|
category: 'transfers',
|
|
167
|
-
cli: '
|
|
167
|
+
cli: 'pal stream <shareId>',
|
|
168
168
|
gui: 'StreamPage',
|
|
169
169
|
tier: 'pro',
|
|
170
170
|
},
|
|
@@ -173,14 +173,14 @@ export const CAPABILITIES = {
|
|
|
173
173
|
'identity.init': {
|
|
174
174
|
label: 'Create Identity',
|
|
175
175
|
category: 'system',
|
|
176
|
-
cli: '
|
|
176
|
+
cli: 'pal init <name>',
|
|
177
177
|
gui: 'SetupWizard',
|
|
178
178
|
tier: 'free',
|
|
179
179
|
},
|
|
180
180
|
'identity.register': {
|
|
181
181
|
label: 'Register Handle',
|
|
182
182
|
category: 'system',
|
|
183
|
-
cli: '
|
|
183
|
+
cli: 'pal register <handle>',
|
|
184
184
|
gui: 'SettingsPage',
|
|
185
185
|
tier: 'free',
|
|
186
186
|
},
|
|
@@ -188,7 +188,7 @@ export const CAPABILITIES = {
|
|
|
188
188
|
label: 'Recover Identity',
|
|
189
189
|
description: 'Restore from recovery phrase',
|
|
190
190
|
category: 'system',
|
|
191
|
-
cli: '
|
|
191
|
+
cli: 'pal recover',
|
|
192
192
|
gui: null,
|
|
193
193
|
tier: 'free',
|
|
194
194
|
gaps: ['No recovery phrase restore flow in GUI'],
|
|
@@ -198,7 +198,7 @@ export const CAPABILITIES = {
|
|
|
198
198
|
'device.list': {
|
|
199
199
|
label: 'List Devices',
|
|
200
200
|
category: 'system',
|
|
201
|
-
cli: '
|
|
201
|
+
cli: 'pal device list',
|
|
202
202
|
gui: 'SettingsPage (partial)',
|
|
203
203
|
tier: 'free',
|
|
204
204
|
gaps: ['No dedicated device management page'],
|
|
@@ -208,7 +208,7 @@ export const CAPABILITIES = {
|
|
|
208
208
|
'billing.status': {
|
|
209
209
|
label: 'Billing Status',
|
|
210
210
|
category: 'system',
|
|
211
|
-
cli: '
|
|
211
|
+
cli: 'pal billing status',
|
|
212
212
|
gui: 'SettingsPage (partial)',
|
|
213
213
|
tier: 'pro',
|
|
214
214
|
},
|
|
@@ -217,7 +217,7 @@ export const CAPABILITIES = {
|
|
|
217
217
|
'extension.install': {
|
|
218
218
|
label: 'Install Extension',
|
|
219
219
|
category: 'system',
|
|
220
|
-
cli: '
|
|
220
|
+
cli: 'pal extension install <name>',
|
|
221
221
|
gui: 'ExtensionsPage',
|
|
222
222
|
tier: 'free',
|
|
223
223
|
},
|
|
@@ -226,7 +226,7 @@ export const CAPABILITIES = {
|
|
|
226
226
|
'favorite.add': {
|
|
227
227
|
label: 'Favorite Share',
|
|
228
228
|
category: 'shares',
|
|
229
|
-
cli: '
|
|
229
|
+
cli: 'pal favorite add <shareId>',
|
|
230
230
|
gui: 'SharesPage (star UI, no filter view)',
|
|
231
231
|
tier: 'free',
|
|
232
232
|
gaps: ['No dedicated favorites view/filter'],
|
|
@@ -237,7 +237,7 @@ export const CAPABILITIES = {
|
|
|
237
237
|
label: 'Search',
|
|
238
238
|
description: 'Search files, shares, and pals',
|
|
239
239
|
category: 'shares',
|
|
240
|
-
cli: '
|
|
240
|
+
cli: 'pal search <query>',
|
|
241
241
|
gui: 'SearchPage + top bar search',
|
|
242
242
|
tier: 'free',
|
|
243
243
|
},
|
|
@@ -1,175 +1,175 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
|
|
3
|
-
export default function analyticsCommand(program) {
|
|
4
|
-
const cmd = program
|
|
5
|
-
.command('analytics')
|
|
6
|
-
.description('transfer analytics and reporting (Pro)')
|
|
7
|
-
.addHelpText('after', `
|
|
8
|
-
Examples:
|
|
9
|
-
$
|
|
10
|
-
$
|
|
11
|
-
$
|
|
12
|
-
$
|
|
13
|
-
$
|
|
14
|
-
$
|
|
15
|
-
`)
|
|
16
|
-
.action(() => { cmd.outputHelp(); });
|
|
17
|
-
|
|
18
|
-
cmd
|
|
19
|
-
.command('metrics')
|
|
20
|
-
.description('show transfer metrics')
|
|
21
|
-
.option('--period <period>', 'time period: day, week, month', 'week')
|
|
22
|
-
.action(async (opts) => {
|
|
23
|
-
try {
|
|
24
|
-
const extConfig = (await import('../utils/config.js')).default;
|
|
25
|
-
const store = extConfig.get('ext_store.reporting-dashboard') || {};
|
|
26
|
-
const metrics = store.reportingMetrics || {};
|
|
27
|
-
const periodMs = { day: 86400000, week: 604800000, month: 2592000000 };
|
|
28
|
-
const cutoff = Date.now() - (periodMs[opts.period] || periodMs.week);
|
|
29
|
-
const transfers = (metrics.transfers || []).filter(t => new Date(t.timestamp).getTime() > cutoff);
|
|
30
|
-
|
|
31
|
-
console.log(chalk.bold(`Transfer Metrics (${opts.period})\n`));
|
|
32
|
-
console.log(` Total transfers: ${chalk.white(transfers.length)}`);
|
|
33
|
-
const totalBytes = transfers.reduce((sum, t) => sum + (t.bytes || 0), 0);
|
|
34
|
-
console.log(` Total data: ${chalk.white(formatBytes(totalBytes))}`);
|
|
35
|
-
const avgSpeed = transfers.length > 0 ? totalBytes / transfers.reduce((sum, t) => sum + (t.duration || 1), 0) : 0;
|
|
36
|
-
console.log(` Avg speed: ${chalk.white(formatBytes(avgSpeed) + '/s')}`);
|
|
37
|
-
console.log(` Shares created: ${chalk.white((metrics.sharesCreated || []).filter(s => new Date(s.timestamp).getTime() > cutoff).length)}`);
|
|
38
|
-
} catch (err) {
|
|
39
|
-
console.log(chalk.red(`Failed to get metrics: ${err.message}`));
|
|
40
|
-
process.exitCode = 1;
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
cmd
|
|
45
|
-
.command('report')
|
|
46
|
-
.description('generate a transfer report')
|
|
47
|
-
.option('--format <format>', 'output format: json, csv, pdf', 'json')
|
|
48
|
-
.option('-o, --output <path>', 'output file path')
|
|
49
|
-
.action(async (opts) => {
|
|
50
|
-
try {
|
|
51
|
-
const fs = await import('fs');
|
|
52
|
-
const path = await import('path');
|
|
53
|
-
const extConfig = (await import('../utils/config.js')).default;
|
|
54
|
-
const store = extConfig.get('ext_store.reporting-dashboard') || {};
|
|
55
|
-
const metrics = store.reportingMetrics || {};
|
|
56
|
-
|
|
57
|
-
const data = {
|
|
58
|
-
generatedAt: new Date().toISOString(),
|
|
59
|
-
format: opts.format,
|
|
60
|
-
transfers: metrics.transfers || [],
|
|
61
|
-
sharesCreated: metrics.sharesCreated || [],
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
let content;
|
|
65
|
-
if (opts.format === 'csv') {
|
|
66
|
-
const rows = (data.transfers || []).map(t => `${t.timestamp},${t.bytes},${t.duration},${t.peer || ''}`);
|
|
67
|
-
content = 'timestamp,bytes,duration,peer\n' + rows.join('\n');
|
|
68
|
-
} else {
|
|
69
|
-
content = JSON.stringify(data, null, 2);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const outPath = opts.output || `palexplorer-report-${new Date().toISOString().slice(0, 10)}.${opts.format}`;
|
|
73
|
-
const resolved = path.resolve(outPath);
|
|
74
|
-
fs.writeFileSync(resolved, content, 'utf8');
|
|
75
|
-
console.log(chalk.green(`✔ Report generated: ${resolved}`));
|
|
76
|
-
} catch (err) {
|
|
77
|
-
console.log(chalk.red(`Report failed: ${err.message}`));
|
|
78
|
-
process.exitCode = 1;
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
cmd
|
|
83
|
-
.command('top-shares')
|
|
84
|
-
.description('show most downloaded shares')
|
|
85
|
-
.option('--limit <n>', 'number of shares to show', '10')
|
|
86
|
-
.action(async (opts) => {
|
|
87
|
-
try {
|
|
88
|
-
const extConfig = (await import('../utils/config.js')).default;
|
|
89
|
-
const store = extConfig.get('ext_store.reporting-dashboard') || {};
|
|
90
|
-
const metrics = store.reportingMetrics || {};
|
|
91
|
-
const shares = metrics.topShares || [];
|
|
92
|
-
const limit = parseInt(opts.limit, 10) || 10;
|
|
93
|
-
|
|
94
|
-
if (shares.length === 0) {
|
|
95
|
-
console.log(chalk.dim('No share data available yet.'));
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
console.log(chalk.bold(`Top ${Math.min(limit, shares.length)} Shares\n`));
|
|
100
|
-
const sorted = shares.sort((a, b) => (b.downloads || 0) - (a.downloads || 0)).slice(0, limit);
|
|
101
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
102
|
-
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.cyan(sorted[i].name)} — ${chalk.white(sorted[i].downloads || 0)} downloads`);
|
|
103
|
-
}
|
|
104
|
-
} catch (err) {
|
|
105
|
-
console.log(chalk.red(`Failed: ${err.message}`));
|
|
106
|
-
process.exitCode = 1;
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
cmd
|
|
111
|
-
.command('storage-trends')
|
|
112
|
-
.description('show 30-day storage usage trends')
|
|
113
|
-
.option('--days <n>', 'number of days', '30')
|
|
114
|
-
.action(async (opts) => {
|
|
115
|
-
try {
|
|
116
|
-
const extConfig = (await import('../utils/config.js')).default;
|
|
117
|
-
const store = extConfig.get('ext_store.reporting-dashboard') || {};
|
|
118
|
-
const metrics = store.reportingMetrics || {};
|
|
119
|
-
const trends = metrics.storageTrends || {};
|
|
120
|
-
const days = parseInt(opts.days, 10) || 30;
|
|
121
|
-
|
|
122
|
-
const entries = Object.entries(trends).sort().slice(-days);
|
|
123
|
-
if (entries.length === 0) {
|
|
124
|
-
console.log(chalk.dim('No storage trend data available yet.'));
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
console.log(chalk.bold(`Storage Trends (last ${entries.length} days)\n`));
|
|
129
|
-
for (const [date, bytes] of entries) {
|
|
130
|
-
console.log(` ${chalk.dim(date)} ${chalk.white(formatBytes(bytes))}`);
|
|
131
|
-
}
|
|
132
|
-
} catch (err) {
|
|
133
|
-
console.log(chalk.red(`Failed: ${err.message}`));
|
|
134
|
-
process.exitCode = 1;
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
cmd
|
|
139
|
-
.command('dashboard')
|
|
140
|
-
.description('open the analytics web dashboard')
|
|
141
|
-
.action(async () => {
|
|
142
|
-
try {
|
|
143
|
-
const extConfig = (await import('../utils/config.js')).default;
|
|
144
|
-
const extConf = extConfig.get('ext.reporting-dashboard') || {};
|
|
145
|
-
const port = extConf.dashboardPort;
|
|
146
|
-
if (!port) {
|
|
147
|
-
console.log(chalk.yellow('Dashboard port not configured. Set it with:'));
|
|
148
|
-
console.log(chalk.dim('
|
|
149
|
-
process.exitCode = 1;
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
const portNum = parseInt(port, 10);
|
|
153
|
-
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
|
154
|
-
console.error('Invalid analytics port');
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
const url = `http://127.0.0.1:${portNum}`;
|
|
158
|
-
console.log(chalk.green(`Dashboard: ${chalk.cyan(url)}`));
|
|
159
|
-
const { exec } = await import('child_process');
|
|
160
|
-
const opener = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
161
|
-
exec(`${opener} ${url}`);
|
|
162
|
-
} catch (err) {
|
|
163
|
-
console.log(chalk.red(`Failed: ${err.message}`));
|
|
164
|
-
process.exitCode = 1;
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function formatBytes(bytes) {
|
|
170
|
-
if (bytes === 0) return '0 B';
|
|
171
|
-
const k = 1024;
|
|
172
|
-
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
173
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
174
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
175
|
-
}
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export default function analyticsCommand(program) {
|
|
4
|
+
const cmd = program
|
|
5
|
+
.command('analytics')
|
|
6
|
+
.description('transfer analytics and reporting (Pro)')
|
|
7
|
+
.addHelpText('after', `
|
|
8
|
+
Examples:
|
|
9
|
+
$ pal analytics metrics Show transfer metrics (default: week)
|
|
10
|
+
$ pal analytics metrics --period month Monthly metrics
|
|
11
|
+
$ pal analytics report --format csv Generate CSV report
|
|
12
|
+
$ pal analytics top-shares Show most downloaded shares
|
|
13
|
+
$ pal analytics storage-trends Show 30-day storage trends
|
|
14
|
+
$ pal analytics dashboard Open web dashboard
|
|
15
|
+
`)
|
|
16
|
+
.action(() => { cmd.outputHelp(); });
|
|
17
|
+
|
|
18
|
+
cmd
|
|
19
|
+
.command('metrics')
|
|
20
|
+
.description('show transfer metrics')
|
|
21
|
+
.option('--period <period>', 'time period: day, week, month', 'week')
|
|
22
|
+
.action(async (opts) => {
|
|
23
|
+
try {
|
|
24
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
25
|
+
const store = extConfig.get('ext_store.reporting-dashboard') || {};
|
|
26
|
+
const metrics = store.reportingMetrics || {};
|
|
27
|
+
const periodMs = { day: 86400000, week: 604800000, month: 2592000000 };
|
|
28
|
+
const cutoff = Date.now() - (periodMs[opts.period] || periodMs.week);
|
|
29
|
+
const transfers = (metrics.transfers || []).filter(t => new Date(t.timestamp).getTime() > cutoff);
|
|
30
|
+
|
|
31
|
+
console.log(chalk.bold(`Transfer Metrics (${opts.period})\n`));
|
|
32
|
+
console.log(` Total transfers: ${chalk.white(transfers.length)}`);
|
|
33
|
+
const totalBytes = transfers.reduce((sum, t) => sum + (t.bytes || 0), 0);
|
|
34
|
+
console.log(` Total data: ${chalk.white(formatBytes(totalBytes))}`);
|
|
35
|
+
const avgSpeed = transfers.length > 0 ? totalBytes / transfers.reduce((sum, t) => sum + (t.duration || 1), 0) : 0;
|
|
36
|
+
console.log(` Avg speed: ${chalk.white(formatBytes(avgSpeed) + '/s')}`);
|
|
37
|
+
console.log(` Shares created: ${chalk.white((metrics.sharesCreated || []).filter(s => new Date(s.timestamp).getTime() > cutoff).length)}`);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.log(chalk.red(`Failed to get metrics: ${err.message}`));
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
cmd
|
|
45
|
+
.command('report')
|
|
46
|
+
.description('generate a transfer report')
|
|
47
|
+
.option('--format <format>', 'output format: json, csv, pdf', 'json')
|
|
48
|
+
.option('-o, --output <path>', 'output file path')
|
|
49
|
+
.action(async (opts) => {
|
|
50
|
+
try {
|
|
51
|
+
const fs = await import('fs');
|
|
52
|
+
const path = await import('path');
|
|
53
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
54
|
+
const store = extConfig.get('ext_store.reporting-dashboard') || {};
|
|
55
|
+
const metrics = store.reportingMetrics || {};
|
|
56
|
+
|
|
57
|
+
const data = {
|
|
58
|
+
generatedAt: new Date().toISOString(),
|
|
59
|
+
format: opts.format,
|
|
60
|
+
transfers: metrics.transfers || [],
|
|
61
|
+
sharesCreated: metrics.sharesCreated || [],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let content;
|
|
65
|
+
if (opts.format === 'csv') {
|
|
66
|
+
const rows = (data.transfers || []).map(t => `${t.timestamp},${t.bytes},${t.duration},${t.peer || ''}`);
|
|
67
|
+
content = 'timestamp,bytes,duration,peer\n' + rows.join('\n');
|
|
68
|
+
} else {
|
|
69
|
+
content = JSON.stringify(data, null, 2);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const outPath = opts.output || `palexplorer-report-${new Date().toISOString().slice(0, 10)}.${opts.format}`;
|
|
73
|
+
const resolved = path.resolve(outPath);
|
|
74
|
+
fs.writeFileSync(resolved, content, 'utf8');
|
|
75
|
+
console.log(chalk.green(`✔ Report generated: ${resolved}`));
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.log(chalk.red(`Report failed: ${err.message}`));
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
cmd
|
|
83
|
+
.command('top-shares')
|
|
84
|
+
.description('show most downloaded shares')
|
|
85
|
+
.option('--limit <n>', 'number of shares to show', '10')
|
|
86
|
+
.action(async (opts) => {
|
|
87
|
+
try {
|
|
88
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
89
|
+
const store = extConfig.get('ext_store.reporting-dashboard') || {};
|
|
90
|
+
const metrics = store.reportingMetrics || {};
|
|
91
|
+
const shares = metrics.topShares || [];
|
|
92
|
+
const limit = parseInt(opts.limit, 10) || 10;
|
|
93
|
+
|
|
94
|
+
if (shares.length === 0) {
|
|
95
|
+
console.log(chalk.dim('No share data available yet.'));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(chalk.bold(`Top ${Math.min(limit, shares.length)} Shares\n`));
|
|
100
|
+
const sorted = shares.sort((a, b) => (b.downloads || 0) - (a.downloads || 0)).slice(0, limit);
|
|
101
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
102
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.cyan(sorted[i].name)} — ${chalk.white(sorted[i].downloads || 0)} downloads`);
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.log(chalk.red(`Failed: ${err.message}`));
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
cmd
|
|
111
|
+
.command('storage-trends')
|
|
112
|
+
.description('show 30-day storage usage trends')
|
|
113
|
+
.option('--days <n>', 'number of days', '30')
|
|
114
|
+
.action(async (opts) => {
|
|
115
|
+
try {
|
|
116
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
117
|
+
const store = extConfig.get('ext_store.reporting-dashboard') || {};
|
|
118
|
+
const metrics = store.reportingMetrics || {};
|
|
119
|
+
const trends = metrics.storageTrends || {};
|
|
120
|
+
const days = parseInt(opts.days, 10) || 30;
|
|
121
|
+
|
|
122
|
+
const entries = Object.entries(trends).sort().slice(-days);
|
|
123
|
+
if (entries.length === 0) {
|
|
124
|
+
console.log(chalk.dim('No storage trend data available yet.'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(chalk.bold(`Storage Trends (last ${entries.length} days)\n`));
|
|
129
|
+
for (const [date, bytes] of entries) {
|
|
130
|
+
console.log(` ${chalk.dim(date)} ${chalk.white(formatBytes(bytes))}`);
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.log(chalk.red(`Failed: ${err.message}`));
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
cmd
|
|
139
|
+
.command('dashboard')
|
|
140
|
+
.description('open the analytics web dashboard')
|
|
141
|
+
.action(async () => {
|
|
142
|
+
try {
|
|
143
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
144
|
+
const extConf = extConfig.get('ext.reporting-dashboard') || {};
|
|
145
|
+
const port = extConf.dashboardPort;
|
|
146
|
+
if (!port) {
|
|
147
|
+
console.log(chalk.yellow('Dashboard port not configured. Set it with:'));
|
|
148
|
+
console.log(chalk.dim(' pal ext config reporting-dashboard dashboardPort 9090'));
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const portNum = parseInt(port, 10);
|
|
153
|
+
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
|
154
|
+
console.error('Invalid analytics port');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const url = `http://127.0.0.1:${portNum}`;
|
|
158
|
+
console.log(chalk.green(`Dashboard: ${chalk.cyan(url)}`));
|
|
159
|
+
const { exec } = await import('child_process');
|
|
160
|
+
const opener = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
161
|
+
exec(`${opener} ${url}`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.log(chalk.red(`Failed: ${err.message}`));
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatBytes(bytes) {
|
|
170
|
+
if (bytes === 0) return '0 B';
|
|
171
|
+
const k = 1024;
|
|
172
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
173
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
174
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
175
|
+
}
|