pal-explorer-cli 0.4.11 → 0.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +149 -149
  2. package/bin/pal.js +63 -2
  3. package/extensions/@palexplorer/analytics/extension.json +20 -1
  4. package/extensions/@palexplorer/analytics/index.js +19 -9
  5. package/extensions/@palexplorer/audit/extension.json +14 -0
  6. package/extensions/@palexplorer/auth-email/extension.json +15 -0
  7. package/extensions/@palexplorer/auth-oauth/extension.json +15 -0
  8. package/extensions/@palexplorer/chat/extension.json +14 -0
  9. package/extensions/@palexplorer/discovery/extension.json +17 -0
  10. package/extensions/@palexplorer/discovery/index.js +1 -1
  11. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  12. package/extensions/@palexplorer/groups/extension.json +15 -0
  13. package/extensions/@palexplorer/share-links/extension.json +15 -0
  14. package/extensions/@palexplorer/sync/extension.json +16 -0
  15. package/extensions/@palexplorer/user-mgmt/extension.json +15 -0
  16. package/lib/capabilities.js +24 -24
  17. package/lib/commands/analytics.js +175 -175
  18. package/lib/commands/api-keys.js +131 -131
  19. package/lib/commands/audit.js +235 -235
  20. package/lib/commands/auth.js +137 -137
  21. package/lib/commands/backup.js +76 -76
  22. package/lib/commands/billing.js +148 -148
  23. package/lib/commands/chat.js +217 -217
  24. package/lib/commands/cloud-backup.js +231 -231
  25. package/lib/commands/comment.js +99 -99
  26. package/lib/commands/completion.js +203 -203
  27. package/lib/commands/compliance.js +218 -218
  28. package/lib/commands/config.js +136 -136
  29. package/lib/commands/connect.js +44 -44
  30. package/lib/commands/dept.js +294 -294
  31. package/lib/commands/device.js +146 -146
  32. package/lib/commands/download.js +240 -226
  33. package/lib/commands/explorer.js +178 -178
  34. package/lib/commands/extension.js +1060 -970
  35. package/lib/commands/favorite.js +90 -90
  36. package/lib/commands/federation.js +270 -270
  37. package/lib/commands/file.js +533 -533
  38. package/lib/commands/group.js +271 -271
  39. package/lib/commands/gui-share.js +29 -29
  40. package/lib/commands/init.js +61 -61
  41. package/lib/commands/invite.js +59 -59
  42. package/lib/commands/list.js +58 -58
  43. package/lib/commands/log.js +116 -116
  44. package/lib/commands/nearby.js +108 -108
  45. package/lib/commands/network.js +251 -251
  46. package/lib/commands/notify.js +198 -198
  47. package/lib/commands/org.js +273 -273
  48. package/lib/commands/pal.js +403 -180
  49. package/lib/commands/permissions.js +216 -216
  50. package/lib/commands/pin.js +97 -97
  51. package/lib/commands/protocol.js +357 -357
  52. package/lib/commands/rbac.js +147 -147
  53. package/lib/commands/recover.js +36 -36
  54. package/lib/commands/register.js +171 -171
  55. package/lib/commands/relay.js +131 -131
  56. package/lib/commands/remote.js +368 -368
  57. package/lib/commands/revoke.js +50 -50
  58. package/lib/commands/scanner.js +280 -280
  59. package/lib/commands/schedule.js +344 -344
  60. package/lib/commands/scim.js +203 -203
  61. package/lib/commands/search.js +181 -181
  62. package/lib/commands/serve.js +438 -438
  63. package/lib/commands/server.js +350 -350
  64. package/lib/commands/share-link.js +199 -199
  65. package/lib/commands/share.js +336 -323
  66. package/lib/commands/sso.js +200 -200
  67. package/lib/commands/status.js +145 -145
  68. package/lib/commands/stream.js +562 -562
  69. package/lib/commands/su.js +187 -187
  70. package/lib/commands/sync.js +979 -979
  71. package/lib/commands/transfers.js +152 -152
  72. package/lib/commands/uninstall.js +188 -188
  73. package/lib/commands/update.js +204 -204
  74. package/lib/commands/user.js +276 -276
  75. package/lib/commands/vfs.js +84 -84
  76. package/lib/commands/web-login.js +79 -79
  77. package/lib/commands/web.js +52 -52
  78. package/lib/commands/webhook.js +180 -180
  79. package/lib/commands/whoami.js +59 -59
  80. package/lib/commands/workspace.js +121 -121
  81. package/lib/core/billing.js +16 -5
  82. package/lib/core/dhtDiscovery.js +9 -2
  83. package/lib/core/discoveryClient.js +13 -7
  84. package/lib/core/extensions.js +142 -1
  85. package/lib/core/identity.js +33 -2
  86. package/lib/core/imageProcessor.js +109 -0
  87. package/lib/core/imageTorrent.js +167 -0
  88. package/lib/core/permissions.js +1 -1
  89. package/lib/core/pro.js +11 -4
  90. package/lib/core/serverList.js +4 -1
  91. package/lib/core/shares.js +12 -1
  92. package/lib/core/signalingServer.js +14 -2
  93. package/lib/core/su.js +1 -1
  94. package/lib/core/users.js +1 -1
  95. package/lib/protocol/messages.js +12 -3
  96. package/lib/utils/explorer.js +1 -1
  97. package/lib/utils/help.js +357 -357
  98. package/lib/utils/torrent.js +1 -0
  99. package/package.json +4 -3
