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,152 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import Conf from 'conf';
5
+ import { removeTransfer, setTransferPaused, getTransferStats, pruneOldHistory } from '../core/transfers.js';
6
+ import config from '../utils/config.js';
7
+
8
+ const transferStore = new Conf({
9
+ projectName: 'palexplorer-cli',
10
+ configName: 'transfers',
11
+ defaults: { active: [], history: [] }
12
+ });
13
+
14
+ function formatProgress(t) {
15
+ const pct = t.progress ? `${(t.progress * 100).toFixed(1)}%` : '0%';
16
+ return pct;
17
+ }
18
+
19
+ function printActive(active) {
20
+ if (active.length === 0) {
21
+ console.log(chalk.gray('No active transfers. Use `pe download <magnet>` to start one.'));
22
+ return;
23
+ }
24
+ console.log('');
25
+ console.log(chalk.cyan('Active Transfers:'));
26
+ active.forEach(t => {
27
+ const status = t.paused ? chalk.yellow('paused') : chalk.blue('downloading');
28
+ const pct = formatProgress(t);
29
+ const name = t.name || 'Unknown';
30
+ const shortMagnet = t.magnet.slice(0, 40) + '...';
31
+ console.log(` ${chalk.white(name)} [${status}] ${pct}`);
32
+ console.log(` ${chalk.gray(t.savePath || '')} ${chalk.gray(shortMagnet)}`);
33
+ });
34
+ }
35
+
36
+ export default function transfersCommand(program) {
37
+ const cmd = program
38
+ .command('transfers')
39
+ .description('view and manage active file transfers')
40
+ .option('--history', 'Show completed transfers')
41
+ .addHelpText('after', `
42
+ Examples:
43
+ $ pe transfers Show active transfers
44
+ $ pe transfers --history Show completed transfers
45
+ $ pe transfers cancel <magnet> Cancel a transfer
46
+ `)
47
+ .action((opts) => {
48
+ if (program.opts().json) {
49
+ const active = transferStore.get('active') || [];
50
+ const history = transferStore.get('history') || [];
51
+ console.log(JSON.stringify(opts.history ? { history } : { active }, null, 2));
52
+ return;
53
+ }
54
+ if (opts.history) {
55
+ const history = transferStore.get('history') || [];
56
+ if (history.length === 0) {
57
+ console.log(chalk.gray('No completed transfers.'));
58
+ return;
59
+ }
60
+ console.log('');
61
+ console.log(chalk.cyan('Completed Transfers:'));
62
+ history.forEach(t => {
63
+ console.log(` ${chalk.white(t.name || 'Unknown')} ${chalk.green('✔ done')} ${chalk.gray(t.completedAt || '')}`);
64
+ console.log(` ${chalk.gray(t.savePath || '')}`);
65
+ });
66
+ return;
67
+ }
68
+ const active = transferStore.get('active') || [];
69
+ printActive(active);
70
+ });
71
+
72
+ cmd
73
+ .command('cancel <magnet>')
74
+ .description('remove a transfer from tracking by its magnet URI')
75
+ .action((magnet) => {
76
+ removeTransfer(magnet);
77
+ console.log(chalk.green('✔ Transfer removed from tracking.'));
78
+ });
79
+
80
+ cmd
81
+ .command('pause <magnet>')
82
+ .description('pause a transfer')
83
+ .action((magnet) => {
84
+ const active = transferStore.get('active') || [];
85
+ const found = active.find(t => t.magnet === magnet);
86
+ if (!found) {
87
+ console.log(chalk.red('Transfer not found.'));
88
+ process.exitCode = 1; return;
89
+ }
90
+ if (found.paused) {
91
+ console.log(chalk.yellow('Transfer is already paused.'));
92
+ return;
93
+ }
94
+ setTransferPaused(magnet, true);
95
+ console.log(chalk.green('✔ Transfer paused.'));
96
+ const pidPath = path.join(path.dirname(config.path), 'serve.pid');
97
+ if (fs.existsSync(pidPath)) {
98
+ console.log(chalk.yellow('Note: Restart `pe serve` for the change to take effect on the running daemon.'));
99
+ }
100
+ });
101
+
102
+ cmd
103
+ .command('resume <magnet>')
104
+ .description('resume a paused transfer')
105
+ .action((magnet) => {
106
+ const active = transferStore.get('active') || [];
107
+ const found = active.find(t => t.magnet === magnet);
108
+ if (!found) {
109
+ console.log(chalk.red('Transfer not found.'));
110
+ process.exitCode = 1; return;
111
+ }
112
+ if (!found.paused) {
113
+ console.log(chalk.yellow('Transfer is not paused.'));
114
+ return;
115
+ }
116
+ setTransferPaused(magnet, false);
117
+ console.log(chalk.green('✔ Transfer resumed.'));
118
+ const pidPath = path.join(path.dirname(config.path), 'serve.pid');
119
+ if (fs.existsSync(pidPath)) {
120
+ console.log(chalk.yellow('Note: Restart `pe serve` for the change to take effect on the running daemon.'));
121
+ }
122
+ });
123
+
124
+ cmd
125
+ .command('stats')
126
+ .description('show transfer statistics')
127
+ .option('--days <n>', 'Number of days to include', '90')
128
+ .action((opts) => {
129
+ const days = parseInt(opts.days, 10) || 90;
130
+ pruneOldHistory(days);
131
+ const stats = getTransferStats(days);
132
+ console.log('');
133
+ console.log(chalk.cyan(`Transfer Stats (last ${days} days):`));
134
+ console.log(` Total transfers: ${chalk.white(stats.totalTransfers)}`);
135
+ console.log(` Total data: ${chalk.white(formatBytes(stats.totalBytes))}`);
136
+ console.log(` Avg speed: ${chalk.white(formatBytes(stats.avgSpeed) + '/s')}`);
137
+ if (Object.keys(stats.perDay).length > 0) {
138
+ console.log('');
139
+ console.log(chalk.cyan(' Per day:'));
140
+ for (const [day, count] of Object.entries(stats.perDay).sort()) {
141
+ console.log(` ${day}: ${count} transfer(s)`);
142
+ }
143
+ }
144
+ });
145
+ }
146
+
147
+ function formatBytes(bytes) {
148
+ if (bytes === 0) return '0 B';
149
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
150
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
151
+ return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
152
+ }
@@ -0,0 +1,188 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import config from '../utils/config.js';
6
+
7
+ const DATA_PATHS = {
8
+ config: () => config.path,
9
+ configDir: () => path.dirname(config.path),
10
+ logs: () => path.join(os.homedir(), '.pal', 'logs'),
11
+ palDir: () => path.join(os.homedir(), '.pal'),
12
+ keysFile: () => path.join(os.homedir(), '.palexplorer', '.keys.json'),
13
+ keysDir: () => path.join(os.homedir(), '.palexplorer'),
14
+ pidFile: () => path.join(path.dirname(config.path), 'serve.pid'),
15
+ };
16
+
17
+ function rmSafe(p) {
18
+ try {
19
+ if (!fs.existsSync(p)) return false;
20
+ const stat = fs.statSync(p);
21
+ if (stat.isDirectory()) {
22
+ fs.rmSync(p, { recursive: true, force: true });
23
+ } else {
24
+ fs.unlinkSync(p);
25
+ }
26
+ return true;
27
+ } catch { return false; }
28
+ }
29
+
30
+ async function clearKeytar() {
31
+ if (process.platform !== 'win32' && process.platform !== 'darwin') return [];
32
+ const removed = [];
33
+ try {
34
+ const keytar = (await import('keytar')).default;
35
+ // Config encryption key
36
+ const configKey = await keytar.getPassword('pal-explorer-config', 'encryption-key');
37
+ if (configKey) {
38
+ await keytar.deletePassword('pal-explorer-config', 'encryption-key');
39
+ removed.push('pal-explorer-config/encryption-key');
40
+ }
41
+ // Identity private keys
42
+ const creds = await keytar.findCredentials('pal-explorer');
43
+ for (const c of creds) {
44
+ await keytar.deletePassword('pal-explorer', c.account);
45
+ removed.push(`pal-explorer/${c.account.slice(0, 16)}...`);
46
+ }
47
+ } catch {}
48
+ return removed;
49
+ }
50
+
51
+ export default function uninstallCommand(program) {
52
+ program
53
+ .command('uninstall')
54
+ .description('remove Palexplorer app data from this machine')
55
+ .option('--purge', 'Also remove identity, private keys, and OS credential store entries')
56
+ .option('--dry-run', 'Show what would be removed without deleting')
57
+ .addHelpText('after', `
58
+ Examples:
59
+ $ pe uninstall Remove app data (keeps identity & keys)
60
+ $ pe uninstall --purge Remove everything including identity & keys
61
+ $ pe uninstall --dry-run Preview what would be removed
62
+ `)
63
+ .action(async (opts) => {
64
+ const dryRun = opts.dryRun;
65
+ const purge = opts.purge;
66
+
67
+ console.log('');
68
+ if (dryRun) console.log(chalk.yellow('DRY RUN — nothing will be deleted\n'));
69
+
70
+ const items = [];
71
+
72
+ // Always removed
73
+ const configPath = DATA_PATHS.config();
74
+ if (fs.existsSync(configPath)) items.push({ path: configPath, label: 'Config file (shares, settings, friends)', category: 'data' });
75
+
76
+ const pidPath = DATA_PATHS.pidFile();
77
+ if (fs.existsSync(pidPath)) items.push({ path: pidPath, label: 'Daemon PID file', category: 'data' });
78
+
79
+ const logDir = DATA_PATHS.logs();
80
+ if (fs.existsSync(logDir)) items.push({ path: logDir, label: 'Log files', category: 'data' });
81
+
82
+ // Purge-only
83
+ if (purge) {
84
+ const keysFile = DATA_PATHS.keysFile();
85
+ if (fs.existsSync(keysFile)) items.push({ path: keysFile, label: 'Private keys file (.keys.json)', category: 'keys' });
86
+
87
+ const keysDir = DATA_PATHS.keysDir();
88
+ if (fs.existsSync(keysDir)) items.push({ path: keysDir, label: '~/.palexplorer directory', category: 'keys' });
89
+ }
90
+
91
+ // Show what will be removed
92
+ if (items.length === 0 && !purge) {
93
+ console.log(chalk.gray('No app data found to remove.'));
94
+ return;
95
+ }
96
+
97
+ console.log(chalk.cyan('Files to remove:'));
98
+ for (const item of items) {
99
+ const icon = item.category === 'keys' ? chalk.red('🔑') : chalk.yellow('📁');
100
+ console.log(` ${icon} ${chalk.white(item.label)}`);
101
+ console.log(` ${chalk.gray(item.path)}`);
102
+ }
103
+
104
+ if (purge) {
105
+ const keytarEntries = await clearKeytarDryRun();
106
+ if (keytarEntries.length > 0) {
107
+ console.log(` ${chalk.red('🔑')} ${chalk.white('OS credential store entries')}`);
108
+ for (const entry of keytarEntries) {
109
+ console.log(` ${chalk.gray(entry)}`);
110
+ }
111
+ }
112
+ }
113
+
114
+ console.log('');
115
+
116
+ if (!purge) {
117
+ console.log(chalk.gray('Identity and private keys will be kept.'));
118
+ console.log(chalk.gray('Use --purge to also remove identity and keys.\n'));
119
+ } else {
120
+ console.log(chalk.red('⚠ This will permanently delete your identity and private keys.'));
121
+ console.log(chalk.red(' Make sure you have your 24-word recovery phrase backed up!\n'));
122
+ }
123
+
124
+ if (dryRun) {
125
+ console.log(chalk.yellow('No changes made (dry run).'));
126
+ return;
127
+ }
128
+
129
+ // Perform deletion
130
+ let removed = 0;
131
+ for (const item of items) {
132
+ if (rmSafe(item.path)) {
133
+ console.log(chalk.green(` ✔ Removed: ${item.label}`));
134
+ removed++;
135
+ }
136
+ }
137
+
138
+ // Clean up empty parent dirs
139
+ const configDir = DATA_PATHS.configDir();
140
+ if (fs.existsSync(configDir)) {
141
+ try {
142
+ const remaining = fs.readdirSync(configDir);
143
+ if (remaining.length === 0) rmSafe(configDir);
144
+ } catch {}
145
+ }
146
+ const palDir = DATA_PATHS.palDir();
147
+ if (fs.existsSync(palDir)) {
148
+ try {
149
+ const remaining = fs.readdirSync(palDir);
150
+ if (remaining.length === 0) rmSafe(palDir);
151
+ } catch {}
152
+ }
153
+
154
+ if (purge) {
155
+ const keytarRemoved = await clearKeytar();
156
+ for (const entry of keytarRemoved) {
157
+ console.log(chalk.green(` ✔ Removed credential: ${entry}`));
158
+ removed++;
159
+ }
160
+ }
161
+
162
+ console.log('');
163
+ if (removed > 0) {
164
+ console.log(chalk.green(`✔ Removed ${removed} item(s).`));
165
+ } else {
166
+ console.log(chalk.gray('Nothing to remove.'));
167
+ }
168
+
169
+ if (!purge) {
170
+ console.log(chalk.gray('\nTo also remove identity and keys: pe uninstall --purge'));
171
+ } else {
172
+ console.log(chalk.gray('\nTo fully uninstall the CLI: npm rm -g pal-explorer-cli'));
173
+ }
174
+ });
175
+ }
176
+
177
+ async function clearKeytarDryRun() {
178
+ if (process.platform !== 'win32' && process.platform !== 'darwin') return [];
179
+ const entries = [];
180
+ try {
181
+ const keytar = (await import('keytar')).default;
182
+ const configKey = await keytar.getPassword('pal-explorer-config', 'encryption-key');
183
+ if (configKey) entries.push('pal-explorer-config/encryption-key');
184
+ const creds = await keytar.findCredentials('pal-explorer');
185
+ for (const c of creds) entries.push(`pal-explorer/${c.account.slice(0, 16)}...`);
186
+ } catch {}
187
+ return entries;
188
+ }
@@ -0,0 +1,204 @@
1
+ import chalk from 'chalk';
2
+ import crypto from 'crypto';
3
+ import { execSync, spawnSync } from 'child_process';
4
+ import fs from 'fs';
5
+ import os from 'os';
6
+ import path from 'path';
7
+ import { pipeline } from 'stream/promises';
8
+ import { createWriteStream } from 'fs';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const ROOT = path.resolve(__dirname, '../..');
13
+ const pkgPath = path.join(ROOT, 'package.json');
14
+
15
+ import { getPrimaryServer } from '../core/discoveryClient.js';
16
+ const UPDATE_SERVER = getPrimaryServer();
17
+
18
+ function detectPlatform() {
19
+ const p = os.platform();
20
+ if (p === 'win32') return 'win';
21
+ if (p === 'darwin') return 'mac';
22
+ if (p === 'linux') return 'linux';
23
+ return p;
24
+ }
25
+
26
+ export default function updateCommand(program) {
27
+ const cmd = program
28
+ .command('update')
29
+ .description('check for updates and install them')
30
+ .option('--force', 'Force update even if on latest version')
31
+ .addHelpText('after', `
32
+ Examples:
33
+ $ pe update check Check if a new version is available
34
+ $ pe update Download and install the latest version
35
+ $ pe update --force Force reinstall even if up to date
36
+ `)
37
+ .action(async (opts) => {
38
+ console.log(chalk.cyan('Checking for updates...'));
39
+ const result = await checkForUpdate();
40
+ if (!result) {
41
+ console.log(chalk.gray('Could not check for updates.'));
42
+ return;
43
+ }
44
+ if (!result.hasUpdate && !opts.force) {
45
+ console.log(chalk.green(`You're on the latest version (${result.current}).`));
46
+ return;
47
+ }
48
+
49
+ if (result.hasUpdate) {
50
+ console.log(chalk.yellow(`Update available: ${result.current} → ${result.latest}`));
51
+ if (result.forced) {
52
+ console.log(chalk.red(`This update is required (minimum version: ${result.minVersion}).`));
53
+ }
54
+ if (result.releaseNotes) {
55
+ console.log(chalk.gray(result.releaseNotes));
56
+ }
57
+ }
58
+
59
+ if (!result.downloadUrl) {
60
+ console.log(chalk.gray('No download URL available for this platform.'));
61
+ return;
62
+ }
63
+
64
+ await installUpdate(result);
65
+ });
66
+
67
+ cmd
68
+ .command('check')
69
+ .description('check if a new version is available')
70
+ .action(async () => {
71
+ const result = await checkForUpdate();
72
+ if (!result) {
73
+ console.log(chalk.gray('Could not check for updates.'));
74
+ return;
75
+ }
76
+ if (result.hasUpdate) {
77
+ console.log(chalk.yellow(`Update available: ${result.current} → ${result.latest}`));
78
+ if (result.releaseNotes) console.log(chalk.gray(result.releaseNotes));
79
+ } else {
80
+ console.log(chalk.green(`Up to date (${result.current}).`));
81
+ }
82
+ });
83
+ }
84
+
85
+ export async function checkForUpdate() {
86
+ try {
87
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
88
+ const current = pkg.version;
89
+ const platform = detectPlatform();
90
+ const url = `${UPDATE_SERVER}/api/v1/updates/check?platform=cli&version=${current}&channel=stable`;
91
+ const res = await fetch(url, {
92
+ signal: AbortSignal.timeout(5000),
93
+ headers: { 'User-Agent': `palexplorer-cli/${current}` },
94
+ });
95
+ if (!res.ok) return null;
96
+ const data = await res.json();
97
+ return {
98
+ current,
99
+ latest: data.latestVersion || current,
100
+ hasUpdate: !!data.available,
101
+ forced: !!data.forced,
102
+ minVersion: data.minVersion,
103
+ downloadUrl: data.downloadUrl,
104
+ sha256: data.sha256,
105
+ releaseNotes: data.releaseNotes,
106
+ };
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ async function installUpdate(update) {
113
+ const tmpDir = path.join(os.tmpdir(), `palexplorer-update-${Date.now()}`);
114
+ const tarballPath = path.join(tmpDir, 'update.tar.gz');
115
+
116
+ try {
117
+ fs.mkdirSync(tmpDir, { recursive: true });
118
+
119
+ // Download
120
+ console.log(chalk.cyan(`Downloading v${update.latest}...`));
121
+ const res = await fetch(update.downloadUrl, { signal: AbortSignal.timeout(120000) });
122
+ if (!res.ok) throw new Error(`Download failed: HTTP ${res.status}`);
123
+ await pipeline(res.body, createWriteStream(tarballPath));
124
+
125
+ const size = fs.statSync(tarballPath).size;
126
+ console.log(chalk.gray(` Downloaded ${(size / 1024 / 1024).toFixed(1)} MB`));
127
+
128
+ // Verify sha256 if provided
129
+ if (update.sha256) {
130
+ const hash = crypto.createHash('sha256').update(fs.readFileSync(tarballPath)).digest('hex');
131
+ if (hash !== update.sha256) {
132
+ throw new Error(`Integrity check failed. Expected ${update.sha256}, got ${hash}`);
133
+ }
134
+ console.log(chalk.gray(' Integrity verified (sha256)'));
135
+ }
136
+
137
+ // Extract
138
+ console.log(chalk.cyan('Installing...'));
139
+ const extractDir = path.join(tmpDir, 'extracted');
140
+ fs.mkdirSync(extractDir, { recursive: true });
141
+ const tarResult = spawnSync('tar', ['-xzf', tarballPath, '-C', extractDir], { stdio: 'pipe' });
142
+ if (tarResult.status !== 0) throw new Error('Failed to extract update package');
143
+
144
+ // Find the package root inside the tarball (may be nested in a directory)
145
+ let sourceDir = extractDir;
146
+ const entries = fs.readdirSync(extractDir);
147
+ if (entries.length === 1 && fs.statSync(path.join(extractDir, entries[0])).isDirectory()) {
148
+ sourceDir = path.join(extractDir, entries[0]);
149
+ }
150
+
151
+ // Verify it's a valid palexplorer package
152
+ const newPkgPath = path.join(sourceDir, 'package.json');
153
+ if (!fs.existsSync(newPkgPath)) {
154
+ throw new Error('Invalid update package: no package.json found');
155
+ }
156
+ const newPkg = JSON.parse(fs.readFileSync(newPkgPath, 'utf8'));
157
+ if (newPkg.name !== 'palexplorer') {
158
+ throw new Error(`Invalid update package: expected palexplorer, got ${newPkg.name}`);
159
+ }
160
+
161
+ // Backup current version
162
+ const backupDir = path.join(os.tmpdir(), `palexplorer-backup-${Date.now()}`);
163
+ console.log(chalk.gray(` Backing up current version to ${backupDir}`));
164
+ fs.mkdirSync(backupDir, { recursive: true });
165
+ for (const item of ['lib', 'bin', 'package.json']) {
166
+ const src = path.join(ROOT, item);
167
+ const dest = path.join(backupDir, item);
168
+ if (fs.existsSync(src)) {
169
+ fs.cpSync(src, dest, { recursive: true });
170
+ }
171
+ }
172
+
173
+ // Copy new files over current installation
174
+ for (const item of ['lib', 'bin', 'package.json']) {
175
+ const src = path.join(sourceDir, item);
176
+ const dest = path.join(ROOT, item);
177
+ if (fs.existsSync(src)) {
178
+ if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
179
+ fs.cpSync(src, dest, { recursive: true });
180
+ }
181
+ }
182
+
183
+ // Install dependencies if node_modules not bundled
184
+ const newNodeModules = path.join(sourceDir, 'node_modules');
185
+ if (fs.existsSync(newNodeModules)) {
186
+ console.log(chalk.gray(' Using bundled node_modules'));
187
+ fs.cpSync(newNodeModules, path.join(ROOT, 'node_modules'), { recursive: true });
188
+ } else {
189
+ console.log(chalk.gray(' Installing dependencies...'));
190
+ execSync('npm install --production', { cwd: ROOT, stdio: 'pipe' });
191
+ }
192
+
193
+ console.log(chalk.green(`\nUpdated to v${newPkg.version}!`));
194
+ console.log(chalk.gray(`Backup saved to: ${backupDir}`));
195
+ console.log(chalk.gray('Run `pe --version` to verify.'));
196
+
197
+ } catch (err) {
198
+ console.error(chalk.red(`Update failed: ${err.message}`));
199
+ console.log(chalk.gray('Your current installation is unchanged.'));
200
+ } finally {
201
+ // Clean up temp dir
202
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
203
+ }
204
+ }