imtoagent 0.3.23 → 0.3.24
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/bin/imtoagent-real +550 -0
- package/modules/cli/setup.ts +102 -36
- package/package.json +1 -1
package/bin/imtoagent-real
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import * as fs from 'fs';
|
|
16
16
|
import * as path from 'path';
|
|
17
|
+
import * as readline from 'readline';
|
|
17
18
|
import { spawn, execSync } from 'child_process';
|
|
18
19
|
import { getDataDir } from '../modules/utils/paths';
|
|
19
20
|
|
|
@@ -54,6 +55,15 @@ switch (command) {
|
|
|
54
55
|
await cmdUpdateBackend(backendType);
|
|
55
56
|
break;
|
|
56
57
|
}
|
|
58
|
+
case 'uninstall':
|
|
59
|
+
await cmdUninstall();
|
|
60
|
+
break;
|
|
61
|
+
case 'health':
|
|
62
|
+
await cmdHealth();
|
|
63
|
+
break;
|
|
64
|
+
case 'autostart':
|
|
65
|
+
await cmdAutostart();
|
|
66
|
+
break;
|
|
57
67
|
case undefined: {
|
|
58
68
|
const dataDir = getDataDir();
|
|
59
69
|
const configPath = path.join(dataDir, 'config.json');
|
|
@@ -108,6 +118,12 @@ Usage:
|
|
|
108
118
|
imtoagent update-system Upgrade imtoagent itself
|
|
109
119
|
imtoagent update-backend Upgrade current Bot's backend
|
|
110
120
|
imtoagent update-backend TYPE Upgrade specific backend (codex|claude|opencode)
|
|
121
|
+
imtoagent uninstall Uninstall imtoagent (keep data by default)
|
|
122
|
+
imtoagent uninstall --purge Uninstall and delete all data
|
|
123
|
+
imtoagent health Run comprehensive health check
|
|
124
|
+
imtoagent autostart enable Enable auto-start on login (launchd)
|
|
125
|
+
imtoagent autostart disable Disable auto-start
|
|
126
|
+
imtoagent autostart status Check auto-start status
|
|
111
127
|
|
|
112
128
|
Data directory: ${getDataDir()}
|
|
113
129
|
`);
|
|
@@ -223,6 +239,9 @@ echo $PID`;
|
|
|
223
239
|
process.exit(1);
|
|
224
240
|
}
|
|
225
241
|
|
|
242
|
+
// Non-blocking version check
|
|
243
|
+
checkForUpdates().then(printUpdateHint).catch(() => {});
|
|
244
|
+
|
|
226
245
|
// Explicitly exit — Bun may keep event loop alive due to inherited stdio
|
|
227
246
|
process.exit(0);
|
|
228
247
|
}
|
|
@@ -616,3 +635,534 @@ async function cmdUpdateBackend(backendType?: 'claude' | 'codex' | 'opencode'):
|
|
|
616
635
|
process.exit(1);
|
|
617
636
|
}
|
|
618
637
|
}
|
|
638
|
+
|
|
639
|
+
// ================================================================
|
|
640
|
+
// uninstall — remove imtoagent
|
|
641
|
+
// ================================================================
|
|
642
|
+
async function cmdUninstall(): Promise<void> {
|
|
643
|
+
const dataDir = getDataDir();
|
|
644
|
+
const purge = process.argv.includes('--purge');
|
|
645
|
+
const force = process.argv.includes('--force') || process.argv.includes('-f');
|
|
646
|
+
|
|
647
|
+
console.log('\n🗑️ imtoagent Uninstall');
|
|
648
|
+
console.log(` Data directory: ${dataDir}`);
|
|
649
|
+
|
|
650
|
+
if (!purge && !force) {
|
|
651
|
+
console.log('\n ⚠️ This will uninstall the imtoagent npm package but KEEP your data.');
|
|
652
|
+
console.log(` Your data in ${dataDir} will be preserved.`);
|
|
653
|
+
console.log('');
|
|
654
|
+
console.log(' To also delete all data, use: imtoagent uninstall --purge');
|
|
655
|
+
console.log('');
|
|
656
|
+
console.log(' Continue? [y/N]');
|
|
657
|
+
} else if (purge && !force) {
|
|
658
|
+
console.log('\n 🔴 This will DELETE all imtoagent data and the npm package.');
|
|
659
|
+
console.log(` The following will be permanently removed:`);
|
|
660
|
+
console.log(` - Data directory: ${dataDir}`);
|
|
661
|
+
console.log(' - npm global package: imtoagent');
|
|
662
|
+
console.log('');
|
|
663
|
+
console.log(' Continue? [y/N]');
|
|
664
|
+
} else {
|
|
665
|
+
console.log(' --force: skipping confirmation\n');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Confirmation
|
|
669
|
+
if (!force) {
|
|
670
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
671
|
+
const answer = await new Promise<string>((resolve) => {
|
|
672
|
+
rl.question(' > ', resolve);
|
|
673
|
+
}).finally(() => rl.close());
|
|
674
|
+
if (answer.trim().toLowerCase() !== 'y' && answer.trim().toLowerCase() !== 'yes') {
|
|
675
|
+
console.log('❌ Uninstall cancelled.');
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Step 1: Stop gateway if running
|
|
681
|
+
if (fs.existsSync(PID_FILE)) {
|
|
682
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
|
|
683
|
+
try {
|
|
684
|
+
process.kill(pid, 0);
|
|
685
|
+
console.log(`⏹ Stopping gateway (PID=${pid})...`);
|
|
686
|
+
process.kill(pid, 'SIGTERM');
|
|
687
|
+
for (let i = 0; i < 10; i++) {
|
|
688
|
+
try { process.kill(pid, 0); await new Promise(r => setTimeout(r, 500)); } catch { break; }
|
|
689
|
+
}
|
|
690
|
+
try { process.kill(pid, 0); process.kill(pid, 'SIGKILL'); } catch {}
|
|
691
|
+
console.log('✅ Gateway stopped');
|
|
692
|
+
} catch {}
|
|
693
|
+
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
694
|
+
} else {
|
|
695
|
+
console.log('ℹ️ Gateway is not running');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Step 2: Remove launchd plist
|
|
699
|
+
const plistPath = path.join(process.env.HOME || '', 'Library', 'LaunchAgents', 'com.imtoagent.gateway.plist');
|
|
700
|
+
if (fs.existsSync(plistPath)) {
|
|
701
|
+
try {
|
|
702
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8' });
|
|
703
|
+
console.log('⏹ Launchd service unloaded');
|
|
704
|
+
} catch {}
|
|
705
|
+
try {
|
|
706
|
+
fs.unlinkSync(plistPath);
|
|
707
|
+
console.log('🗑️ Launchd plist removed');
|
|
708
|
+
} catch {}
|
|
709
|
+
} else {
|
|
710
|
+
console.log('ℹ️ No launchd service configured');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Step 3: Uninstall npm package
|
|
714
|
+
console.log('\n📦 Uninstalling npm package...');
|
|
715
|
+
try {
|
|
716
|
+
execSync('npm uninstall -g imtoagent', {
|
|
717
|
+
encoding: 'utf-8',
|
|
718
|
+
stdio: 'inherit',
|
|
719
|
+
timeout: 30000,
|
|
720
|
+
});
|
|
721
|
+
console.log('✅ npm package uninstalled');
|
|
722
|
+
} catch (e: any) {
|
|
723
|
+
console.error(`⚠️ npm uninstall may have failed: ${e.message}`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Step 4: Purge data if requested
|
|
727
|
+
if (purge) {
|
|
728
|
+
console.log(`\n🔥 Deleting data directory: ${dataDir}`);
|
|
729
|
+
try {
|
|
730
|
+
execSync(`rm -rf "${dataDir}"`, { encoding: 'utf-8', timeout: 10000 });
|
|
731
|
+
console.log('✅ Data directory deleted');
|
|
732
|
+
} catch (e: any) {
|
|
733
|
+
console.error(`⚠️ Failed to delete data directory: ${e.message}`);
|
|
734
|
+
console.error(` Please remove manually: rm -rf "${dataDir}"`);
|
|
735
|
+
}
|
|
736
|
+
} else {
|
|
737
|
+
console.log(`\n💾 Data preserved at: ${dataDir}`);
|
|
738
|
+
console.log(' To remove it later: rm -rf ~/.imtoagent');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
console.log('\n✅ imtoagent uninstalled');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// ================================================================
|
|
745
|
+
// health — comprehensive health check
|
|
746
|
+
// ================================================================
|
|
747
|
+
async function cmdHealth(): Promise<void> {
|
|
748
|
+
const dataDir = getDataDir();
|
|
749
|
+
const configPath = path.join(dataDir, 'config.json');
|
|
750
|
+
const logFile = path.join(dataDir, 'logs', 'imtoagent.log');
|
|
751
|
+
|
|
752
|
+
console.log(`\n🔍 imtoagent Health Check`);
|
|
753
|
+
console.log(` Data directory: ${dataDir}`);
|
|
754
|
+
console.log(` Time: ${new Date().toISOString()}\n`);
|
|
755
|
+
|
|
756
|
+
let errorCount = 0;
|
|
757
|
+
let warnCount = 0;
|
|
758
|
+
|
|
759
|
+
// ---- 1. Gateway Process ----
|
|
760
|
+
console.log('── Gateway Process ──');
|
|
761
|
+
if (fs.existsSync(PID_FILE)) {
|
|
762
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
|
|
763
|
+
try {
|
|
764
|
+
process.kill(pid, 0);
|
|
765
|
+
console.log(` ✅ Running (PID=${pid})`);
|
|
766
|
+
} catch {
|
|
767
|
+
console.log(` ❌ PID file exists but process ${pid} is gone (stale)`);
|
|
768
|
+
errorCount++;
|
|
769
|
+
}
|
|
770
|
+
} else {
|
|
771
|
+
console.log(` ⏸ Not running`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ---- 2. Configuration ----
|
|
775
|
+
console.log('\n── Configuration ──');
|
|
776
|
+
if (fs.existsSync(configPath)) {
|
|
777
|
+
try {
|
|
778
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
779
|
+
console.log(` ✅ config.json parse OK`);
|
|
780
|
+
|
|
781
|
+
// Check required fields
|
|
782
|
+
if (!cfg.bots || !Array.isArray(cfg.bots) || cfg.bots.length === 0) {
|
|
783
|
+
console.log(` ⚠️ No bots configured`);
|
|
784
|
+
warnCount++;
|
|
785
|
+
} else {
|
|
786
|
+
console.log(` ✅ ${cfg.bots.length} Bot(s) configured`);
|
|
787
|
+
for (const bot of cfg.bots) {
|
|
788
|
+
const issues: string[] = [];
|
|
789
|
+
if (!bot.name) issues.push('missing name');
|
|
790
|
+
if (!bot.im) issues.push('missing IM platform');
|
|
791
|
+
if (!bot.backend) issues.push('missing backend');
|
|
792
|
+
if (issues.length > 0) {
|
|
793
|
+
console.log(` ⚠️ Bot "${bot.name || '?'}": ${issues.join(', ')}`);
|
|
794
|
+
warnCount++;
|
|
795
|
+
} else {
|
|
796
|
+
console.log(` ✅ ${bot.name} (${bot.im} + ${bot.backend})`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Check providers.json
|
|
802
|
+
const providersPath = path.join(dataDir, 'providers.json');
|
|
803
|
+
if (fs.existsSync(providersPath)) {
|
|
804
|
+
try {
|
|
805
|
+
const prov = JSON.parse(fs.readFileSync(providersPath, 'utf-8'));
|
|
806
|
+
console.log(` ✅ providers.json parse OK`);
|
|
807
|
+
// Check for placeholder API keys
|
|
808
|
+
const provStr = JSON.stringify(prov);
|
|
809
|
+
if (provStr.includes('YOUR_') || provStr.includes('sk-xxx') || provStr.includes('placehold')) {
|
|
810
|
+
console.log(` ⚠️ providers.json may contain placeholder API keys`);
|
|
811
|
+
warnCount++;
|
|
812
|
+
}
|
|
813
|
+
} catch {
|
|
814
|
+
console.log(` ❌ providers.json parse error`);
|
|
815
|
+
errorCount++;
|
|
816
|
+
}
|
|
817
|
+
} else {
|
|
818
|
+
console.log(` ⚠️ providers.json not found`);
|
|
819
|
+
warnCount++;
|
|
820
|
+
}
|
|
821
|
+
} catch {
|
|
822
|
+
console.log(` ❌ config.json parse error`);
|
|
823
|
+
errorCount++;
|
|
824
|
+
}
|
|
825
|
+
} else {
|
|
826
|
+
console.log(` ❌ config.json not found (run "imtoagent setup")`);
|
|
827
|
+
errorCount++;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ---- 3. Backend Status ----
|
|
831
|
+
console.log('\n── Backend Status ──');
|
|
832
|
+
try {
|
|
833
|
+
const { checkBackend } = await import('../modules/utils/backend-check');
|
|
834
|
+
if (fs.existsSync(configPath)) {
|
|
835
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
836
|
+
const checkedTypes = new Set<string>();
|
|
837
|
+
for (const bot of cfg.bots || []) {
|
|
838
|
+
if (bot.backend && ['claude', 'codex', 'opencode'].includes(bot.backend) && !checkedTypes.has(bot.backend)) {
|
|
839
|
+
checkedTypes.add(bot.backend);
|
|
840
|
+
const info = checkBackend(bot.backend as any);
|
|
841
|
+
if (info.installed) {
|
|
842
|
+
console.log(` ✅ ${info.label} v${info.version} (${info.installSource})`);
|
|
843
|
+
} else {
|
|
844
|
+
console.log(` ❌ ${info.label} not installed → ${info.installHint}`);
|
|
845
|
+
errorCount++;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (checkedTypes.size === 0) {
|
|
850
|
+
console.log(` ℹ️ No backends to check`);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
} catch {
|
|
854
|
+
console.log(` ⚠️ Backend check skipped`);
|
|
855
|
+
warnCount++;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// ---- 4. Proxy Port ----
|
|
859
|
+
console.log('\n── Proxy Port (:18899) ──');
|
|
860
|
+
try {
|
|
861
|
+
const net = await import('net');
|
|
862
|
+
const checkPort = (port: number): Promise<boolean> => {
|
|
863
|
+
return new Promise((resolve) => {
|
|
864
|
+
const socket = new net.Socket();
|
|
865
|
+
socket.setTimeout(2000);
|
|
866
|
+
socket.on('connect', () => { socket.destroy(); resolve(true); });
|
|
867
|
+
socket.on('error', () => resolve(false));
|
|
868
|
+
socket.on('timeout', () => { socket.destroy(); resolve(false); });
|
|
869
|
+
socket.connect(port, '127.0.0.1');
|
|
870
|
+
});
|
|
871
|
+
};
|
|
872
|
+
const reachable = await checkPort(18899);
|
|
873
|
+
if (reachable) {
|
|
874
|
+
console.log(` ✅ Port 18899 is reachable`);
|
|
875
|
+
} else {
|
|
876
|
+
// Check if port is in use by another process
|
|
877
|
+
try {
|
|
878
|
+
const lsofOut = execSync(`lsof -i :18899 2>/dev/null`, { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
879
|
+
if (lsofOut) {
|
|
880
|
+
console.log(` ❌ Port 18899 is occupied by another process:`);
|
|
881
|
+
lsofOut.split('\n').forEach(line => console.log(` ${line}`));
|
|
882
|
+
} else {
|
|
883
|
+
console.log(` ⏸ Port 18899 is free (gateway not running)`);
|
|
884
|
+
}
|
|
885
|
+
} catch {
|
|
886
|
+
console.log(` ⏸ Port 18899 is free (gateway not running)`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
} catch {
|
|
890
|
+
console.log(` ⚠️ Port check failed`);
|
|
891
|
+
warnCount++;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// ---- 5. Recent Log Errors ----
|
|
895
|
+
console.log('\n── Recent Log Errors ──');
|
|
896
|
+
if (fs.existsSync(logFile)) {
|
|
897
|
+
const stats = fs.statSync(logFile);
|
|
898
|
+
const size = stats.size > 1024 * 1024
|
|
899
|
+
? (stats.size / (1024 * 1024)).toFixed(1) + ' MB'
|
|
900
|
+
: (stats.size / 1024).toFixed(1) + ' KB';
|
|
901
|
+
console.log(` Log: ${size}`);
|
|
902
|
+
|
|
903
|
+
// Read last 50 lines looking for ERROR/WARN
|
|
904
|
+
try {
|
|
905
|
+
const content = fs.readFileSync(logFile, 'utf-8');
|
|
906
|
+
const lines = content.split('\n');
|
|
907
|
+
const recent = lines.slice(-50);
|
|
908
|
+
const errors = recent.filter(l => l.includes('ERROR') || l.includes('[error]'));
|
|
909
|
+
const warns = recent.filter(l => l.includes('WARN') || l.includes('[warn]'));
|
|
910
|
+
|
|
911
|
+
if (errors.length > 0) {
|
|
912
|
+
console.log(` ❌ ${errors.length} recent ERROR(s):`);
|
|
913
|
+
for (const line of errors.slice(-3)) {
|
|
914
|
+
console.log(` ${line.trim().slice(0, 120)}`);
|
|
915
|
+
}
|
|
916
|
+
errorCount++;
|
|
917
|
+
} else {
|
|
918
|
+
console.log(` ✅ No recent errors`);
|
|
919
|
+
}
|
|
920
|
+
if (warns.length > 0 && errors.length === 0) {
|
|
921
|
+
console.log(` ℹ️ ${warns.length} recent warning(s) (non-critical)`);
|
|
922
|
+
}
|
|
923
|
+
} catch {
|
|
924
|
+
console.log(` ⚠️ Could not read log file`);
|
|
925
|
+
warnCount++;
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
console.log(` ℹ️ No log file yet`);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ---- Summary ----
|
|
932
|
+
console.log('\n── Summary ──');
|
|
933
|
+
if (errorCount === 0 && warnCount === 0) {
|
|
934
|
+
console.log(` ✅ All checks passed!`);
|
|
935
|
+
} else {
|
|
936
|
+
if (errorCount > 0) console.log(` ❌ ${errorCount} error(s)`);
|
|
937
|
+
if (warnCount > 0) console.log(` ⚠️ ${warnCount} warning(s)`);
|
|
938
|
+
}
|
|
939
|
+
console.log();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// ================================================================
|
|
943
|
+
// autostart — launchd integration (macOS only)
|
|
944
|
+
// ================================================================
|
|
945
|
+
async function cmdAutostart(): Promise<void> {
|
|
946
|
+
const subcommand = process.argv[3];
|
|
947
|
+
const home = process.env.HOME || '';
|
|
948
|
+
const plistDir = path.join(home, 'Library', 'LaunchAgents');
|
|
949
|
+
const plistPath = path.join(plistDir, 'com.imtoagent.gateway.plist');
|
|
950
|
+
|
|
951
|
+
if (process.platform !== 'darwin') {
|
|
952
|
+
console.error('❌ autostart is only supported on macOS (launchd)');
|
|
953
|
+
process.exit(1);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
switch (subcommand) {
|
|
957
|
+
case 'enable':
|
|
958
|
+
await cmdAutostartEnable(plistPath, plistDir, home);
|
|
959
|
+
break;
|
|
960
|
+
case 'disable':
|
|
961
|
+
await cmdAutostartDisable(plistPath);
|
|
962
|
+
break;
|
|
963
|
+
case 'status':
|
|
964
|
+
await cmdAutostartStatus(plistPath);
|
|
965
|
+
break;
|
|
966
|
+
default:
|
|
967
|
+
console.log('Usage:');
|
|
968
|
+
console.log(' imtoagent autostart enable Enable auto-start on login');
|
|
969
|
+
console.log(' imtoagent autostart disable Disable auto-start');
|
|
970
|
+
console.log(' imtoagent autostart status Check auto-start status');
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async function cmdAutostartEnable(plistPath: string, plistDir: string, home: string): Promise<void> {
|
|
975
|
+
const dataDir = getDataDir();
|
|
976
|
+
const binPath = execSync('command -v imtoagent', { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
977
|
+
|
|
978
|
+
if (!binPath) {
|
|
979
|
+
console.error('❌ Cannot find imtoagent binary');
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Find node and bun paths for launchd (which has minimal PATH)
|
|
984
|
+
const nodePath = execSync('which node', { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
985
|
+
const bunPath = execSync('which bun', { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
986
|
+
const launchdPATH = `${path.dirname(nodePath)}:${path.dirname(bunPath)}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/opt/homebrew/opt/node@24/bin`;
|
|
987
|
+
|
|
988
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
989
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
990
|
+
<plist version="1.0">
|
|
991
|
+
<dict>
|
|
992
|
+
<key>Label</key>
|
|
993
|
+
<string>com.imtoagent.gateway</string>
|
|
994
|
+
<key>ProgramArguments</key>
|
|
995
|
+
<array>
|
|
996
|
+
<string>${binPath}</string>
|
|
997
|
+
<string>daemon</string>
|
|
998
|
+
</array>
|
|
999
|
+
<key>WorkingDirectory</key>
|
|
1000
|
+
<string>${dataDir}</string>
|
|
1001
|
+
<key>EnvironmentVariables</key>
|
|
1002
|
+
<dict>
|
|
1003
|
+
<key>IMTOAGENT_HOME</key>
|
|
1004
|
+
<string>${dataDir}</string>
|
|
1005
|
+
<key>PATH</key>
|
|
1006
|
+
<string>${launchdPATH}</string>
|
|
1007
|
+
</dict>
|
|
1008
|
+
<key>StandardOutPath</key>
|
|
1009
|
+
<string>${path.join(dataDir, 'logs', 'launchd.log')}</string>
|
|
1010
|
+
<key>StandardErrorPath</key>
|
|
1011
|
+
<string>${path.join(dataDir, 'logs', 'launchd-error.log')}</string>
|
|
1012
|
+
<key>RunAtLoad</key>
|
|
1013
|
+
<true/>
|
|
1014
|
+
<key>KeepAlive</key>
|
|
1015
|
+
<dict>
|
|
1016
|
+
<key>SuccessfulExit</key>
|
|
1017
|
+
<false/>
|
|
1018
|
+
</dict>
|
|
1019
|
+
<key>ThrottleInterval</key>
|
|
1020
|
+
<integer>30</integer>
|
|
1021
|
+
<key>ProcessType</key>
|
|
1022
|
+
<string>Background</string>
|
|
1023
|
+
</dict>
|
|
1024
|
+
</plist>
|
|
1025
|
+
`;
|
|
1026
|
+
|
|
1027
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
1028
|
+
fs.writeFileSync(plistPath, plist);
|
|
1029
|
+
console.log(`✅ Plist written: ${plistPath}`);
|
|
1030
|
+
|
|
1031
|
+
// Unload existing (if any) then reload
|
|
1032
|
+
try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8' }); } catch {}
|
|
1033
|
+
|
|
1034
|
+
try {
|
|
1035
|
+
execSync(`launchctl load "${plistPath}"`, { encoding: 'utf-8', timeout: 5000 });
|
|
1036
|
+
console.log('✅ launchd service loaded');
|
|
1037
|
+
} catch (e: any) {
|
|
1038
|
+
console.error(`⚠️ launchctl load failed: ${e.message}`);
|
|
1039
|
+
console.error(' Plist written. Manual: launchctl load ' + plistPath);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
console.log(`\n📌 Auto-start enabled. Gateway starts on login.`);
|
|
1043
|
+
console.log(` Logs: ${path.join(dataDir, 'logs', 'launchd.log')}`);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async function cmdAutostartDisable(plistPath: string): Promise<void> {
|
|
1047
|
+
if (!fs.existsSync(plistPath)) {
|
|
1048
|
+
console.log('ℹ️ No autostart configured');
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
try {
|
|
1053
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8', timeout: 5000 });
|
|
1054
|
+
console.log('✅ launchd service unloaded');
|
|
1055
|
+
} catch {
|
|
1056
|
+
console.log('ℹ️ Service was not loaded');
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
try {
|
|
1060
|
+
fs.unlinkSync(plistPath);
|
|
1061
|
+
console.log('✅ Plist removed');
|
|
1062
|
+
} catch (e: any) {
|
|
1063
|
+
console.error(`⚠️ Failed to remove plist: ${e.message}`);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
console.log('\n📌 Auto-start disabled');
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async function cmdAutostartStatus(plistPath: string): Promise<void> {
|
|
1070
|
+
const exists = fs.existsSync(plistPath);
|
|
1071
|
+
console.log(`\n📌 Autostart Status`);
|
|
1072
|
+
console.log(` Plist file: ${plistPath}`);
|
|
1073
|
+
console.log(` Installed: ${exists ? '✅ Yes' : '❌ No'}`);
|
|
1074
|
+
|
|
1075
|
+
if (exists) {
|
|
1076
|
+
try {
|
|
1077
|
+
const out = execSync(`launchctl list | grep com.imtoagent`, { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
1078
|
+
console.log(` Running: ${out ? '✅ Yes' : '❌ No (plist exists but not loaded)'}`);
|
|
1079
|
+
if (out) console.log(` ${out}`);
|
|
1080
|
+
} catch {
|
|
1081
|
+
console.log(` Running: ❌ No`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
console.log();
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// ================================================================
|
|
1088
|
+
// version-check — non-blocking npm registry check
|
|
1089
|
+
// ================================================================
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Check if a newer version is available on npm.
|
|
1093
|
+
* Returns null if check fails or no update available.
|
|
1094
|
+
* Caches result for 24 hours.
|
|
1095
|
+
*/
|
|
1096
|
+
async function checkForUpdates(): Promise<string | null> {
|
|
1097
|
+
const dataDir = getDataDir();
|
|
1098
|
+
const cacheFile = path.join(dataDir, '.last-version-check');
|
|
1099
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
1100
|
+
const currentVer = pkg.version;
|
|
1101
|
+
|
|
1102
|
+
// Check cache (24h TTL)
|
|
1103
|
+
try {
|
|
1104
|
+
if (fs.existsSync(cacheFile)) {
|
|
1105
|
+
const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
|
|
1106
|
+
const ageMs = Date.now() - cached.timestamp;
|
|
1107
|
+
if (ageMs < 24 * 60 * 60 * 1000) {
|
|
1108
|
+
// Cache hit — return stored result
|
|
1109
|
+
if (cached.latest && cached.latest !== currentVer) {
|
|
1110
|
+
return cached.latest;
|
|
1111
|
+
}
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
} catch {}
|
|
1116
|
+
|
|
1117
|
+
// Fetch from npm registry (3s timeout, npmmirror for China)
|
|
1118
|
+
try {
|
|
1119
|
+
const http = await import('http');
|
|
1120
|
+
const https = await import('https');
|
|
1121
|
+
|
|
1122
|
+
const fetchJson = (url: string): Promise<any> => {
|
|
1123
|
+
return new Promise((resolve, reject) => {
|
|
1124
|
+
const client = url.startsWith('https') ? https : http;
|
|
1125
|
+
const req = client.get(url, { timeout: 3000 }, (res) => {
|
|
1126
|
+
let data = '';
|
|
1127
|
+
res.on('data', chunk => data += chunk);
|
|
1128
|
+
res.on('end', () => {
|
|
1129
|
+
try { resolve(JSON.parse(data)); } catch { reject(new Error('parse error')); }
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
req.on('error', reject);
|
|
1133
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
1134
|
+
});
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
// Try npmmirror first (China-friendly), fallback to npmjs
|
|
1138
|
+
let result: any;
|
|
1139
|
+
try {
|
|
1140
|
+
result = await fetchJson('https://registry.npmmirror.com/imtoagent');
|
|
1141
|
+
} catch {
|
|
1142
|
+
result = await fetchJson('https://registry.npmjs.org/imtoagent');
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const latest = result['dist-tags']?.latest;
|
|
1146
|
+
if (latest && latest !== currentVer) {
|
|
1147
|
+
// Cache the result
|
|
1148
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ latest, timestamp: Date.now() }));
|
|
1149
|
+
return latest;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Cache "no update" result too
|
|
1153
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ latest: currentVer, timestamp: Date.now() }));
|
|
1154
|
+
return null;
|
|
1155
|
+
} catch {
|
|
1156
|
+
// Silently fail — non-blocking
|
|
1157
|
+
return null;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/** Print version update hint if available */
|
|
1162
|
+
function printUpdateHint(latestVer: string | null): void {
|
|
1163
|
+
if (latestVer) {
|
|
1164
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
1165
|
+
console.log(`\n⬆️ New version available: ${pkg.version} → ${latestVer}`);
|
|
1166
|
+
console.log(` Upgrade: imtoagent update-system`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
package/modules/cli/setup.ts
CHANGED
|
@@ -91,10 +91,14 @@ async function selectMenu(title: string, options: string[]): Promise<number> {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// ================================================================
|
|
94
|
-
// Text input (Enter confirm, ESC returns
|
|
94
|
+
// Text input (Enter confirm, ESC returns null)
|
|
95
95
|
// ================================================================
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Prompt the user for text input.
|
|
99
|
+
* @returns The entered string, or `null` if user pressed ESC.
|
|
100
|
+
*/
|
|
101
|
+
async function promptText(label: string, defaultValue = ''): Promise<string | null> {
|
|
98
102
|
const buf: string[] = [];
|
|
99
103
|
const defaultHint = defaultValue ? ` [${defaultValue}]` : '';
|
|
100
104
|
|
|
@@ -110,7 +114,7 @@ async function promptText(label: string, defaultValue = ''): Promise<string> {
|
|
|
110
114
|
break;
|
|
111
115
|
} else if (key === KEY.ESC) {
|
|
112
116
|
process.stdout.write('\x1B[0K\n');
|
|
113
|
-
return
|
|
117
|
+
return null;
|
|
114
118
|
} else if (key === KEY.BACKSPACE) {
|
|
115
119
|
if (buf.length > 0) {
|
|
116
120
|
buf.pop();
|
|
@@ -377,8 +381,18 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
377
381
|
// 3b: Auto-generate Bot name, customizable
|
|
378
382
|
const defaultName = IM_PLATFORMS[imIdx].label + 'Bot';
|
|
379
383
|
const nameInput = await promptText('Bot name', defaultName);
|
|
380
|
-
if (
|
|
381
|
-
const botName = nameInput || defaultName;
|
|
384
|
+
if (nameInput === null) { if (bots.length === 0) return; break; } // ESC
|
|
385
|
+
const botName = nameInput || defaultName;
|
|
386
|
+
|
|
387
|
+
// Validate Bot name — no whitespace-only, no dangerous characters
|
|
388
|
+
if (!botName || !/^\S/.test(botName)) {
|
|
389
|
+
console.log('⚠️ Bot name must not be empty or whitespace-only. Using default.');
|
|
390
|
+
}
|
|
391
|
+
// Sanitize: remove characters that would break directory names
|
|
392
|
+
const sanitized = botName.replace(/[^\w\s.-]/g, '');
|
|
393
|
+
if (sanitized !== botName) {
|
|
394
|
+
console.log(`⚠️ Bot name sanitized: "${botName}" → "${sanitized}"`);
|
|
395
|
+
}
|
|
382
396
|
|
|
383
397
|
// 3c: Select backend
|
|
384
398
|
const backendLabels = backendStatus.map(b =>
|
|
@@ -419,14 +433,24 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
419
433
|
|
|
420
434
|
for (const field of fields) {
|
|
421
435
|
const val = await promptText(field.label + (field.required ? '' : ' (optional)'));
|
|
422
|
-
if (
|
|
436
|
+
if (val === null) { credentials._escaped = 'true'; break; } // ESC
|
|
423
437
|
credentials[field.key] = val;
|
|
424
438
|
}
|
|
425
439
|
if (credentials._escaped) continue; // ESC go back and re-select backend
|
|
426
440
|
|
|
427
|
-
// 3e: Working directory
|
|
428
|
-
|
|
429
|
-
if (
|
|
441
|
+
// 3e: Working directory (validated)
|
|
442
|
+
let cwd = await promptText('Working directory', os.homedir());
|
|
443
|
+
if (cwd === null) continue;
|
|
444
|
+
cwd = cwd.trim() || os.homedir();
|
|
445
|
+
|
|
446
|
+
// Validate path — reject obviously invalid / dangerous paths
|
|
447
|
+
const badPaths = ['/dev/null', '/dev/zero', '/dev/random', '/etc/passwd', '/etc/shadow', '/System'];
|
|
448
|
+
if (badPaths.some(bp => cwd === bp || cwd.startsWith(bp + '/'))) {
|
|
449
|
+
console.log(`⚠️ Invalid path "${cwd}". Using home directory instead.`);
|
|
450
|
+
cwd = os.homedir();
|
|
451
|
+
}
|
|
452
|
+
// Resolve to absolute path
|
|
453
|
+
cwd = path.resolve(cwd);
|
|
430
454
|
|
|
431
455
|
// Generate unique ID (UUID, for directory isolation, renaming doesn't affect it)
|
|
432
456
|
const botId = randomUUID();
|
|
@@ -509,14 +533,22 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
509
533
|
|
|
510
534
|
const wsLabels = ['Sandbox mode (isolated per Bot)', 'Global mode (shared root path)'];
|
|
511
535
|
const wsIdx = await selectMenu('Select workspace mode', wsLabels);
|
|
512
|
-
|
|
536
|
+
if (wsIdx === -1) { console.log('\n👋 Cancelled'); process.exit(0); }
|
|
513
537
|
workspaceMode = wsIdx === 1 ? 'global' : 'sandbox';
|
|
514
538
|
|
|
515
539
|
if (workspaceMode === 'global') {
|
|
516
540
|
const defaultGlobal = os.homedir() + '/imtoagent-workspace';
|
|
517
541
|
const gpInput = await promptText('Global workspace root path', defaultGlobal);
|
|
518
|
-
if (
|
|
519
|
-
|
|
542
|
+
if (gpInput === null) { console.log('\n👋 Cancelled'); process.exit(0); }
|
|
543
|
+
let resolved = (gpInput || defaultGlobal).trim();
|
|
544
|
+
|
|
545
|
+
// Validate path
|
|
546
|
+
const badPaths = ['/dev/null', '/dev/zero', '/dev/random', '/etc', '/System', '/usr'];
|
|
547
|
+
if (badPaths.some(bp => resolved === bp || resolved.startsWith(bp + '/'))) {
|
|
548
|
+
console.log(`⚠️ Invalid path "${resolved}". Using default instead.`);
|
|
549
|
+
resolved = defaultGlobal;
|
|
550
|
+
}
|
|
551
|
+
workspaceGlobalPath = path.resolve(resolved);
|
|
520
552
|
}
|
|
521
553
|
|
|
522
554
|
const home = process.env.HOME || process.env.USERPROFILE?.replace(/\\/g, '/') || '';
|
|
@@ -573,19 +605,27 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
573
605
|
console.log(` Format: ${preset.format}`);
|
|
574
606
|
console.log(` Models: ${preset.models.join(', ')}\n`);
|
|
575
607
|
|
|
576
|
-
// Confirm/edit short name
|
|
608
|
+
// Confirm/edit short name (validate — must be safe JSON key)
|
|
577
609
|
const nameEdit = await promptText('Provider name (leave blank to confirm)', provName);
|
|
578
|
-
if (
|
|
610
|
+
if (nameEdit === null) continue;
|
|
579
611
|
provName = nameEdit || provName;
|
|
612
|
+
// Sanitize provider name: only alphanumeric, hyphens, underscores
|
|
613
|
+
const sanitizedProv = provName.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
614
|
+
if (!sanitizedProv) {
|
|
615
|
+
console.log('⚠️ Invalid provider name. Using original.');
|
|
616
|
+
} else if (sanitizedProv !== provName) {
|
|
617
|
+
console.log(`⚠️ Provider name sanitized: "${provName}" → "${sanitizedProv}"`);
|
|
618
|
+
provName = sanitizedProv;
|
|
619
|
+
}
|
|
580
620
|
|
|
581
621
|
// Confirm/edit Base URL
|
|
582
622
|
const urlEdit = await promptText('Base URL', baseUrl);
|
|
583
|
-
if (
|
|
623
|
+
if (urlEdit === null) continue;
|
|
584
624
|
baseUrl = urlEdit || baseUrl;
|
|
585
625
|
|
|
586
626
|
// Confirm/edit model list
|
|
587
627
|
const modelsEdit = await promptText('Model list (comma-separated)', models.join(', '));
|
|
588
|
-
if (
|
|
628
|
+
if (modelsEdit === null) continue;
|
|
589
629
|
if (modelsEdit) models = modelsEdit.split(',').map(s => s.trim()).filter(Boolean);
|
|
590
630
|
|
|
591
631
|
if (providers[provName]) {
|
|
@@ -593,17 +633,18 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
593
633
|
}
|
|
594
634
|
} else {
|
|
595
635
|
// Custom
|
|
596
|
-
|
|
597
|
-
if (
|
|
636
|
+
let customName = await promptText('Provider name (e.g. deepseek, dashscope)');
|
|
637
|
+
if (customName === null) { addingProviders = false; continue; }
|
|
638
|
+
provName = customName.trim().toLowerCase().replace(/[^a-zA-Z0-9_-]/g, '');
|
|
598
639
|
if (!provName) { addingProviders = false; continue; }
|
|
599
640
|
if (providers[provName]) {
|
|
600
641
|
console.log(`⚠️ Provider "${provName}" already exists, will overwrite\n`);
|
|
601
642
|
}
|
|
602
643
|
|
|
603
644
|
baseUrl = await promptText('Base URL (e.g. https://api.deepseek.com/v1)');
|
|
604
|
-
if (
|
|
645
|
+
if (baseUrl === null) continue;
|
|
605
646
|
const modelsStr = await promptText('Model list (comma-separated)');
|
|
606
|
-
if (
|
|
647
|
+
if (modelsStr === null) continue;
|
|
607
648
|
models = (modelsStr || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
608
649
|
|
|
609
650
|
const formatIdx = await selectMenu('API format', ['openai', 'anthropic']);
|
|
@@ -613,14 +654,14 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
613
654
|
|
|
614
655
|
// API Key (required for all providers)
|
|
615
656
|
const apiKey = await promptText('API Key');
|
|
616
|
-
if (
|
|
657
|
+
if (apiKey === null) continue;
|
|
617
658
|
if (!apiKey) {
|
|
618
659
|
console.log('⚠️ API Key is empty, this provider will be temporarily unavailable\n');
|
|
619
660
|
}
|
|
620
661
|
|
|
621
662
|
// Pricing (optional)
|
|
622
663
|
const priceInput = await promptText('Pricing (in/out per million tokens, e.g. 0.55,2.19, leave blank to skip)');
|
|
623
|
-
if (
|
|
664
|
+
if (priceInput === null) continue;
|
|
624
665
|
|
|
625
666
|
const pricing: any = {};
|
|
626
667
|
if (priceInput) {
|
|
@@ -663,10 +704,10 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
663
704
|
if (allModels.length > 0) {
|
|
664
705
|
const existingDefault = existingConfig?.defaultModel || allModels[0];
|
|
665
706
|
const val = await promptText('Default model', existingDefault);
|
|
666
|
-
defaultModel =
|
|
707
|
+
defaultModel = val === null ? existingDefault : (val || existingDefault);
|
|
667
708
|
} else {
|
|
668
|
-
|
|
669
|
-
|
|
709
|
+
const val = await promptText('Default model (provider/model)');
|
|
710
|
+
defaultModel = val === null ? 'deepseek/deepseek-v4-pro' : (val || 'deepseek/deepseek-v4-pro');
|
|
670
711
|
}
|
|
671
712
|
|
|
672
713
|
// ===== Step 7: Generate soul files =====
|
|
@@ -719,6 +760,8 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
719
760
|
|
|
720
761
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
721
762
|
|
|
763
|
+
// Atomic config write: write to temp file first, then rename.
|
|
764
|
+
// If the write fails, the original config (if any) is preserved.
|
|
722
765
|
const config: any = {
|
|
723
766
|
system: existingConfig?.system || {
|
|
724
767
|
defaultProjectDir: os.homedir(),
|
|
@@ -758,19 +801,42 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
758
801
|
bots,
|
|
759
802
|
};
|
|
760
803
|
|
|
761
|
-
|
|
762
|
-
|
|
804
|
+
// Write to temp file first for atomicity
|
|
805
|
+
const configTmpPath = configPath + '.tmp';
|
|
806
|
+
const providersTmpPath = path.join(dataDir, 'providers.json.tmp');
|
|
807
|
+
let writeOk = true;
|
|
763
808
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
809
|
+
try {
|
|
810
|
+
fs.writeFileSync(configTmpPath, JSON.stringify(config, null, 2) + '\n');
|
|
811
|
+
fs.renameSync(configTmpPath, configPath);
|
|
812
|
+
console.log(`✅ ${configPath}`);
|
|
813
|
+
} catch (e: any) {
|
|
814
|
+
console.error(`❌ Failed to write config.json: ${e.message}`);
|
|
815
|
+
writeOk = false;
|
|
816
|
+
}
|
|
768
817
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
818
|
+
if (writeOk) {
|
|
819
|
+
try {
|
|
820
|
+
const providersFile: any = { providers, defaultModel, modelAliases: config.modelAliases };
|
|
821
|
+
const providersPath = path.join(dataDir, 'providers.json');
|
|
822
|
+
fs.writeFileSync(providersTmpPath, JSON.stringify(providersFile, null, 2) + '\n');
|
|
823
|
+
fs.renameSync(providersTmpPath, providersPath);
|
|
824
|
+
console.log(`✅ ${providersPath}`);
|
|
825
|
+
} catch (e: any) {
|
|
826
|
+
console.error(`❌ Failed to write providers.json: ${e.message}`);
|
|
827
|
+
console.error('⚠️ config.json was written successfully, but providers.json failed.');
|
|
828
|
+
console.error(' Please re-run "imtoagent setup" to fix.');
|
|
829
|
+
writeOk = false;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (writeOk) {
|
|
834
|
+
const opencodePath = path.join(dataDir, 'opencode.json');
|
|
835
|
+
const opencodeTemplate = getTemplatePath('opencode.template.json');
|
|
836
|
+
if (fs.existsSync(opencodeTemplate)) {
|
|
837
|
+
fs.writeFileSync(opencodePath, fs.readFileSync(opencodeTemplate, 'utf-8'));
|
|
838
|
+
console.log(`✅ ${opencodePath}`);
|
|
839
|
+
}
|
|
774
840
|
}
|
|
775
841
|
|
|
776
842
|
fs.mkdirSync(path.join(dataDir, 'sessions'), { recursive: true });
|