filemayor 2.0.3 → 2.0.4

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 CHANGED
@@ -52,7 +52,7 @@ operating as FileMayor, and its contributors.
52
52
  - Server / data center deployment
53
53
  - SOP AI Engine features
54
54
 
55
- requires a valid Paid Tier license. Contact licensing@filemayor.app
55
+ requires a valid Paid Tier license. Contact licensing@filemayor.com
56
56
  for commercial licensing inquiries.
57
57
 
58
58
  5. AI FEATURES
@@ -86,5 +86,5 @@ operating as FileMayor, and its contributors.
86
86
 
87
87
  This License shall be governed by the laws of the Republic of South Africa.
88
88
 
89
- For licensing inquiries: licensing@filemayor.app
90
- For support: support@filemayor.app
89
+ For licensing inquiries: licensing@filemayor.com
90
+ For support: support@filemayor.com
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ═══════════════════════════════════════════════════════════════════
5
+ * FILEMAYOR CORE — LICENSE
6
+ * Offline license key validation, tier management, and feature gating.
7
+ * Created by Lehlohonolo Goodwill Nchefu (Chevza)
8
+ * ═══════════════════════════════════════════════════════════════════
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+ const crypto = require('crypto');
17
+
18
+ // ─── Constants ────────────────────────────────────────────────────
19
+
20
+ const LICENSE_FILE = path.join(os.homedir(), '.filemayor-license.json');
21
+ const KEY_PREFIX = 'FM';
22
+ const CHECKSUM_SECRET = 'filemayor-chevza-2026';
23
+
24
+ /**
25
+ * License tiers with capabilities
26
+ */
27
+ const TIERS = {
28
+ free: {
29
+ name: 'Free',
30
+ features: ['scan', 'organize', 'clean', 'undo', 'init', 'info'],
31
+ limits: { bulkOrganize: 50 },
32
+ },
33
+ pro: {
34
+ name: 'Pro',
35
+ features: ['scan', 'organize', 'clean', 'undo', 'init', 'info',
36
+ 'watch', 'sop-ai', 'bulk-organize', 'csv-export'],
37
+ limits: { bulkOrganize: Infinity },
38
+ },
39
+ enterprise: {
40
+ name: 'Enterprise',
41
+ features: ['scan', 'organize', 'clean', 'undo', 'init', 'info',
42
+ 'watch', 'sop-ai', 'bulk-organize', 'csv-export',
43
+ 'api-access', 'custom-categories', 'team-sharing'],
44
+ limits: { bulkOrganize: Infinity },
45
+ },
46
+ owner: {
47
+ name: 'Owner',
48
+ features: ['*'],
49
+ limits: { bulkOrganize: Infinity },
50
+ },
51
+ };
52
+
53
+ /**
54
+ * Built-in keys (hardcoded, never expire)
55
+ */
56
+ const BUILTIN_KEYS = {
57
+ 'FM-OWN-CHEV-2026-PRMNT': { tier: 'owner', name: 'Owner — Chevza', expires: null },
58
+ 'FM-TST-DEV0-TEST-00000': { tier: 'pro', name: 'Test — Development', expires: null },
59
+ };
60
+
61
+ // ─── Key Generation & Validation ──────────────────────────────────
62
+
63
+ /**
64
+ * Generate a checksum for a key body
65
+ * @param {string} body - Key body without checksum segment
66
+ * @returns {string} 5-char checksum
67
+ */
68
+ function generateChecksum(body) {
69
+ const hash = crypto.createHmac('sha256', CHECKSUM_SECRET)
70
+ .update(body)
71
+ .digest('hex');
72
+ return hash.substring(0, 5).toUpperCase();
73
+ }
74
+
75
+ /**
76
+ * Validate key format: FM-XXX-XXXX-XXXX-XXXXX
77
+ * Last 5 chars are HMAC checksum of the rest
78
+ * @param {string} key - License key
79
+ * @returns {{ valid: boolean, tier: string|null, reason: string }}
80
+ */
81
+ function validateKeyFormat(key) {
82
+ if (!key || typeof key !== 'string') {
83
+ return { valid: false, tier: null, reason: 'No key provided' };
84
+ }
85
+
86
+ const k = key.trim().toUpperCase();
87
+
88
+ // Check built-in keys first
89
+ if (BUILTIN_KEYS[k]) {
90
+ return { valid: true, tier: BUILTIN_KEYS[k].tier, reason: 'Built-in key' };
91
+ }
92
+
93
+ // Format: FM-XXX-XXXX-XXXX-XXXXX
94
+ const pattern = /^FM-(PRO|ENT|OWN|TST)-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{5}$/;
95
+ if (!pattern.test(k)) {
96
+ return { valid: false, tier: null, reason: 'Invalid key format' };
97
+ }
98
+
99
+ // Extract parts
100
+ const parts = k.split('-');
101
+ const tierCode = parts[1];
102
+ const body = parts.slice(0, 4).join('-'); // FM-XXX-XXXX-XXXX
103
+ const checksum = parts[4]; // XXXXX
104
+
105
+ // Verify checksum
106
+ const expectedChecksum = generateChecksum(body);
107
+ if (checksum !== expectedChecksum) {
108
+ return { valid: false, tier: null, reason: 'Invalid key (checksum failed)' };
109
+ }
110
+
111
+ // Map tier code
112
+ const tierMap = { PRO: 'pro', ENT: 'enterprise', OWN: 'owner', TST: 'pro' };
113
+ const tier = tierMap[tierCode] || 'free';
114
+
115
+ return { valid: true, tier, reason: 'Valid' };
116
+ }
117
+
118
+ /**
119
+ * Generate a new license key
120
+ * @param {'pro'|'enterprise'|'owner'} tier
121
+ * @returns {string} Generated key
122
+ */
123
+ function generateKey(tier = 'pro') {
124
+ const tierMap = { pro: 'PRO', enterprise: 'ENT', owner: 'OWN' };
125
+ const code = tierMap[tier] || 'PRO';
126
+
127
+ const seg1 = crypto.randomBytes(2).toString('hex').toUpperCase().substring(0, 4);
128
+ const seg2 = crypto.randomBytes(2).toString('hex').toUpperCase().substring(0, 4);
129
+ const body = `FM-${code}-${seg1}-${seg2}`;
130
+ const checksum = generateChecksum(body);
131
+
132
+ return `${body}-${checksum}`;
133
+ }
134
+
135
+ // ─── License Storage ──────────────────────────────────────────────
136
+
137
+ /**
138
+ * Read stored license from disk
139
+ * @returns {{ key: string, tier: string, activatedAt: string, name: string }|null}
140
+ */
141
+ function readLicense() {
142
+ try {
143
+ if (!fs.existsSync(LICENSE_FILE)) return null;
144
+ const data = JSON.parse(fs.readFileSync(LICENSE_FILE, 'utf8'));
145
+ return data;
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Write license to disk
153
+ * @param {Object} licenseData
154
+ */
155
+ function writeLicense(licenseData) {
156
+ fs.writeFileSync(LICENSE_FILE, JSON.stringify(licenseData, null, 2), 'utf8');
157
+ }
158
+
159
+ /**
160
+ * Remove stored license
161
+ */
162
+ function removeLicense() {
163
+ if (fs.existsSync(LICENSE_FILE)) {
164
+ fs.unlinkSync(LICENSE_FILE);
165
+ }
166
+ }
167
+
168
+ // ─── License Management ──────────────────────────────────────────
169
+
170
+ /**
171
+ * Activate a license key
172
+ * @param {string} key - License key to activate
173
+ * @returns {{ success: boolean, message: string, tier: string|null }}
174
+ */
175
+ function activateLicense(key) {
176
+ const validation = validateKeyFormat(key);
177
+
178
+ if (!validation.valid) {
179
+ return { success: false, message: validation.reason, tier: null };
180
+ }
181
+
182
+ const k = key.trim().toUpperCase();
183
+ const builtin = BUILTIN_KEYS[k];
184
+
185
+ const licenseData = {
186
+ key: k,
187
+ tier: validation.tier,
188
+ name: builtin ? builtin.name : `${TIERS[validation.tier].name} License`,
189
+ activatedAt: new Date().toISOString(),
190
+ expires: builtin ? null : null, // LemonSqueezy keys can add expiry later
191
+ };
192
+
193
+ writeLicense(licenseData);
194
+
195
+ return {
196
+ success: true,
197
+ message: `${TIERS[validation.tier].name} license activated successfully`,
198
+ tier: validation.tier,
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Deactivate the current license
204
+ * @returns {{ success: boolean, message: string }}
205
+ */
206
+ function deactivateLicense() {
207
+ const current = readLicense();
208
+ if (!current) {
209
+ return { success: false, message: 'No active license found' };
210
+ }
211
+ removeLicense();
212
+ return { success: true, message: 'License deactivated. Reverted to Free tier.' };
213
+ }
214
+
215
+ /**
216
+ * Get current license info
217
+ * @returns {{ tier: string, name: string, features: string[], limits: Object, key: string|null, active: boolean }}
218
+ */
219
+ function getLicenseInfo() {
220
+ const stored = readLicense();
221
+
222
+ if (!stored) {
223
+ return {
224
+ tier: 'free',
225
+ name: 'Free',
226
+ features: TIERS.free.features,
227
+ limits: TIERS.free.limits,
228
+ key: null,
229
+ active: false,
230
+ };
231
+ }
232
+
233
+ // Re-validate stored key
234
+ const validation = validateKeyFormat(stored.key);
235
+ if (!validation.valid) {
236
+ removeLicense();
237
+ return {
238
+ tier: 'free',
239
+ name: 'Free (invalid key removed)',
240
+ features: TIERS.free.features,
241
+ limits: TIERS.free.limits,
242
+ key: null,
243
+ active: false,
244
+ };
245
+ }
246
+
247
+ const tier = TIERS[stored.tier] || TIERS.free;
248
+
249
+ return {
250
+ tier: stored.tier,
251
+ name: stored.name,
252
+ features: tier.features,
253
+ limits: tier.limits,
254
+ key: stored.key,
255
+ active: true,
256
+ activatedAt: stored.activatedAt,
257
+ };
258
+ }
259
+
260
+ // ─── Feature Gating ──────────────────────────────────────────────
261
+
262
+ /**
263
+ * Check if a feature is available in the current license
264
+ * @param {string} feature - Feature identifier
265
+ * @returns {boolean}
266
+ */
267
+ function hasFeature(feature) {
268
+ const info = getLicenseInfo();
269
+ if (info.features.includes('*')) return true; // Owner has everything
270
+ return info.features.includes(feature);
271
+ }
272
+
273
+ /**
274
+ * Check if a Pro feature is gated and return appropriate message
275
+ * @param {string} feature - Feature identifier
276
+ * @param {string} featureLabel - Human-readable feature name
277
+ * @returns {{ allowed: boolean, message: string|null }}
278
+ */
279
+ function checkProFeature(feature, featureLabel) {
280
+ if (hasFeature(feature)) {
281
+ return { allowed: true, message: null };
282
+ }
283
+
284
+ return {
285
+ allowed: false,
286
+ message: [
287
+ `⚡ ${featureLabel} is a Pro feature`,
288
+ '',
289
+ ' Upgrade to Pro to unlock:',
290
+ ' • Real-time file watching & auto-organize',
291
+ ' • AI-powered SOP parsing (Gemini)',
292
+ ' • Bulk organize (unlimited files)',
293
+ ' • CSV export for all reports',
294
+ '',
295
+ ' Get your license: https://filemayor.lemonsqueezy.com/checkout/buy/d2795526-eb05-4272-8084-98b6c7a118bb',
296
+ ' Then run: filemayor license activate YOUR-KEY',
297
+ '',
298
+ ].join('\n'),
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Check bulk organize limit
304
+ * @param {number} fileCount - Number of files to organize
305
+ * @returns {{ allowed: boolean, message: string|null }}
306
+ */
307
+ function checkBulkLimit(fileCount) {
308
+ const info = getLicenseInfo();
309
+ if (info.limits.bulkOrganize >= fileCount) {
310
+ return { allowed: true, message: null };
311
+ }
312
+
313
+ return {
314
+ allowed: false,
315
+ message: [
316
+ `⚡ Bulk organize (${fileCount} files) requires a Pro license`,
317
+ ` Free tier is limited to ${info.limits.bulkOrganize} files per operation.`,
318
+ '',
319
+ ' Upgrade: https://filemayor.lemonsqueezy.com/checkout/buy/d2795526-eb05-4272-8084-98b6c7a118bb',
320
+ ' Then run: filemayor license activate YOUR-KEY',
321
+ ].join('\n'),
322
+ };
323
+ }
324
+
325
+ // ─── Exports ─────────────────────────────────────────────────────
326
+
327
+ module.exports = {
328
+ TIERS,
329
+ BUILTIN_KEYS,
330
+ validateKeyFormat,
331
+ generateKey,
332
+ readLicense,
333
+ activateLicense,
334
+ deactivateLicense,
335
+ getLicenseInfo,
336
+ hasFeature,
337
+ checkProFeature,
338
+ checkBulkLimit,
339
+ LICENSE_FILE,
340
+ };
package/index.js CHANGED
@@ -33,11 +33,28 @@ const reporter = require('./core/reporter');
33
33
  const { formatBytes } = require('./core/scanner');
34
34
  const { getCategories } = require('./core/categories');
35
35
  const { checkPermissions } = require('./core/security');
36
+ const { activateLicense, deactivateLicense, getLicenseInfo, checkProFeature, checkBulkLimit } = require('./core/license');
36
37
 
37
38
  const { c, banner, Spinner, success, error, warn, info } = reporter;
38
39
 
39
40
  // ─── Version ──────────────────────────────────────────────────────
40
- const VERSION = '2.0.0';
41
+ const VERSION = '2.0.4';
42
+
43
+ // ─── Path Helpers ─────────────────────────────────────────────────
44
+
45
+ /**
46
+ * Expand ~ to user home directory (cross-platform).
47
+ * On Unix shells, ~ is expanded by the shell before reaching Node.
48
+ * On Windows PowerShell, it is NOT — so we handle it here.
49
+ */
50
+ function expandTilde(p) {
51
+ if (!p) return p;
52
+ if (p === '~') return os.homedir();
53
+ if (p.startsWith('~/') || p.startsWith('~\\')) {
54
+ return path.join(os.homedir(), p.slice(2));
55
+ }
56
+ return p;
57
+ }
41
58
 
42
59
  // ─── Argument Parser ──────────────────────────────────────────────
43
60
 
@@ -108,7 +125,7 @@ function parseArgs(argv) {
108
125
  } else if (!args.command) {
109
126
  args.command = arg;
110
127
  } else if (!args.target) {
111
- args.target = arg;
128
+ args.target = expandTilde(arg);
112
129
  } else {
113
130
  args.positional.push(arg);
114
131
  }
@@ -140,10 +157,11 @@ ${banner()}
140
157
  ${c('yellow', 'scan')} ${c('dim', '<path>')} Scan directory and report contents
141
158
  ${c('yellow', 'organize')} ${c('dim', '<path>')} Organize files into categories
142
159
  ${c('yellow', 'clean')} ${c('dim', '<path>')} Find and remove junk files
143
- ${c('yellow', 'watch')} ${c('dim', '<path>')} Watch directory for changes (daemon)
160
+ ${c('yellow', 'watch')} ${c('dim', '<path>')} Watch directory for changes ${c('magenta', '[PRO]')}
144
161
  ${c('yellow', 'init')} Create .filemayor.yml config
145
162
  ${c('yellow', 'undo')} ${c('dim', '<path>')} Undo last organization
146
163
  ${c('yellow', 'info')} System info and version
164
+ ${c('yellow', 'license')} ${c('dim', '<action>')} Manage license (activate|status|deactivate)
147
165
 
148
166
  ${c('bold', 'OPTIONS')}
149
167
  ${c('dim', '--dry-run')} Preview changes without executing
@@ -178,7 +196,7 @@ ${banner()}
178
196
  ${c('bold', 'INSTALL')}
179
197
  ${c('dim', '$')} npm install -g filemayor
180
198
 
181
- ${c('dim', `FileMayor v${VERSION} — https://filemayor.app`)}
199
+ ${c('dim', `FileMayor v${VERSION} — https://filemayor.com`)}
182
200
  `);
183
201
  }
184
202
 
@@ -301,6 +319,13 @@ async function cmdClean(target, flags, config) {
301
319
  }
302
320
 
303
321
  async function cmdWatch(target, flags, config) {
322
+ // Pro feature gate
323
+ const gate = checkProFeature('watch', 'Watch Mode');
324
+ if (!gate.allowed) {
325
+ console.log(gate.message);
326
+ process.exit(0);
327
+ }
328
+
304
329
  const targetPath = path.resolve(target || '.');
305
330
  const format = flags.format || config.output.format;
306
331
 
@@ -429,6 +454,87 @@ async function cmdInfo(config) {
429
454
  console.log('');
430
455
  }
431
456
 
457
+ // ─── License Command ──────────────────────────────────────────────
458
+
459
+ async function cmdLicense(action, positional, flags) {
460
+ const subAction = action || 'status';
461
+
462
+ switch (subAction) {
463
+ case 'activate': {
464
+ const key = positional[0] || flags.key;
465
+ if (!key) {
466
+ console.error(error('Usage: filemayor license activate <key>'));
467
+ console.log(c('dim', ' Example: filemayor license activate FM-PRO-XXXX-XXXX-XXXXX'));
468
+ process.exit(1);
469
+ }
470
+ const result = activateLicense(key);
471
+ if (result.success) {
472
+ console.log(success(result.message));
473
+ console.log(c('dim', ` Tier: ${result.tier}`));
474
+ } else {
475
+ console.error(error(result.message));
476
+ process.exit(1);
477
+ }
478
+ break;
479
+ }
480
+
481
+ case 'deactivate':
482
+ case 'remove': {
483
+ const result = deactivateLicense();
484
+ if (result.success) {
485
+ console.log(success(result.message));
486
+ } else {
487
+ console.log(info(result.message));
488
+ }
489
+ break;
490
+ }
491
+
492
+ case 'status':
493
+ default: {
494
+ const li = getLicenseInfo();
495
+ console.log(banner());
496
+ console.log(c('bold', ' License Status'));
497
+ console.log(c('dim', ` ${'─'.repeat(40)}`));
498
+ console.log(` ${c('bold', 'Tier:')} ${li.active ? c('green', li.name) : c('yellow', 'Free')}`);
499
+ console.log(` ${c('bold', 'Status:')} ${li.active ? c('green', '● Active') : c('dim', '○ No license')}`);
500
+ if (li.key) {
501
+ const masked = li.key.substring(0, 7) + '****' + li.key.substring(li.key.length - 5);
502
+ console.log(` ${c('bold', 'Key:')} ${c('dim', masked)}`);
503
+ }
504
+ if (li.activatedAt) {
505
+ console.log(` ${c('bold', 'Activated:')} ${c('dim', new Date(li.activatedAt).toLocaleDateString())}`);
506
+ }
507
+ console.log('');
508
+ console.log(c('bold', ' Features'));
509
+ console.log(c('dim', ` ${'─'.repeat(40)}`));
510
+ const allFeatures = [
511
+ { id: 'scan', label: 'Directory Scanning', free: true },
512
+ { id: 'organize', label: 'File Organization', free: true },
513
+ { id: 'clean', label: 'Junk Cleanup', free: true },
514
+ { id: 'undo', label: 'Undo Operations', free: true },
515
+ { id: 'watch', label: 'Watch Mode', free: false },
516
+ { id: 'sop-ai', label: 'AI SOP Parsing', free: false },
517
+ { id: 'bulk-organize', label: 'Bulk Organize (50+ files)', free: false },
518
+ { id: 'csv-export', label: 'CSV Export', free: false },
519
+ ];
520
+ for (const feat of allFeatures) {
521
+ const has = li.features.includes('*') || li.features.includes(feat.id);
522
+ const icon = has ? c('green', '✓') : c('dim', '○');
523
+ const label = has ? feat.label : c('dim', feat.label);
524
+ const tag = !feat.free && !has ? c('magenta', ' [PRO]') : '';
525
+ console.log(` ${icon} ${label}${tag}`);
526
+ }
527
+ console.log('');
528
+ if (!li.active) {
529
+ console.log(c('dim', ' Get a license: https://filemayor.lemonsqueezy.com/checkout/buy/d2795526-eb05-4272-8084-98b6c7a118bb'));
530
+ console.log(c('dim', ' Activate: filemayor license activate <key>'));
531
+ console.log('');
532
+ }
533
+ break;
534
+ }
535
+ }
536
+ }
537
+
432
538
  // ─── Main Entry Point ─────────────────────────────────────────────
433
539
 
434
540
  async function main() {
@@ -515,6 +621,12 @@ async function main() {
515
621
  await cmdInfo(config);
516
622
  break;
517
623
 
624
+ case 'license':
625
+ case 'lic':
626
+ case 'l':
627
+ await cmdLicense(args.target, args.positional, args.flags);
628
+ break;
629
+
518
630
  case 'help':
519
631
  printHelp();
520
632
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filemayor",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "Enterprise file management engine — scan, organize, clean, and watch your filesystem from any terminal",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -26,6 +26,7 @@
26
26
  ],
27
27
  "author": {
28
28
  "name": "Lehlohonolo Goodwill Nchefu (Chevza)",
29
+ "email": "nchefuh@gmail.com",
29
30
  "url": "https://github.com/Hrypopo"
30
31
  },
31
32
  "license": "PROPRIETARY",
@@ -36,7 +37,7 @@
36
37
  "bugs": {
37
38
  "url": "https://github.com/Hrypopo/FileMayor/issues"
38
39
  },
39
- "homepage": "https://github.com/Hrypopo/FileMayor#readme",
40
+ "homepage": "https://filemayor.com",
40
41
  "engines": {
41
42
  "node": ">=18.0.0"
42
43
  },