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.
- 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
|
@@ -1,970 +1,1060 @@
|
|
|
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 readline from 'readline';
|
|
8
|
-
import config from '../utils/config.js';
|
|
9
|
-
|
|
10
|
-
async function getApiKey() {
|
|
11
|
-
if (process.env.PAL_API_KEY) return process.env.PAL_API_KEY;
|
|
12
|
-
try {
|
|
13
|
-
const keytar = (await import('keytar')).default;
|
|
14
|
-
const key = await keytar.getPassword('palexplorer', 'apiKey');
|
|
15
|
-
if (key) return key;
|
|
16
|
-
} catch {}
|
|
17
|
-
return config.get('apiKey');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function confirm(question) {
|
|
21
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
22
|
-
return new Promise(resolve => {
|
|
23
|
-
rl.question(question, answer => {
|
|
24
|
-
rl.close();
|
|
25
|
-
resolve(answer.toLowerCase().startsWith('y'));
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default function extensionCommand(program) {
|
|
31
|
-
const cmdName = process.argv[2] === 'extension' ? 'extension' : 'ext';
|
|
32
|
-
const cmd = program
|
|
33
|
-
.command(cmdName)
|
|
34
|
-
.description('manage extensions (install, remove, enable, disable)')
|
|
35
|
-
.addHelpText('after', `
|
|
36
|
-
Examples:
|
|
37
|
-
$
|
|
38
|
-
$
|
|
39
|
-
$
|
|
40
|
-
$
|
|
41
|
-
$
|
|
42
|
-
$
|
|
43
|
-
$
|
|
44
|
-
$
|
|
45
|
-
$
|
|
46
|
-
$
|
|
47
|
-
`)
|
|
48
|
-
.action(async () => {
|
|
49
|
-
await listExtensions();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
cmd
|
|
53
|
-
.command('list')
|
|
54
|
-
.description('list installed extensions')
|
|
55
|
-
.action(async () => { await listExtensions(); });
|
|
56
|
-
|
|
57
|
-
cmd
|
|
58
|
-
.command('install <source>')
|
|
59
|
-
.description('install extension from path or git URL')
|
|
60
|
-
.option('-y, --yes', 'Skip confirmation prompt')
|
|
61
|
-
.action(async (source, opts) => {
|
|
62
|
-
try {
|
|
63
|
-
const { installExtension, HIGH_RISK_PERMISSIONS } = await import('../core/extensions.js');
|
|
64
|
-
console.log(chalk.cyan(`Installing extension from ${source}...`));
|
|
65
|
-
const result = await installExtension(source);
|
|
66
|
-
|
|
67
|
-
// Show security info
|
|
68
|
-
const riskColor = result.risk === 'high' ? chalk.red : result.risk === 'medium' ? chalk.yellow : chalk.green;
|
|
69
|
-
console.log('');
|
|
70
|
-
console.log(` Name: ${chalk.white(result.name)} v${result.version}`);
|
|
71
|
-
console.log(` Risk: ${riskColor(result.risk.toUpperCase())}`);
|
|
72
|
-
console.log(` Signed: ${result.signature?.verified ? chalk.green('verified ✔') : chalk.yellow('not signed')}`);
|
|
73
|
-
|
|
74
|
-
if (result.permissions?.length) {
|
|
75
|
-
console.log(` Permissions:`);
|
|
76
|
-
for (const p of result.permissions) {
|
|
77
|
-
const isHigh = HIGH_RISK_PERMISSIONS.has(p) || p.startsWith('exec:');
|
|
78
|
-
console.log(` ${isHigh ? chalk.red('⚠') : chalk.gray('·')} ${isHigh ? chalk.red(p) : p}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Require confirmation for unsigned or high-risk extensions
|
|
83
|
-
if (!opts.yes && (result.risk !== 'low' || !result.signature?.verified)) {
|
|
84
|
-
console.log('');
|
|
85
|
-
if (!result.signature?.verified) {
|
|
86
|
-
console.log(chalk.yellow(' ⚠ This extension is NOT signed. It has not been reviewed by the Palexplorer team.'));
|
|
87
|
-
}
|
|
88
|
-
if (result.risk === 'high') {
|
|
89
|
-
console.log(chalk.red(' ⚠ HIGH RISK: This extension requests dangerous permissions.'));
|
|
90
|
-
}
|
|
91
|
-
const ok = await confirm(chalk.white('\n Proceed with installation? (y/N) '));
|
|
92
|
-
if (!ok) {
|
|
93
|
-
// Remove the already-copied files
|
|
94
|
-
const { removeExtension } = await import('../core/extensions.js');
|
|
95
|
-
try { removeExtension(result.name); } catch {}
|
|
96
|
-
console.log(chalk.gray(' Installation cancelled.'));
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
console.log(chalk.green(`\n✔ Installed "${result.name}" v${result.version}`));
|
|
102
|
-
} catch (err) {
|
|
103
|
-
console.error(chalk.red(`Install failed: ${err.message}`));
|
|
104
|
-
process.exitCode = 1;
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
cmd
|
|
109
|
-
.command('remove <name>')
|
|
110
|
-
.description('uninstall an extension')
|
|
111
|
-
.action(async (extName) => {
|
|
112
|
-
try {
|
|
113
|
-
const { removeExtension } = await import('../core/extensions.js');
|
|
114
|
-
removeExtension(extName);
|
|
115
|
-
console.log(chalk.green(`✔ Removed "${extName}"`));
|
|
116
|
-
} catch (err) {
|
|
117
|
-
console.error(chalk.red(`Remove failed: ${err.message}`));
|
|
118
|
-
process.exitCode = 1;
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
cmd
|
|
123
|
-
.command('enable <name>')
|
|
124
|
-
.description('enable a disabled extension')
|
|
125
|
-
.action(async (extName) => {
|
|
126
|
-
const { enableExtension } = await import('../core/extensions.js');
|
|
127
|
-
enableExtension(extName);
|
|
128
|
-
console.log(chalk.green(`✔ Enabled "${extName}"`));
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
cmd
|
|
132
|
-
.command('disable <name>')
|
|
133
|
-
.description('disable an extension without uninstalling')
|
|
134
|
-
.action(async (extName) => {
|
|
135
|
-
const { disableExtension } = await import('../core/extensions.js');
|
|
136
|
-
disableExtension(extName);
|
|
137
|
-
console.log(chalk.green(`✔ Disabled "${extName}"`));
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
cmd
|
|
141
|
-
.command('info <name>')
|
|
142
|
-
.description('show extension details')
|
|
143
|
-
.action(async (extName) => {
|
|
144
|
-
const { getInstalledExtensions, getExtensionConfig } = await import('../core/extensions.js');
|
|
145
|
-
const extensions = getInstalledExtensions();
|
|
146
|
-
const ext = extensions.find(e => e.name === extName || `@palexplorer/${e.name}` === extName);
|
|
147
|
-
if (!ext) {
|
|
148
|
-
console.log(chalk.red(`Extension "${extName}" not found.`));
|
|
149
|
-
process.exitCode = 1;
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
console.log('');
|
|
153
|
-
console.log(chalk.cyan(ext.bundled ? `@palexplorer/${ext.name}` : ext.name));
|
|
154
|
-
console.log(` Version: ${chalk.white(ext.version)}`);
|
|
155
|
-
console.log(` Description: ${chalk.gray(ext.description || 'N/A')}`);
|
|
156
|
-
console.log(` Author: ${chalk.gray(ext.author || 'N/A')}`);
|
|
157
|
-
console.log(` Status: ${ext.enabled ? chalk.green('enabled') : chalk.red('disabled')}`);
|
|
158
|
-
console.log(` Bundled: ${ext.bundled ? chalk.blue('yes') : 'no'}`);
|
|
159
|
-
const tier = ext.tier || (ext.pro ? 'pro' : 'free');
|
|
160
|
-
const tierColor = tier === 'enterprise' ? chalk.magenta : tier === 'pro' ? chalk.yellow : chalk.green;
|
|
161
|
-
console.log(` Tier: ${tierColor(tier.toUpperCase())}${ext.price ? ` (${ext.price})` : ''}`);
|
|
162
|
-
if (ext.hooks?.length) {
|
|
163
|
-
console.log(` Hooks: ${chalk.gray(ext.hooks.join(', '))}`);
|
|
164
|
-
}
|
|
165
|
-
if (ext.permissions?.length) {
|
|
166
|
-
console.log(` Permissions:`);
|
|
167
|
-
for (const p of ext.permissions) {
|
|
168
|
-
console.log(` - ${p}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
const cfg = getExtensionConfig(extName);
|
|
172
|
-
if (Object.keys(cfg).length) {
|
|
173
|
-
console.log(` Config:`);
|
|
174
|
-
for (const [k, v] of Object.entries(cfg)) {
|
|
175
|
-
console.log(` ${k}: ${chalk.white(JSON.stringify(v))}`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
cmd
|
|
181
|
-
.command('
|
|
182
|
-
.description('
|
|
183
|
-
.action(async (extName
|
|
184
|
-
const { getExtensionConfig,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
console.log(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
console.log(
|
|
232
|
-
|
|
233
|
-
console.log(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
.
|
|
276
|
-
.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
console.log(chalk.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
console.
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
console.log(chalk.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
if (!
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
console.
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
console.
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
console.
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
};
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
`);
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
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 readline from 'readline';
|
|
8
|
+
import config from '../utils/config.js';
|
|
9
|
+
|
|
10
|
+
async function getApiKey() {
|
|
11
|
+
if (process.env.PAL_API_KEY) return process.env.PAL_API_KEY;
|
|
12
|
+
try {
|
|
13
|
+
const keytar = (await import('keytar')).default;
|
|
14
|
+
const key = await keytar.getPassword('palexplorer', 'apiKey');
|
|
15
|
+
if (key) return key;
|
|
16
|
+
} catch {}
|
|
17
|
+
return config.get('apiKey');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function confirm(question) {
|
|
21
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
22
|
+
return new Promise(resolve => {
|
|
23
|
+
rl.question(question, answer => {
|
|
24
|
+
rl.close();
|
|
25
|
+
resolve(answer.toLowerCase().startsWith('y'));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function extensionCommand(program) {
|
|
31
|
+
const cmdName = process.argv[2] === 'extension' ? 'extension' : 'ext';
|
|
32
|
+
const cmd = program
|
|
33
|
+
.command(cmdName)
|
|
34
|
+
.description('manage extensions (install, remove, enable, disable)')
|
|
35
|
+
.addHelpText('after', `
|
|
36
|
+
Examples:
|
|
37
|
+
$ pal ext list List installed extensions
|
|
38
|
+
$ pal ext install ./my-extension Install from local path
|
|
39
|
+
$ pal ext install https://github.com/user/pe-ext-slack.git
|
|
40
|
+
$ pal ext remove slack-notify Uninstall an extension
|
|
41
|
+
$ pal ext enable slack-notify Enable a disabled extension
|
|
42
|
+
$ pal ext disable slack-notify Disable without uninstalling
|
|
43
|
+
$ pal ext info slack-notify Show extension details
|
|
44
|
+
$ pal ext config slack-notify webhookUrl https://hooks.slack.com/...
|
|
45
|
+
$ pal ext create my-extension Scaffold a new extension
|
|
46
|
+
$ pal ext audit Security audit all extensions
|
|
47
|
+
`)
|
|
48
|
+
.action(async () => {
|
|
49
|
+
await listExtensions();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
cmd
|
|
53
|
+
.command('list')
|
|
54
|
+
.description('list installed extensions')
|
|
55
|
+
.action(async () => { await listExtensions(); });
|
|
56
|
+
|
|
57
|
+
cmd
|
|
58
|
+
.command('install <source>')
|
|
59
|
+
.description('install extension from path or git URL')
|
|
60
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
61
|
+
.action(async (source, opts) => {
|
|
62
|
+
try {
|
|
63
|
+
const { installExtension, HIGH_RISK_PERMISSIONS } = await import('../core/extensions.js');
|
|
64
|
+
console.log(chalk.cyan(`Installing extension from ${source}...`));
|
|
65
|
+
const result = await installExtension(source);
|
|
66
|
+
|
|
67
|
+
// Show security info
|
|
68
|
+
const riskColor = result.risk === 'high' ? chalk.red : result.risk === 'medium' ? chalk.yellow : chalk.green;
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log(` Name: ${chalk.white(result.name)} v${result.version}`);
|
|
71
|
+
console.log(` Risk: ${riskColor(result.risk.toUpperCase())}`);
|
|
72
|
+
console.log(` Signed: ${result.signature?.verified ? chalk.green('verified ✔') : chalk.yellow('not signed')}`);
|
|
73
|
+
|
|
74
|
+
if (result.permissions?.length) {
|
|
75
|
+
console.log(` Permissions:`);
|
|
76
|
+
for (const p of result.permissions) {
|
|
77
|
+
const isHigh = HIGH_RISK_PERMISSIONS.has(p) || p.startsWith('exec:');
|
|
78
|
+
console.log(` ${isHigh ? chalk.red('⚠') : chalk.gray('·')} ${isHigh ? chalk.red(p) : p}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Require confirmation for unsigned or high-risk extensions
|
|
83
|
+
if (!opts.yes && (result.risk !== 'low' || !result.signature?.verified)) {
|
|
84
|
+
console.log('');
|
|
85
|
+
if (!result.signature?.verified) {
|
|
86
|
+
console.log(chalk.yellow(' ⚠ This extension is NOT signed. It has not been reviewed by the Palexplorer team.'));
|
|
87
|
+
}
|
|
88
|
+
if (result.risk === 'high') {
|
|
89
|
+
console.log(chalk.red(' ⚠ HIGH RISK: This extension requests dangerous permissions.'));
|
|
90
|
+
}
|
|
91
|
+
const ok = await confirm(chalk.white('\n Proceed with installation? (y/N) '));
|
|
92
|
+
if (!ok) {
|
|
93
|
+
// Remove the already-copied files
|
|
94
|
+
const { removeExtension } = await import('../core/extensions.js');
|
|
95
|
+
try { removeExtension(result.name); } catch {}
|
|
96
|
+
console.log(chalk.gray(' Installation cancelled.'));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(chalk.green(`\n✔ Installed "${result.name}" v${result.version}`));
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error(chalk.red(`Install failed: ${err.message}`));
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
cmd
|
|
109
|
+
.command('remove <name>')
|
|
110
|
+
.description('uninstall an extension')
|
|
111
|
+
.action(async (extName) => {
|
|
112
|
+
try {
|
|
113
|
+
const { removeExtension } = await import('../core/extensions.js');
|
|
114
|
+
removeExtension(extName);
|
|
115
|
+
console.log(chalk.green(`✔ Removed "${extName}"`));
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error(chalk.red(`Remove failed: ${err.message}`));
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
cmd
|
|
123
|
+
.command('enable <name>')
|
|
124
|
+
.description('enable a disabled extension')
|
|
125
|
+
.action(async (extName) => {
|
|
126
|
+
const { enableExtension } = await import('../core/extensions.js');
|
|
127
|
+
enableExtension(extName);
|
|
128
|
+
console.log(chalk.green(`✔ Enabled "${extName}"`));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
cmd
|
|
132
|
+
.command('disable <name>')
|
|
133
|
+
.description('disable an extension without uninstalling')
|
|
134
|
+
.action(async (extName) => {
|
|
135
|
+
const { disableExtension } = await import('../core/extensions.js');
|
|
136
|
+
disableExtension(extName);
|
|
137
|
+
console.log(chalk.green(`✔ Disabled "${extName}"`));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
cmd
|
|
141
|
+
.command('info <name>')
|
|
142
|
+
.description('show extension details')
|
|
143
|
+
.action(async (extName) => {
|
|
144
|
+
const { getInstalledExtensions, getExtensionConfig } = await import('../core/extensions.js');
|
|
145
|
+
const extensions = getInstalledExtensions();
|
|
146
|
+
const ext = extensions.find(e => e.name === extName || `@palexplorer/${e.name}` === extName);
|
|
147
|
+
if (!ext) {
|
|
148
|
+
console.log(chalk.red(`Extension "${extName}" not found.`));
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(chalk.cyan(ext.bundled ? `@palexplorer/${ext.name}` : ext.name));
|
|
154
|
+
console.log(` Version: ${chalk.white(ext.version)}`);
|
|
155
|
+
console.log(` Description: ${chalk.gray(ext.description || 'N/A')}`);
|
|
156
|
+
console.log(` Author: ${chalk.gray(ext.author || 'N/A')}`);
|
|
157
|
+
console.log(` Status: ${ext.enabled ? chalk.green('enabled') : chalk.red('disabled')}`);
|
|
158
|
+
console.log(` Bundled: ${ext.bundled ? chalk.blue('yes') : 'no'}`);
|
|
159
|
+
const tier = ext.tier || (ext.pro ? 'pro' : 'free');
|
|
160
|
+
const tierColor = tier === 'enterprise' ? chalk.magenta : tier === 'pro' ? chalk.yellow : chalk.green;
|
|
161
|
+
console.log(` Tier: ${tierColor(tier.toUpperCase())}${ext.price ? ` (${ext.price})` : ''}`);
|
|
162
|
+
if (ext.hooks?.length) {
|
|
163
|
+
console.log(` Hooks: ${chalk.gray(ext.hooks.join(', '))}`);
|
|
164
|
+
}
|
|
165
|
+
if (ext.permissions?.length) {
|
|
166
|
+
console.log(` Permissions:`);
|
|
167
|
+
for (const p of ext.permissions) {
|
|
168
|
+
console.log(` - ${p}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const cfg = getExtensionConfig(extName);
|
|
172
|
+
if (Object.keys(cfg).length) {
|
|
173
|
+
console.log(` Config:`);
|
|
174
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
175
|
+
console.log(` ${k}: ${chalk.white(JSON.stringify(v))}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
cmd
|
|
181
|
+
.command('help <name>')
|
|
182
|
+
.description('show extension help and usage')
|
|
183
|
+
.action(async (extName) => {
|
|
184
|
+
const { getInstalledExtensions, getExtensionConfig, readManifest } = await import('../core/extensions.js');
|
|
185
|
+
const extensions = getInstalledExtensions();
|
|
186
|
+
const ext = extensions.find(e => e.name === extName || `@palexplorer/${e.name}` === extName);
|
|
187
|
+
if (!ext) {
|
|
188
|
+
console.log(chalk.red(`Extension "${extName}" not found.`));
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const manifest = readManifest(ext.path);
|
|
194
|
+
const help = manifest?.help;
|
|
195
|
+
const fullName = ext.bundled ? `@palexplorer/${ext.name}` : ext.name;
|
|
196
|
+
|
|
197
|
+
console.log('');
|
|
198
|
+
console.log(chalk.cyan.bold(fullName) + chalk.gray(` v${ext.version}`));
|
|
199
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
200
|
+
|
|
201
|
+
// Summary / description
|
|
202
|
+
console.log(chalk.white(help?.summary || ext.description || 'No description.'));
|
|
203
|
+
console.log('');
|
|
204
|
+
|
|
205
|
+
// Usage
|
|
206
|
+
if (help?.usage) {
|
|
207
|
+
console.log(chalk.yellow.bold('Usage'));
|
|
208
|
+
console.log(` ${help.usage}`);
|
|
209
|
+
console.log('');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Examples
|
|
213
|
+
if (help?.examples?.length) {
|
|
214
|
+
console.log(chalk.yellow.bold('Examples'));
|
|
215
|
+
for (const ex of help.examples) {
|
|
216
|
+
console.log(` ${chalk.green('$')} ${ex}`);
|
|
217
|
+
}
|
|
218
|
+
console.log('');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Config reference
|
|
222
|
+
const configSchema = manifest?.config;
|
|
223
|
+
if (help?.configReference && Object.keys(help.configReference).length) {
|
|
224
|
+
console.log(chalk.yellow.bold('Configuration'));
|
|
225
|
+
for (const [key, desc] of Object.entries(help.configReference)) {
|
|
226
|
+
const def = configSchema?.[key]?.default;
|
|
227
|
+
const defStr = def !== undefined ? chalk.gray(` (default: ${JSON.stringify(def)})`) : '';
|
|
228
|
+
console.log(` ${chalk.white(key)}${defStr}`);
|
|
229
|
+
console.log(` ${chalk.gray(desc)}`);
|
|
230
|
+
}
|
|
231
|
+
console.log('');
|
|
232
|
+
} else if (configSchema && Object.keys(configSchema).length) {
|
|
233
|
+
console.log(chalk.yellow.bold('Configuration'));
|
|
234
|
+
for (const [key, schema] of Object.entries(configSchema)) {
|
|
235
|
+
const def = schema.default !== undefined ? chalk.gray(` (default: ${JSON.stringify(schema.default)})`) : '';
|
|
236
|
+
console.log(` ${chalk.white(key)}${def}`);
|
|
237
|
+
if (schema.description) console.log(` ${chalk.gray(schema.description)}`);
|
|
238
|
+
}
|
|
239
|
+
console.log('');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Links
|
|
243
|
+
if (help?.links && Object.keys(help.links).length) {
|
|
244
|
+
console.log(chalk.yellow.bold('Links'));
|
|
245
|
+
for (const [label, url] of Object.entries(help.links)) {
|
|
246
|
+
console.log(` ${chalk.white(label)}: ${chalk.blue(url)}`);
|
|
247
|
+
}
|
|
248
|
+
console.log('');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Quick commands
|
|
252
|
+
console.log(chalk.gray(` pal ext info ${ext.name} Show metadata`));
|
|
253
|
+
console.log(chalk.gray(` pal ext config ${ext.name} <k> <v> Set config`));
|
|
254
|
+
console.log(chalk.gray(` pal ext disable ${ext.name} Disable`));
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
cmd
|
|
258
|
+
.command('config <name> <key> [value]')
|
|
259
|
+
.description('get or set extension config value')
|
|
260
|
+
.action(async (extName, key, value) => {
|
|
261
|
+
const { getExtensionConfig, setExtensionConfig } = await import('../core/extensions.js');
|
|
262
|
+
if (value === undefined) {
|
|
263
|
+
const cfg = getExtensionConfig(extName);
|
|
264
|
+
console.log(cfg[key] !== undefined ? JSON.stringify(cfg[key]) : chalk.gray('(not set)'));
|
|
265
|
+
} else {
|
|
266
|
+
// Try to parse as JSON, fallback to string
|
|
267
|
+
let parsed = value;
|
|
268
|
+
try { parsed = JSON.parse(value); } catch {}
|
|
269
|
+
setExtensionConfig(extName, key, parsed);
|
|
270
|
+
console.log(chalk.green(`✔ ${extName}.${key} = ${JSON.stringify(parsed)}`));
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
cmd
|
|
275
|
+
.command('audit')
|
|
276
|
+
.description('security audit all installed extensions')
|
|
277
|
+
.action(async () => {
|
|
278
|
+
const {
|
|
279
|
+
getInstalledExtensions, verifySignature, computeIntegrityHash,
|
|
280
|
+
getSecurityRisk, hasBlockedImports, scanDangerousPatterns, BLOCKED_MODULES,
|
|
281
|
+
} = await import('../core/extensions.js');
|
|
282
|
+
const config = (await import('../utils/config.js')).default;
|
|
283
|
+
const extensions = getInstalledExtensions();
|
|
284
|
+
|
|
285
|
+
if (extensions.length === 0) {
|
|
286
|
+
console.log(chalk.gray('No extensions installed.'));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.log('');
|
|
291
|
+
console.log(chalk.cyan.bold('Extension Security Audit'));
|
|
292
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
293
|
+
let issues = 0;
|
|
294
|
+
|
|
295
|
+
for (const ext of extensions) {
|
|
296
|
+
const fullName = ext.bundled ? `@palexplorer/${ext.name}` : ext.name;
|
|
297
|
+
const sig = verifySignature(ext.path, ext);
|
|
298
|
+
const risk = getSecurityRisk(ext);
|
|
299
|
+
const mainPath = path.join(ext.path, ext.main || 'index.js');
|
|
300
|
+
const blocked = hasBlockedImports(mainPath);
|
|
301
|
+
const storedHash = config.get(`ext_integrity.${ext.name}`);
|
|
302
|
+
const currentHash = computeIntegrityHash(ext.path, ext);
|
|
303
|
+
const tampered = storedHash && storedHash !== currentHash;
|
|
304
|
+
|
|
305
|
+
const riskColor = risk === 'high' ? chalk.red : risk === 'medium' ? chalk.yellow : chalk.green;
|
|
306
|
+
console.log('');
|
|
307
|
+
console.log(` ${ext.bundled ? chalk.blue(fullName) : chalk.white(fullName)} v${ext.version}`);
|
|
308
|
+
console.log(` Status: ${ext.enabled ? chalk.green('enabled') : chalk.red('disabled')}`);
|
|
309
|
+
console.log(` Risk: ${riskColor(risk)}`);
|
|
310
|
+
console.log(` Signed: ${sig.verified ? chalk.green('verified ✔') : sig.signed ? chalk.red('INVALID ✖') : chalk.yellow('unsigned')}`);
|
|
311
|
+
console.log(` Integrity: ${tampered ? chalk.red('TAMPERED ✖') : storedHash ? chalk.green('OK ✔') : chalk.gray('no baseline')}`);
|
|
312
|
+
|
|
313
|
+
const dangerous = !ext.bundled ? scanDangerousPatterns(mainPath) : [];
|
|
314
|
+
|
|
315
|
+
if (blocked) {
|
|
316
|
+
console.log(` ${chalk.red(`⚠ BLOCKED: imports '${blocked}'`)}`);
|
|
317
|
+
issues++;
|
|
318
|
+
}
|
|
319
|
+
if (dangerous.length > 0) {
|
|
320
|
+
console.log(` ${chalk.yellow(`⚠ Dangerous patterns: ${dangerous.join(', ')}`)}`);
|
|
321
|
+
issues++;
|
|
322
|
+
}
|
|
323
|
+
if (tampered) {
|
|
324
|
+
console.log(` ${chalk.red('⚠ Files modified since installation!')}`);
|
|
325
|
+
issues++;
|
|
326
|
+
}
|
|
327
|
+
if (!ext.bundled && !sig.verified) {
|
|
328
|
+
console.log(` ${chalk.yellow('⚠ Not signed — not reviewed by Palexplorer team')}`);
|
|
329
|
+
issues++;
|
|
330
|
+
}
|
|
331
|
+
if (risk === 'high') {
|
|
332
|
+
const highPerms = (ext.permissions || []).filter(p =>
|
|
333
|
+
['fs:write', 'fs:delete', 'net:http', 'net:ws', 'messages:send', 'shares:write'].includes(p) || p.startsWith('exec:')
|
|
334
|
+
);
|
|
335
|
+
console.log(` ${chalk.red(`⚠ High-risk permissions: ${highPerms.join(', ')}`)}`);
|
|
336
|
+
issues++;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
console.log('');
|
|
341
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
342
|
+
if (issues === 0) {
|
|
343
|
+
console.log(chalk.green(`✔ No security issues found across ${extensions.length} extension(s).`));
|
|
344
|
+
} else {
|
|
345
|
+
console.log(chalk.yellow(`⚠ ${issues} issue(s) found across ${extensions.length} extension(s).`));
|
|
346
|
+
}
|
|
347
|
+
console.log('');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
cmd
|
|
351
|
+
.command('sign <extPath>')
|
|
352
|
+
.description('sign an extension with your identity key')
|
|
353
|
+
.action(async (extPath) => {
|
|
354
|
+
try {
|
|
355
|
+
const { readManifest } = await import('../core/extensions.js');
|
|
356
|
+
const { getIdentity } = await import('../core/identity.js');
|
|
357
|
+
const sodium = (await import('sodium-native')).default;
|
|
358
|
+
|
|
359
|
+
const resolvedPath = path.resolve(extPath);
|
|
360
|
+
const manifest = readManifest(resolvedPath);
|
|
361
|
+
if (!manifest) {
|
|
362
|
+
console.error(chalk.red('No valid extension.json found at path'));
|
|
363
|
+
process.exitCode = 1;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const identity = await getIdentity();
|
|
368
|
+
if (!identity || !identity.privateKey) {
|
|
369
|
+
console.error(chalk.red('No identity found. Run "pal init" first.'));
|
|
370
|
+
process.exitCode = 1;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
manifest.signerPublicKey = identity.publicKey;
|
|
375
|
+
const manifestJson = JSON.stringify(manifest, null, 2) + '\n';
|
|
376
|
+
fs.writeFileSync(path.join(resolvedPath, 'extension.json'), manifestJson);
|
|
377
|
+
|
|
378
|
+
const manifestData = Buffer.from(manifestJson);
|
|
379
|
+
const mainData = fs.readFileSync(path.join(resolvedPath, manifest.main || 'index.js'));
|
|
380
|
+
const content = Buffer.concat([manifestData, mainData]);
|
|
381
|
+
|
|
382
|
+
const secretKey = Buffer.from(identity.privateKey, 'hex');
|
|
383
|
+
const sig = Buffer.alloc(sodium.crypto_sign_BYTES);
|
|
384
|
+
sodium.crypto_sign_detached(sig, content, secretKey);
|
|
385
|
+
|
|
386
|
+
fs.writeFileSync(path.join(resolvedPath, 'extension.sig'), sig);
|
|
387
|
+
|
|
388
|
+
console.log(chalk.green(`\nSigned "${manifest.name}" with your identity key`));
|
|
389
|
+
console.log(` Signer: ${chalk.gray(identity.publicKey)}`);
|
|
390
|
+
console.log(` Signature: ${chalk.gray(path.join(resolvedPath, 'extension.sig'))}`);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.error(chalk.red(`Sign failed: ${err.message}`));
|
|
393
|
+
process.exitCode = 1;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
cmd
|
|
398
|
+
.command('search [query]')
|
|
399
|
+
.description('search the extension marketplace')
|
|
400
|
+
.option('-s, --sort <sort>', 'Sort by: downloads, rating, newest', 'downloads')
|
|
401
|
+
.option('-l, --limit <n>', 'Results per page', '10')
|
|
402
|
+
.action(async (query, opts) => {
|
|
403
|
+
const { fetchFirst } = await import('../core/discoveryClient.js');
|
|
404
|
+
const params = new URLSearchParams();
|
|
405
|
+
if (query) params.set('q', query);
|
|
406
|
+
params.set('sort', opts.sort);
|
|
407
|
+
params.set('limit', opts.limit);
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const res = await fetchFirst(`/api/v1/marketplace?${params}`);
|
|
411
|
+
if (!res) {
|
|
412
|
+
console.error(chalk.red('Failed to connect to marketplace.'));
|
|
413
|
+
process.exitCode = 1;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const data = await res.json();
|
|
417
|
+
if (!data?.results?.length) {
|
|
418
|
+
console.log(chalk.gray('No extensions found.'));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
console.log('');
|
|
422
|
+
console.log(chalk.cyan(`Marketplace (${data.total} results):`));
|
|
423
|
+
for (const ext of data.results) {
|
|
424
|
+
const verified = ext.verified ? chalk.green(' [Verified]') : '';
|
|
425
|
+
const pro = ext.pro ? chalk.yellow(' [Pro]') : '';
|
|
426
|
+
const stars = '\u2605'.repeat(Math.round(ext.rating || 0)) + '\u2606'.repeat(5 - Math.round(ext.rating || 0));
|
|
427
|
+
console.log(` ${chalk.white(ext.name)} v${ext.version}${verified}${pro}`);
|
|
428
|
+
console.log(` ${chalk.gray(ext.description || '')}`);
|
|
429
|
+
console.log(` ${chalk.yellow(stars)} (${ext.reviewCount || 0}) \u00b7 ${chalk.cyan(ext.downloads || 0)} downloads \u00b7 by ${chalk.blue('@' + (ext.authorHandle || 'unknown'))}`);
|
|
430
|
+
console.log('');
|
|
431
|
+
}
|
|
432
|
+
console.log(chalk.gray('Install: pal ext install-remote <name>'));
|
|
433
|
+
} catch (err) {
|
|
434
|
+
console.error(chalk.red(`Search failed: ${err.message}`));
|
|
435
|
+
process.exitCode = 1;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
cmd
|
|
440
|
+
.command('install-remote <name>')
|
|
441
|
+
.description('install extension from marketplace')
|
|
442
|
+
.action(async (name) => {
|
|
443
|
+
const { fetchFirst } = await import('../core/discoveryClient.js');
|
|
444
|
+
try {
|
|
445
|
+
const searchRes = await fetchFirst(`/api/v1/marketplace?q=${encodeURIComponent(name)}&limit=1`);
|
|
446
|
+
if (!searchRes) {
|
|
447
|
+
console.error(chalk.red('Failed to connect to marketplace.'));
|
|
448
|
+
process.exitCode = 1;
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const data = await searchRes.json();
|
|
452
|
+
const ext = data?.results?.find(e => e.name === name);
|
|
453
|
+
if (!ext) {
|
|
454
|
+
console.error(chalk.red(`Extension "${name}" not found in marketplace.`));
|
|
455
|
+
process.exitCode = 1;
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const dlRes = await fetchFirst(`/api/v1/marketplace/${ext.id}/download`);
|
|
460
|
+
if (!dlRes) {
|
|
461
|
+
console.error(chalk.red('Failed to get download URL.'));
|
|
462
|
+
process.exitCode = 1;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const dlInfo = await dlRes.json();
|
|
466
|
+
if (!dlInfo?.bundleUrl) {
|
|
467
|
+
console.error(chalk.red('Failed to get download URL.'));
|
|
468
|
+
process.exitCode = 1;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
console.log(chalk.cyan(`Installing ${name} v${ext.version} from marketplace...`));
|
|
473
|
+
|
|
474
|
+
// Download bundle (with SSRF protection)
|
|
475
|
+
const { getPrimaryServer } = await import('../core/discoveryClient.js');
|
|
476
|
+
const bundleUrl = dlInfo.bundleUrl.startsWith('http') ? dlInfo.bundleUrl : `${getPrimaryServer()}${dlInfo.bundleUrl}`;
|
|
477
|
+
try {
|
|
478
|
+
const parsed = new URL(bundleUrl);
|
|
479
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') throw new Error('Invalid URL scheme');
|
|
480
|
+
const host = parsed.hostname;
|
|
481
|
+
if (host === 'localhost' || host.startsWith('127.') || host.startsWith('10.') ||
|
|
482
|
+
host.startsWith('192.168.') || host.startsWith('169.254.') || host === '0.0.0.0' ||
|
|
483
|
+
/^172\.(1[6-9]|2\d|3[01])\./.test(host)) {
|
|
484
|
+
throw new Error('Bundle URL points to private network');
|
|
485
|
+
}
|
|
486
|
+
} catch (e) { if (e.message.includes('private') || e.message.includes('scheme')) throw e; }
|
|
487
|
+
const bundleRes = await fetch(bundleUrl, { signal: AbortSignal.timeout(30000) });
|
|
488
|
+
if (!bundleRes.ok) throw new Error(`Failed to download bundle: ${bundleRes.status}`);
|
|
489
|
+
const bundleData = Buffer.from(await bundleRes.arrayBuffer());
|
|
490
|
+
|
|
491
|
+
// Verify hash
|
|
492
|
+
if (dlInfo.bundleHash) {
|
|
493
|
+
const hash = crypto.createHash('sha256').update(bundleData).digest('hex');
|
|
494
|
+
if (hash !== dlInfo.bundleHash) throw new Error('Bundle hash mismatch — file may be corrupted');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Extract to temp dir with zip-slip protection
|
|
498
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pe-ext-dl-'));
|
|
499
|
+
const tarPath = path.join(tmpDir, `${name}.tar.gz`);
|
|
500
|
+
fs.writeFileSync(tarPath, bundleData);
|
|
501
|
+
spawnSync('tar', ['xzf', tarPath, '-C', tmpDir], { stdio: 'pipe' });
|
|
502
|
+
|
|
503
|
+
// Zip-slip protection: verify all extracted files are within tmpDir
|
|
504
|
+
const realTmpDir = fs.realpathSync(tmpDir);
|
|
505
|
+
function validateExtractedPaths(dir) {
|
|
506
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
507
|
+
const fullPath = path.join(dir, entry.name);
|
|
508
|
+
const realPath = fs.realpathSync(fullPath);
|
|
509
|
+
if (!realPath.startsWith(realTmpDir)) {
|
|
510
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
511
|
+
throw new Error(`Zip-slip detected: ${entry.name} escapes extraction directory`);
|
|
512
|
+
}
|
|
513
|
+
if (entry.isDirectory()) validateExtractedPaths(fullPath);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
validateExtractedPaths(tmpDir);
|
|
517
|
+
|
|
518
|
+
// Find the extracted extension directory
|
|
519
|
+
const entries = fs.readdirSync(tmpDir).filter(e => e !== path.basename(tarPath));
|
|
520
|
+
const extDir = entries.length === 1 ? path.join(tmpDir, entries[0]) : tmpDir;
|
|
521
|
+
|
|
522
|
+
const { installExtension } = await import('../core/extensions.js');
|
|
523
|
+
const result = await installExtension(extDir);
|
|
524
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
525
|
+
console.log(chalk.green(`\u2714 Installed "${result.name}" v${result.version || ext.version}`));
|
|
526
|
+
} catch (err) {
|
|
527
|
+
console.error(chalk.red(`Install failed: ${err.message}`));
|
|
528
|
+
process.exitCode = 1;
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
cmd
|
|
533
|
+
.command('publish <path>')
|
|
534
|
+
.description('publish extension to marketplace')
|
|
535
|
+
.action(async (extPath) => {
|
|
536
|
+
const { readManifest, validateManifest } = await import('../core/extensions.js');
|
|
537
|
+
const { getIdentity } = await import('../core/identity.js');
|
|
538
|
+
const { fetchFirst, getPrimaryServer } = await import('../core/discoveryClient.js');
|
|
539
|
+
const sodium = (await import('sodium-native')).default;
|
|
540
|
+
|
|
541
|
+
const resolvedPath = path.resolve(extPath);
|
|
542
|
+
const manifest = readManifest(resolvedPath);
|
|
543
|
+
if (!manifest) {
|
|
544
|
+
console.error(chalk.red('No valid extension.json found.'));
|
|
545
|
+
process.exitCode = 1;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const validationError = validateManifest(manifest);
|
|
550
|
+
if (validationError) {
|
|
551
|
+
console.error(chalk.red(`Invalid manifest: ${validationError}`));
|
|
552
|
+
process.exitCode = 1;
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const identity = await getIdentity();
|
|
557
|
+
if (!identity?.publicKey) {
|
|
558
|
+
console.error(chalk.red('No identity. Run pal init first.'));
|
|
559
|
+
process.exitCode = 1;
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (!identity.privateKey) {
|
|
563
|
+
console.error(chalk.red('No private key available. Cannot sign bundle.'));
|
|
564
|
+
process.exitCode = 1;
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const apiKey = await getApiKey();
|
|
569
|
+
if (!apiKey) {
|
|
570
|
+
console.error(chalk.red('No API key. Create one: pal api-keys create'));
|
|
571
|
+
process.exitCode = 1;
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Create .tar.gz bundle
|
|
576
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pe-ext-'));
|
|
577
|
+
const bundlePath = path.join(tmpDir, `${manifest.name}-${manifest.version}.tar.gz`);
|
|
578
|
+
try {
|
|
579
|
+
spawnSync('tar', ['czf', bundlePath, '-C', path.dirname(resolvedPath), path.basename(resolvedPath)], {
|
|
580
|
+
stdio: 'pipe',
|
|
581
|
+
});
|
|
582
|
+
} catch (err) {
|
|
583
|
+
console.error(chalk.red(`Failed to create bundle: ${err.message}`));
|
|
584
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
585
|
+
process.exitCode = 1;
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Compute SHA-256 hash of the bundle
|
|
590
|
+
const bundleData = fs.readFileSync(bundlePath);
|
|
591
|
+
const bundleHash = crypto.createHash('sha256').update(bundleData).digest('hex');
|
|
592
|
+
|
|
593
|
+
// Sign the hash with Ed25519
|
|
594
|
+
const secretKey = Buffer.from(identity.privateKey, 'hex');
|
|
595
|
+
const sig = Buffer.alloc(sodium.crypto_sign_BYTES);
|
|
596
|
+
sodium.crypto_sign_detached(sig, Buffer.from(bundleHash), secretKey);
|
|
597
|
+
const signature = sig.toString('hex');
|
|
598
|
+
|
|
599
|
+
// Read README if present
|
|
600
|
+
let readme = '';
|
|
601
|
+
for (const name of ['README.md', 'readme.md', 'README', 'readme.txt']) {
|
|
602
|
+
const readmePath = path.join(resolvedPath, name);
|
|
603
|
+
if (fs.existsSync(readmePath)) {
|
|
604
|
+
readme = fs.readFileSync(readmePath, 'utf-8');
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
console.log(chalk.cyan(`Publishing ${manifest.name} v${manifest.version}...`));
|
|
610
|
+
console.log(chalk.gray(` Bundle: ${(bundleData.length / 1024).toFixed(1)} KB`));
|
|
611
|
+
console.log(chalk.gray(` Hash: ${bundleHash.slice(0, 16)}...`));
|
|
612
|
+
|
|
613
|
+
try {
|
|
614
|
+
const baseUrl = getPrimaryServer();
|
|
615
|
+
const form = new FormData();
|
|
616
|
+
form.append('bundle', new Blob([bundleData]), path.basename(bundlePath));
|
|
617
|
+
form.append('name', manifest.name);
|
|
618
|
+
form.append('version', manifest.version);
|
|
619
|
+
form.append('description', manifest.description || '');
|
|
620
|
+
form.append('author', manifest.author || '');
|
|
621
|
+
form.append('permissions', JSON.stringify(manifest.permissions || []));
|
|
622
|
+
form.append('hooks', JSON.stringify(manifest.hooks || []));
|
|
623
|
+
form.append('pro', String(manifest.pro || false));
|
|
624
|
+
form.append('license', manifest.license || 'MIT');
|
|
625
|
+
form.append('readme', readme);
|
|
626
|
+
form.append('bundleHash', bundleHash);
|
|
627
|
+
form.append('signerPublicKey', manifest.signerPublicKey || identity.publicKey);
|
|
628
|
+
form.append('signature', signature);
|
|
629
|
+
if (manifest.pricing) form.append('pricing', manifest.pricing);
|
|
630
|
+
if (manifest.price != null) form.append('price', String(manifest.price));
|
|
631
|
+
|
|
632
|
+
const res = await fetch(`${baseUrl}/api/v1/marketplace/publish`, {
|
|
633
|
+
method: 'POST',
|
|
634
|
+
headers: { 'x-api-key': apiKey },
|
|
635
|
+
body: form,
|
|
636
|
+
signal: AbortSignal.timeout(30000),
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const result = await res.json();
|
|
640
|
+
if (res.ok && result?.success) {
|
|
641
|
+
console.log(chalk.green(`\u2714 Published ${manifest.name} v${manifest.version}`));
|
|
642
|
+
if (result.url) console.log(chalk.gray(` ${result.url}`));
|
|
643
|
+
} else {
|
|
644
|
+
console.error(chalk.red(`Publish failed: ${result?.error || 'Unknown error'}`));
|
|
645
|
+
process.exitCode = 1;
|
|
646
|
+
}
|
|
647
|
+
} catch (err) {
|
|
648
|
+
console.error(chalk.red(`Publish failed: ${err.message}`));
|
|
649
|
+
process.exitCode = 1;
|
|
650
|
+
} finally {
|
|
651
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
cmd
|
|
656
|
+
.command('buy <name>')
|
|
657
|
+
.description('purchase a paid extension from the marketplace')
|
|
658
|
+
.action(async (name) => {
|
|
659
|
+
const { fetchFirst, getPrimaryServer } = await import('../core/discoveryClient.js');
|
|
660
|
+
const { getIdentity } = await import('../core/identity.js');
|
|
661
|
+
try {
|
|
662
|
+
const searchRes = await fetchFirst(`/api/v1/marketplace?q=${encodeURIComponent(name)}&limit=1`);
|
|
663
|
+
if (!searchRes) {
|
|
664
|
+
console.error(chalk.red('Failed to connect to marketplace.'));
|
|
665
|
+
process.exitCode = 1;
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const data = await searchRes.json();
|
|
669
|
+
const ext = data?.results?.find(e => e.name === name);
|
|
670
|
+
if (!ext) {
|
|
671
|
+
console.error(chalk.red(`Extension "${name}" not found in marketplace.`));
|
|
672
|
+
process.exitCode = 1;
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (!ext.price || ext.price === 0) {
|
|
677
|
+
console.log(chalk.gray(`"${name}" is free. Use: pal ext install-remote ${name}`));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
console.log('');
|
|
682
|
+
console.log(` ${chalk.white(ext.name)} v${ext.version}`);
|
|
683
|
+
console.log(` ${chalk.gray(ext.description || '')}`);
|
|
684
|
+
console.log(` Price: ${chalk.yellow('$' + ext.price.toFixed(2))}`);
|
|
685
|
+
console.log('');
|
|
686
|
+
|
|
687
|
+
const ok = await confirm(chalk.white(` Purchase ${ext.name} for $${ext.price.toFixed(2)}? (y/N) `));
|
|
688
|
+
if (!ok) {
|
|
689
|
+
console.log(chalk.gray(' Cancelled.'));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const identity = await getIdentity();
|
|
694
|
+
if (!identity?.publicKey) {
|
|
695
|
+
console.error(chalk.red('No identity. Run pal init first.'));
|
|
696
|
+
process.exitCode = 1;
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const apiKey = await getApiKey();
|
|
701
|
+
if (!apiKey) {
|
|
702
|
+
console.error(chalk.red('No API key. Create one: pal api-keys create'));
|
|
703
|
+
process.exitCode = 1;
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const baseUrl = getPrimaryServer();
|
|
708
|
+
const checkoutRes = await fetch(`${baseUrl}/api/v1/marketplace/${ext.id}/checkout`, {
|
|
709
|
+
method: 'POST',
|
|
710
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
711
|
+
signal: AbortSignal.timeout(10000),
|
|
712
|
+
});
|
|
713
|
+
if (!checkoutRes.ok) {
|
|
714
|
+
console.error(chalk.red('Failed to get checkout URL.'));
|
|
715
|
+
process.exitCode = 1;
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const checkout = await checkoutRes.json();
|
|
719
|
+
if (checkout.checkoutUrl) {
|
|
720
|
+
console.log(chalk.cyan(`\n Opening checkout: ${checkout.checkoutUrl}\n`));
|
|
721
|
+
try {
|
|
722
|
+
const parsed = new URL(checkout.checkoutUrl);
|
|
723
|
+
if (parsed.protocol === 'https:') {
|
|
724
|
+
const { exec } = await import('child_process');
|
|
725
|
+
exec(`start "" "${parsed.href}"`);
|
|
726
|
+
}
|
|
727
|
+
} catch {}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
731
|
+
const token = await new Promise(resolve => {
|
|
732
|
+
rl.question(chalk.white(' Paste your purchase token: '), answer => {
|
|
733
|
+
rl.close();
|
|
734
|
+
resolve(answer.trim());
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
if (!token) {
|
|
739
|
+
console.log(chalk.gray(' No token provided. Cancelled.'));
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const purchaseRes = await fetch(`${baseUrl}/api/v1/marketplace/${ext.id}/purchase`, {
|
|
744
|
+
method: 'POST',
|
|
745
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
746
|
+
body: JSON.stringify({ paymentToken: token }),
|
|
747
|
+
signal: AbortSignal.timeout(10000),
|
|
748
|
+
});
|
|
749
|
+
if (!purchaseRes.ok) {
|
|
750
|
+
const err = await purchaseRes.json().catch(() => ({}));
|
|
751
|
+
console.error(chalk.red(`Purchase failed: ${err.error || 'Unknown error'}`));
|
|
752
|
+
process.exitCode = 1;
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const purchaseData = await purchaseRes.json();
|
|
756
|
+
if (purchaseData.success && purchaseData.bundleUrl) {
|
|
757
|
+
console.log(chalk.cyan(`Installing ${name}...`));
|
|
758
|
+
const { installExtension } = await import('../core/extensions.js');
|
|
759
|
+
const result = await installExtension(purchaseData.bundleUrl);
|
|
760
|
+
console.log(chalk.green(`\u2714 Purchased and installed "${result.name}" v${result.version || ext.version}`));
|
|
761
|
+
} else if (purchaseData.success) {
|
|
762
|
+
console.log(chalk.green(`\u2714 Purchase confirmed. Install: pal ext install-remote ${name}`));
|
|
763
|
+
} else {
|
|
764
|
+
console.error(chalk.red(`Purchase failed: ${purchaseData.error || 'Unknown error'}`));
|
|
765
|
+
process.exitCode = 1;
|
|
766
|
+
}
|
|
767
|
+
} catch (err) {
|
|
768
|
+
console.error(chalk.red(`Buy failed: ${err.message}`));
|
|
769
|
+
process.exitCode = 1;
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
cmd
|
|
774
|
+
.command('earnings')
|
|
775
|
+
.description('show developer earnings from published extensions')
|
|
776
|
+
.action(async () => {
|
|
777
|
+
const { fetchFirst, getPrimaryServer } = await import('../core/discoveryClient.js');
|
|
778
|
+
const { getIdentity } = await import('../core/identity.js');
|
|
779
|
+
try {
|
|
780
|
+
const identity = await getIdentity();
|
|
781
|
+
if (!identity?.publicKey) {
|
|
782
|
+
console.error(chalk.red('No identity. Run pal init first.'));
|
|
783
|
+
process.exitCode = 1;
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const apiKey = await getApiKey();
|
|
787
|
+
if (!apiKey) {
|
|
788
|
+
console.error(chalk.red('No API key. Create one: pal api-keys create'));
|
|
789
|
+
process.exitCode = 1;
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const baseUrl = getPrimaryServer();
|
|
793
|
+
const res = await fetch(`${baseUrl}/api/v1/marketplace/earnings`, {
|
|
794
|
+
headers: { 'x-api-key': apiKey },
|
|
795
|
+
signal: AbortSignal.timeout(10000),
|
|
796
|
+
});
|
|
797
|
+
if (!res.ok) {
|
|
798
|
+
console.error(chalk.red('Failed to fetch earnings.'));
|
|
799
|
+
process.exitCode = 1;
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const data = await res.json();
|
|
803
|
+
console.log('');
|
|
804
|
+
console.log(chalk.cyan.bold('Developer Earnings'));
|
|
805
|
+
console.log(chalk.gray('\u2500'.repeat(40)));
|
|
806
|
+
console.log(` Total Revenue: ${chalk.white('$' + (data.totalRevenue || 0).toFixed(2))}`);
|
|
807
|
+
console.log(` Available: ${chalk.green('$' + (data.available || 0).toFixed(2))}`);
|
|
808
|
+
console.log(` Paid Out: ${chalk.gray('$' + (data.paidOut || 0).toFixed(2))}`);
|
|
809
|
+
|
|
810
|
+
if (data.recentSales?.length) {
|
|
811
|
+
console.log('');
|
|
812
|
+
console.log(chalk.cyan('Recent Sales:'));
|
|
813
|
+
for (const sale of data.recentSales) {
|
|
814
|
+
console.log(` ${chalk.gray(new Date(sale.date).toLocaleDateString())} ${chalk.white(sale.extName)} $${sale.amount.toFixed(2)}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (data.payouts?.length) {
|
|
819
|
+
console.log('');
|
|
820
|
+
console.log(chalk.cyan('Payout History:'));
|
|
821
|
+
for (const p of data.payouts) {
|
|
822
|
+
const statusColor = p.status === 'paid' ? chalk.green : p.status === 'pending' ? chalk.yellow : chalk.gray;
|
|
823
|
+
console.log(` ${chalk.gray(new Date(p.date).toLocaleDateString())} $${p.amount.toFixed(2)} ${statusColor(p.status)} ${chalk.gray(p.email)}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
console.log('');
|
|
827
|
+
} catch (err) {
|
|
828
|
+
console.error(chalk.red(`Earnings failed: ${err.message}`));
|
|
829
|
+
process.exitCode = 1;
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
cmd
|
|
834
|
+
.command('my-extensions')
|
|
835
|
+
.description('list your published extensions with stats')
|
|
836
|
+
.action(async () => {
|
|
837
|
+
const { fetchFirst, getPrimaryServer } = await import('../core/discoveryClient.js');
|
|
838
|
+
const { getIdentity } = await import('../core/identity.js');
|
|
839
|
+
try {
|
|
840
|
+
const identity = await getIdentity();
|
|
841
|
+
if (!identity?.publicKey) {
|
|
842
|
+
console.error(chalk.red('No identity. Run pal init first.'));
|
|
843
|
+
process.exitCode = 1;
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const apiKey = await getApiKey();
|
|
847
|
+
if (!apiKey) {
|
|
848
|
+
console.error(chalk.red('No API key. Create one: pal api-keys create'));
|
|
849
|
+
process.exitCode = 1;
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const baseUrl = getPrimaryServer();
|
|
853
|
+
const res = await fetch(`${baseUrl}/api/v1/marketplace/my-extensions`, {
|
|
854
|
+
headers: { 'x-api-key': apiKey },
|
|
855
|
+
signal: AbortSignal.timeout(10000),
|
|
856
|
+
});
|
|
857
|
+
if (!res.ok) {
|
|
858
|
+
console.error(chalk.red('Failed to fetch extensions.'));
|
|
859
|
+
process.exitCode = 1;
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const data = await res.json();
|
|
863
|
+
if (!data.extensions?.length) {
|
|
864
|
+
console.log(chalk.gray('No published extensions.'));
|
|
865
|
+
console.log(chalk.gray(' pal ext publish <path>'));
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
console.log('');
|
|
869
|
+
console.log(chalk.cyan('My Published Extensions:'));
|
|
870
|
+
console.log(chalk.gray('\u2500'.repeat(60)));
|
|
871
|
+
for (const ext of data.extensions) {
|
|
872
|
+
const price = ext.price ? chalk.yellow(`$${ext.price.toFixed(2)}`) : chalk.green('Free');
|
|
873
|
+
const stars = '\u2605'.repeat(Math.round(ext.rating || 0)) + '\u2606'.repeat(5 - Math.round(ext.rating || 0));
|
|
874
|
+
console.log(` ${chalk.white(ext.name)} v${ext.version} ${price}`);
|
|
875
|
+
console.log(` ${chalk.yellow(stars)} (${ext.reviewCount || 0}) \u00b7 ${chalk.cyan(ext.downloads || 0)} downloads \u00b7 ${chalk.green(ext.purchases || 0)} purchases`);
|
|
876
|
+
console.log(` Revenue: ${chalk.green('$' + (ext.revenue || 0).toFixed(2))}`);
|
|
877
|
+
console.log('');
|
|
878
|
+
}
|
|
879
|
+
} catch (err) {
|
|
880
|
+
console.error(chalk.red(`Failed: ${err.message}`));
|
|
881
|
+
process.exitCode = 1;
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
cmd
|
|
886
|
+
.command('create <name>')
|
|
887
|
+
.description('scaffold a new extension')
|
|
888
|
+
.action(async (extName) => {
|
|
889
|
+
const dir = path.resolve(extName);
|
|
890
|
+
const baseName = path.basename(dir);
|
|
891
|
+
if (fs.existsSync(dir)) {
|
|
892
|
+
console.log(chalk.red(`Directory "${extName}" already exists.`));
|
|
893
|
+
process.exitCode = 1;
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
fs.mkdirSync(path.join(dir, 'docs'), { recursive: true });
|
|
898
|
+
fs.mkdirSync(path.join(dir, 'test'), { recursive: true });
|
|
899
|
+
|
|
900
|
+
const manifest = {
|
|
901
|
+
name: baseName,
|
|
902
|
+
version: '1.0.0',
|
|
903
|
+
description: `${baseName} extension for Palexplorer`,
|
|
904
|
+
author: '',
|
|
905
|
+
license: 'MIT',
|
|
906
|
+
main: 'index.js',
|
|
907
|
+
hooks: ['on:app:ready'],
|
|
908
|
+
permissions: ['config:read'],
|
|
909
|
+
config: {
|
|
910
|
+
enabled: { type: 'boolean', default: true, description: 'Enable this extension' },
|
|
911
|
+
},
|
|
912
|
+
help: {
|
|
913
|
+
summary: `${baseName} — brief description of what this extension does.`,
|
|
914
|
+
usage: `Enable: pal ext config ${baseName} enabled true`,
|
|
915
|
+
examples: [
|
|
916
|
+
`pal ext help ${baseName}`,
|
|
917
|
+
`pal ext config ${baseName} enabled true`,
|
|
918
|
+
`pal ext info ${baseName}`,
|
|
919
|
+
],
|
|
920
|
+
configReference: {
|
|
921
|
+
enabled: 'Enable or disable this extension (true/false)',
|
|
922
|
+
},
|
|
923
|
+
links: {},
|
|
924
|
+
},
|
|
925
|
+
pro: false,
|
|
926
|
+
minAppVersion: '0.5.0',
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
fs.writeFileSync(
|
|
930
|
+
path.join(dir, 'extension.json'),
|
|
931
|
+
JSON.stringify(manifest, null, 2) + '\n'
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
fs.writeFileSync(path.join(dir, 'index.js'), `let ctx = null;
|
|
935
|
+
|
|
936
|
+
export function activate(context) {
|
|
937
|
+
ctx = context;
|
|
938
|
+
|
|
939
|
+
context.hooks.on('on:app:ready', async () => {
|
|
940
|
+
context.logger.info('${baseName} ready');
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
export function deactivate() {
|
|
945
|
+
ctx = null;
|
|
946
|
+
}
|
|
947
|
+
`);
|
|
948
|
+
|
|
949
|
+
fs.writeFileSync(path.join(dir, 'test', `${baseName}.test.js`), `import { describe, it, beforeEach, mock } from 'node:test';
|
|
950
|
+
import assert from 'node:assert/strict';
|
|
951
|
+
|
|
952
|
+
function createMockContext(overrides = {}) {
|
|
953
|
+
return {
|
|
954
|
+
hooks: { on: mock.fn() },
|
|
955
|
+
config: { get: mock.fn(), set: mock.fn() },
|
|
956
|
+
store: { get: mock.fn(), set: mock.fn(), delete: mock.fn() },
|
|
957
|
+
logger: { info: mock.fn(), warn: mock.fn(), error: mock.fn() },
|
|
958
|
+
app: { version: '0.5.0', platform: 'linux', dataDir: '/tmp/test' },
|
|
959
|
+
...overrides,
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
describe('${baseName}', () => {
|
|
964
|
+
let ext;
|
|
965
|
+
let ctx;
|
|
966
|
+
|
|
967
|
+
beforeEach(async () => {
|
|
968
|
+
ext = await import('../index.js');
|
|
969
|
+
ctx = createMockContext();
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it('should activate and register hooks', () => {
|
|
973
|
+
ext.activate(ctx);
|
|
974
|
+
assert.ok(ctx.hooks.on.mock.calls.length > 0);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
it('should deactivate cleanly', () => {
|
|
978
|
+
ext.activate(ctx);
|
|
979
|
+
ext.deactivate();
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
`);
|
|
983
|
+
|
|
984
|
+
fs.writeFileSync(path.join(dir, 'README.md'), `# ${baseName}
|
|
985
|
+
|
|
986
|
+
Brief description of what this extension does.
|
|
987
|
+
|
|
988
|
+
## Configuration
|
|
989
|
+
|
|
990
|
+
| Key | Type | Default | Description |
|
|
991
|
+
|-----|------|---------|-------------|
|
|
992
|
+
| \`enabled\` | boolean | \`true\` | Enable this extension |
|
|
993
|
+
|
|
994
|
+
\`\`\`bash
|
|
995
|
+
pal ext config ${baseName} enabled true
|
|
996
|
+
\`\`\`
|
|
997
|
+
`);
|
|
998
|
+
|
|
999
|
+
fs.writeFileSync(path.join(dir, 'docs', 'PLAN.md'), `# ${baseName} — Plan
|
|
1000
|
+
|
|
1001
|
+
## Goal
|
|
1002
|
+
|
|
1003
|
+
What problem does this extension solve?
|
|
1004
|
+
|
|
1005
|
+
## Design
|
|
1006
|
+
|
|
1007
|
+
- How it hooks into core
|
|
1008
|
+
- Key decisions and trade-offs
|
|
1009
|
+
`);
|
|
1010
|
+
|
|
1011
|
+
fs.writeFileSync(path.join(dir, 'docs', 'MONETIZATION.md'), `# ${baseName} — Monetization
|
|
1012
|
+
|
|
1013
|
+
## Tier
|
|
1014
|
+
|
|
1015
|
+
- [ ] Free
|
|
1016
|
+
- [ ] Pro
|
|
1017
|
+
- [ ] Enterprise
|
|
1018
|
+
|
|
1019
|
+
## Rationale
|
|
1020
|
+
|
|
1021
|
+
Why this tier?
|
|
1022
|
+
`);
|
|
1023
|
+
|
|
1024
|
+
console.log(chalk.green(`✔ Scaffolded extension at ./${baseName}/`));
|
|
1025
|
+
console.log(chalk.gray(' Files created:'));
|
|
1026
|
+
console.log(chalk.gray(' extension.json'));
|
|
1027
|
+
console.log(chalk.gray(' index.js'));
|
|
1028
|
+
console.log(chalk.gray(' README.md'));
|
|
1029
|
+
console.log(chalk.gray(' docs/PLAN.md'));
|
|
1030
|
+
console.log(chalk.gray(' docs/MONETIZATION.md'));
|
|
1031
|
+
console.log(chalk.gray(` test/${baseName}.test.js`));
|
|
1032
|
+
console.log('');
|
|
1033
|
+
console.log(chalk.cyan(' Install it:'));
|
|
1034
|
+
console.log(chalk.white(` pal ext install ./${baseName}`));
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
async function listExtensions() {
|
|
1039
|
+
const { getInstalledExtensions } = await import('../core/extensions.js');
|
|
1040
|
+
const extensions = getInstalledExtensions();
|
|
1041
|
+
if (extensions.length === 0) {
|
|
1042
|
+
console.log(chalk.gray('No extensions installed.'));
|
|
1043
|
+
console.log(chalk.gray(' pal ext install <path|git-url>'));
|
|
1044
|
+
console.log(chalk.gray(' pal ext create <name>'));
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
console.log('');
|
|
1049
|
+
console.log(chalk.cyan('Extensions:'));
|
|
1050
|
+
for (const ext of extensions) {
|
|
1051
|
+
const name = ext.bundled ? chalk.blue(`@palexplorer/${ext.name}`) : chalk.white(ext.name);
|
|
1052
|
+
const status = ext.enabled ? chalk.green('●') : chalk.red('○');
|
|
1053
|
+
const version = chalk.gray(`v${ext.version}`);
|
|
1054
|
+
const tier = ext.tier || (ext.pro ? 'pro' : 'free');
|
|
1055
|
+
const tierBadge = tier === 'enterprise' ? chalk.magenta(' [Enterprise]')
|
|
1056
|
+
: tier === 'pro' ? chalk.yellow(' [Pro]') : '';
|
|
1057
|
+
console.log(` ${status} ${name} ${version}${tierBadge}`);
|
|
1058
|
+
if (ext.description) console.log(` ${chalk.gray(ext.description)}`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|