pal-explorer-cli 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +18 -0
- package/README.md +314 -0
- package/bin/pal.js +230 -0
- package/extensions/@palexplorer/analytics/README.md +45 -0
- package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
- package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
- package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
- package/extensions/@palexplorer/analytics/extension.json +27 -0
- package/extensions/@palexplorer/analytics/index.js +186 -0
- package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
- package/extensions/@palexplorer/audit/extension.json +17 -0
- package/extensions/@palexplorer/audit/index.js +2 -0
- package/extensions/@palexplorer/auth-email/extension.json +17 -0
- package/extensions/@palexplorer/auth-email/index.js +102 -0
- package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
- package/extensions/@palexplorer/auth-oauth/index.js +199 -0
- package/extensions/@palexplorer/chat/extension.json +17 -0
- package/extensions/@palexplorer/chat/index.js +2 -0
- package/extensions/@palexplorer/discovery/extension.json +16 -0
- package/extensions/@palexplorer/discovery/index.js +111 -0
- package/extensions/@palexplorer/email-notifications/extension.json +23 -0
- package/extensions/@palexplorer/email-notifications/index.js +242 -0
- package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
- package/extensions/@palexplorer/explorer-integration/index.js +122 -0
- package/extensions/@palexplorer/groups/extension.json +17 -0
- package/extensions/@palexplorer/groups/index.js +2 -0
- package/extensions/@palexplorer/networks/extension.json +17 -0
- package/extensions/@palexplorer/networks/index.js +2 -0
- package/extensions/@palexplorer/share-links/extension.json +17 -0
- package/extensions/@palexplorer/share-links/index.js +2 -0
- package/extensions/@palexplorer/sync/extension.json +17 -0
- package/extensions/@palexplorer/sync/index.js +2 -0
- package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
- package/extensions/@palexplorer/user-mgmt/index.js +2 -0
- package/extensions/@palexplorer/vfs/extension.json +17 -0
- package/extensions/@palexplorer/vfs/index.js +167 -0
- package/lib/capabilities.js +263 -0
- package/lib/commands/analytics.js +175 -0
- package/lib/commands/api-keys.js +131 -0
- package/lib/commands/audit.js +235 -0
- package/lib/commands/auth.js +137 -0
- package/lib/commands/backup.js +76 -0
- package/lib/commands/billing.js +148 -0
- package/lib/commands/chat.js +217 -0
- package/lib/commands/cloud-backup.js +231 -0
- package/lib/commands/comment.js +99 -0
- package/lib/commands/completion.js +203 -0
- package/lib/commands/compliance.js +218 -0
- package/lib/commands/config.js +136 -0
- package/lib/commands/connect.js +44 -0
- package/lib/commands/dept.js +294 -0
- package/lib/commands/device.js +146 -0
- package/lib/commands/download.js +226 -0
- package/lib/commands/explorer.js +178 -0
- package/lib/commands/extension.js +970 -0
- package/lib/commands/favorite.js +90 -0
- package/lib/commands/federation.js +270 -0
- package/lib/commands/file.js +533 -0
- package/lib/commands/group.js +271 -0
- package/lib/commands/gui-share.js +29 -0
- package/lib/commands/init.js +61 -0
- package/lib/commands/invite.js +59 -0
- package/lib/commands/list.js +59 -0
- package/lib/commands/log.js +116 -0
- package/lib/commands/nearby.js +108 -0
- package/lib/commands/network.js +251 -0
- package/lib/commands/notify.js +198 -0
- package/lib/commands/org.js +273 -0
- package/lib/commands/pal.js +180 -0
- package/lib/commands/permissions.js +216 -0
- package/lib/commands/pin.js +97 -0
- package/lib/commands/protocol.js +357 -0
- package/lib/commands/rbac.js +147 -0
- package/lib/commands/recover.js +36 -0
- package/lib/commands/register.js +171 -0
- package/lib/commands/relay.js +131 -0
- package/lib/commands/remote.js +368 -0
- package/lib/commands/revoke.js +50 -0
- package/lib/commands/scanner.js +280 -0
- package/lib/commands/schedule.js +344 -0
- package/lib/commands/scim.js +203 -0
- package/lib/commands/search.js +181 -0
- package/lib/commands/serve.js +438 -0
- package/lib/commands/server.js +350 -0
- package/lib/commands/share-link.js +199 -0
- package/lib/commands/share.js +323 -0
- package/lib/commands/sso.js +200 -0
- package/lib/commands/status.js +136 -0
- package/lib/commands/stream.js +562 -0
- package/lib/commands/su.js +187 -0
- package/lib/commands/sync.js +827 -0
- package/lib/commands/transfers.js +152 -0
- package/lib/commands/uninstall.js +188 -0
- package/lib/commands/update.js +204 -0
- package/lib/commands/user.js +276 -0
- package/lib/commands/vfs.js +84 -0
- package/lib/commands/web.js +52 -0
- package/lib/commands/webhook.js +180 -0
- package/lib/commands/whoami.js +59 -0
- package/lib/commands/workspace.js +121 -0
- package/lib/core/accessLog.js +54 -0
- package/lib/core/analytics.js +99 -0
- package/lib/core/backup.js +84 -0
- package/lib/core/billing.js +336 -0
- package/lib/core/bitfieldStore.js +53 -0
- package/lib/core/connectionManager.js +182 -0
- package/lib/core/dhtDiscovery.js +148 -0
- package/lib/core/discoveryClient.js +408 -0
- package/lib/core/extensionAnalyzer.js +357 -0
- package/lib/core/extensionSandbox.js +250 -0
- package/lib/core/extensionWorkerHost.js +166 -0
- package/lib/core/extensions.js +1082 -0
- package/lib/core/fileDiff.js +69 -0
- package/lib/core/groups.js +119 -0
- package/lib/core/identity.js +340 -0
- package/lib/core/mdnsService.js +126 -0
- package/lib/core/networks.js +81 -0
- package/lib/core/permissions.js +109 -0
- package/lib/core/pro.js +27 -0
- package/lib/core/resolver.js +74 -0
- package/lib/core/serverList.js +224 -0
- package/lib/core/sharePolicy.js +69 -0
- package/lib/core/shares.js +325 -0
- package/lib/core/signalingServer.js +441 -0
- package/lib/core/streamTransport.js +106 -0
- package/lib/core/su.js +55 -0
- package/lib/core/syncEngine.js +264 -0
- package/lib/core/syncState.js +159 -0
- package/lib/core/transfers.js +259 -0
- package/lib/core/users.js +225 -0
- package/lib/core/vfs.js +216 -0
- package/lib/core/webServer.js +702 -0
- package/lib/core/webrtcStream.js +396 -0
- package/lib/crypto/chatEncryption.js +57 -0
- package/lib/crypto/shareEncryption.js +195 -0
- package/lib/crypto/sharePassword.js +35 -0
- package/lib/crypto/streamEncryption.js +189 -0
- package/lib/package.json +1 -0
- package/lib/protocol/envelope.js +271 -0
- package/lib/protocol/handler.js +191 -0
- package/lib/protocol/index.js +27 -0
- package/lib/protocol/messages.js +247 -0
- package/lib/protocol/negotiation.js +127 -0
- package/lib/protocol/policy.js +142 -0
- package/lib/protocol/router.js +86 -0
- package/lib/protocol/sync.js +122 -0
- package/lib/utils/cli.js +15 -0
- package/lib/utils/config.js +123 -0
- package/lib/utils/configIntegrity.js +87 -0
- package/lib/utils/downloadDir.js +9 -0
- package/lib/utils/explorer.js +83 -0
- package/lib/utils/format.js +12 -0
- package/lib/utils/help.js +357 -0
- package/lib/utils/logger.js +103 -0
- package/lib/utils/torrent.js +203 -0
- package/package.json +71 -0
|
@@ -0,0 +1,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
|
+
}
|