pal-explorer-cli 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE.md +18 -0
  2. package/README.md +314 -0
  3. package/bin/pal.js +230 -0
  4. package/extensions/@palexplorer/analytics/README.md +45 -0
  5. package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
  6. package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
  7. package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
  8. package/extensions/@palexplorer/analytics/extension.json +27 -0
  9. package/extensions/@palexplorer/analytics/index.js +186 -0
  10. package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
  11. package/extensions/@palexplorer/audit/extension.json +17 -0
  12. package/extensions/@palexplorer/audit/index.js +2 -0
  13. package/extensions/@palexplorer/auth-email/extension.json +17 -0
  14. package/extensions/@palexplorer/auth-email/index.js +102 -0
  15. package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
  16. package/extensions/@palexplorer/auth-oauth/index.js +199 -0
  17. package/extensions/@palexplorer/chat/extension.json +17 -0
  18. package/extensions/@palexplorer/chat/index.js +2 -0
  19. package/extensions/@palexplorer/discovery/extension.json +16 -0
  20. package/extensions/@palexplorer/discovery/index.js +111 -0
  21. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  22. package/extensions/@palexplorer/email-notifications/index.js +242 -0
  23. package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
  24. package/extensions/@palexplorer/explorer-integration/index.js +122 -0
  25. package/extensions/@palexplorer/groups/extension.json +17 -0
  26. package/extensions/@palexplorer/groups/index.js +2 -0
  27. package/extensions/@palexplorer/networks/extension.json +17 -0
  28. package/extensions/@palexplorer/networks/index.js +2 -0
  29. package/extensions/@palexplorer/share-links/extension.json +17 -0
  30. package/extensions/@palexplorer/share-links/index.js +2 -0
  31. package/extensions/@palexplorer/sync/extension.json +17 -0
  32. package/extensions/@palexplorer/sync/index.js +2 -0
  33. package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
  34. package/extensions/@palexplorer/user-mgmt/index.js +2 -0
  35. package/extensions/@palexplorer/vfs/extension.json +17 -0
  36. package/extensions/@palexplorer/vfs/index.js +167 -0
  37. package/lib/capabilities.js +263 -0
  38. package/lib/commands/analytics.js +175 -0
  39. package/lib/commands/api-keys.js +131 -0
  40. package/lib/commands/audit.js +235 -0
  41. package/lib/commands/auth.js +137 -0
  42. package/lib/commands/backup.js +76 -0
  43. package/lib/commands/billing.js +148 -0
  44. package/lib/commands/chat.js +217 -0
  45. package/lib/commands/cloud-backup.js +231 -0
  46. package/lib/commands/comment.js +99 -0
  47. package/lib/commands/completion.js +203 -0
  48. package/lib/commands/compliance.js +218 -0
  49. package/lib/commands/config.js +136 -0
  50. package/lib/commands/connect.js +44 -0
  51. package/lib/commands/dept.js +294 -0
  52. package/lib/commands/device.js +146 -0
  53. package/lib/commands/download.js +226 -0
  54. package/lib/commands/explorer.js +178 -0
  55. package/lib/commands/extension.js +970 -0
  56. package/lib/commands/favorite.js +90 -0
  57. package/lib/commands/federation.js +270 -0
  58. package/lib/commands/file.js +533 -0
  59. package/lib/commands/group.js +271 -0
  60. package/lib/commands/gui-share.js +29 -0
  61. package/lib/commands/init.js +61 -0
  62. package/lib/commands/invite.js +59 -0
  63. package/lib/commands/list.js +59 -0
  64. package/lib/commands/log.js +116 -0
  65. package/lib/commands/nearby.js +108 -0
  66. package/lib/commands/network.js +251 -0
  67. package/lib/commands/notify.js +198 -0
  68. package/lib/commands/org.js +273 -0
  69. package/lib/commands/pal.js +180 -0
  70. package/lib/commands/permissions.js +216 -0
  71. package/lib/commands/pin.js +97 -0
  72. package/lib/commands/protocol.js +357 -0
  73. package/lib/commands/rbac.js +147 -0
  74. package/lib/commands/recover.js +36 -0
  75. package/lib/commands/register.js +171 -0
  76. package/lib/commands/relay.js +131 -0
  77. package/lib/commands/remote.js +368 -0
  78. package/lib/commands/revoke.js +50 -0
  79. package/lib/commands/scanner.js +280 -0
  80. package/lib/commands/schedule.js +344 -0
  81. package/lib/commands/scim.js +203 -0
  82. package/lib/commands/search.js +181 -0
  83. package/lib/commands/serve.js +438 -0
  84. package/lib/commands/server.js +350 -0
  85. package/lib/commands/share-link.js +199 -0
  86. package/lib/commands/share.js +323 -0
  87. package/lib/commands/sso.js +200 -0
  88. package/lib/commands/status.js +136 -0
  89. package/lib/commands/stream.js +562 -0
  90. package/lib/commands/su.js +187 -0
  91. package/lib/commands/sync.js +827 -0
  92. package/lib/commands/transfers.js +152 -0
  93. package/lib/commands/uninstall.js +188 -0
  94. package/lib/commands/update.js +204 -0
  95. package/lib/commands/user.js +276 -0
  96. package/lib/commands/vfs.js +84 -0
  97. package/lib/commands/web.js +52 -0
  98. package/lib/commands/webhook.js +180 -0
  99. package/lib/commands/whoami.js +59 -0
  100. package/lib/commands/workspace.js +121 -0
  101. package/lib/core/accessLog.js +54 -0
  102. package/lib/core/analytics.js +99 -0
  103. package/lib/core/backup.js +84 -0
  104. package/lib/core/billing.js +336 -0
  105. package/lib/core/bitfieldStore.js +53 -0
  106. package/lib/core/connectionManager.js +182 -0
  107. package/lib/core/dhtDiscovery.js +148 -0
  108. package/lib/core/discoveryClient.js +408 -0
  109. package/lib/core/extensionAnalyzer.js +357 -0
  110. package/lib/core/extensionSandbox.js +250 -0
  111. package/lib/core/extensionWorkerHost.js +166 -0
  112. package/lib/core/extensions.js +1082 -0
  113. package/lib/core/fileDiff.js +69 -0
  114. package/lib/core/groups.js +119 -0
  115. package/lib/core/identity.js +340 -0
  116. package/lib/core/mdnsService.js +126 -0
  117. package/lib/core/networks.js +81 -0
  118. package/lib/core/permissions.js +109 -0
  119. package/lib/core/pro.js +27 -0
  120. package/lib/core/resolver.js +74 -0
  121. package/lib/core/serverList.js +224 -0
  122. package/lib/core/sharePolicy.js +69 -0
  123. package/lib/core/shares.js +325 -0
  124. package/lib/core/signalingServer.js +441 -0
  125. package/lib/core/streamTransport.js +106 -0
  126. package/lib/core/su.js +55 -0
  127. package/lib/core/syncEngine.js +264 -0
  128. package/lib/core/syncState.js +159 -0
  129. package/lib/core/transfers.js +259 -0
  130. package/lib/core/users.js +225 -0
  131. package/lib/core/vfs.js +216 -0
  132. package/lib/core/webServer.js +702 -0
  133. package/lib/core/webrtcStream.js +396 -0
  134. package/lib/crypto/chatEncryption.js +57 -0
  135. package/lib/crypto/shareEncryption.js +195 -0
  136. package/lib/crypto/sharePassword.js +35 -0
  137. package/lib/crypto/streamEncryption.js +189 -0
  138. package/lib/package.json +1 -0
  139. package/lib/protocol/envelope.js +271 -0
  140. package/lib/protocol/handler.js +191 -0
  141. package/lib/protocol/index.js +27 -0
  142. package/lib/protocol/messages.js +247 -0
  143. package/lib/protocol/negotiation.js +127 -0
  144. package/lib/protocol/policy.js +142 -0
  145. package/lib/protocol/router.js +86 -0
  146. package/lib/protocol/sync.js +122 -0
  147. package/lib/utils/cli.js +15 -0
  148. package/lib/utils/config.js +123 -0
  149. package/lib/utils/configIntegrity.js +87 -0
  150. package/lib/utils/downloadDir.js +9 -0
  151. package/lib/utils/explorer.js +83 -0
  152. package/lib/utils/format.js +12 -0
  153. package/lib/utils/help.js +357 -0
  154. package/lib/utils/logger.js +103 -0
  155. package/lib/utils/torrent.js +203 -0
  156. package/package.json +71 -0