@@ -1,61 +1,61 @@
1
- import { createIdentity } from '../core/identity.js';
2
- import { addProfile, getProfiles, migrateToMultiUser } from '../core/users.js';
3
- import config from '../utils/config.js';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
-
7
- export default function initCommand(program) {
8
- program
9
- .command('init <name>')
10
- .description('create a new Pal Explorer identity with a name')
11
- .option('-f, --force', 'Overwrite existing identity')
12
- .addHelpText('after', `
13
- Examples:
14
- $ pe init alice Create identity as "alice"
15
- $ pe init bob --force Overwrite existing identity
16
- `)
17
- .action(async (name, options) => {
18
- const spinner = ora('Initializing Identity...').start();
19
- try {
20
- const id = await createIdentity(name, options);
21
-
22
- // Create user profile (owner if first user, otherwise member)
23
- migrateToMultiUser();
24
- const profiles = getProfiles();
25
- const isFirst = profiles.length === 0 || (profiles.length === 1 && profiles[0].publicKey === id.publicKey);
26
- const role = isFirst ? 'owner' : 'user';
27
- try {
28
- addProfile(id.publicKey, null, name, role);
29
- } catch {
30
- // Profile may already exist from migration
31
- }
32
-
33
- spinner.succeed(chalk.green(`Identity Created for '${name}'!`));
34
- console.log(chalk.cyan(`Your Public Key (ID): ${id.publicKey}`));
35
- if (role === 'owner') {
36
- console.log(chalk.yellow(`Role: Owner (full control)`));
37
- }
38
- console.log(chalk.gray('Next: Run `pe register <handle>` to claim your handle.'));
39
- console.log('');
40
- console.log(chalk.bgYellow.black(' RECOVERY PHRASE — WRITE THIS DOWN AND KEEP IT SAFE '));
41
- console.log(chalk.yellow(id.mnemonic));
42
- console.log(chalk.red('This will NOT be shown again. Use `pe recover` to restore your identity.'));
43
-
44
- // Opt-in to error reporting (first-time setup)
45
- const settings = config.get('settings') || {};
46
- if (settings.errorReportingEnabled === undefined) {
47
- console.log('');
48
- console.log(chalk.cyan('Help improve Palexplorer?'));
49
- console.log(chalk.gray('Automatically send anonymous crash reports and errors to help us fix bugs.'));
50
- console.log(chalk.gray('No personal data, files, or encryption keys are ever sent.'));
51
- console.log(chalk.gray('You can change this anytime: pe config set errorReportingEnabled false'));
52
- settings.errorReportingEnabled = true;
53
- config.set('settings', settings);
54
- console.log(chalk.green('✔ Error reporting enabled (opt-out: pe config set errorReportingEnabled false)'));
55
- }
56
- } catch (err) {
57
- spinner.fail(chalk.red('Initialization Failed'));
58
- console.error(chalk.red(`Error: ${err.message}`));
59
- }
60
- });
61
- }
1
+ import { createIdentity } from '../core/identity.js';
2
+ import { addProfile, getProfiles, migrateToMultiUser } from '../core/users.js';
3
+ import config from '../utils/config.js';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+
7
+ export default function initCommand(program) {
8
+ program
9
+ .command('init <name>')
10
+ .description('create a new Pal Explorer identity with a name')
11
+ .option('-f, --force', 'Overwrite existing identity')
12
+ .addHelpText('after', `
13
+ Examples:
14
+ $ pal init alice Create identity as "alice"
15
+ $ pal init bob --force Overwrite existing identity
16
+ `)
17
+ .action(async (name, options) => {
18
+ const spinner = ora('Initializing Identity...').start();
19
+ try {
20
+ const id = await createIdentity(name, options);
21
+
22
+ // Create user profile (owner if first user, otherwise member)
23
+ migrateToMultiUser();
24
+ const profiles = getProfiles();
25
+ const isFirst = profiles.length === 0 || (profiles.length === 1 && profiles[0].publicKey === id.publicKey);
26
+ const role = isFirst ? 'owner' : 'user';
27
+ try {
28
+ addProfile(id.publicKey, null, name, role);
29
+ } catch {
30
+ // Profile may already exist from migration
31
+ }
32
+
33
+ spinner.succeed(chalk.green(`Identity Created for '${name}'!`));
34
+ console.log(chalk.cyan(`Your Public Key (ID): ${id.publicKey}`));
35
+ if (role === 'owner') {
36
+ console.log(chalk.yellow(`Role: Owner (full control)`));
37
+ }
38
+ console.log(chalk.gray('Next: Run `pal register <handle>` to claim your handle.'));
39
+ console.log('');
40
+ console.log(chalk.bgYellow.black(' RECOVERY PHRASE — WRITE THIS DOWN AND KEEP IT SAFE '));
41
+ console.log(chalk.yellow(id.mnemonic));
42
+ console.log(chalk.red('This will NOT be shown again. Use `pal recover` to restore your identity.'));
43
+
44
+ // Opt-in to error reporting (first-time setup)
45
+ const settings = config.get('settings') || {};
46
+ if (settings.errorReportingEnabled === undefined) {
47
+ console.log('');
48
+ console.log(chalk.cyan('Help improve Palexplorer?'));
49
+ console.log(chalk.gray('Automatically send anonymous crash reports and errors to help us fix bugs.'));
50
+ console.log(chalk.gray('No personal data, files, or encryption keys are ever sent.'));
51
+ console.log(chalk.gray('You can change this anytime: pal config set errorReportingEnabled false'));
52
+ settings.errorReportingEnabled = true;
53
+ config.set('settings', settings);
54
+ console.log(chalk.green('✔ Error reporting enabled (opt-out: pal config set errorReportingEnabled false)'));
55
+ }
56
+ } catch (err) {
57
+ spinner.fail(chalk.red('Initialization Failed'));
58
+ console.error(chalk.red(`Error: ${err.message}`));
59
+ }
60
+ });
61
+ }
@@ -1,59 +1,59 @@
1
- import chalk from 'chalk';
2
- import qrcode from 'qrcode-terminal';
3
- import { getIdentity } from '../core/identity.js';
4
-
5
- export function encodeInvite(identity) {
6
- const payload = JSON.stringify({
7
- pk: identity.publicKey,
8
- h: identity.handle || null,
9
- n: identity.name || null
10
- });
11
- return 'pal://' + Buffer.from(payload).toString('base64url');
12
- }
13
-
14
- export function decodeInvite(url) {
15
- if (!url.startsWith('pal://')) return null;
16
- try {
17
- const raw = Buffer.from(url.slice(6), 'base64url').toString('utf8');
18
- const parsed = JSON.parse(raw);
19
- if (!parsed.pk || typeof parsed.pk !== 'string') return null;
20
- return parsed;
21
- } catch {
22
- return null;
23
- }
24
- }
25
-
26
- export default function inviteCommand(program) {
27
- program
28
- .command('invite')
29
- .description('generate a magic invite link so others can add you as a pal')
30
- .option('--qr', 'Also render the invite as a QR code in the terminal')
31
- .addHelpText('after', `
32
- Examples:
33
- $ pe invite Generate invite link
34
- $ pe invite --qr Show invite as QR code
35
- `)
36
- .action(async (opts) => {
37
- const identity = await getIdentity();
38
- if (!identity) {
39
- console.log(chalk.red('No identity found. Run `pe init <name>` first.'));
40
- process.exitCode = 1;
41
- return;
42
- }
43
-
44
- const url = encodeInvite(identity);
45
-
46
- console.log('');
47
- console.log(chalk.cyan('Your invite link:'));
48
- console.log(chalk.white(url));
49
- console.log('');
50
- console.log(chalk.gray('Share this over any channel. Recipient runs:'));
51
- console.log(chalk.white(` pe pal add ${url}`));
52
-
53
- if (opts.qr) {
54
- console.log('');
55
- console.log(chalk.cyan('QR Code:'));
56
- qrcode.generate(url, { small: true });
57
- }
58
- });
59
- }
1
+ import chalk from 'chalk';
2
+ import qrcode from 'qrcode-terminal';
3
+ import { getIdentity } from '../core/identity.js';
4
+
5
+ export function encodeInvite(identity) {
6
+ const payload = JSON.stringify({
7
+ pk: identity.publicKey,
8
+ h: identity.handle || null,
9
+ n: identity.name || null
10
+ });
11
+ return 'pal://' + Buffer.from(payload).toString('base64url');
12
+ }
13
+
14
+ export function decodeInvite(url) {
15
+ if (!url.startsWith('pal://')) return null;
16
+ try {
17
+ const raw = Buffer.from(url.slice(6), 'base64url').toString('utf8');
18
+ const parsed = JSON.parse(raw);
19
+ if (!parsed.pk || typeof parsed.pk !== 'string') return null;
20
+ return parsed;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ export default function inviteCommand(program) {
27
+ program
28
+ .command('invite')
29
+ .description('generate a magic invite link so others can add you as a pal')
30
+ .option('--qr', 'Also render the invite as a QR code in the terminal')
31
+ .addHelpText('after', `
32
+ Examples:
33
+ $ pal invite Generate invite link
34
+ $ pal invite --qr Show invite as QR code
35
+ `)
36
+ .action(async (opts) => {
37
+ const identity = await getIdentity();
38
+ if (!identity) {
39
+ console.log(chalk.red('No identity found. Run `pal init <name>` first.'));
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+
44
+ const url = encodeInvite(identity);
45
+
46
+ console.log('');
47
+ console.log(chalk.cyan('Your invite link:'));
48
+ console.log(chalk.white(url));
49
+ console.log('');
50
+ console.log(chalk.gray('Share this over any channel. Recipient runs:'));
51
+ console.log(chalk.white(` pal pal add ${url}`));
52
+
53
+ if (opts.qr) {
54
+ console.log('');
55
+ console.log(chalk.cyan('QR Code:'));
56
+ qrcode.generate(url, { small: true });
57
+ }
58
+ });
59
+ }
@@ -1,59 +1,59 @@
1
- import chalk from 'chalk';
2
- import { listShares, getSharesByDrive } from '../core/shares.js';
3
-
4
- export default function listCommand(program) {
5
- program
6
- .command('list')
7
- .description('list shared resources')
8
- .option('--by-drive', 'Group shares by drive')
9
- .addHelpText('after', `
10
- Examples:
11
- $ pe list List all shared resources
12
- `)
13
- .action((options) => {
14
- const shares = listShares();
15
- if (program.opts().json) {
16
- const safe = shares.map(s => {
17
- const { password, encryptedShareKeys, ...rest } = s;
18
- return { ...rest, password: password ? '***' : undefined };
19
- });
20
- console.log(JSON.stringify(safe, null, 2));
21
- return;
22
- }
23
- if (shares.length === 0) {
24
- console.log(chalk.gray('No shared resources found. Use `pe share <path>` to add one.'));
25
- return;
26
- }
27
-
28
- if (options.byDrive) {
29
- const byDrive = getSharesByDrive();
30
- Object.keys(byDrive).forEach(drive => {
31
- console.log(''); // New line
32
- console.log(chalk.yellow(`Drive: ${drive}`));
33
- byDrive[drive].forEach(share => {
34
- const vis = share.visibility || 'global';
35
- const visColors = { global: 'green', public: 'green', private: 'red', group: 'blue', network: 'cyan', 'link-only': 'yellow' };
36
- const visibilityStr = chalk[visColors[vis] || 'white'](`[${vis.toUpperCase()}]`);
37
- const streamTag = share.streamable ? chalk.magenta(' [STREAM]') : '';
38
- const expiredTag = share.expired ? chalk.red(' [EXPIRED]') : '';
39
- console.log(` - ${visibilityStr}${streamTag}${expiredTag} [${chalk.cyan(share.id)}] ${share.path} (${chalk.gray(share.type)})`);
40
- });
41
- });
42
- } else {
43
- console.log(''); // New line
44
- console.log(chalk.green('Active Shares:'));
45
- shares.forEach(share => {
46
- const vis = share.visibility || 'global';
47
- const visColors = { global: 'green', public: 'green', private: 'red', group: 'blue', network: 'cyan', 'link-only': 'yellow' };
48
- const visibilityStr = chalk[visColors[vis] || 'white'](`[${vis.toUpperCase()}]`);
49
- const streamTag = share.streamable ? chalk.magenta(' [STREAM]') : '';
50
- const recipCount = (share.recipients || []).length;
51
- const recipStr = recipCount > 0 ? chalk.gray(` → ${recipCount} recipient${recipCount > 1 ? 's' : ''}`) : '';
52
- const expiredTag = share.expired ? chalk.red(' [EXPIRED]') : '';
53
- const expiryStr = share.expiresAt && !share.expired ? chalk.gray(` expires ${new Date(share.expiresAt).toLocaleDateString()}`) : '';
54
- const dlStr = share.maxDownloads ? chalk.gray(` ${share.downloads || 0}/${share.maxDownloads} downloads`) : '';
55
- console.log(`- ${visibilityStr}${streamTag}${expiredTag} [${chalk.cyan(share.id)}] ${share.path} (${chalk.gray(share.type)})${recipStr}${expiryStr}${dlStr}`);
56
- });
57
- }
58
- });
1
+ import chalk from 'chalk';
2
+ import { listShares, getSharesByDrive } from '../core/shares.js';
3
+
4
+ export default function listCommand(program) {
5
+ program
6
+ .command('list')
7
+ .description('list shared resources')
8
+ .option('--by-drive', 'Group shares by drive')
9
+ .addHelpText('after', `
10
+ Examples:
11
+ $ pal list List all shared resources
12
+ `)
13
+ .action((options) => {
14
+ const shares = listShares();
15
+ if (program.opts().json) {
16
+ const safe = shares.map(s => {
17
+ const { password, encryptedShareKeys, ...rest } = s;
18
+ return { ...rest, password: password ? '***' : undefined };
19
+ });
20
+ console.log(JSON.stringify(safe, null, 2));
21
+ return;
22
+ }
23
+ if (shares.length === 0) {
24
+ console.log(chalk.gray('No shared resources found. Use `pal share <path>` to add one.'));
25
+ return;
26
+ }
27
+
28
+ if (options.byDrive) {
29
+ const byDrive = getSharesByDrive();
30
+ Object.keys(byDrive).forEach(drive => {
31
+ console.log(''); // New line
32
+ console.log(chalk.yellow(`Drive: ${drive}`));
33
+ byDrive[drive].forEach(share => {
34
+ const vis = share.visibility || 'global';
35
+ const visColors = { global: 'green', public: 'green', private: 'red', group: 'blue', network: 'cyan', 'link-only': 'yellow' };
36
+ const visibilityStr = chalk[visColors[vis] || 'white'](`[${vis.toUpperCase()}]`);
37
+ const streamTag = share.streamable ? chalk.magenta(' [STREAM]') : '';
38
+ const expiredTag = share.expired ? chalk.red(' [EXPIRED]') : '';
39
+ console.log(` - ${visibilityStr}${streamTag}${expiredTag} [${chalk.cyan(share.id)}] ${share.path} (${chalk.gray(share.type)})`);
40
+ });
41
+ });
42
+ } else {
43
+ console.log(''); // New line
44
+ console.log(chalk.green('Active Shares:'));
45
+ shares.forEach(share => {
46
+ const vis = share.visibility || 'global';
47
+ const visColors = { global: 'green', public: 'green', private: 'red', group: 'blue', network: 'cyan', 'link-only': 'yellow' };
48
+ const visibilityStr = chalk[visColors[vis] || 'white'](`[${vis.toUpperCase()}]`);
49
+ const streamTag = share.streamable ? chalk.magenta(' [STREAM]') : '';
50
+ const recipCount = (share.recipients || []).length;
51
+ const recipStr = recipCount > 0 ? chalk.gray(` → ${recipCount} recipient${recipCount > 1 ? 's' : ''}`) : '';
52
+ const expiredTag = share.expired ? chalk.red(' [EXPIRED]') : '';
53
+ const expiryStr = share.expiresAt && !share.expired ? chalk.gray(` expires ${new Date(share.expiresAt).toLocaleDateString()}`) : '';
54
+ const dlStr = share.maxDownloads ? chalk.gray(` ${share.downloads || 0}/${share.maxDownloads} downloads`) : '';
55
+ console.log(`- ${visibilityStr}${streamTag}${expiredTag} [${chalk.cyan(share.id)}] ${share.path} (${chalk.gray(share.type)})${recipStr}${expiryStr}${dlStr}`);
56
+ });
57
+ }
58
+ });
59
59
  }
@@ -1,116 +1,116 @@
1
- import chalk from 'chalk';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import os from 'os';
5
-
6
- const LOG_DIR = path.join(os.homedir(), '.pal', 'logs');
7
- const LOG_FILE = path.join(LOG_DIR, 'pal.log');
8
-
9
- function colorLevel(level) {
10
- switch (level) {
11
- case 'error': return chalk.red(level);
12
- case 'warn': return chalk.yellow(level);
13
- case 'info': return chalk.blue(level);
14
- case 'debug': return chalk.gray(level);
15
- default: return level;
16
- }
17
- }
18
-
19
- function formatEntry(line) {
20
- try {
21
- const entry = JSON.parse(line);
22
- const ts = entry.ts?.slice(11, 19) || '';
23
- return `${chalk.gray(ts)} ${colorLevel(entry.level).padEnd(15)} ${entry.msg}`;
24
- } catch {
25
- return line;
26
- }
27
- }
28
-
29
- export default function logCommand(program) {
30
- program
31
- .command('log')
32
- .description('view application logs')
33
- .option('--tail', 'Follow new log entries')
34
- .option('--level <level>', 'Filter by level (debug/info/warn/error)')
35
- .option('-n, --lines <count>', 'Number of recent lines to show', '20')
36
- .option('--clear', 'Clear the log file')
37
- .addHelpText('after', `
38
- Examples:
39
- $ pe log Show recent log entries
40
- $ pe log --tail Follow log in real-time
41
- $ pe log --level error Filter by log level
42
- $ pe log -n 50 Show last 50 entries
43
- `)
44
- .action(async (opts) => {
45
- try {
46
- if (opts.clear) {
47
- if (fs.existsSync(LOG_FILE)) {
48
- fs.writeFileSync(LOG_FILE, '');
49
- console.log(chalk.green('✔ Log file cleared.'));
50
- } else {
51
- console.log(chalk.gray('No log file to clear.'));
52
- }
53
- return;
54
- }
55
-
56
- if (!fs.existsSync(LOG_FILE)) {
57
- console.log(chalk.gray('No log file found. Logs will appear after using commands.'));
58
- return;
59
- }
60
-
61
- const content = fs.readFileSync(LOG_FILE, 'utf8').trim();
62
- if (!content) {
63
- console.log(chalk.gray('Log file is empty.'));
64
- return;
65
- }
66
-
67
- let lines = content.split('\n');
68
-
69
- if (opts.level) {
70
- lines = lines.filter(line => {
71
- try {
72
- const entry = JSON.parse(line);
73
- return entry.level === opts.level;
74
- } catch { return true; }
75
- });
76
- }
77
-
78
- const count = parseInt(opts.lines, 10) || 20;
79
- lines = lines.slice(-count);
80
-
81
- for (const line of lines) {
82
- console.log(formatEntry(line));
83
- }
84
-
85
- if (opts.tail) {
86
- console.log(chalk.gray('--- tailing (Ctrl+C to stop) ---'));
87
- let size = fs.statSync(LOG_FILE).size;
88
- const watcher = fs.watch(LOG_FILE, () => {
89
- try {
90
- const newSize = fs.statSync(LOG_FILE).size;
91
- if (newSize <= size) { size = newSize; return; }
92
- const fd = fs.openSync(LOG_FILE, 'r');
93
- const buf = Buffer.alloc(newSize - size);
94
- fs.readSync(fd, buf, 0, buf.length, size);
95
- fs.closeSync(fd);
96
- size = newSize;
97
- const newLines = buf.toString().trim().split('\n');
98
- for (const line of newLines) {
99
- if (opts.level) {
100
- try {
101
- const entry = JSON.parse(line);
102
- if (entry.level !== opts.level) continue;
103
- } catch { /* show anyway */ }
104
- }
105
- console.log(formatEntry(line));
106
- }
107
- } catch { /* ignore */ }
108
- });
109
- await new Promise(() => {}); // block forever
110
- }
111
- } catch (err) {
112
- console.error(chalk.red('Log viewer error:'), err.message);
113
- process.exitCode = 1;
114
- }
115
- });
116
- }
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const LOG_DIR = path.join(os.homedir(), '.pal', 'logs');
7
+ const LOG_FILE = path.join(LOG_DIR, 'pal.log');
8
+
9
+ function colorLevel(level) {
10
+ switch (level) {
11
+ case 'error': return chalk.red(level);
12
+ case 'warn': return chalk.yellow(level);
13
+ case 'info': return chalk.blue(level);
14
+ case 'debug': return chalk.gray(level);
15
+ default: return level;
16
+ }
17
+ }
18
+
19
+ function formatEntry(line) {
20
+ try {
21
+ const entry = JSON.parse(line);
22
+ const ts = entry.ts?.slice(11, 19) || '';
23
+ return `${chalk.gray(ts)} ${colorLevel(entry.level).padEnd(15)} ${entry.msg}`;
24
+ } catch {
25
+ return line;
26
+ }
27
+ }
28
+
29
+ export default function logCommand(program) {
30
+ program
31
+ .command('log')
32
+ .description('view application logs')
33
+ .option('--tail', 'Follow new log entries')
34
+ .option('--level <level>', 'Filter by level (debug/info/warn/error)')
35
+ .option('-n, --lines <count>', 'Number of recent lines to show', '20')
36
+ .option('--clear', 'Clear the log file')
37
+ .addHelpText('after', `
38
+ Examples:
39
+ $ pal log Show recent log entries
40
+ $ pal log --tail Follow log in real-time
41
+ $ pal log --level error Filter by log level
42
+ $ pal log -n 50 Show last 50 entries
43
+ `)
44
+ .action(async (opts) => {
45
+ try {
46
+ if (opts.clear) {
47
+ if (fs.existsSync(LOG_FILE)) {
48
+ fs.writeFileSync(LOG_FILE, '');
49
+ console.log(chalk.green('✔ Log file cleared.'));
50
+ } else {
51
+ console.log(chalk.gray('No log file to clear.'));
52
+ }
53
+ return;
54
+ }
55
+
56
+ if (!fs.existsSync(LOG_FILE)) {
57
+ console.log(chalk.gray('No log file found. Logs will appear after using commands.'));
58
+ return;
59
+ }
60
+
61
+ const content = fs.readFileSync(LOG_FILE, 'utf8').trim();
62
+ if (!content) {
63
+ console.log(chalk.gray('Log file is empty.'));
64
+ return;
65
+ }
66
+
67
+ let lines = content.split('\n');
68
+
69
+ if (opts.level) {
70
+ lines = lines.filter(line => {
71
+ try {
72
+ const entry = JSON.parse(line);
73
+ return entry.level === opts.level;
74
+ } catch { return true; }
75
+ });
76
+ }
77
+
78
+ const count = parseInt(opts.lines, 10) || 20;
79
+ lines = lines.slice(-count);
80
+
81
+ for (const line of lines) {
82
+ console.log(formatEntry(line));
83
+ }
84
+
85
+ if (opts.tail) {
86
+ console.log(chalk.gray('--- tailing (Ctrl+C to stop) ---'));
87
+ let size = fs.statSync(LOG_FILE).size;
88
+ const watcher = fs.watch(LOG_FILE, () => {
89
+ try {
90
+ const newSize = fs.statSync(LOG_FILE).size;
91
+ if (newSize <= size) { size = newSize; return; }
92
+ const fd = fs.openSync(LOG_FILE, 'r');
93
+ const buf = Buffer.alloc(newSize - size);
94
+ fs.readSync(fd, buf, 0, buf.length, size);
95
+ fs.closeSync(fd);
96
+ size = newSize;
97
+ const newLines = buf.toString().trim().split('\n');
98
+ for (const line of newLines) {
99
+ if (opts.level) {
100
+ try {
101
+ const entry = JSON.parse(line);
102
+ if (entry.level !== opts.level) continue;
103
+ } catch { /* show anyway */ }
104
+ }
105
+ console.log(formatEntry(line));
106
+ }
107
+ } catch { /* ignore */ }
108
+ });
109
+ await new Promise(() => {}); // block forever
110
+ }
111
+ } catch (err) {
112
+ console.error(chalk.red('Log viewer error:'), err.message);
113
+ process.exitCode = 1;
114
+ }
115
+ });
116
+ }