pal-explorer-cli 0.4.12 → 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.
Files changed (99) hide show
  1. package/README.md +149 -149
  2. package/bin/pal.js +63 -2
  3. package/extensions/@palexplorer/analytics/extension.json +20 -1
  4. package/extensions/@palexplorer/analytics/index.js +19 -9
  5. package/extensions/@palexplorer/audit/extension.json +14 -0
  6. package/extensions/@palexplorer/auth-email/extension.json +15 -0
  7. package/extensions/@palexplorer/auth-oauth/extension.json +15 -0
  8. package/extensions/@palexplorer/chat/extension.json +14 -0
  9. package/extensions/@palexplorer/discovery/extension.json +17 -0
  10. package/extensions/@palexplorer/discovery/index.js +1 -1
  11. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  12. package/extensions/@palexplorer/groups/extension.json +15 -0
  13. package/extensions/@palexplorer/share-links/extension.json +15 -0
  14. package/extensions/@palexplorer/sync/extension.json +16 -0
  15. package/extensions/@palexplorer/user-mgmt/extension.json +15 -0
  16. package/lib/capabilities.js +24 -24
  17. package/lib/commands/analytics.js +175 -175
  18. package/lib/commands/api-keys.js +131 -131
  19. package/lib/commands/audit.js +235 -235
  20. package/lib/commands/auth.js +137 -137
  21. package/lib/commands/backup.js +76 -76
  22. package/lib/commands/billing.js +148 -148
  23. package/lib/commands/chat.js +217 -217
  24. package/lib/commands/cloud-backup.js +231 -231
  25. package/lib/commands/comment.js +99 -99
  26. package/lib/commands/completion.js +203 -203
  27. package/lib/commands/compliance.js +218 -218
  28. package/lib/commands/config.js +136 -136
  29. package/lib/commands/connect.js +44 -44
  30. package/lib/commands/dept.js +294 -294
  31. package/lib/commands/device.js +146 -146
  32. package/lib/commands/download.js +240 -226
  33. package/lib/commands/explorer.js +178 -178
  34. package/lib/commands/extension.js +1060 -970
  35. package/lib/commands/favorite.js +90 -90
  36. package/lib/commands/federation.js +270 -270
  37. package/lib/commands/file.js +533 -533
  38. package/lib/commands/group.js +271 -271
  39. package/lib/commands/gui-share.js +29 -29
  40. package/lib/commands/init.js +61 -61
  41. package/lib/commands/invite.js +59 -59
  42. package/lib/commands/list.js +58 -58
  43. package/lib/commands/log.js +116 -116
  44. package/lib/commands/nearby.js +108 -108
  45. package/lib/commands/network.js +251 -251
  46. package/lib/commands/notify.js +198 -198
  47. package/lib/commands/org.js +273 -273
  48. package/lib/commands/pal.js +403 -180
  49. package/lib/commands/permissions.js +216 -216
  50. package/lib/commands/pin.js +97 -97
  51. package/lib/commands/protocol.js +357 -357
  52. package/lib/commands/rbac.js +147 -147
  53. package/lib/commands/recover.js +36 -36
  54. package/lib/commands/register.js +171 -171
  55. package/lib/commands/relay.js +131 -131
  56. package/lib/commands/remote.js +368 -368
  57. package/lib/commands/revoke.js +50 -50
  58. package/lib/commands/scanner.js +280 -280
  59. package/lib/commands/schedule.js +344 -344
  60. package/lib/commands/scim.js +203 -203
  61. package/lib/commands/search.js +181 -181
  62. package/lib/commands/serve.js +438 -438
  63. package/lib/commands/server.js +350 -350
  64. package/lib/commands/share-link.js +199 -199
  65. package/lib/commands/share.js +336 -323
  66. package/lib/commands/sso.js +200 -200
  67. package/lib/commands/status.js +145 -145
  68. package/lib/commands/stream.js +562 -562
  69. package/lib/commands/su.js +187 -187
  70. package/lib/commands/sync.js +979 -979
  71. package/lib/commands/transfers.js +152 -152
  72. package/lib/commands/uninstall.js +188 -188
  73. package/lib/commands/update.js +204 -204
  74. package/lib/commands/user.js +276 -276
  75. package/lib/commands/vfs.js +84 -84
  76. package/lib/commands/web-login.js +79 -79
  77. package/lib/commands/web.js +52 -52
  78. package/lib/commands/webhook.js +180 -180
  79. package/lib/commands/whoami.js +59 -59
  80. package/lib/commands/workspace.js +121 -121
  81. package/lib/core/billing.js +16 -5
  82. package/lib/core/dhtDiscovery.js +9 -2
  83. package/lib/core/discoveryClient.js +13 -7
  84. package/lib/core/extensions.js +142 -1
  85. package/lib/core/identity.js +33 -2
  86. package/lib/core/imageProcessor.js +109 -0
  87. package/lib/core/imageTorrent.js +167 -0
  88. package/lib/core/permissions.js +1 -1
  89. package/lib/core/pro.js +11 -4
  90. package/lib/core/serverList.js +4 -1
  91. package/lib/core/shares.js +12 -1
  92. package/lib/core/signalingServer.js +14 -2
  93. package/lib/core/su.js +1 -1
  94. package/lib/core/users.js +1 -1
  95. package/lib/protocol/messages.js +12 -3
  96. package/lib/utils/explorer.js +1 -1
  97. package/lib/utils/help.js +357 -357
  98. package/lib/utils/torrent.js +1 -0
  99. 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
  }
@@ -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: 'pe share <path>',
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: 'pe revoke <shareId>',
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: 'pe list',
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: 'pe share-link create',
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: 'pe download <magnet>',
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: 'pe pal add <identifier>',
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: 'pe pal remove <handle>',
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: 'pe pal list',
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: 'pe group create <name>',
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: 'pe group list',
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: 'pe sync push <dir> <pal>',
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: 'pe sync pull <dir> <pal>',
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: 'pe chat send <pal> <message>',
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: 'pe invite --qr',
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: 'pe remote browse <pal>',
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: 'pe stream <shareId>',
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: 'pe init <name>',
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: 'pe register <handle>',
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: 'pe recover',
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: 'pe device list',
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: 'pe billing status',
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: 'pe extension install <name>',
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: 'pe favorite add <shareId>',
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: 'pe search <query>',
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
- $ pe analytics metrics Show transfer metrics (default: week)
10
- $ pe analytics metrics --period month Monthly metrics
11
- $ pe analytics report --format csv Generate CSV report
12
- $ pe analytics top-shares Show most downloaded shares
13
- $ pe analytics storage-trends Show 30-day storage trends
14
- $ pe 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(' pe 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
- }
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
+ }