@@ -0,0 +1,271 @@
1
+ import chalk from 'chalk';
2
+ import { getFriends } from '../core/users.js';
3
+ import config from '../utils/config.js';
4
+ import { getIdentity } from '../core/identity.js';
5
+ import {
6
+ getGroups,
7
+ getGroup,
8
+ getGroupMembers,
9
+ createGroup,
10
+ updateGroup,
11
+ addMemberToGroup,
12
+ removeMemberFromGroup,
13
+ deleteGroup
14
+ } from '../core/groups.js';
15
+ import { checkLimit } from '../core/pro.js';
16
+ import { announceGroups } from '../core/discoveryClient.js';
17
+
18
+ export default function groupCommand(program) {
19
+ const cmd = program
20
+ .command('group')
21
+ .description('manage sharing groups')
22
+ .addHelpText('after', `
23
+ Examples:
24
+ $ pe group create team --public Create a public group called "team"
25
+ $ pe group edit team --desc "info" Edit group description
26
+ $ pe group add team @alice Add alice to the group
27
+ $ pe group remove team @alice Remove alice from the group
28
+ $ pe group list List all groups
29
+ $ pe group announce Announce public groups to discovery server
30
+ $ pe group delete team Delete a group
31
+ `);
32
+
33
+ cmd
34
+ .command('create <name>')
35
+ .description('create a new group')
36
+ .option('--public', 'Make group discoverable by others')
37
+ .option('--desc <description>', 'Group description')
38
+ .action((name, opts) => {
39
+ if (!name || !name.trim()) {
40
+ console.log(chalk.red('Group name cannot be empty.'));
41
+ process.exitCode = 1;
42
+ return;
43
+ }
44
+ try {
45
+ const groups = getGroups();
46
+ checkLimit('maxGroups', groups.length);
47
+ const identity = config.get('identity');
48
+ const group = createGroup(name, identity?.publicKey, {
49
+ visibility: opts.public ? 'public' : 'private',
50
+ description: opts.desc || null
51
+ });
52
+ console.log(chalk.green(`Group '${name}' created.`));
53
+ if (opts.public) {
54
+ console.log(chalk.yellow('Note: Public groups are discoverable. Use `pe group announce` to publish.'));
55
+ }
56
+ console.log(chalk.gray(`ID: ${group.id}`));
57
+ } catch (err) {
58
+ console.log(chalk.red(err.message));
59
+ process.exitCode = 1;
60
+ }
61
+ });
62
+
63
+ cmd
64
+ .command('edit <group>')
65
+ .description('edit a group (rename, description, visibility)')
66
+ .option('--name <name>', 'New group name')
67
+ .option('--desc <description>', 'Group description')
68
+ .option('--public', 'Make group public')
69
+ .option('--private', 'Make group private')
70
+ .action((groupName, opts) => {
71
+ try {
72
+ if (!opts.name && !opts.desc && !opts.public && !opts.private) {
73
+ console.log(chalk.yellow('Specify at least one option to update.'));
74
+ process.exitCode = 1;
75
+ return;
76
+ }
77
+ const identity = config.get('identity');
78
+ const updates = {};
79
+ if (opts.name) updates.name = opts.name;
80
+ if (opts.desc) updates.description = opts.desc;
81
+ if (opts.public) updates.visibility = 'public';
82
+ if (opts.private) updates.visibility = 'private';
83
+ const group = updateGroup(groupName, updates, identity?.publicKey);
84
+ console.log(chalk.green(`Group '${group.name}' updated.`));
85
+ if (group.visibility === 'public') {
86
+ console.log(chalk.yellow('Remember to run `pe group announce` to sync changes to discovery server.'));
87
+ }
88
+ } catch (err) {
89
+ console.log(chalk.red(err.message));
90
+ process.exitCode = 1;
91
+ }
92
+ });
93
+
94
+ cmd
95
+ .command('announce')
96
+ .description('announce public groups to the primary discovery server')
97
+ .action(async () => {
98
+ try {
99
+ const groups = getGroups();
100
+ const publicGroups = groups.filter(g => g.visibility === 'public');
101
+ if (publicGroups.length === 0) {
102
+ console.log(chalk.yellow('No public groups to announce.'));
103
+ return;
104
+ }
105
+ process.stdout.write(chalk.gray(`Announcing ${publicGroups.length} public group(s)... `));
106
+ const res = await announceGroups(groups);
107
+ if (res && res.success) {
108
+ console.log(chalk.green('done.'));
109
+ } else {
110
+ console.log(chalk.red('failed.'));
111
+ process.exitCode = 1;
112
+ }
113
+ } catch (err) {
114
+ console.log(chalk.red(`\nError: ${err.message}`));
115
+ process.exitCode = 1;
116
+ }
117
+ });
118
+
119
+ cmd
120
+ .command('add <group> <pal>')
121
+ .description('add a pal to a group')
122
+ .action((groupName, palName) => {
123
+ const friends = getFriends();
124
+ const stripped = palName.replace(/^@/, '');
125
+ const pal = friends.find(f =>
126
+ f.name === palName || f.id === palName || f.handle === palName || f.publicKey === palName ||
127
+ f.name === stripped || f.id === stripped || f.handle === stripped || f.publicKey === stripped
128
+ );
129
+ if (!pal) {
130
+ console.log(chalk.red(`Pal '${palName}' not found. Add them first with \`pe pal add\`.`));
131
+ process.exitCode = 1;
132
+ return;
133
+ }
134
+ try {
135
+ const members = getGroupMembers(groupName);
136
+ checkLimit('maxGroupMembers', members.length);
137
+ addMemberToGroup(groupName, pal);
138
+ console.log(chalk.green(`Added '${pal.name}' to '${groupName}'.`));
139
+ } catch (err) {
140
+ console.log(chalk.red(err.message));
141
+ process.exitCode = 1;
142
+ }
143
+ });
144
+
145
+ cmd
146
+ .command('remove <group> <pal>')
147
+ .description('remove a pal from a group')
148
+ .action((groupName, palName) => {
149
+ try {
150
+ removeMemberFromGroup(groupName, palName);
151
+ console.log(chalk.green(`Removed '${palName}' from '${groupName}'.`));
152
+ } catch (err) {
153
+ console.log(chalk.red(err.message));
154
+ process.exitCode = 1;
155
+ }
156
+ });
157
+
158
+ cmd
159
+ .command('delete <group>')
160
+ .description('delete a group')
161
+ .action((groupName) => {
162
+ const group = getGroup(groupName);
163
+ if (!group) {
164
+ console.log(chalk.red(`Group '${groupName}' not found.`));
165
+ process.exitCode = 1;
166
+ return;
167
+ }
168
+ const identity = config.get('identity');
169
+ deleteGroup(groupName, identity?.publicKey);
170
+ console.log(chalk.green(`Group '${group.name}' deleted.`));
171
+ });
172
+
173
+ cmd
174
+ .command('list')
175
+ .description('list all groups')
176
+ .action(() => {
177
+ const groups = getGroups();
178
+ if (program.opts().json) {
179
+ console.log(JSON.stringify({ groups }, null, 2));
180
+ return;
181
+ }
182
+ if (groups.length === 0) {
183
+ console.log(chalk.gray('No groups. Create one with `pe group create <name>`.'));
184
+ return;
185
+ }
186
+ console.log('');
187
+ console.log(chalk.cyan('Groups:'));
188
+ for (const g of groups) {
189
+ console.log(` ${chalk.white(g.name)} ${chalk.gray(`(${g.members.length} members)`)}`);
190
+ for (const m of g.members) {
191
+ console.log(` - ${m.name}${m.handle ? chalk.gray(` @${m.handle}`) : ''}`);
192
+ }
193
+ }
194
+ console.log('');
195
+ });
196
+
197
+ cmd
198
+ .command('broadcast <group> <message>')
199
+ .description('send a message to all group members')
200
+ .action(async (groupName, message) => {
201
+ try {
202
+ const group = getGroup(groupName);
203
+ if (!group) {
204
+ console.log(chalk.red(`Group '${groupName}' not found.`));
205
+ process.exitCode = 1;
206
+ return;
207
+ }
208
+ if (group.members.length === 0) {
209
+ console.log(chalk.yellow(`Group '${group.name}' has no members.`));
210
+ return;
211
+ }
212
+ const identity = await getIdentity();
213
+ if (!identity?.handle) {
214
+ console.log(chalk.red('You must register a handle first: pe register'));
215
+ process.exitCode = 1;
216
+ return;
217
+ }
218
+ const { getPrimaryServer } = await import('../core/discoveryClient.js');
219
+ const discoveryUrl = config.get('settings')?.discovery_servers?.[0]
220
+ || config.get('settings')?.discovery_url
221
+ || process.env.PAL_DISCOVERY_URL
222
+ || getPrimaryServer();
223
+
224
+ let sent = 0;
225
+ let failed = 0;
226
+ for (const member of group.members) {
227
+ const handle = member.handle;
228
+ if (!handle) {
229
+ console.log(chalk.yellow(` Skipping ${member.name} (no handle)`));
230
+ failed++;
231
+ continue;
232
+ }
233
+ try {
234
+ const res = await fetch(`${discoveryUrl}/api/v1/messages`, {
235
+ method: 'POST',
236
+ headers: { 'Content-Type': 'application/json' },
237
+ body: JSON.stringify({
238
+ toHandle: handle,
239
+ fromHandle: identity.handle,
240
+ fromDeviceId: identity.deviceId || null,
241
+ payload: { type: 'chat', text: message, timestamp: Date.now() },
242
+ }),
243
+ signal: AbortSignal.timeout(10000),
244
+ });
245
+ if (res.ok) {
246
+ sent++;
247
+ } else {
248
+ console.log(chalk.yellow(` Failed to send to @${handle}`));
249
+ failed++;
250
+ }
251
+ } catch {
252
+ console.log(chalk.yellow(` Failed to send to @${handle}`));
253
+ failed++;
254
+ }
255
+ }
256
+ console.log(chalk.green(`Broadcast to '${group.name}': ${sent} sent, ${failed} failed.`));
257
+ } catch (err) {
258
+ console.log(chalk.red(err.message));
259
+ process.exitCode = 1;
260
+ }
261
+ });
262
+
263
+ cmd.action(() => {
264
+ const groups = getGroups();
265
+ if (groups.length === 0) {
266
+ console.log(chalk.gray('No groups. Create one with `pe group create <name>`.'));
267
+ } else {
268
+ console.log(chalk.cyan(`${groups.length} group(s). Use \`pe group list\` for details.`));
269
+ }
270
+ });
271
+ }
@@ -0,0 +1,29 @@
1
+ import chalk from 'chalk';
2
+ import { spawn } from 'child_process';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ export default function guiShareCommand(program) {
9
+ program
10
+ .command('gui-share <path>')
11
+ .description('internal: Open GUI to share a specific file/folder')
12
+ .action((filePath) => {
13
+ const fullPath = path.resolve(filePath);
14
+ console.log(chalk.cyan(`Opening Palexplorer to share: ${fullPath}`));
15
+
16
+ // In a real app, we'd check if Electron is already running and send an IPC message
17
+ // For now, we'll just launch the GUI with an environment variable or argument
18
+ const guiPath = path.resolve(__dirname, '../../gui');
19
+
20
+ const child = spawn('npm', ['run', 'electron', '--', `--share=${fullPath}`], {
21
+ cwd: guiPath,
22
+ detached: true,
23
+ stdio: 'ignore',
24
+ });
25
+
26
+ child.unref();
27
+ process.exit(0);
28
+ });
29
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ });
59
+ }
@@ -0,0 +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
+ }