imtoagent 0.3.23 → 0.3.25
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 +734 -0
- package/index.ts +31 -4
- package/modules/cli/setup.ts +103 -36
- package/modules/core/types.ts +1 -0
- package/modules/utils/config-manager.ts +359 -0
- package/modules/utils/doctor.ts +462 -0
- package/modules/utils/workspace-manager.ts +23 -3
- 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,21 @@ 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 'doctor':
|
|
65
|
+
await cmdDoctor();
|
|
66
|
+
break;
|
|
67
|
+
case 'config':
|
|
68
|
+
await cmdConfig();
|
|
69
|
+
break;
|
|
70
|
+
case 'autostart':
|
|
71
|
+
await cmdAutostart();
|
|
72
|
+
break;
|
|
57
73
|
case undefined: {
|
|
58
74
|
const dataDir = getDataDir();
|
|
59
75
|
const configPath = path.join(dataDir, 'config.json');
|
|
@@ -108,6 +124,19 @@ Usage:
|
|
|
108
124
|
imtoagent update-system Upgrade imtoagent itself
|
|
109
125
|
imtoagent update-backend Upgrade current Bot's backend
|
|
110
126
|
imtoagent update-backend TYPE Upgrade specific backend (codex|claude|opencode)
|
|
127
|
+
imtoagent uninstall Uninstall imtoagent (keep data by default)
|
|
128
|
+
imtoagent uninstall --purge Uninstall and delete all data
|
|
129
|
+
imtoagent health Run comprehensive health check
|
|
130
|
+
imtoagent doctor Diagnose & fix configuration issues
|
|
131
|
+
imtoagent config Manage Bot configuration
|
|
132
|
+
imtoagent config list List all Bots
|
|
133
|
+
imtoagent config show NAME Show Bot details
|
|
134
|
+
imtoagent config add Add a new Bot
|
|
135
|
+
imtoagent config remove NAME Remove a Bot
|
|
136
|
+
imtoagent config modify NAME Modify Bot settings
|
|
137
|
+
imtoagent autostart enable Enable auto-start on login (launchd)
|
|
138
|
+
imtoagent autostart disable Disable auto-start
|
|
139
|
+
imtoagent autostart status Check auto-start status
|
|
111
140
|
|
|
112
141
|
Data directory: ${getDataDir()}
|
|
113
142
|
`);
|
|
@@ -223,6 +252,9 @@ echo $PID`;
|
|
|
223
252
|
process.exit(1);
|
|
224
253
|
}
|
|
225
254
|
|
|
255
|
+
// Non-blocking version check
|
|
256
|
+
checkForUpdates().then(printUpdateHint).catch(() => {});
|
|
257
|
+
|
|
226
258
|
// Explicitly exit — Bun may keep event loop alive due to inherited stdio
|
|
227
259
|
process.exit(0);
|
|
228
260
|
}
|
|
@@ -616,3 +648,705 @@ async function cmdUpdateBackend(backendType?: 'claude' | 'codex' | 'opencode'):
|
|
|
616
648
|
process.exit(1);
|
|
617
649
|
}
|
|
618
650
|
}
|
|
651
|
+
|
|
652
|
+
// ================================================================
|
|
653
|
+
// uninstall — remove imtoagent
|
|
654
|
+
// ================================================================
|
|
655
|
+
async function cmdUninstall(): Promise<void> {
|
|
656
|
+
const dataDir = getDataDir();
|
|
657
|
+
const purge = process.argv.includes('--purge');
|
|
658
|
+
const force = process.argv.includes('--force') || process.argv.includes('-f');
|
|
659
|
+
|
|
660
|
+
console.log('\n🗑️ imtoagent Uninstall');
|
|
661
|
+
console.log(` Data directory: ${dataDir}`);
|
|
662
|
+
|
|
663
|
+
if (!purge && !force) {
|
|
664
|
+
console.log('\n ⚠️ This will uninstall the imtoagent npm package but KEEP your data.');
|
|
665
|
+
console.log(` Your data in ${dataDir} will be preserved.`);
|
|
666
|
+
console.log('');
|
|
667
|
+
console.log(' To also delete all data, use: imtoagent uninstall --purge');
|
|
668
|
+
console.log('');
|
|
669
|
+
console.log(' Continue? [y/N]');
|
|
670
|
+
} else if (purge && !force) {
|
|
671
|
+
console.log('\n 🔴 This will DELETE all imtoagent data and the npm package.');
|
|
672
|
+
console.log(` The following will be permanently removed:`);
|
|
673
|
+
console.log(` - Data directory: ${dataDir}`);
|
|
674
|
+
console.log(' - npm global package: imtoagent');
|
|
675
|
+
console.log('');
|
|
676
|
+
console.log(' Continue? [y/N]');
|
|
677
|
+
} else {
|
|
678
|
+
console.log(' --force: skipping confirmation\n');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Confirmation
|
|
682
|
+
if (!force) {
|
|
683
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
684
|
+
const answer = await new Promise<string>((resolve) => {
|
|
685
|
+
rl.question(' > ', resolve);
|
|
686
|
+
}).finally(() => rl.close());
|
|
687
|
+
if (answer.trim().toLowerCase() !== 'y' && answer.trim().toLowerCase() !== 'yes') {
|
|
688
|
+
console.log('❌ Uninstall cancelled.');
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Step 1: Stop gateway if running
|
|
694
|
+
if (fs.existsSync(PID_FILE)) {
|
|
695
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
|
|
696
|
+
try {
|
|
697
|
+
process.kill(pid, 0);
|
|
698
|
+
console.log(`⏹ Stopping gateway (PID=${pid})...`);
|
|
699
|
+
process.kill(pid, 'SIGTERM');
|
|
700
|
+
for (let i = 0; i < 10; i++) {
|
|
701
|
+
try { process.kill(pid, 0); await new Promise(r => setTimeout(r, 500)); } catch { break; }
|
|
702
|
+
}
|
|
703
|
+
try { process.kill(pid, 0); process.kill(pid, 'SIGKILL'); } catch {}
|
|
704
|
+
console.log('✅ Gateway stopped');
|
|
705
|
+
} catch {}
|
|
706
|
+
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
707
|
+
} else {
|
|
708
|
+
console.log('ℹ️ Gateway is not running');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Step 2: Remove launchd plist
|
|
712
|
+
const plistPath = path.join(process.env.HOME || '', 'Library', 'LaunchAgents', 'com.imtoagent.gateway.plist');
|
|
713
|
+
if (fs.existsSync(plistPath)) {
|
|
714
|
+
try {
|
|
715
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8' });
|
|
716
|
+
console.log('⏹ Launchd service unloaded');
|
|
717
|
+
} catch {}
|
|
718
|
+
try {
|
|
719
|
+
fs.unlinkSync(plistPath);
|
|
720
|
+
console.log('🗑️ Launchd plist removed');
|
|
721
|
+
} catch {}
|
|
722
|
+
} else {
|
|
723
|
+
console.log('ℹ️ No launchd service configured');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Step 3: Uninstall npm package
|
|
727
|
+
console.log('\n📦 Uninstalling npm package...');
|
|
728
|
+
try {
|
|
729
|
+
execSync('npm uninstall -g imtoagent', {
|
|
730
|
+
encoding: 'utf-8',
|
|
731
|
+
stdio: 'inherit',
|
|
732
|
+
timeout: 30000,
|
|
733
|
+
});
|
|
734
|
+
console.log('✅ npm package uninstalled');
|
|
735
|
+
} catch (e: any) {
|
|
736
|
+
console.error(`⚠️ npm uninstall may have failed: ${e.message}`);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Step 4: Purge data if requested
|
|
740
|
+
if (purge) {
|
|
741
|
+
console.log(`\n🔥 Deleting data directory: ${dataDir}`);
|
|
742
|
+
try {
|
|
743
|
+
execSync(`rm -rf "${dataDir}"`, { encoding: 'utf-8', timeout: 10000 });
|
|
744
|
+
console.log('✅ Data directory deleted');
|
|
745
|
+
} catch (e: any) {
|
|
746
|
+
console.error(`⚠️ Failed to delete data directory: ${e.message}`);
|
|
747
|
+
console.error(` Please remove manually: rm -rf "${dataDir}"`);
|
|
748
|
+
}
|
|
749
|
+
} else {
|
|
750
|
+
console.log(`\n💾 Data preserved at: ${dataDir}`);
|
|
751
|
+
console.log(' To remove it later: rm -rf ~/.imtoagent');
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
console.log('\n✅ imtoagent uninstalled');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ================================================================
|
|
758
|
+
// health — comprehensive health check
|
|
759
|
+
// ================================================================
|
|
760
|
+
async function cmdHealth(): Promise<void> {
|
|
761
|
+
const dataDir = getDataDir();
|
|
762
|
+
const configPath = path.join(dataDir, 'config.json');
|
|
763
|
+
const logFile = path.join(dataDir, 'logs', 'imtoagent.log');
|
|
764
|
+
|
|
765
|
+
console.log(`\n🔍 imtoagent Health Check`);
|
|
766
|
+
console.log(` Data directory: ${dataDir}`);
|
|
767
|
+
console.log(` Time: ${new Date().toISOString()}\n`);
|
|
768
|
+
|
|
769
|
+
let errorCount = 0;
|
|
770
|
+
let warnCount = 0;
|
|
771
|
+
|
|
772
|
+
// ---- 1. Gateway Process ----
|
|
773
|
+
console.log('── Gateway Process ──');
|
|
774
|
+
if (fs.existsSync(PID_FILE)) {
|
|
775
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
|
|
776
|
+
try {
|
|
777
|
+
process.kill(pid, 0);
|
|
778
|
+
console.log(` ✅ Running (PID=${pid})`);
|
|
779
|
+
} catch {
|
|
780
|
+
console.log(` ❌ PID file exists but process ${pid} is gone (stale)`);
|
|
781
|
+
errorCount++;
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
console.log(` ⏸ Not running`);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// ---- 2. Configuration ----
|
|
788
|
+
console.log('\n── Configuration ──');
|
|
789
|
+
if (fs.existsSync(configPath)) {
|
|
790
|
+
try {
|
|
791
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
792
|
+
console.log(` ✅ config.json parse OK`);
|
|
793
|
+
|
|
794
|
+
// Check required fields
|
|
795
|
+
if (!cfg.bots || !Array.isArray(cfg.bots) || cfg.bots.length === 0) {
|
|
796
|
+
console.log(` ⚠️ No bots configured`);
|
|
797
|
+
warnCount++;
|
|
798
|
+
} else {
|
|
799
|
+
console.log(` ✅ ${cfg.bots.length} Bot(s) configured`);
|
|
800
|
+
for (const bot of cfg.bots) {
|
|
801
|
+
const issues: string[] = [];
|
|
802
|
+
if (!bot.name) issues.push('missing name');
|
|
803
|
+
if (!bot.im) issues.push('missing IM platform');
|
|
804
|
+
if (!bot.backend) issues.push('missing backend');
|
|
805
|
+
if (issues.length > 0) {
|
|
806
|
+
console.log(` ⚠️ Bot "${bot.name || '?'}": ${issues.join(', ')}`);
|
|
807
|
+
warnCount++;
|
|
808
|
+
} else {
|
|
809
|
+
console.log(` ✅ ${bot.name} (${bot.im} + ${bot.backend})`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Check providers.json
|
|
815
|
+
const providersPath = path.join(dataDir, 'providers.json');
|
|
816
|
+
if (fs.existsSync(providersPath)) {
|
|
817
|
+
try {
|
|
818
|
+
const prov = JSON.parse(fs.readFileSync(providersPath, 'utf-8'));
|
|
819
|
+
console.log(` ✅ providers.json parse OK`);
|
|
820
|
+
// Check for placeholder API keys
|
|
821
|
+
const provStr = JSON.stringify(prov);
|
|
822
|
+
if (provStr.includes('YOUR_') || provStr.includes('sk-xxx') || provStr.includes('placehold')) {
|
|
823
|
+
console.log(` ⚠️ providers.json may contain placeholder API keys`);
|
|
824
|
+
warnCount++;
|
|
825
|
+
}
|
|
826
|
+
} catch {
|
|
827
|
+
console.log(` ❌ providers.json parse error`);
|
|
828
|
+
errorCount++;
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
console.log(` ⚠️ providers.json not found`);
|
|
832
|
+
warnCount++;
|
|
833
|
+
}
|
|
834
|
+
} catch {
|
|
835
|
+
console.log(` ❌ config.json parse error`);
|
|
836
|
+
errorCount++;
|
|
837
|
+
}
|
|
838
|
+
} else {
|
|
839
|
+
console.log(` ❌ config.json not found (run "imtoagent setup")`);
|
|
840
|
+
errorCount++;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ---- 3. Backend Status ----
|
|
844
|
+
console.log('\n── Backend Status ──');
|
|
845
|
+
try {
|
|
846
|
+
const { checkBackend } = await import('../modules/utils/backend-check');
|
|
847
|
+
if (fs.existsSync(configPath)) {
|
|
848
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
849
|
+
const checkedTypes = new Set<string>();
|
|
850
|
+
for (const bot of cfg.bots || []) {
|
|
851
|
+
if (bot.backend && ['claude', 'codex', 'opencode'].includes(bot.backend) && !checkedTypes.has(bot.backend)) {
|
|
852
|
+
checkedTypes.add(bot.backend);
|
|
853
|
+
const info = checkBackend(bot.backend as any);
|
|
854
|
+
if (info.installed) {
|
|
855
|
+
console.log(` ✅ ${info.label} v${info.version} (${info.installSource})`);
|
|
856
|
+
} else {
|
|
857
|
+
console.log(` ❌ ${info.label} not installed → ${info.installHint}`);
|
|
858
|
+
errorCount++;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (checkedTypes.size === 0) {
|
|
863
|
+
console.log(` ℹ️ No backends to check`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
} catch {
|
|
867
|
+
console.log(` ⚠️ Backend check skipped`);
|
|
868
|
+
warnCount++;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ---- 4. Proxy Port ----
|
|
872
|
+
console.log('\n── Proxy Port (:18899) ──');
|
|
873
|
+
try {
|
|
874
|
+
const net = await import('net');
|
|
875
|
+
const checkPort = (port: number): Promise<boolean> => {
|
|
876
|
+
return new Promise((resolve) => {
|
|
877
|
+
const socket = new net.Socket();
|
|
878
|
+
socket.setTimeout(2000);
|
|
879
|
+
socket.on('connect', () => { socket.destroy(); resolve(true); });
|
|
880
|
+
socket.on('error', () => resolve(false));
|
|
881
|
+
socket.on('timeout', () => { socket.destroy(); resolve(false); });
|
|
882
|
+
socket.connect(port, '127.0.0.1');
|
|
883
|
+
});
|
|
884
|
+
};
|
|
885
|
+
const reachable = await checkPort(18899);
|
|
886
|
+
if (reachable) {
|
|
887
|
+
console.log(` ✅ Port 18899 is reachable`);
|
|
888
|
+
} else {
|
|
889
|
+
// Check if port is in use by another process
|
|
890
|
+
try {
|
|
891
|
+
const lsofOut = execSync(`lsof -i :18899 2>/dev/null`, { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
892
|
+
if (lsofOut) {
|
|
893
|
+
console.log(` ❌ Port 18899 is occupied by another process:`);
|
|
894
|
+
lsofOut.split('\n').forEach(line => console.log(` ${line}`));
|
|
895
|
+
} else {
|
|
896
|
+
console.log(` ⏸ Port 18899 is free (gateway not running)`);
|
|
897
|
+
}
|
|
898
|
+
} catch {
|
|
899
|
+
console.log(` ⏸ Port 18899 is free (gateway not running)`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
} catch {
|
|
903
|
+
console.log(` ⚠️ Port check failed`);
|
|
904
|
+
warnCount++;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// ---- 5. Recent Log Errors ----
|
|
908
|
+
console.log('\n── Recent Log Errors ──');
|
|
909
|
+
if (fs.existsSync(logFile)) {
|
|
910
|
+
const stats = fs.statSync(logFile);
|
|
911
|
+
const size = stats.size > 1024 * 1024
|
|
912
|
+
? (stats.size / (1024 * 1024)).toFixed(1) + ' MB'
|
|
913
|
+
: (stats.size / 1024).toFixed(1) + ' KB';
|
|
914
|
+
console.log(` Log: ${size}`);
|
|
915
|
+
|
|
916
|
+
// Read last 50 lines looking for ERROR/WARN
|
|
917
|
+
try {
|
|
918
|
+
const content = fs.readFileSync(logFile, 'utf-8');
|
|
919
|
+
const lines = content.split('\n');
|
|
920
|
+
const recent = lines.slice(-50);
|
|
921
|
+
const errors = recent.filter(l => l.includes('ERROR') || l.includes('[error]'));
|
|
922
|
+
const warns = recent.filter(l => l.includes('WARN') || l.includes('[warn]'));
|
|
923
|
+
|
|
924
|
+
if (errors.length > 0) {
|
|
925
|
+
console.log(` ❌ ${errors.length} recent ERROR(s):`);
|
|
926
|
+
for (const line of errors.slice(-3)) {
|
|
927
|
+
console.log(` ${line.trim().slice(0, 120)}`);
|
|
928
|
+
}
|
|
929
|
+
errorCount++;
|
|
930
|
+
} else {
|
|
931
|
+
console.log(` ✅ No recent errors`);
|
|
932
|
+
}
|
|
933
|
+
if (warns.length > 0 && errors.length === 0) {
|
|
934
|
+
console.log(` ℹ️ ${warns.length} recent warning(s) (non-critical)`);
|
|
935
|
+
}
|
|
936
|
+
} catch {
|
|
937
|
+
console.log(` ⚠️ Could not read log file`);
|
|
938
|
+
warnCount++;
|
|
939
|
+
}
|
|
940
|
+
} else {
|
|
941
|
+
console.log(` ℹ️ No log file yet`);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// ---- Summary ----
|
|
945
|
+
console.log('\n── Summary ──');
|
|
946
|
+
if (errorCount === 0 && warnCount === 0) {
|
|
947
|
+
console.log(` ✅ All checks passed!`);
|
|
948
|
+
} else {
|
|
949
|
+
if (errorCount > 0) console.log(` ❌ ${errorCount} error(s)`);
|
|
950
|
+
if (warnCount > 0) console.log(` ⚠️ ${warnCount} warning(s)`);
|
|
951
|
+
}
|
|
952
|
+
console.log();
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// ================================================================
|
|
956
|
+
// config — Bot 配置管理
|
|
957
|
+
// ================================================================
|
|
958
|
+
async function cmdConfig(): Promise<void> {
|
|
959
|
+
const subcommand = process.argv[3];
|
|
960
|
+
|
|
961
|
+
switch (subcommand) {
|
|
962
|
+
case 'list':
|
|
963
|
+
await cmdConfigList();
|
|
964
|
+
break;
|
|
965
|
+
case 'show': {
|
|
966
|
+
const name = process.argv[4];
|
|
967
|
+
if (!name) { console.error('Usage: imtoagent config show <name>'); process.exit(1); }
|
|
968
|
+
await cmdConfigShow(name);
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
case 'add':
|
|
972
|
+
await cmdConfigAdd();
|
|
973
|
+
break;
|
|
974
|
+
case 'remove': {
|
|
975
|
+
const name = process.argv[4];
|
|
976
|
+
if (!name) { console.error('Usage: imtoagent config remove <name>'); process.exit(1); }
|
|
977
|
+
await cmdConfigRemove(name);
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
case 'modify': {
|
|
981
|
+
const name = process.argv[4];
|
|
982
|
+
if (!name) { console.error('Usage: imtoagent config modify <name>'); process.exit(1); }
|
|
983
|
+
await cmdConfigModify(name);
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
case undefined:
|
|
987
|
+
case 'help':
|
|
988
|
+
case '--help':
|
|
989
|
+
case '-h':
|
|
990
|
+
console.log(`
|
|
991
|
+
imtoagent config — Manage Bot configuration
|
|
992
|
+
|
|
993
|
+
Usage:
|
|
994
|
+
imtoagent config list List all Bots
|
|
995
|
+
imtoagent config show NAME Show Bot details
|
|
996
|
+
imtoagent config add Add a new Bot (interactive)
|
|
997
|
+
imtoagent config remove NAME Remove a Bot
|
|
998
|
+
imtoagent config modify NAME Modify Bot settings
|
|
999
|
+
`);
|
|
1000
|
+
break;
|
|
1001
|
+
default:
|
|
1002
|
+
console.error(`❌ Unknown config subcommand: ${subcommand}`);
|
|
1003
|
+
console.log(' Run "imtoagent config help" for usage.');
|
|
1004
|
+
process.exit(1);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// ---- Config subcommand wrappers ----
|
|
1009
|
+
async function cmdConfigList(): Promise<void> {
|
|
1010
|
+
const m = await import('../modules/utils/config-manager');
|
|
1011
|
+
await m.cmdConfigList();
|
|
1012
|
+
}
|
|
1013
|
+
async function cmdConfigShow(name: string): Promise<void> {
|
|
1014
|
+
const m = await import('../modules/utils/config-manager');
|
|
1015
|
+
await m.cmdConfigShow(name);
|
|
1016
|
+
}
|
|
1017
|
+
async function cmdConfigAdd(): Promise<void> {
|
|
1018
|
+
const m = await import('../modules/utils/config-manager');
|
|
1019
|
+
await m.cmdConfigAdd();
|
|
1020
|
+
}
|
|
1021
|
+
async function cmdConfigRemove(name: string): Promise<void> {
|
|
1022
|
+
const m = await import('../modules/utils/config-manager');
|
|
1023
|
+
await m.cmdConfigRemove(name);
|
|
1024
|
+
}
|
|
1025
|
+
async function cmdConfigModify(name: string): Promise<void> {
|
|
1026
|
+
const m = await import('../modules/utils/config-manager');
|
|
1027
|
+
await m.cmdConfigModify(name);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
// ================================================================
|
|
1032
|
+
// doctor — 配置诊断与自动修复
|
|
1033
|
+
// ================================================================
|
|
1034
|
+
async function cmdDoctor(): Promise<void> {
|
|
1035
|
+
console.log(`\n🔧 imtoagent Doctor — Configuration Diagnosis\n`);
|
|
1036
|
+
|
|
1037
|
+
try {
|
|
1038
|
+
const { runDoctorChecks, formatIssues } = await import('../modules/utils/doctor');
|
|
1039
|
+
const issues = await runDoctorChecks();
|
|
1040
|
+
|
|
1041
|
+
// 分组
|
|
1042
|
+
const fixableIssues = issues.filter(i => i.fixable);
|
|
1043
|
+
const unfixableIssues = issues.filter(i => !i.fixable);
|
|
1044
|
+
const errors = issues.filter(i => i.severity === 'error');
|
|
1045
|
+
const warnings = issues.filter(i => i.severity === 'warning');
|
|
1046
|
+
const infos = issues.filter(i => i.severity === 'info');
|
|
1047
|
+
|
|
1048
|
+
// 打印所有问题
|
|
1049
|
+
if (issues.length === 0) {
|
|
1050
|
+
console.log(' ✅ All checks passed! Nothing to fix.\n');
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// 打印 errors
|
|
1055
|
+
if (errors.length > 0) {
|
|
1056
|
+
console.log('❌ Errors:');
|
|
1057
|
+
for (const e of errors) {
|
|
1058
|
+
console.log(` ${e.message}`);
|
|
1059
|
+
if (e.fixable && e.fixDescription) {
|
|
1060
|
+
console.log(` → 🔧 ${e.fixDescription}`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
console.log();
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// 打印 warnings
|
|
1067
|
+
if (warnings.length > 0) {
|
|
1068
|
+
console.log('⚠️ Warnings:');
|
|
1069
|
+
for (const w of warnings) {
|
|
1070
|
+
console.log(` ${w.message}`);
|
|
1071
|
+
}
|
|
1072
|
+
console.log();
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// 打印 infos
|
|
1076
|
+
if (infos.length > 0) {
|
|
1077
|
+
console.log('✅ OK:');
|
|
1078
|
+
for (const i of infos) {
|
|
1079
|
+
console.log(` ${i.message}`);
|
|
1080
|
+
}
|
|
1081
|
+
console.log();
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// 尝试自动修复
|
|
1085
|
+
if (fixableIssues.length > 0) {
|
|
1086
|
+
console.log(`─── Auto-Fix ───`);
|
|
1087
|
+
let fixed = 0;
|
|
1088
|
+
for (const issue of fixableIssues) {
|
|
1089
|
+
if (!issue.fix) continue;
|
|
1090
|
+
try {
|
|
1091
|
+
console.log(`\n🔧 Fixing: ${issue.fixDescription}`);
|
|
1092
|
+
const success = await issue.fix();
|
|
1093
|
+
if (success) {
|
|
1094
|
+
console.log(` ✅ Fixed`);
|
|
1095
|
+
fixed++;
|
|
1096
|
+
} else {
|
|
1097
|
+
console.log(` ❌ Fix failed`);
|
|
1098
|
+
}
|
|
1099
|
+
} catch (e: any) {
|
|
1100
|
+
console.log(` ❌ Fix failed: ${e.message}`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
console.log(`\n ${fixed}/${fixableIssues.length} issues fixed`);
|
|
1104
|
+
if (fixed > 0) {
|
|
1105
|
+
console.log(`\n💡 Run "imtoagent doctor" again to re-check after fixes.\n`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Summary
|
|
1110
|
+
console.log('── Summary ──');
|
|
1111
|
+
if (errors.length === 0) {
|
|
1112
|
+
console.log(' ✅ No errors found');
|
|
1113
|
+
} else {
|
|
1114
|
+
console.log(` ❌ ${errors.length} error(s) — some may be fixable`);
|
|
1115
|
+
}
|
|
1116
|
+
if (warnings.length > 0) {
|
|
1117
|
+
console.log(` ⚠️ ${warnings.length} warning(s) — review recommended`);
|
|
1118
|
+
}
|
|
1119
|
+
console.log();
|
|
1120
|
+
|
|
1121
|
+
} catch (e: any) {
|
|
1122
|
+
console.error(`❌ Doctor check failed: ${e.message}\n`);
|
|
1123
|
+
process.exit(1);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
// ================================================================
|
|
1127
|
+
// autostart — launchd integration (macOS only)
|
|
1128
|
+
// ================================================================
|
|
1129
|
+
async function cmdAutostart(): Promise<void> {
|
|
1130
|
+
const subcommand = process.argv[3];
|
|
1131
|
+
const home = process.env.HOME || '';
|
|
1132
|
+
const plistDir = path.join(home, 'Library', 'LaunchAgents');
|
|
1133
|
+
const plistPath = path.join(plistDir, 'com.imtoagent.gateway.plist');
|
|
1134
|
+
|
|
1135
|
+
if (process.platform !== 'darwin') {
|
|
1136
|
+
console.error('❌ autostart is only supported on macOS (launchd)');
|
|
1137
|
+
process.exit(1);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
switch (subcommand) {
|
|
1141
|
+
case 'enable':
|
|
1142
|
+
await cmdAutostartEnable(plistPath, plistDir, home);
|
|
1143
|
+
break;
|
|
1144
|
+
case 'disable':
|
|
1145
|
+
await cmdAutostartDisable(plistPath);
|
|
1146
|
+
break;
|
|
1147
|
+
case 'status':
|
|
1148
|
+
await cmdAutostartStatus(plistPath);
|
|
1149
|
+
break;
|
|
1150
|
+
default:
|
|
1151
|
+
console.log('Usage:');
|
|
1152
|
+
console.log(' imtoagent autostart enable Enable auto-start on login');
|
|
1153
|
+
console.log(' imtoagent autostart disable Disable auto-start');
|
|
1154
|
+
console.log(' imtoagent autostart status Check auto-start status');
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
async function cmdAutostartEnable(plistPath: string, plistDir: string, home: string): Promise<void> {
|
|
1159
|
+
const dataDir = getDataDir();
|
|
1160
|
+
const binPath = execSync('command -v imtoagent', { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
1161
|
+
|
|
1162
|
+
if (!binPath) {
|
|
1163
|
+
console.error('❌ Cannot find imtoagent binary');
|
|
1164
|
+
process.exit(1);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Find node and bun paths for launchd (which has minimal PATH)
|
|
1168
|
+
const nodePath = execSync('which node', { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
1169
|
+
const bunPath = execSync('which bun', { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
1170
|
+
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`;
|
|
1171
|
+
|
|
1172
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1173
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1174
|
+
<plist version="1.0">
|
|
1175
|
+
<dict>
|
|
1176
|
+
<key>Label</key>
|
|
1177
|
+
<string>com.imtoagent.gateway</string>
|
|
1178
|
+
<key>ProgramArguments</key>
|
|
1179
|
+
<array>
|
|
1180
|
+
<string>${binPath}</string>
|
|
1181
|
+
<string>daemon</string>
|
|
1182
|
+
</array>
|
|
1183
|
+
<key>WorkingDirectory</key>
|
|
1184
|
+
<string>${dataDir}</string>
|
|
1185
|
+
<key>EnvironmentVariables</key>
|
|
1186
|
+
<dict>
|
|
1187
|
+
<key>IMTOAGENT_HOME</key>
|
|
1188
|
+
<string>${dataDir}</string>
|
|
1189
|
+
<key>PATH</key>
|
|
1190
|
+
<string>${launchdPATH}</string>
|
|
1191
|
+
</dict>
|
|
1192
|
+
<key>StandardOutPath</key>
|
|
1193
|
+
<string>${path.join(dataDir, 'logs', 'launchd.log')}</string>
|
|
1194
|
+
<key>StandardErrorPath</key>
|
|
1195
|
+
<string>${path.join(dataDir, 'logs', 'launchd-error.log')}</string>
|
|
1196
|
+
<key>RunAtLoad</key>
|
|
1197
|
+
<true/>
|
|
1198
|
+
<key>KeepAlive</key>
|
|
1199
|
+
<dict>
|
|
1200
|
+
<key>SuccessfulExit</key>
|
|
1201
|
+
<false/>
|
|
1202
|
+
</dict>
|
|
1203
|
+
<key>ThrottleInterval</key>
|
|
1204
|
+
<integer>30</integer>
|
|
1205
|
+
<key>ProcessType</key>
|
|
1206
|
+
<string>Background</string>
|
|
1207
|
+
</dict>
|
|
1208
|
+
</plist>
|
|
1209
|
+
`;
|
|
1210
|
+
|
|
1211
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
1212
|
+
fs.writeFileSync(plistPath, plist);
|
|
1213
|
+
console.log(`✅ Plist written: ${plistPath}`);
|
|
1214
|
+
|
|
1215
|
+
// Unload existing (if any) then reload
|
|
1216
|
+
try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8' }); } catch {}
|
|
1217
|
+
|
|
1218
|
+
try {
|
|
1219
|
+
execSync(`launchctl load "${plistPath}"`, { encoding: 'utf-8', timeout: 5000 });
|
|
1220
|
+
console.log('✅ launchd service loaded');
|
|
1221
|
+
} catch (e: any) {
|
|
1222
|
+
console.error(`⚠️ launchctl load failed: ${e.message}`);
|
|
1223
|
+
console.error(' Plist written. Manual: launchctl load ' + plistPath);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
console.log(`\n📌 Auto-start enabled. Gateway starts on login.`);
|
|
1227
|
+
console.log(` Logs: ${path.join(dataDir, 'logs', 'launchd.log')}`);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
async function cmdAutostartDisable(plistPath: string): Promise<void> {
|
|
1231
|
+
if (!fs.existsSync(plistPath)) {
|
|
1232
|
+
console.log('ℹ️ No autostart configured');
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
try {
|
|
1237
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8', timeout: 5000 });
|
|
1238
|
+
console.log('✅ launchd service unloaded');
|
|
1239
|
+
} catch {
|
|
1240
|
+
console.log('ℹ️ Service was not loaded');
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
try {
|
|
1244
|
+
fs.unlinkSync(plistPath);
|
|
1245
|
+
console.log('✅ Plist removed');
|
|
1246
|
+
} catch (e: any) {
|
|
1247
|
+
console.error(`⚠️ Failed to remove plist: ${e.message}`);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
console.log('\n📌 Auto-start disabled');
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
async function cmdAutostartStatus(plistPath: string): Promise<void> {
|
|
1254
|
+
const exists = fs.existsSync(plistPath);
|
|
1255
|
+
console.log(`\n📌 Autostart Status`);
|
|
1256
|
+
console.log(` Plist file: ${plistPath}`);
|
|
1257
|
+
console.log(` Installed: ${exists ? '✅ Yes' : '❌ No'}`);
|
|
1258
|
+
|
|
1259
|
+
if (exists) {
|
|
1260
|
+
try {
|
|
1261
|
+
const out = execSync(`launchctl list | grep com.imtoagent`, { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
1262
|
+
console.log(` Running: ${out ? '✅ Yes' : '❌ No (plist exists but not loaded)'}`);
|
|
1263
|
+
if (out) console.log(` ${out}`);
|
|
1264
|
+
} catch {
|
|
1265
|
+
console.log(` Running: ❌ No`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
console.log();
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// ================================================================
|
|
1272
|
+
// version-check — non-blocking npm registry check
|
|
1273
|
+
// ================================================================
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Check if a newer version is available on npm.
|
|
1277
|
+
* Returns null if check fails or no update available.
|
|
1278
|
+
* Caches result for 24 hours.
|
|
1279
|
+
*/
|
|
1280
|
+
async function checkForUpdates(): Promise<string | null> {
|
|
1281
|
+
const dataDir = getDataDir();
|
|
1282
|
+
const cacheFile = path.join(dataDir, '.last-version-check');
|
|
1283
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
1284
|
+
const currentVer = pkg.version;
|
|
1285
|
+
|
|
1286
|
+
// Check cache (24h TTL)
|
|
1287
|
+
try {
|
|
1288
|
+
if (fs.existsSync(cacheFile)) {
|
|
1289
|
+
const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
|
|
1290
|
+
const ageMs = Date.now() - cached.timestamp;
|
|
1291
|
+
if (ageMs < 24 * 60 * 60 * 1000) {
|
|
1292
|
+
// Cache hit — return stored result
|
|
1293
|
+
if (cached.latest && cached.latest !== currentVer) {
|
|
1294
|
+
return cached.latest;
|
|
1295
|
+
}
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
} catch {}
|
|
1300
|
+
|
|
1301
|
+
// Fetch from npm registry (3s timeout, npmmirror for China)
|
|
1302
|
+
try {
|
|
1303
|
+
const http = await import('http');
|
|
1304
|
+
const https = await import('https');
|
|
1305
|
+
|
|
1306
|
+
const fetchJson = (url: string): Promise<any> => {
|
|
1307
|
+
return new Promise((resolve, reject) => {
|
|
1308
|
+
const client = url.startsWith('https') ? https : http;
|
|
1309
|
+
const req = client.get(url, { timeout: 3000 }, (res) => {
|
|
1310
|
+
let data = '';
|
|
1311
|
+
res.on('data', chunk => data += chunk);
|
|
1312
|
+
res.on('end', () => {
|
|
1313
|
+
try { resolve(JSON.parse(data)); } catch { reject(new Error('parse error')); }
|
|
1314
|
+
});
|
|
1315
|
+
});
|
|
1316
|
+
req.on('error', reject);
|
|
1317
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
1318
|
+
});
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
// Try npmmirror first (China-friendly), fallback to npmjs
|
|
1322
|
+
let result: any;
|
|
1323
|
+
try {
|
|
1324
|
+
result = await fetchJson('https://registry.npmmirror.com/imtoagent');
|
|
1325
|
+
} catch {
|
|
1326
|
+
result = await fetchJson('https://registry.npmjs.org/imtoagent');
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const latest = result['dist-tags']?.latest;
|
|
1330
|
+
if (latest && latest !== currentVer) {
|
|
1331
|
+
// Cache the result
|
|
1332
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ latest, timestamp: Date.now() }));
|
|
1333
|
+
return latest;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Cache "no update" result too
|
|
1337
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ latest: currentVer, timestamp: Date.now() }));
|
|
1338
|
+
return null;
|
|
1339
|
+
} catch {
|
|
1340
|
+
// Silently fail — non-blocking
|
|
1341
|
+
return null;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/** Print version update hint if available */
|
|
1346
|
+
function printUpdateHint(latestVer: string | null): void {
|
|
1347
|
+
if (latestVer) {
|
|
1348
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
1349
|
+
console.log(`\n⬆️ New version available: ${pkg.version} → ${latestVer}`);
|
|
1350
|
+
console.log(` Upgrade: imtoagent update-system`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|