imtoagent 0.3.22 → 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 +555 -3
- package/index.ts +50 -12
- package/modules/cli/setup.ts +185 -54
- package/modules/utils/migrate-workspaces.ts +230 -0
- package/modules/utils/paths.ts +1 -1
- package/modules/utils/workspace-manager.ts +209 -0
- 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
|
|
|
@@ -26,7 +27,7 @@ const command = process.argv[2];
|
|
|
26
27
|
|
|
27
28
|
switch (command) {
|
|
28
29
|
case 'setup':
|
|
29
|
-
await cmdSetup();
|
|
30
|
+
await cmdSetup(process.argv[3]);
|
|
30
31
|
break;
|
|
31
32
|
case 'start':
|
|
32
33
|
await cmdStart();
|
|
@@ -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');
|
|
@@ -98,6 +108,7 @@ imtoagent — IM ↔ Agent Unified Gateway
|
|
|
98
108
|
|
|
99
109
|
Usage:
|
|
100
110
|
imtoagent setup Interactive setup wizard
|
|
111
|
+
imtoagent setup --quick Quick mode (sandbox workspace, skip workspace step)
|
|
101
112
|
imtoagent start Start gateway in background (returns immediately)
|
|
102
113
|
imtoagent run Start gateway in foreground (Ctrl+C to stop)
|
|
103
114
|
imtoagent stop Stop gateway
|
|
@@ -107,6 +118,12 @@ Usage:
|
|
|
107
118
|
imtoagent update-system Upgrade imtoagent itself
|
|
108
119
|
imtoagent update-backend Upgrade current Bot's backend
|
|
109
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
|
|
110
127
|
|
|
111
128
|
Data directory: ${getDataDir()}
|
|
112
129
|
`);
|
|
@@ -115,9 +132,10 @@ Data directory: ${getDataDir()}
|
|
|
115
132
|
// ================================================================
|
|
116
133
|
// setup — interactive wizard
|
|
117
134
|
// ================================================================
|
|
118
|
-
async function cmdSetup() {
|
|
135
|
+
async function cmdSetup(mode) {
|
|
136
|
+
const quick = mode === '--quick' || mode === '-q';
|
|
119
137
|
const { runSetupWizard } = await import('../modules/cli/setup');
|
|
120
|
-
await runSetupWizard();
|
|
138
|
+
await runSetupWizard(quick ? { quick: true } : undefined);
|
|
121
139
|
}
|
|
122
140
|
|
|
123
141
|
// ================================================================
|
|
@@ -221,6 +239,9 @@ echo $PID`;
|
|
|
221
239
|
process.exit(1);
|
|
222
240
|
}
|
|
223
241
|
|
|
242
|
+
// Non-blocking version check
|
|
243
|
+
checkForUpdates().then(printUpdateHint).catch(() => {});
|
|
244
|
+
|
|
224
245
|
// Explicitly exit — Bun may keep event loop alive due to inherited stdio
|
|
225
246
|
process.exit(0);
|
|
226
247
|
}
|
|
@@ -614,3 +635,534 @@ async function cmdUpdateBackend(backendType?: 'claude' | 'codex' | 'opencode'):
|
|
|
614
635
|
process.exit(1);
|
|
615
636
|
}
|
|
616
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
|
+
}
|