pal-explorer-cli 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +18 -0
- package/README.md +314 -0
- package/bin/pal.js +230 -0
- package/extensions/@palexplorer/analytics/README.md +45 -0
- package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
- package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
- package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
- package/extensions/@palexplorer/analytics/extension.json +27 -0
- package/extensions/@palexplorer/analytics/index.js +186 -0
- package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
- package/extensions/@palexplorer/audit/extension.json +17 -0
- package/extensions/@palexplorer/audit/index.js +2 -0
- package/extensions/@palexplorer/auth-email/extension.json +17 -0
- package/extensions/@palexplorer/auth-email/index.js +102 -0
- package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
- package/extensions/@palexplorer/auth-oauth/index.js +199 -0
- package/extensions/@palexplorer/chat/extension.json +17 -0
- package/extensions/@palexplorer/chat/index.js +2 -0
- package/extensions/@palexplorer/discovery/extension.json +16 -0
- package/extensions/@palexplorer/discovery/index.js +111 -0
- package/extensions/@palexplorer/email-notifications/extension.json +23 -0
- package/extensions/@palexplorer/email-notifications/index.js +242 -0
- package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
- package/extensions/@palexplorer/explorer-integration/index.js +122 -0
- package/extensions/@palexplorer/groups/extension.json +17 -0
- package/extensions/@palexplorer/groups/index.js +2 -0
- package/extensions/@palexplorer/networks/extension.json +17 -0
- package/extensions/@palexplorer/networks/index.js +2 -0
- package/extensions/@palexplorer/share-links/extension.json +17 -0
- package/extensions/@palexplorer/share-links/index.js +2 -0
- package/extensions/@palexplorer/sync/extension.json +17 -0
- package/extensions/@palexplorer/sync/index.js +2 -0
- package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
- package/extensions/@palexplorer/user-mgmt/index.js +2 -0
- package/extensions/@palexplorer/vfs/extension.json +17 -0
- package/extensions/@palexplorer/vfs/index.js +167 -0
- package/lib/capabilities.js +263 -0
- package/lib/commands/analytics.js +175 -0
- package/lib/commands/api-keys.js +131 -0
- package/lib/commands/audit.js +235 -0
- package/lib/commands/auth.js +137 -0
- package/lib/commands/backup.js +76 -0
- package/lib/commands/billing.js +148 -0
- package/lib/commands/chat.js +217 -0
- package/lib/commands/cloud-backup.js +231 -0
- package/lib/commands/comment.js +99 -0
- package/lib/commands/completion.js +203 -0
- package/lib/commands/compliance.js +218 -0
- package/lib/commands/config.js +136 -0
- package/lib/commands/connect.js +44 -0
- package/lib/commands/dept.js +294 -0
- package/lib/commands/device.js +146 -0
- package/lib/commands/download.js +226 -0
- package/lib/commands/explorer.js +178 -0
- package/lib/commands/extension.js +970 -0
- package/lib/commands/favorite.js +90 -0
- package/lib/commands/federation.js +270 -0
- package/lib/commands/file.js +533 -0
- package/lib/commands/group.js +271 -0
- package/lib/commands/gui-share.js +29 -0
- package/lib/commands/init.js +61 -0
- package/lib/commands/invite.js +59 -0
- package/lib/commands/list.js +59 -0
- package/lib/commands/log.js +116 -0
- package/lib/commands/nearby.js +108 -0
- package/lib/commands/network.js +251 -0
- package/lib/commands/notify.js +198 -0
- package/lib/commands/org.js +273 -0
- package/lib/commands/pal.js +180 -0
- package/lib/commands/permissions.js +216 -0
- package/lib/commands/pin.js +97 -0
- package/lib/commands/protocol.js +357 -0
- package/lib/commands/rbac.js +147 -0
- package/lib/commands/recover.js +36 -0
- package/lib/commands/register.js +171 -0
- package/lib/commands/relay.js +131 -0
- package/lib/commands/remote.js +368 -0
- package/lib/commands/revoke.js +50 -0
- package/lib/commands/scanner.js +280 -0
- package/lib/commands/schedule.js +344 -0
- package/lib/commands/scim.js +203 -0
- package/lib/commands/search.js +181 -0
- package/lib/commands/serve.js +438 -0
- package/lib/commands/server.js +350 -0
- package/lib/commands/share-link.js +199 -0
- package/lib/commands/share.js +323 -0
- package/lib/commands/sso.js +200 -0
- package/lib/commands/status.js +136 -0
- package/lib/commands/stream.js +562 -0
- package/lib/commands/su.js +187 -0
- package/lib/commands/sync.js +827 -0
- package/lib/commands/transfers.js +152 -0
- package/lib/commands/uninstall.js +188 -0
- package/lib/commands/update.js +204 -0
- package/lib/commands/user.js +276 -0
- package/lib/commands/vfs.js +84 -0
- package/lib/commands/web.js +52 -0
- package/lib/commands/webhook.js +180 -0
- package/lib/commands/whoami.js +59 -0
- package/lib/commands/workspace.js +121 -0
- package/lib/core/accessLog.js +54 -0
- package/lib/core/analytics.js +99 -0
- package/lib/core/backup.js +84 -0
- package/lib/core/billing.js +336 -0
- package/lib/core/bitfieldStore.js +53 -0
- package/lib/core/connectionManager.js +182 -0
- package/lib/core/dhtDiscovery.js +148 -0
- package/lib/core/discoveryClient.js +408 -0
- package/lib/core/extensionAnalyzer.js +357 -0
- package/lib/core/extensionSandbox.js +250 -0
- package/lib/core/extensionWorkerHost.js +166 -0
- package/lib/core/extensions.js +1082 -0
- package/lib/core/fileDiff.js +69 -0
- package/lib/core/groups.js +119 -0
- package/lib/core/identity.js +340 -0
- package/lib/core/mdnsService.js +126 -0
- package/lib/core/networks.js +81 -0
- package/lib/core/permissions.js +109 -0
- package/lib/core/pro.js +27 -0
- package/lib/core/resolver.js +74 -0
- package/lib/core/serverList.js +224 -0
- package/lib/core/sharePolicy.js +69 -0
- package/lib/core/shares.js +325 -0
- package/lib/core/signalingServer.js +441 -0
- package/lib/core/streamTransport.js +106 -0
- package/lib/core/su.js +55 -0
- package/lib/core/syncEngine.js +264 -0
- package/lib/core/syncState.js +159 -0
- package/lib/core/transfers.js +259 -0
- package/lib/core/users.js +225 -0
- package/lib/core/vfs.js +216 -0
- package/lib/core/webServer.js +702 -0
- package/lib/core/webrtcStream.js +396 -0
- package/lib/crypto/chatEncryption.js +57 -0
- package/lib/crypto/shareEncryption.js +195 -0
- package/lib/crypto/sharePassword.js +35 -0
- package/lib/crypto/streamEncryption.js +189 -0
- package/lib/package.json +1 -0
- package/lib/protocol/envelope.js +271 -0
- package/lib/protocol/handler.js +191 -0
- package/lib/protocol/index.js +27 -0
- package/lib/protocol/messages.js +247 -0
- package/lib/protocol/negotiation.js +127 -0
- package/lib/protocol/policy.js +142 -0
- package/lib/protocol/router.js +86 -0
- package/lib/protocol/sync.js +122 -0
- package/lib/utils/cli.js +15 -0
- package/lib/utils/config.js +123 -0
- package/lib/utils/configIntegrity.js +87 -0
- package/lib/utils/downloadDir.js +9 -0
- package/lib/utils/explorer.js +83 -0
- package/lib/utils/format.js +12 -0
- package/lib/utils/help.js +357 -0
- package/lib/utils/logger.js +103 -0
- package/lib/utils/torrent.js +203 -0
- package/package.json +71 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { removeShare, rotateShareKey, removeRecipientFromShare, listShares } from '../core/shares.js';
|
|
3
|
+
|
|
4
|
+
export default function revokeCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command('revoke <idOrPath>')
|
|
7
|
+
.description('revoke access to a shared resource or remove a specific recipient')
|
|
8
|
+
.option('--pal <palId>', 'Remove a specific pal from the share (triggers key rotation)')
|
|
9
|
+
.addHelpText('after', `
|
|
10
|
+
Examples:
|
|
11
|
+
$ pe revoke abc123 Revoke share by ID
|
|
12
|
+
$ pe revoke ./photos Revoke share by path
|
|
13
|
+
$ pe revoke abc123 --pal alice Remove one recipient (rotates key)
|
|
14
|
+
`)
|
|
15
|
+
.action(async (idOrPath, opts) => {
|
|
16
|
+
try {
|
|
17
|
+
if (opts.pal) {
|
|
18
|
+
// Remove a specific recipient and rotate keys
|
|
19
|
+
const shares = listShares();
|
|
20
|
+
const share = shares.find(s => s.id === idOrPath || s.path === idOrPath);
|
|
21
|
+
if (!share) {
|
|
22
|
+
console.error(chalk.red('Share not found.'));
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
removeRecipientFromShare(share.id, opts.pal);
|
|
28
|
+
console.log(chalk.yellow(`Removed ${opts.pal} from share ${share.id}`));
|
|
29
|
+
|
|
30
|
+
if (share.visibility === 'private' && (share.recipients || []).length > 0) {
|
|
31
|
+
console.log(chalk.blue('Rotating share key and re-encrypting...'));
|
|
32
|
+
const { newEncryptedShareKeys } = await rotateShareKey(share.id);
|
|
33
|
+
const remaining = Object.keys(newEncryptedShareKeys).length;
|
|
34
|
+
console.log(chalk.green(`Key rotated. ${remaining} recipient(s) will need new magnet on next serve.`));
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Full revoke
|
|
40
|
+
const removed = removeShare(idOrPath);
|
|
41
|
+
if (removed) {
|
|
42
|
+
console.log(chalk.green(`Successfully revoked share: ${idOrPath}`));
|
|
43
|
+
} else {
|
|
44
|
+
console.error(chalk.red('Share not found.'));
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const BUILTIN_RULES = [
|
|
6
|
+
{ name: 'SSN', pattern: '\\d{3}-\\d{2}-\\d{4}', severity: 'critical', description: 'Social Security Number' },
|
|
7
|
+
{ name: 'CreditCard', pattern: '4\\d{3}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}', severity: 'critical', description: 'Credit card number (Visa)' },
|
|
8
|
+
{ name: 'CreditCardMC', pattern: '5[1-5]\\d{2}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}', severity: 'critical', description: 'Credit card number (Mastercard)' },
|
|
9
|
+
{ name: 'APIKey', pattern: '(?:sk_|pk_|api_|key_)[a-zA-Z0-9]{16,}', severity: 'high', description: 'API key' },
|
|
10
|
+
{ name: 'PrivateKey', pattern: '-----BEGIN PRIVATE KEY-----', severity: 'critical', description: 'Private key block' },
|
|
11
|
+
{ name: 'AWSKey', pattern: 'AKIA[0-9A-Z]{16}', severity: 'critical', description: 'AWS access key' },
|
|
12
|
+
{ name: 'Email', pattern: '\\b[\\w.-]+@[\\w.-]+\\.\\w{2,}\\b', severity: 'low', description: 'Email address' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const SEVERITY_ORDER = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
16
|
+
const SEVERITY_COLORS = {
|
|
17
|
+
critical: chalk.red,
|
|
18
|
+
high: chalk.yellow,
|
|
19
|
+
medium: chalk.cyan,
|
|
20
|
+
low: chalk.gray,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function classifyFile(maxSeverity) {
|
|
24
|
+
if (maxSeverity === 'critical') return 'Restricted';
|
|
25
|
+
if (maxSeverity === 'high') return 'Confidential';
|
|
26
|
+
if (maxSeverity === 'medium') return 'Internal';
|
|
27
|
+
return 'Public';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function scanContent(content, rules) {
|
|
31
|
+
const findings = [];
|
|
32
|
+
const lines = content.split('\n');
|
|
33
|
+
for (const rule of rules) {
|
|
34
|
+
const re = new RegExp(rule.pattern, 'g');
|
|
35
|
+
for (let i = 0; i < lines.length; i++) {
|
|
36
|
+
let match;
|
|
37
|
+
while ((match = re.exec(lines[i])) !== null) {
|
|
38
|
+
findings.push({
|
|
39
|
+
rule: rule.name,
|
|
40
|
+
severity: rule.severity,
|
|
41
|
+
line: i + 1,
|
|
42
|
+
match: match[0].length > 40 ? match[0].slice(0, 37) + '...' : match[0],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return findings;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default function scannerCommand(program) {
|
|
51
|
+
const cmd = program
|
|
52
|
+
.command('scanner')
|
|
53
|
+
.description('content scanning and DLP (Enterprise)')
|
|
54
|
+
.addHelpText('after', `
|
|
55
|
+
Examples:
|
|
56
|
+
$ pe scanner scan ./documents Scan directory for sensitive data
|
|
57
|
+
$ pe scanner scan ./secrets.txt Scan a single file
|
|
58
|
+
$ pe scanner rules List active DLP rules
|
|
59
|
+
$ pe scanner rules --add MyRule "\\d{6}" high Add a custom rule
|
|
60
|
+
$ pe scanner rules --remove MyRule Remove a custom rule
|
|
61
|
+
$ pe scanner quarantine List quarantined files
|
|
62
|
+
$ pe scanner release <id> Release file from quarantine
|
|
63
|
+
`)
|
|
64
|
+
.action(() => { cmd.outputHelp(); });
|
|
65
|
+
|
|
66
|
+
cmd
|
|
67
|
+
.command('scan <path>')
|
|
68
|
+
.description('scan a file or directory for sensitive data')
|
|
69
|
+
.action(async (targetPath) => {
|
|
70
|
+
try {
|
|
71
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
72
|
+
const extSettings = extConfig.get('ext.content-scanner') || {};
|
|
73
|
+
const customRules = extSettings.rules || [];
|
|
74
|
+
const allRules = [...BUILTIN_RULES, ...customRules];
|
|
75
|
+
|
|
76
|
+
const resolved = path.resolve(targetPath);
|
|
77
|
+
if (!fs.existsSync(resolved)) {
|
|
78
|
+
console.log(chalk.red(`Path not found: ${resolved}`));
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const stat = fs.statSync(resolved);
|
|
84
|
+
const files = [];
|
|
85
|
+
if (stat.isDirectory()) {
|
|
86
|
+
const entries = fs.readdirSync(resolved, { recursive: true });
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const full = path.join(resolved, entry);
|
|
89
|
+
try {
|
|
90
|
+
if (fs.statSync(full).isFile()) files.push(full);
|
|
91
|
+
} catch { /* skip unreadable */ }
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
files.push(resolved);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (files.length === 0) {
|
|
98
|
+
console.log(chalk.gray('No files to scan.'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(chalk.cyan(`Scanning ${files.length} file(s)...\n`));
|
|
103
|
+
let totalFindings = 0;
|
|
104
|
+
let maxSeverity = 'low';
|
|
105
|
+
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
let content;
|
|
108
|
+
try {
|
|
109
|
+
content = fs.readFileSync(file, 'utf8');
|
|
110
|
+
} catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const findings = scanContent(content, allRules);
|
|
115
|
+
if (findings.length === 0) continue;
|
|
116
|
+
|
|
117
|
+
totalFindings += findings.length;
|
|
118
|
+
const rel = path.relative(process.cwd(), file);
|
|
119
|
+
console.log(` ${chalk.white(rel)}`);
|
|
120
|
+
|
|
121
|
+
for (const f of findings) {
|
|
122
|
+
const colorFn = SEVERITY_COLORS[f.severity] || chalk.white;
|
|
123
|
+
console.log(` Line ${chalk.gray(f.line)}: ${colorFn(`[${f.severity.toUpperCase()}]`)} ${chalk.white(f.rule)} — ${chalk.dim(f.match)}`);
|
|
124
|
+
if (SEVERITY_ORDER[f.severity] > SEVERITY_ORDER[maxSeverity]) {
|
|
125
|
+
maxSeverity = f.severity;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
console.log('');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (totalFindings === 0) {
|
|
132
|
+
console.log(chalk.green('✔ No sensitive data found.'));
|
|
133
|
+
console.log(` Classification: ${chalk.green('Public')}`);
|
|
134
|
+
} else {
|
|
135
|
+
const classification = classifyFile(maxSeverity);
|
|
136
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
137
|
+
console.log(` Total findings: ${chalk.white(totalFindings)}`);
|
|
138
|
+
console.log(` Classification: ${SEVERITY_COLORS[maxSeverity](classification)}`);
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.log(chalk.red(`Scan failed: ${err.message}`));
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
cmd
|
|
147
|
+
.command('rules')
|
|
148
|
+
.description('list active DLP rules')
|
|
149
|
+
.option('--add <name>', 'add a custom rule (requires <pattern> <severity> as args)')
|
|
150
|
+
.option('--remove <name>', 'remove a custom rule')
|
|
151
|
+
.action(async (opts, command) => {
|
|
152
|
+
try {
|
|
153
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
154
|
+
const extSettings = extConfig.get('ext.content-scanner') || {};
|
|
155
|
+
|
|
156
|
+
if (opts.remove) {
|
|
157
|
+
const customRules = extSettings.rules || [];
|
|
158
|
+
const idx = customRules.findIndex(r => r.name === opts.remove);
|
|
159
|
+
if (idx === -1) {
|
|
160
|
+
console.log(chalk.yellow(`Rule not found: ${opts.remove}`));
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
customRules.splice(idx, 1);
|
|
165
|
+
extSettings.rules = customRules;
|
|
166
|
+
extConfig.set('ext.content-scanner', extSettings);
|
|
167
|
+
console.log(chalk.green(`✔ Rule removed: ${opts.remove}`));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (opts.add) {
|
|
172
|
+
const args = command.args;
|
|
173
|
+
if (args.length < 2) {
|
|
174
|
+
console.log(chalk.red('Usage: pe scanner rules --add <name> <pattern> <severity>'));
|
|
175
|
+
process.exitCode = 1;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const pattern = args[0];
|
|
179
|
+
try {
|
|
180
|
+
const testRe = new RegExp(pattern);
|
|
181
|
+
const start = Date.now();
|
|
182
|
+
'a'.repeat(25).match(testRe);
|
|
183
|
+
if (Date.now() - start > 100) throw new Error('Pattern too complex');
|
|
184
|
+
} catch (e) {
|
|
185
|
+
console.error(chalk.red(`Invalid or unsafe regex pattern: ${e.message}`));
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const severity = args[1] || 'medium';
|
|
190
|
+
if (!['critical', 'high', 'medium', 'low'].includes(severity)) {
|
|
191
|
+
console.log(chalk.red('Severity must be: critical, high, medium, or low'));
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const customRules = extSettings.rules || [];
|
|
196
|
+
if (customRules.find(r => r.name === opts.add)) {
|
|
197
|
+
console.log(chalk.yellow(`Rule already exists: ${opts.add}`));
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
customRules.push({ name: opts.add, pattern, severity, description: `Custom rule: ${opts.add}` });
|
|
202
|
+
extSettings.rules = customRules;
|
|
203
|
+
extConfig.set('ext.content-scanner', extSettings);
|
|
204
|
+
console.log(chalk.green(`✔ Rule added: ${opts.add}`));
|
|
205
|
+
console.log(` Pattern: ${chalk.white(pattern)}`);
|
|
206
|
+
console.log(` Severity: ${SEVERITY_COLORS[severity](severity)}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// List all rules
|
|
211
|
+
const customRules = extSettings.rules || [];
|
|
212
|
+
const allRules = [...BUILTIN_RULES, ...customRules];
|
|
213
|
+
console.log(chalk.bold(`DLP Rules (${allRules.length})\n`));
|
|
214
|
+
console.log(chalk.cyan(' Built-in:'));
|
|
215
|
+
for (const r of BUILTIN_RULES) {
|
|
216
|
+
const colorFn = SEVERITY_COLORS[r.severity] || chalk.white;
|
|
217
|
+
console.log(` ${chalk.white(r.name)} ${colorFn(`[${r.severity}]`)} — ${chalk.dim(r.description)}`);
|
|
218
|
+
}
|
|
219
|
+
if (customRules.length > 0) {
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log(chalk.cyan(' Custom:'));
|
|
222
|
+
for (const r of customRules) {
|
|
223
|
+
const colorFn = SEVERITY_COLORS[r.severity] || chalk.white;
|
|
224
|
+
console.log(` ${chalk.white(r.name)} ${colorFn(`[${r.severity}]`)} — ${chalk.dim(r.pattern)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.log(chalk.red(`Rules failed: ${err.message}`));
|
|
229
|
+
process.exitCode = 1;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
cmd
|
|
234
|
+
.command('quarantine')
|
|
235
|
+
.description('list quarantined files')
|
|
236
|
+
.action(async () => {
|
|
237
|
+
try {
|
|
238
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
239
|
+
const store = extConfig.get('ext_store.content-scanner') || {};
|
|
240
|
+
const quarantined = store.quarantined || [];
|
|
241
|
+
if (quarantined.length === 0) {
|
|
242
|
+
console.log(chalk.dim('No quarantined files.'));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
console.log(chalk.bold(`Quarantined Files (${quarantined.length})\n`));
|
|
246
|
+
for (const f of quarantined) {
|
|
247
|
+
const colorFn = SEVERITY_COLORS[f.severity] || chalk.white;
|
|
248
|
+
console.log(` ${chalk.white(f.id)} ${colorFn(`[${f.severity}]`)} ${chalk.cyan(f.path)}`);
|
|
249
|
+
console.log(` Reason: ${chalk.dim(f.reason || 'DLP violation')} Quarantined: ${chalk.dim(f.quarantinedAt)}`);
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.log(chalk.red(`Quarantine list failed: ${err.message}`));
|
|
253
|
+
process.exitCode = 1;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
cmd
|
|
258
|
+
.command('release <id>')
|
|
259
|
+
.description('release a file from quarantine')
|
|
260
|
+
.action(async (id) => {
|
|
261
|
+
try {
|
|
262
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
263
|
+
const store = extConfig.get('ext_store.content-scanner') || {};
|
|
264
|
+
const quarantined = store.quarantined || [];
|
|
265
|
+
const idx = quarantined.findIndex(f => f.id === id);
|
|
266
|
+
if (idx === -1) {
|
|
267
|
+
console.log(chalk.yellow(`Quarantined file not found: ${id}`));
|
|
268
|
+
process.exitCode = 1;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const released = quarantined.splice(idx, 1)[0];
|
|
272
|
+
store.quarantined = quarantined;
|
|
273
|
+
extConfig.set('ext_store.content-scanner', store);
|
|
274
|
+
console.log(chalk.green(`✔ Released from quarantine: ${released.path}`));
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.log(chalk.red(`Release failed: ${err.message}`));
|
|
277
|
+
process.exitCode = 1;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import config from '../utils/config.js';
|
|
3
|
+
import { parseCommaList } from '../utils/cli.js';
|
|
4
|
+
|
|
5
|
+
function loadTasks() {
|
|
6
|
+
return config.get('scheduledTasks') || [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function saveTasks(tasks) {
|
|
10
|
+
config.set('scheduledTasks', tasks);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatDate(ts) {
|
|
14
|
+
return new Date(ts).toLocaleString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const STATUS_COLORS = {
|
|
18
|
+
pending: chalk.blue,
|
|
19
|
+
running: chalk.yellow,
|
|
20
|
+
completed: chalk.green,
|
|
21
|
+
failed: chalk.red,
|
|
22
|
+
cancelled: chalk.gray,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const VALID_EVERY = ['hourly', 'daily', 'weekly', 'monthly'];
|
|
26
|
+
|
|
27
|
+
export function parseEvery(val) {
|
|
28
|
+
if (!val) return null;
|
|
29
|
+
const lower = val.toLowerCase();
|
|
30
|
+
if (VALID_EVERY.includes(lower)) return lower;
|
|
31
|
+
const match = lower.match(/^(\d+)\s*(m|min|minutes?|h|hr|hours?|d|days?)$/);
|
|
32
|
+
if (match) {
|
|
33
|
+
const n = parseInt(match[1], 10);
|
|
34
|
+
const unit = match[2][0];
|
|
35
|
+
if (unit === 'm') return n * 60_000;
|
|
36
|
+
if (unit === 'h') return n * 3_600_000;
|
|
37
|
+
if (unit === 'd') return n * 86_400_000;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function computeNextRun(task) {
|
|
43
|
+
if (!task.every) return null;
|
|
44
|
+
const raw = task.lastRunAt || task.executeAt || Date.now();
|
|
45
|
+
const base = typeof raw === 'string' ? new Date(raw).getTime() : raw;
|
|
46
|
+
if (typeof task.every === 'number') return base + task.every;
|
|
47
|
+
const d = new Date(base);
|
|
48
|
+
if (task.every === 'hourly') d.setHours(d.getHours() + 1);
|
|
49
|
+
else if (task.every === 'daily') d.setDate(d.getDate() + 1);
|
|
50
|
+
else if (task.every === 'weekly') d.setDate(d.getDate() + 7);
|
|
51
|
+
else if (task.every === 'monthly') d.setMonth(d.getMonth() + 1);
|
|
52
|
+
return d.getTime();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function parseWindow(val) {
|
|
56
|
+
if (!val) return null;
|
|
57
|
+
const match = val.match(/^(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})$/);
|
|
58
|
+
if (!match) return null;
|
|
59
|
+
const sh = parseInt(match[1], 10), sm = parseInt(match[2], 10);
|
|
60
|
+
const eh = parseInt(match[3], 10), em = parseInt(match[4], 10);
|
|
61
|
+
if (sh > 23 || sm > 59 || eh > 23 || em > 59) return null;
|
|
62
|
+
return { startHour: sh, startMin: sm, endHour: eh, endMin: em };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isInWindow(window, now) {
|
|
66
|
+
if (!window) return true;
|
|
67
|
+
const d = now || new Date();
|
|
68
|
+
const current = d.getHours() * 60 + d.getMinutes();
|
|
69
|
+
const start = window.startHour * 60 + window.startMin;
|
|
70
|
+
const end = window.endHour * 60 + window.endMin;
|
|
71
|
+
if (start <= end) return current >= start && current < end;
|
|
72
|
+
// Overnight window (e.g. 22:00-06:00)
|
|
73
|
+
return current >= start || current < end;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isTaskReady(task, now) {
|
|
77
|
+
if (task.status !== 'pending') return false;
|
|
78
|
+
const ts = now || Date.now();
|
|
79
|
+
if (task.executeAt > ts) return false;
|
|
80
|
+
if (task.window && !isInWindow(task.window, new Date(ts))) return false;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default function scheduleCommand(program) {
|
|
85
|
+
const cmd = program
|
|
86
|
+
.command('schedule')
|
|
87
|
+
.description('manage scheduled tasks (shares, downloads, auto-revokes)')
|
|
88
|
+
.addHelpText('after', `
|
|
89
|
+
Examples:
|
|
90
|
+
$ pe schedule List all scheduled tasks
|
|
91
|
+
$ pe schedule add share /path --at "2026-03-15 09:00"
|
|
92
|
+
$ pe schedule add share /path --at "2026-03-15 09:00" --every daily
|
|
93
|
+
$ pe schedule add download --magnet "magnet:..." --at "22:00" --window "22:00-06:00"
|
|
94
|
+
$ pe schedule add revoke --share-path /path --at "2026-04-01 00:00"
|
|
95
|
+
$ pe schedule cancel <taskId>
|
|
96
|
+
$ pe schedule run-once Execute due tasks once and exit
|
|
97
|
+
$ pe schedule daemon Run scheduler in foreground (30s interval)
|
|
98
|
+
`)
|
|
99
|
+
.action(() => {
|
|
100
|
+
const tasks = loadTasks();
|
|
101
|
+
if (tasks.length === 0) {
|
|
102
|
+
console.log(chalk.gray('No scheduled tasks. Use `pe schedule add` to create one.'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log(chalk.cyan('Scheduled Tasks:'));
|
|
107
|
+
for (const t of tasks) {
|
|
108
|
+
const colorFn = STATUS_COLORS[t.status] || chalk.white;
|
|
109
|
+
console.log(` ${chalk.white(t.id)} [${colorFn(t.status)}] ${chalk.yellow(t.type)}`);
|
|
110
|
+
console.log(` Execute at: ${chalk.white(formatDate(t.executeAt))}`);
|
|
111
|
+
if (t.every) console.log(` Repeat: ${chalk.cyan(typeof t.every === 'number' ? `${t.every / 60000}m` : t.every)}`);
|
|
112
|
+
if (t.window) console.log(` Window: ${chalk.cyan(`${String(t.window.startHour).padStart(2, '0')}:${String(t.window.startMin).padStart(2, '0')}-${String(t.window.endHour).padStart(2, '0')}:${String(t.window.endMin).padStart(2, '0')}`)}`);
|
|
113
|
+
if (t.data.folderPath) console.log(` Path: ${chalk.gray(t.data.folderPath)}`);
|
|
114
|
+
if (t.data.magnet) console.log(` Magnet: ${chalk.gray(t.data.magnet.slice(0, 50))}...`);
|
|
115
|
+
if (t.data.sharePath) console.log(` Share: ${chalk.gray(t.data.sharePath)}`);
|
|
116
|
+
if (t.completedAt) console.log(` Completed: ${chalk.gray(t.completedAt)}`);
|
|
117
|
+
if (t.error) console.log(` Error: ${chalk.red(t.error)}`);
|
|
118
|
+
if (t.runCount) console.log(` Runs: ${chalk.gray(t.runCount)}`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const addCmd = cmd
|
|
123
|
+
.command('add <type>')
|
|
124
|
+
.description('schedule a new task (share, download, revoke)')
|
|
125
|
+
.option('--at <datetime>', 'When to execute (ISO 8601 or local datetime string)')
|
|
126
|
+
.option('--every <interval>', 'Repeat: hourly, daily, weekly, monthly, or Nm/Nh/Nd (e.g. 30m, 2h, 1d)')
|
|
127
|
+
.option('--window <range>', 'Only run during time window (e.g. "22:00-06:00")')
|
|
128
|
+
.option('--magnet <uri>', 'Magnet URI (for download tasks)')
|
|
129
|
+
.option('--name <name>', 'Name for the download')
|
|
130
|
+
.option('--share-path <path>', 'Share path (for revoke tasks)')
|
|
131
|
+
.option('--visibility <vis>', 'Share visibility (global|private)', 'global')
|
|
132
|
+
.option('--recipients <handles>', 'Comma-separated recipient handles')
|
|
133
|
+
.action((type, opts) => {
|
|
134
|
+
if (!['share', 'download', 'revoke'].includes(type)) {
|
|
135
|
+
console.log(chalk.red('Invalid task type. Use: share, download, revoke'));
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!opts.at) {
|
|
141
|
+
console.log(chalk.red('--at is required. Example: --at "2026-03-15 09:00"'));
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const executeAt = new Date(opts.at).getTime();
|
|
147
|
+
if (isNaN(executeAt) || executeAt <= Date.now()) {
|
|
148
|
+
console.log(chalk.red('--at must be a valid future datetime.'));
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let every = null;
|
|
154
|
+
if (opts.every) {
|
|
155
|
+
every = parseEvery(opts.every);
|
|
156
|
+
if (every === null) {
|
|
157
|
+
console.log(chalk.red('Invalid --every value. Use: hourly, daily, weekly, monthly, or Nm/Nh/Nd'));
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let window = null;
|
|
164
|
+
if (opts.window) {
|
|
165
|
+
window = parseWindow(opts.window);
|
|
166
|
+
if (!window) {
|
|
167
|
+
console.log(chalk.red('Invalid --window format. Use: "HH:MM-HH:MM" (e.g. "22:00-06:00")'));
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let data = {};
|
|
174
|
+
if (type === 'share') {
|
|
175
|
+
const folderPath = addCmd.args[1];
|
|
176
|
+
if (!folderPath) {
|
|
177
|
+
console.log(chalk.red('Usage: pe schedule add share <path> --at <datetime>'));
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
data = {
|
|
182
|
+
folderPath,
|
|
183
|
+
visibility: opts.visibility,
|
|
184
|
+
recipients: parseCommaList(opts.recipients),
|
|
185
|
+
};
|
|
186
|
+
} else if (type === 'download') {
|
|
187
|
+
if (!opts.magnet) {
|
|
188
|
+
console.log(chalk.red('--magnet is required for download tasks.'));
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
data = { magnet: opts.magnet, name: opts.name || 'Scheduled Download' };
|
|
193
|
+
} else if (type === 'revoke') {
|
|
194
|
+
if (!opts.sharePath) {
|
|
195
|
+
console.log(chalk.red('--share-path is required for revoke tasks.'));
|
|
196
|
+
process.exitCode = 1;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
data = { sharePath: opts.sharePath };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const tasks = loadTasks();
|
|
203
|
+
const task = {
|
|
204
|
+
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
205
|
+
type,
|
|
206
|
+
executeAt,
|
|
207
|
+
data,
|
|
208
|
+
status: 'pending',
|
|
209
|
+
createdAt: new Date().toISOString(),
|
|
210
|
+
};
|
|
211
|
+
if (every) task.every = every;
|
|
212
|
+
if (window) task.window = window;
|
|
213
|
+
tasks.push(task);
|
|
214
|
+
saveTasks(tasks);
|
|
215
|
+
|
|
216
|
+
console.log(chalk.green(`✔ Task scheduled: ${task.id}`));
|
|
217
|
+
console.log(` Type: ${chalk.yellow(type)}`);
|
|
218
|
+
console.log(` Execute at: ${chalk.white(formatDate(executeAt))}`);
|
|
219
|
+
if (every) console.log(` Repeat: ${chalk.cyan(opts.every)}`);
|
|
220
|
+
if (window) console.log(` Window: ${chalk.cyan(opts.window)}`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
cmd
|
|
224
|
+
.command('cancel <taskId>')
|
|
225
|
+
.description('cancel a pending scheduled task')
|
|
226
|
+
.action((taskId) => {
|
|
227
|
+
const tasks = loadTasks();
|
|
228
|
+
const task = tasks.find(t => t.id === taskId);
|
|
229
|
+
if (!task) {
|
|
230
|
+
console.log(chalk.red('Task not found.'));
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (task.status !== 'pending') {
|
|
235
|
+
console.log(chalk.red(`Cannot cancel task with status: ${task.status}`));
|
|
236
|
+
process.exitCode = 1;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
task.status = 'cancelled';
|
|
240
|
+
saveTasks(tasks);
|
|
241
|
+
console.log(chalk.green(`✔ Task ${taskId} cancelled.`));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
cmd
|
|
245
|
+
.command('clear')
|
|
246
|
+
.description('remove all completed/failed/cancelled tasks')
|
|
247
|
+
.action(() => {
|
|
248
|
+
const tasks = loadTasks();
|
|
249
|
+
const remaining = tasks.filter(t => t.status === 'pending' || t.status === 'running');
|
|
250
|
+
const removed = tasks.length - remaining.length;
|
|
251
|
+
saveTasks(remaining);
|
|
252
|
+
console.log(chalk.green(`✔ Cleared ${removed} tasks. ${remaining.length} remaining.`));
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
cmd
|
|
256
|
+
.command('run-once')
|
|
257
|
+
.description('check and execute any due tasks, then exit')
|
|
258
|
+
.action(async () => {
|
|
259
|
+
const executed = await runCliScheduler();
|
|
260
|
+
if (executed === 0) {
|
|
261
|
+
console.log(chalk.gray('No tasks due.'));
|
|
262
|
+
} else {
|
|
263
|
+
console.log(chalk.green(`✔ Executed ${executed} task(s).`));
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
cmd
|
|
268
|
+
.command('daemon')
|
|
269
|
+
.description('run scheduler in foreground, checking every 30 seconds')
|
|
270
|
+
.option('--interval <seconds>', 'Check interval in seconds', '30')
|
|
271
|
+
.action(async (opts) => {
|
|
272
|
+
const interval = Math.max(5, parseInt(opts.interval, 10) || 30) * 1000;
|
|
273
|
+
console.log(chalk.cyan(`Scheduler daemon running (interval: ${interval / 1000}s). Press Ctrl+C to stop.`));
|
|
274
|
+
const run = async () => {
|
|
275
|
+
const executed = await runCliScheduler();
|
|
276
|
+
if (executed > 0) console.log(chalk.green(`[${new Date().toLocaleTimeString()}] Executed ${executed} task(s).`));
|
|
277
|
+
};
|
|
278
|
+
await run();
|
|
279
|
+
const timer = setInterval(run, interval);
|
|
280
|
+
process.on('SIGINT', () => { clearInterval(timer); process.exit(0); });
|
|
281
|
+
process.on('SIGTERM', () => { clearInterval(timer); process.exit(0); });
|
|
282
|
+
// Keep alive
|
|
283
|
+
await new Promise(() => {});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function runCliScheduler() {
|
|
288
|
+
const tasks = loadTasks();
|
|
289
|
+
const now = Date.now();
|
|
290
|
+
let executed = 0;
|
|
291
|
+
|
|
292
|
+
for (const task of tasks) {
|
|
293
|
+
if (!isTaskReady(task, now)) continue;
|
|
294
|
+
|
|
295
|
+
task.status = 'running';
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
if (task.type === 'share') {
|
|
299
|
+
console.log(chalk.cyan(`Sharing ${task.data.folderPath}...`));
|
|
300
|
+
const { default: shareAction } = await import('../core/shares.js');
|
|
301
|
+
if (typeof shareAction.addShare === 'function') {
|
|
302
|
+
await shareAction.addShare(task.data.folderPath, {
|
|
303
|
+
visibility: task.data.visibility,
|
|
304
|
+
recipients: task.data.recipients,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
} else if (task.type === 'download') {
|
|
308
|
+
console.log(chalk.cyan(`Starting download: ${task.data.name}...`));
|
|
309
|
+
// CLI downloads need the torrent client - best-effort
|
|
310
|
+
console.log(chalk.gray(` Magnet: ${task.data.magnet.slice(0, 60)}...`));
|
|
311
|
+
console.log(chalk.gray(' Note: CLI downloads require `pe serve` for full torrent support.'));
|
|
312
|
+
} else if (task.type === 'revoke') {
|
|
313
|
+
console.log(chalk.cyan(`Revoking share: ${task.data.sharePath}...`));
|
|
314
|
+
const shares = config.get('shares') || [];
|
|
315
|
+
const idx = shares.findIndex(s => s.path === task.data.sharePath);
|
|
316
|
+
if (idx >= 0) {
|
|
317
|
+
shares.splice(idx, 1);
|
|
318
|
+
config.set('shares', shares);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
task.lastRunAt = new Date().toISOString();
|
|
323
|
+
task.runCount = (task.runCount || 0) + 1;
|
|
324
|
+
|
|
325
|
+
if (task.every) {
|
|
326
|
+
// Recurring: reschedule
|
|
327
|
+
task.executeAt = computeNextRun(task);
|
|
328
|
+
task.status = 'pending';
|
|
329
|
+
console.log(chalk.gray(` Next run: ${formatDate(task.executeAt)}`));
|
|
330
|
+
} else {
|
|
331
|
+
task.status = 'completed';
|
|
332
|
+
task.completedAt = new Date().toISOString();
|
|
333
|
+
}
|
|
334
|
+
executed++;
|
|
335
|
+
} catch (err) {
|
|
336
|
+
console.error(chalk.red(` Task ${task.id} failed: ${err.message}`));
|
|
337
|
+
task.status = 'failed';
|
|
338
|
+
task.error = err.message;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (executed > 0) saveTasks(tasks);
|
|
343
|
+
return executed;
|
|
344
|
+
}
|