klio 1.4.1 → 1.4.3
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/README.md +35 -5
- package/commands.db +0 -0
- package/package.json +4 -1
- package/src/astrology/astrologyService.js +219 -1
- package/src/cli/cli.js +138 -1
- package/src/gui/commandLogger.js +67 -0
- package/src/gui/database.js +135 -0
- package/src/gui/public/index.html +381 -0
- package/src/gui/routes/api.js +134 -0
- package/src/gui/server.js +82 -0
package/README.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
A command-line tool for astrological calculations, health analysis, and personalized astrology insights.
|
|
4
4
|
|
|
5
|
+
**Features:**
|
|
6
|
+
- Command-line interface for astrological calculations
|
|
7
|
+
- Web-based GUI for easy exploration
|
|
8
|
+
- Health data analysis from Apple Health exports
|
|
9
|
+
- CSV data analysis with statistical testing
|
|
10
|
+
- Personalized astrology with multiple chart support
|
|
11
|
+
- AI integration for astrological insights
|
|
12
|
+
|
|
5
13
|
## Prerequisites
|
|
6
14
|
|
|
7
15
|
### For Linux Users
|
|
@@ -28,6 +36,26 @@ npm install -g klio
|
|
|
28
36
|
klio [options]
|
|
29
37
|
```
|
|
30
38
|
|
|
39
|
+
### Web Interface
|
|
40
|
+
|
|
41
|
+
AstroCLI includes a web-based GUI that you can launch with the `--gui` flag:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
klio --gui
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This will start a web server on port 37421 (or a different port if specified) and open a browser interface where you can:
|
|
48
|
+
|
|
49
|
+
- **Run commands** directly from the browser
|
|
50
|
+
- **View command output** in a formatted display
|
|
51
|
+
- **Browse all available commands** with examples and documentation
|
|
52
|
+
- **Access command history** to reuse previous commands
|
|
53
|
+
|
|
54
|
+
**Custom Port:**
|
|
55
|
+
```bash
|
|
56
|
+
klio --gui --gui-port 8080
|
|
57
|
+
```
|
|
58
|
+
|
|
31
59
|
### Configuration
|
|
32
60
|
|
|
33
61
|
- **Show status**: `--status` - Shows the stored configuration data
|
|
@@ -62,8 +90,6 @@ It's possible to analyze Apple Health data. This happens on the device and no se
|
|
|
62
90
|
- **Step analysis**: `klio [planet] --apple <filepath> --steps` - Analyzes step patterns of any planet
|
|
63
91
|
- **Stress analysis**: `klio [planet] --apple <filepath> --stress` - Analyzes stress patterns of planet sign stress based on HRV
|
|
64
92
|
|
|
65
|
-
> Maybe I'll add some more in the future.
|
|
66
|
-
|
|
67
93
|
### CSV Analysis
|
|
68
94
|
It's possible to analyze a csv with a column of either ISO date time or unix time stamp.
|
|
69
95
|
|
|
@@ -72,9 +98,6 @@ It's possible to analyze a csv with a column of either ISO date time or unix tim
|
|
|
72
98
|
|
|
73
99
|
The command also returns a Chi-Square.
|
|
74
100
|
|
|
75
|
-
You can research a lot of fun things with this. You find a lot of free datasets on Kaggle.
|
|
76
|
-
|
|
77
|
-
|
|
78
101
|
### Adding different charts
|
|
79
102
|
|
|
80
103
|
- **Use personal data**: `--i` - Uses birth data from setup for calculations
|
|
@@ -99,6 +122,13 @@ Then, instead using `--i` for the commands from above you can use `--wp <id>` i.
|
|
|
99
122
|
- **Past aspects**: `klio --v <count> [planet1] [aspect-type] [planet2]` - Shows past aspects (Format: --v <count> planet1 aspectType planet2) Available aspect types: c, o, s, t, se (conjunction, opposition, square, trine, sextile)
|
|
100
123
|
- **Future aspects**: `klio --z <count> [planet1] [aspect-type] [planet2]` - Shows future aspects (Format: --z <count> planet1 aspectType planet2)
|
|
101
124
|
|
|
125
|
+
### Planet Ingresses
|
|
126
|
+
|
|
127
|
+
- **Planet ingress**: `klio [planet] --in [count]` - Shows when a planet enters a new zodiac sign. Optional count parameter for multiple ingresses (default: 1). Works with any planet (moon, mercury, venus, etc.)
|
|
128
|
+
- Example: `klio moon --in` - Shows next moon ingress
|
|
129
|
+
- Example: `klio saturn --in 2` - Shows next 2 Saturn ingresses (note: slow planets may have limited future data)
|
|
130
|
+
- Example: `klio mercury --in --d "15.03.2026"` - Shows next Mercury ingress from a specific date
|
|
131
|
+
|
|
102
132
|
### AI Integration
|
|
103
133
|
|
|
104
134
|
- **AI model selection**: `--ai <model>` - Sets a specific AI model (e.g., "google/gemma-3n-e4b") for LM Studio ([lmstudio.ai](https://lmstudio.ai))
|
package/commands.db
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klio",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "A CLI for astrological calculations",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -23,8 +23,11 @@
|
|
|
23
23
|
"commander": "^14.0.3",
|
|
24
24
|
"csv": "^6.4.1",
|
|
25
25
|
"csv-parser": "^3.0.0",
|
|
26
|
+
"express": "^4.19.2",
|
|
26
27
|
"fast-xml-parser": "^5.3.4",
|
|
27
28
|
"moment-timezone": "^0.6.0",
|
|
29
|
+
"node-fetch": "^3.3.2",
|
|
30
|
+
"sqlite3": "^5.1.7",
|
|
28
31
|
"swisseph": "^0.5.17"
|
|
29
32
|
}
|
|
30
33
|
}
|
|
@@ -2491,6 +2491,223 @@ function analyzeHouseDistributionSignificance(houseDistribution) {
|
|
|
2491
2491
|
};
|
|
2492
2492
|
}
|
|
2493
2493
|
|
|
2494
|
+
// Function to calculate the next planet ingress (when planet enters a new sign)
|
|
2495
|
+
function calculateNextPlanetIngress(planetName, startDate = null, count = 1) {
|
|
2496
|
+
// Use current date if no start date is provided
|
|
2497
|
+
const startDateObj = startDate ? new Date(startDate.year, startDate.month - 1, startDate.day, startDate.hour, startDate.minute) : new Date();
|
|
2498
|
+
|
|
2499
|
+
const results = [];
|
|
2500
|
+
let currentDate = new Date(startDateObj);
|
|
2501
|
+
|
|
2502
|
+
// Calculate timezone offset for the start date
|
|
2503
|
+
const config = loadConfig();
|
|
2504
|
+
let timezone = 'Europe/Zurich'; // Default
|
|
2505
|
+
|
|
2506
|
+
if (config && config.currentLocation && config.currentLocation.timezone) {
|
|
2507
|
+
timezone = config.currentLocation.timezone;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
const timezoneOffsetMinutes = getTimezoneOffset({
|
|
2511
|
+
year: currentDate.getFullYear(),
|
|
2512
|
+
month: currentDate.getMonth() + 1,
|
|
2513
|
+
day: currentDate.getDate(),
|
|
2514
|
+
hour: currentDate.getHours(),
|
|
2515
|
+
minute: currentDate.getMinutes()
|
|
2516
|
+
}, timezone);
|
|
2517
|
+
|
|
2518
|
+
// Get current planet position
|
|
2519
|
+
const currentPlanetData = getAstrologicalData(planetName, {
|
|
2520
|
+
year: currentDate.getFullYear(),
|
|
2521
|
+
month: currentDate.getMonth() + 1,
|
|
2522
|
+
day: currentDate.getDate(),
|
|
2523
|
+
hour: currentDate.getHours(),
|
|
2524
|
+
minute: currentDate.getMinutes()
|
|
2525
|
+
});
|
|
2526
|
+
|
|
2527
|
+
let currentSignIndex = signs.indexOf(currentPlanetData.sign);
|
|
2528
|
+
let targetSignIndex = (currentSignIndex + 1) % 12; // Next sign
|
|
2529
|
+
|
|
2530
|
+
for (let i = 0; i < count; i++) {
|
|
2531
|
+
// Find the next ingress by checking positions in 1-hour increments
|
|
2532
|
+
let foundIngress = false;
|
|
2533
|
+
let ingressDate = null;
|
|
2534
|
+
let attempts = 0;
|
|
2535
|
+
|
|
2536
|
+
// Dynamic search strategy based on planet speed
|
|
2537
|
+
const verySlowPlanets = ['saturn', 'uranus', 'neptune', 'pluto'];
|
|
2538
|
+
const mediumSlowPlanets = ['jupiter'];
|
|
2539
|
+
const isVerySlowPlanet = verySlowPlanets.includes(planetName);
|
|
2540
|
+
const isMediumSlowPlanet = mediumSlowPlanets.includes(planetName);
|
|
2541
|
+
|
|
2542
|
+
// Determine time step based on planet speed
|
|
2543
|
+
const timeStepHours = isVerySlowPlanet ? 24 :
|
|
2544
|
+
isMediumSlowPlanet ? 6 :
|
|
2545
|
+
1; // Daily for very slow, 6-hourly for medium, hourly for fast
|
|
2546
|
+
|
|
2547
|
+
// Maximum search window (in days)
|
|
2548
|
+
// For very slow planets, we need to search much further ahead
|
|
2549
|
+
// Saturn: ~2.5 years per sign, so for multiple ingresses we need ~5-10 years
|
|
2550
|
+
// Jupiter: ~1 year per sign, so for multiple ingresses we need ~3-5 years
|
|
2551
|
+
// Uranus: ~7 years per sign, so we need ~15-20 years
|
|
2552
|
+
// Neptune: ~13.75 years per sign, so we need ~25-30 years
|
|
2553
|
+
// Pluto: ~20.7 years per sign, so we need ~35-40 years
|
|
2554
|
+
// For fast planets (Moon, Mercury, Venus, Mars, Sun), we need a reasonable window per ingress
|
|
2555
|
+
const fastPlanets = ['moon', 'mercury', 'venus', 'mars', 'sun'];
|
|
2556
|
+
const isFastPlanet = fastPlanets.includes(planetName);
|
|
2557
|
+
|
|
2558
|
+
const maxDays = isVerySlowPlanet ? 365 * 90 :
|
|
2559
|
+
isMediumSlowPlanet ? 365 * 90 :
|
|
2560
|
+
isFastPlanet ? 90 : // 30 days for fast planets (enough for multiple moon ingresses)
|
|
2561
|
+
365 * 90; // Default for other planets
|
|
2562
|
+
const maxAttempts = maxDays * 24 / timeStepHours;
|
|
2563
|
+
|
|
2564
|
+
while (!foundIngress && attempts < maxAttempts) {
|
|
2565
|
+
attempts++;
|
|
2566
|
+
currentDate.setHours(currentDate.getHours() + timeStepHours);
|
|
2567
|
+
|
|
2568
|
+
const testPlanetData = getAstrologicalData(planetName, {
|
|
2569
|
+
year: currentDate.getFullYear(),
|
|
2570
|
+
month: currentDate.getMonth() + 1,
|
|
2571
|
+
day: currentDate.getDate(),
|
|
2572
|
+
hour: currentDate.getHours(),
|
|
2573
|
+
minute: currentDate.getMinutes()
|
|
2574
|
+
});
|
|
2575
|
+
|
|
2576
|
+
const testSignIndex = signs.indexOf(testPlanetData.sign);
|
|
2577
|
+
|
|
2578
|
+
if (testSignIndex === targetSignIndex) {
|
|
2579
|
+
// Found the ingress! Now refine to get more precise time
|
|
2580
|
+
ingressDate = findExactPlanetIngressTime(planetName, currentDate, targetSignIndex, timezoneOffsetMinutes);
|
|
2581
|
+
foundIngress = true;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
if (foundIngress) {
|
|
2586
|
+
results.push({
|
|
2587
|
+
sign: signs[targetSignIndex],
|
|
2588
|
+
date: ingressDate,
|
|
2589
|
+
fromSign: signs[currentSignIndex]
|
|
2590
|
+
});
|
|
2591
|
+
|
|
2592
|
+
// Prepare for next ingress
|
|
2593
|
+
currentSignIndex = targetSignIndex;
|
|
2594
|
+
targetSignIndex = (currentSignIndex + 1) % 12;
|
|
2595
|
+
// Convert ingressDate object to Date object
|
|
2596
|
+
currentDate = new Date(ingressDate.year, ingressDate.month - 1, ingressDate.day, ingressDate.hour, ingressDate.minute);
|
|
2597
|
+
// For fast planets, start searching a few hours after the ingress to avoid missing the next one
|
|
2598
|
+
if (isFastPlanet) {
|
|
2599
|
+
currentDate.setHours(currentDate.getHours() + 6); // Start 6 hours after ingress
|
|
2600
|
+
}
|
|
2601
|
+
} else {
|
|
2602
|
+
const daysLimit = isVerySlowPlanet ? 365 * 40 :
|
|
2603
|
+
isMediumSlowPlanet ? 365 * 5 :
|
|
2604
|
+
isFastPlanet ? 30 :
|
|
2605
|
+
60;
|
|
2606
|
+
console.log(`Could not find ${planetName} ingress to ${signs[targetSignIndex]} within ${daysLimit} days`);
|
|
2607
|
+
if (isVerySlowPlanet) {
|
|
2608
|
+
console.log(`💡 Note: For very slow planets like ${planetName}, the ephemeris data may not extend far enough into the future.`);
|
|
2609
|
+
} else if (isMediumSlowPlanet) {
|
|
2610
|
+
console.log(`💡 Note: For medium-slow planets like ${planetName}, the ephemeris data may not extend far enough into the future.`);
|
|
2611
|
+
} else {
|
|
2612
|
+
console.log(`💡 Note: For ${planetName}, the requested number of ingresses may be too high.`);
|
|
2613
|
+
}
|
|
2614
|
+
console.log(`💡 Try requesting fewer ingresses or use a shorter time frame with the --d option.`);
|
|
2615
|
+
// Continue to next iteration instead of breaking the entire loop
|
|
2616
|
+
continue;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
return results;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
// Helper function to find exact planet ingress time using binary search
|
|
2624
|
+
function findExactPlanetIngressTime(planetName, approximateDate, targetSignIndex, timezoneOffsetMinutes) {
|
|
2625
|
+
const targetLongitude = targetSignIndex * 30; // Each sign is 30 degrees
|
|
2626
|
+
|
|
2627
|
+
// Convert approximate date to Julian Day
|
|
2628
|
+
const approxJulianDay = calculateJulianDayUTC({
|
|
2629
|
+
year: approximateDate.getFullYear(),
|
|
2630
|
+
month: approximateDate.getMonth() + 1,
|
|
2631
|
+
day: approximateDate.getDate(),
|
|
2632
|
+
hour: approximateDate.getHours(),
|
|
2633
|
+
minute: approximateDate.getMinutes()
|
|
2634
|
+
}, timezoneOffsetMinutes);
|
|
2635
|
+
|
|
2636
|
+
// Binary search parameters
|
|
2637
|
+
let low = approxJulianDay - 0.1; // 2.4 hours before
|
|
2638
|
+
let high = approxJulianDay + 0.1; // 2.4 hours after
|
|
2639
|
+
let precision = 0.0001; // About 1.44 minutes precision
|
|
2640
|
+
|
|
2641
|
+
// Perform binary search
|
|
2642
|
+
for (let i = 0; i < 50; i++) {
|
|
2643
|
+
const mid = (low + high) / 2;
|
|
2644
|
+
const result = swisseph.swe_calc_ut(mid, planets[planetName], swisseph.SEFLG_SWIEPH | swisseph.SEFLG_SPEED);
|
|
2645
|
+
|
|
2646
|
+
if (result.error) {
|
|
2647
|
+
// Fallback to Moshier
|
|
2648
|
+
const moshierResult = swisseph.swe_calc_ut(mid, planets[planetName], swisseph.SEFLG_MOSEPH | swisseph.SEFLG_SPEED);
|
|
2649
|
+
if (moshierResult.error) {
|
|
2650
|
+
console.error(`Error calculating ${planetName} position:`, moshierResult.error);
|
|
2651
|
+
break;
|
|
2652
|
+
}
|
|
2653
|
+
result.longitude = moshierResult.longitude;
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
if (result.longitude >= targetLongitude) {
|
|
2657
|
+
high = mid;
|
|
2658
|
+
} else {
|
|
2659
|
+
low = mid;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
if (high - low < precision) {
|
|
2663
|
+
break;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
// Convert Julian Day back to date components
|
|
2668
|
+
const finalJulianDay = (low + high) / 2;
|
|
2669
|
+
return convertJulianDayToDate(finalJulianDay, timezoneOffsetMinutes);
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// Helper function to convert Julian Day to date components
|
|
2673
|
+
function convertJulianDayToDate(julianDay, timezoneOffsetMinutes) {
|
|
2674
|
+
// This is a simplified conversion - for more accuracy, use a proper algorithm
|
|
2675
|
+
// We'll use the Swiss Ephemeris function if available
|
|
2676
|
+
try {
|
|
2677
|
+
const result = swisseph.swe_revjul(julianDay, swisseph.SE_GREG_CAL);
|
|
2678
|
+
|
|
2679
|
+
// Extract minutes from the decimal part of the hour
|
|
2680
|
+
const hour = Math.floor(result.hour);
|
|
2681
|
+
const minute = Math.round((result.hour - hour) * 60);
|
|
2682
|
+
|
|
2683
|
+
// Adjust for timezone
|
|
2684
|
+
const utcDate = new Date(Date.UTC(result.year, result.month - 1, result.day, hour, minute));
|
|
2685
|
+
utcDate.setMinutes(utcDate.getMinutes() + timezoneOffsetMinutes);
|
|
2686
|
+
|
|
2687
|
+
return {
|
|
2688
|
+
year: utcDate.getFullYear(),
|
|
2689
|
+
month: utcDate.getMonth() + 1,
|
|
2690
|
+
day: utcDate.getDate(),
|
|
2691
|
+
hour: utcDate.getHours(),
|
|
2692
|
+
minute: utcDate.getMinutes()
|
|
2693
|
+
};
|
|
2694
|
+
} catch (error) {
|
|
2695
|
+
console.error('Error converting Julian Day to date:', error);
|
|
2696
|
+
|
|
2697
|
+
// Fallback: return approximate date
|
|
2698
|
+
const baseDate = new Date();
|
|
2699
|
+
baseDate.setTime((julianDay - 2440587.5) * 86400000); // Convert Julian Day to milliseconds
|
|
2700
|
+
|
|
2701
|
+
return {
|
|
2702
|
+
year: baseDate.getFullYear(),
|
|
2703
|
+
month: baseDate.getMonth() + 1,
|
|
2704
|
+
day: baseDate.getDate(),
|
|
2705
|
+
hour: baseDate.getHours(),
|
|
2706
|
+
minute: baseDate.getMinutes()
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2494
2711
|
|
|
2495
2712
|
module.exports = {
|
|
2496
2713
|
calculateHouses,
|
|
@@ -2527,5 +2744,6 @@ module.exports = {
|
|
|
2527
2744
|
analyzeSignDistributionSignificance,
|
|
2528
2745
|
calculateAspectStatistics,
|
|
2529
2746
|
calculatePlanetComboAspects,
|
|
2530
|
-
showPlanetComboAspects
|
|
2747
|
+
showPlanetComboAspects,
|
|
2748
|
+
calculateNextPlanetIngress
|
|
2531
2749
|
};
|
package/src/cli/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
2
|
const { planets, signs } = require('../astrology/astrologyConstants');
|
|
3
3
|
const { showRetrogradePlanets } = require('../astrology/retrogradeService');
|
|
4
|
-
const { getCurrentTimeInTimezone, showAspectFigures, analyzeElementDistribution, getTimezoneOffset, calculateJulianDayUTC, calculateHouses, getAstrologicalData, getPlanetHouse, showPlanetAspects, calculatePlanetAspects, getAllActiveAspects, showAllActiveAspects, getBirthDataFromConfig, getPersonDataFromConfig, detectAspectFigures, calculatePersonalTransits, showPersonalTransitAspects, showCombinedAnalysis, calculatePersonalTransitAspects, determineAspectPhase, findExactAspectTime, findLastExactAspectTime, getAspectAngle, getFutureAspects, getPastAspects, analyzeCSVWithDatetime, analyzeHouseDistributionSignificance, analyzeAspectDistributionSignificance, analyzeSignDistributionSignificance, calculateAspectStatistics, calculatePlanetComboAspects, showPlanetComboAspects, getCriticalPlanets, getHouseSystemCode } = require('../astrology/astrologyService');
|
|
4
|
+
const { getCurrentTimeInTimezone, showAspectFigures, analyzeElementDistribution, getTimezoneOffset, calculateJulianDayUTC, calculateHouses, getAstrologicalData, getPlanetHouse, showPlanetAspects, calculatePlanetAspects, getAllActiveAspects, showAllActiveAspects, getBirthDataFromConfig, getPersonDataFromConfig, detectAspectFigures, calculatePersonalTransits, showPersonalTransitAspects, showCombinedAnalysis, calculatePersonalTransitAspects, determineAspectPhase, findExactAspectTime, findLastExactAspectTime, getAspectAngle, getFutureAspects, getPastAspects, analyzeCSVWithDatetime, analyzeHouseDistributionSignificance, analyzeAspectDistributionSignificance, analyzeSignDistributionSignificance, calculateAspectStatistics, calculatePlanetComboAspects, showPlanetComboAspects, getCriticalPlanets, getHouseSystemCode, calculateNextPlanetIngress } = require('../astrology/astrologyService');
|
|
5
5
|
const { performSetup, showConfigStatus, loadConfig, setAIModel, askAIModel, setPerson1, setPerson2, setPerson, getPersonData, listPeople, deletePerson } = require('../config/configService');
|
|
6
6
|
const { parseAppleHealthXML } = require('../health/healthService');
|
|
7
7
|
const { analyzeStepsByPlanetSign, analyzeStressByPlanetAspects, analyzePlanetAspectsForSleep } = require('../health/healthAnalysis');
|
|
@@ -11,6 +11,15 @@ const swisseph = require('swisseph');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
|
|
14
|
+
// GUI Server import
|
|
15
|
+
let guiServer = null;
|
|
16
|
+
try {
|
|
17
|
+
guiServer = require('../gui/server');
|
|
18
|
+
} catch (error) {
|
|
19
|
+
// GUI server not available (optional feature)
|
|
20
|
+
console.debug('GUI server not available');
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
const program = new Command();
|
|
15
24
|
|
|
16
25
|
// Helper function to check if person data should be used (including new options)
|
|
@@ -245,11 +254,40 @@ program
|
|
|
245
254
|
.option('--v <count>', 'Shows past aspects between two planets (Format: --v <count> planet1 aspectType planet2)')
|
|
246
255
|
.option('--z <count>', 'Shows future aspects between two planets (Format: --z <count> planet1 aspectType planet2)')
|
|
247
256
|
.option('--csv <filepath>', 'Analyzes a CSV file with ISO-Datetime values or Unix timestamps')
|
|
257
|
+
.option('--in [count]', 'Shows next planet ingress (entering new sign). Optional count for multiple ingresses')
|
|
258
|
+
.option('--gui', 'Launches the web interface for command history (port 37421)')
|
|
259
|
+
.option('--gui-port <port>', 'Specify custom port for GUI server')
|
|
248
260
|
.description('Shows astrological data for a planet')
|
|
249
261
|
.action(async (planetArg, planet2Arg, options) => {
|
|
250
262
|
// If planet2Arg is an object, it contains the options (commander behavior)
|
|
251
263
|
let planet2 = typeof planet2Arg === 'string' ? planet2Arg : null;
|
|
252
264
|
let actualOptions = typeof planet2Arg === 'object' ? planet2Arg : options;
|
|
265
|
+
|
|
266
|
+
// Handle GUI launch if --gui flag is specified
|
|
267
|
+
if (options.gui) {
|
|
268
|
+
if (!guiServer) {
|
|
269
|
+
console.error('❌ GUI server is not available. Please install required dependencies.');
|
|
270
|
+
console.error('💡 Run: npm install express sqlite3');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
await guiServer.initialize();
|
|
276
|
+
const port = options.guiPort ? parseInt(options.guiPort) : 37421;
|
|
277
|
+
await guiServer.start(port);
|
|
278
|
+
|
|
279
|
+
console.log(`\n🌐 GUI Server started successfully!`);
|
|
280
|
+
console.log(`📊 Open your browser and navigate to: http://localhost:${guiServer.getPort()}`);
|
|
281
|
+
console.log(`💡 Press Ctrl+C to stop the server and return to CLI`);
|
|
282
|
+
|
|
283
|
+
// Keep the process running
|
|
284
|
+
await new Promise(() => {});
|
|
285
|
+
return;
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.error('❌ Failed to start GUI server:', err.message);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
253
291
|
|
|
254
292
|
// Show critical planets if --c option is specified (no planet required)
|
|
255
293
|
if (actualOptions.c) {
|
|
@@ -975,6 +1013,105 @@ program
|
|
|
975
1013
|
return;
|
|
976
1014
|
}
|
|
977
1015
|
|
|
1016
|
+
// Show next planet ingress if --in option is specified
|
|
1017
|
+
if (options.in) {
|
|
1018
|
+
// Determine the planet to use (default to moon if no planet specified)
|
|
1019
|
+
const planetName = planetArg ? planetArg.toLowerCase() : 'moon';
|
|
1020
|
+
|
|
1021
|
+
// Validate planet name
|
|
1022
|
+
if (planets[planetName] === undefined) {
|
|
1023
|
+
console.error(`Error: Invalid planet '${planetName}'. Available planets: ${Object.keys(planets).join(', ')}`);
|
|
1024
|
+
process.exit(1);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Parse the count parameter (default to 1 if not specified)
|
|
1028
|
+
const count = options.in && typeof options.in === 'string' && options.in.trim() ? parseInt(options.in) : 1;
|
|
1029
|
+
|
|
1030
|
+
// Use custom date if specified
|
|
1031
|
+
let customDate = null;
|
|
1032
|
+
if (options.d) {
|
|
1033
|
+
// Try to parse the date as DD.MM.YYYY or DD.MM.YYYY HH:MM
|
|
1034
|
+
const dateRegex = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?$/;
|
|
1035
|
+
const match = options.d.match(dateRegex);
|
|
1036
|
+
|
|
1037
|
+
if (match) {
|
|
1038
|
+
const day = parseInt(match[1], 10);
|
|
1039
|
+
const month = parseInt(match[2], 10);
|
|
1040
|
+
const year = parseInt(match[3], 10);
|
|
1041
|
+
const hour = match[4] ? parseInt(match[4], 10) : 12; // Default: 12 o'clock
|
|
1042
|
+
const minute = match[5] ? parseInt(match[5], 10) : 0; // Default: 0 minutes
|
|
1043
|
+
|
|
1044
|
+
// Check if the date is valid
|
|
1045
|
+
const date = new Date(year, month - 1, day, hour, minute);
|
|
1046
|
+
if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
|
|
1047
|
+
customDate = {
|
|
1048
|
+
day: day,
|
|
1049
|
+
month: month,
|
|
1050
|
+
year: year,
|
|
1051
|
+
hour: hour,
|
|
1052
|
+
minute: minute
|
|
1053
|
+
};
|
|
1054
|
+
console.log(`Using custom date: ${day}.${month}.${year} ${hour}:${minute.toString().padStart(2, '0')}`);
|
|
1055
|
+
} else {
|
|
1056
|
+
console.error('Invalid date:', options.d);
|
|
1057
|
+
console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
|
|
1058
|
+
process.exit(1);
|
|
1059
|
+
}
|
|
1060
|
+
} else {
|
|
1061
|
+
console.error('Invalid date:', options.d);
|
|
1062
|
+
console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Calculate next planet ingress(es)
|
|
1068
|
+
const ingresses = calculateNextPlanetIngress(planetName, customDate, count);
|
|
1069
|
+
|
|
1070
|
+
if (ingresses.length === 0) {
|
|
1071
|
+
console.log(`No ${planetName} ingresses found.`);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const planetNameCapitalized = planetName.charAt(0).toUpperCase() + planetName.slice(1);
|
|
1076
|
+
console.log(`Next ${planetNameCapitalized} Ingress(es):`);
|
|
1077
|
+
console.log('================================================================================');
|
|
1078
|
+
console.log('| # | Date & Time | From Sign | To Sign |');
|
|
1079
|
+
console.log('================================================================================');
|
|
1080
|
+
|
|
1081
|
+
ingresses.forEach((ingress, index) => {
|
|
1082
|
+
const dateStr = `${ingress.date.day.toString().padStart(2, '0')}.${ingress.date.month.toString().padStart(2, '0')}.${ingress.date.year} ${ingress.date.hour.toString().padStart(2, '0')}:${ingress.date.minute.toString().padStart(2, '0')}`;
|
|
1083
|
+
const fromSign = ingress.fromSign.padEnd(12, ' ');
|
|
1084
|
+
const toSign = ingress.sign.padEnd(12, ' ');
|
|
1085
|
+
|
|
1086
|
+
console.log(`| ${(index + 1).toString().padEnd(2)} | ${dateStr.padEnd(21)} | ${fromSign} | ${toSign} |`);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
console.log('================================================================================');
|
|
1090
|
+
|
|
1091
|
+
if (count > 1) {
|
|
1092
|
+
console.log(`\nShowing ${ingresses.length} ${planetName} ingresses starting from ${customDate ? 'the specified date' : 'now'}.`);
|
|
1093
|
+
} else {
|
|
1094
|
+
console.log(`\nShowing the next ${planetName} ingress from ${customDate ? 'the specified date' : 'now'}.`);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Handle AI request
|
|
1098
|
+
if (options.p) {
|
|
1099
|
+
const aiData = {
|
|
1100
|
+
planet: planetName,
|
|
1101
|
+
sign: 'Multiple',
|
|
1102
|
+
degreeInSign: 'Multiple',
|
|
1103
|
+
dignity: `Next ${planetName} ingress: ${ingresses[0].sign} at ${ingresses[0].date.day}.${ingresses[0].date.month}.${ingresses[0].date.year} ${ingresses[0].date.hour}:${ingresses[0].date.minute.toString().padStart(2, '0')}`,
|
|
1104
|
+
element: 'Multiple',
|
|
1105
|
+
decan: 'Multiple',
|
|
1106
|
+
planetIngresses: ingresses
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
await handleAIRequest(options, aiData);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
978
1115
|
// For other options, a planet is required (except for --a or --s without planet)
|
|
979
1116
|
if (!planetArg && !options.a && !options.s) {
|
|
980
1117
|
console.error('Error: Planet is required for this operation.');
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const commandDatabase = require('./database');
|
|
2
|
+
|
|
3
|
+
class CommandLogger {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.isInitialized = false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async initialize() {
|
|
9
|
+
try {
|
|
10
|
+
await commandDatabase.initialize();
|
|
11
|
+
this.isInitialized = true;
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error('Failed to initialize command logger:', err);
|
|
14
|
+
this.isInitialized = false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async logCommand(command, arguments = '', category = 'general', tags = '') {
|
|
19
|
+
if (!this.isInitialized) {
|
|
20
|
+
await this.initialize();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!this.isInitialized) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Capture the command result by temporarily overriding console.log
|
|
29
|
+
const originalLog = console.log;
|
|
30
|
+
let capturedOutput = '';
|
|
31
|
+
|
|
32
|
+
console.log = (...args) => {
|
|
33
|
+
capturedOutput += args.join(' ') + '\n';
|
|
34
|
+
originalLog(...args);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Execute the command (this would be called from CLI)
|
|
38
|
+
// For now, we'll just save the command structure
|
|
39
|
+
|
|
40
|
+
const commandId = await commandDatabase.saveCommand(
|
|
41
|
+
command,
|
|
42
|
+
arguments,
|
|
43
|
+
'', // Result will be captured later
|
|
44
|
+
tags,
|
|
45
|
+
category
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Restore original console.log
|
|
49
|
+
console.log = originalLog;
|
|
50
|
+
|
|
51
|
+
return commandId;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error('Error logging command:', err);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async close() {
|
|
59
|
+
try {
|
|
60
|
+
await commandDatabase.close();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error('Error closing command logger:', err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = new CommandLogger();
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
class CommandDatabase {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.dbPath = path.join(__dirname, '..', '..', 'commands.db');
|
|
8
|
+
this.db = null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async initialize() {
|
|
12
|
+
// Create directory if it doesn't exist
|
|
13
|
+
const dbDir = path.dirname(this.dbPath);
|
|
14
|
+
if (!fs.existsSync(dbDir)) {
|
|
15
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.db = new sqlite3.Database(this.dbPath);
|
|
19
|
+
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
this.db.serialize(() => {
|
|
22
|
+
this.db.run(`
|
|
23
|
+
CREATE TABLE IF NOT EXISTS commands (
|
|
24
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
26
|
+
command TEXT NOT NULL,
|
|
27
|
+
arguments TEXT,
|
|
28
|
+
result TEXT,
|
|
29
|
+
tags TEXT,
|
|
30
|
+
category TEXT
|
|
31
|
+
)
|
|
32
|
+
`);
|
|
33
|
+
|
|
34
|
+
// Create indexes for better search performance
|
|
35
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_command ON commands(command)');
|
|
36
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_timestamp ON commands(timestamp)');
|
|
37
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_tags ON commands(tags)');
|
|
38
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_category ON commands(category)');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
resolve();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async saveCommand(command, args = '', result = '', tags = '', category = '') {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const stmt = this.db.prepare(
|
|
48
|
+
'INSERT INTO commands (command, arguments, result, tags, category) VALUES (?, ?, ?, ?, ?)'
|
|
49
|
+
);
|
|
50
|
+
stmt.run([command, args, result, tags, category], function(err) {
|
|
51
|
+
if (err) {
|
|
52
|
+
reject(err);
|
|
53
|
+
} else {
|
|
54
|
+
resolve(this.lastID);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
stmt.finalize();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async getAllCommands() {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
this.db.all('SELECT * FROM commands ORDER BY timestamp DESC', (err, rows) => {
|
|
64
|
+
if (err) {
|
|
65
|
+
reject(err);
|
|
66
|
+
} else {
|
|
67
|
+
resolve(rows);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async searchCommands(query) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const searchQuery = `%${query}%`;
|
|
76
|
+
this.db.all(`
|
|
77
|
+
SELECT * FROM commands
|
|
78
|
+
WHERE command LIKE ?
|
|
79
|
+
OR arguments LIKE ?
|
|
80
|
+
OR result LIKE ?
|
|
81
|
+
OR tags LIKE ?
|
|
82
|
+
OR category LIKE ?
|
|
83
|
+
ORDER BY timestamp DESC
|
|
84
|
+
`, [searchQuery, searchQuery, searchQuery, searchQuery, searchQuery], (err, rows) => {
|
|
85
|
+
if (err) {
|
|
86
|
+
reject(err);
|
|
87
|
+
} else {
|
|
88
|
+
resolve(rows);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async getCommandById(id) {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
this.db.get('SELECT * FROM commands WHERE id = ?', [id], (err, row) => {
|
|
97
|
+
if (err) {
|
|
98
|
+
reject(err);
|
|
99
|
+
} else {
|
|
100
|
+
resolve(row);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async getCommandsByCategory(category) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
this.db.all('SELECT * FROM commands WHERE category = ? ORDER BY timestamp DESC', [category], (err, rows) => {
|
|
109
|
+
if (err) {
|
|
110
|
+
reject(err);
|
|
111
|
+
} else {
|
|
112
|
+
resolve(rows);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async close() {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
if (this.db) {
|
|
121
|
+
this.db.close((err) => {
|
|
122
|
+
if (err) {
|
|
123
|
+
reject(err);
|
|
124
|
+
} else {
|
|
125
|
+
resolve();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
resolve();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = new CommandDatabase();
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Klio GUI</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
+
}
|
|
12
|
+
.command-input {
|
|
13
|
+
font-family: 'Courier New', monospace;
|
|
14
|
+
}
|
|
15
|
+
.output-area {
|
|
16
|
+
font-family: 'Courier New', monospace;
|
|
17
|
+
white-space: pre-wrap;
|
|
18
|
+
}
|
|
19
|
+
.nav-link {
|
|
20
|
+
transition: all 0.2s ease;
|
|
21
|
+
}
|
|
22
|
+
.nav-link:hover {
|
|
23
|
+
background-color: rgba(59, 130, 246, 0.1);
|
|
24
|
+
}
|
|
25
|
+
.nav-link.active {
|
|
26
|
+
background-color: rgba(59, 130, 246, 0.2);
|
|
27
|
+
border-left: 3px solid #3b82f6;
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body class="bg-gray-50 min-h-screen">
|
|
32
|
+
<div class="flex min-h-screen">
|
|
33
|
+
<!-- Sidebar Navigation -->
|
|
34
|
+
<div class="w-64 bg-white border-r border-gray-200 flex flex-col">
|
|
35
|
+
<div class="p-4 border-b border-gray-200">
|
|
36
|
+
<h1 class="text-xl font-bold text-gray-800">Klio GUI</h1>
|
|
37
|
+
<p class="text-sm text-gray-600">Web Interface</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<nav class="flex-1 p-4">
|
|
41
|
+
<ul class="space-y-2">
|
|
42
|
+
<li>
|
|
43
|
+
<a href="#" onclick="showPage('runner')" id="nav-runner" class="nav-link flex items-center p-3 rounded-lg text-gray-700 active">
|
|
44
|
+
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
45
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
|
46
|
+
</svg>
|
|
47
|
+
Command Runner
|
|
48
|
+
</a>
|
|
49
|
+
</li>
|
|
50
|
+
<li>
|
|
51
|
+
<a href="#" onclick="showPage('commands')" id="nav-commands" class="nav-link flex items-center p-3 rounded-lg text-gray-700">
|
|
52
|
+
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
53
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
54
|
+
</svg>
|
|
55
|
+
Commands Reference
|
|
56
|
+
</a>
|
|
57
|
+
</li>
|
|
58
|
+
<li>
|
|
59
|
+
<a href="#" onclick="showPage('history')" id="nav-history" class="nav-link flex items-center p-3 rounded-lg text-gray-700">
|
|
60
|
+
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
61
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
62
|
+
</svg>
|
|
63
|
+
Command History
|
|
64
|
+
</a>
|
|
65
|
+
</li>
|
|
66
|
+
</ul>
|
|
67
|
+
</nav>
|
|
68
|
+
|
|
69
|
+
<div class="p-4 border-t border-gray-200">
|
|
70
|
+
<div class="text-xs text-gray-500">
|
|
71
|
+
<p>Klio GUI v0.1</p>
|
|
72
|
+
<p>GUI Interface</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Main Content -->
|
|
78
|
+
<div class="flex-1 flex flex-col">
|
|
79
|
+
<!-- Header -->
|
|
80
|
+
<header class="bg-white border-b border-gray-200 p-4">
|
|
81
|
+
<div class="flex items-center justify-between">
|
|
82
|
+
<h1 class="text-lg font-semibold text-gray-800" id="page-title">Command Runner</h1>
|
|
83
|
+
</div>
|
|
84
|
+
</header>
|
|
85
|
+
|
|
86
|
+
<!-- Page Content -->
|
|
87
|
+
<main class="flex-1 overflow-y-auto p-6">
|
|
88
|
+
<!-- Command Runner Page -->
|
|
89
|
+
<div id="runner-page">
|
|
90
|
+
<div class="bg-white shadow-sm rounded-lg p-6 mb-6">
|
|
91
|
+
<div class="mb-4">
|
|
92
|
+
<label for="commandInput" class="block text-sm font-medium text-gray-700 mb-2">Command</label>
|
|
93
|
+
<div class="flex gap-2">
|
|
94
|
+
<input
|
|
95
|
+
type="text"
|
|
96
|
+
id="commandInput"
|
|
97
|
+
placeholder="e.g., moon --s"
|
|
98
|
+
class="flex-1 command-input px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
99
|
+
>
|
|
100
|
+
<button
|
|
101
|
+
onclick="runCommand()"
|
|
102
|
+
class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
103
|
+
>
|
|
104
|
+
Run
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="text-sm text-gray-600">
|
|
109
|
+
<p class="mb-2">Examples:</p>
|
|
110
|
+
<div class="flex flex-wrap gap-4 text-xs">
|
|
111
|
+
<button onclick="commandInput.value = '--v 3 saturn neptune'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">moon --s</button>
|
|
112
|
+
<button onclick="commandInput.value = '--rx --i'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--rx</button>
|
|
113
|
+
<button onclick="commandInput.value = '--c'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--c</button>
|
|
114
|
+
<button onclick="commandInput.value = 'mars venus --k'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">mars venus --k</button>
|
|
115
|
+
<button onclick="commandInput.value = '--el'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--el</button>
|
|
116
|
+
<button onclick="commandInput.value = '--s'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--el</button>
|
|
117
|
+
<button onclick="commandInput.value = '--a'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--el</button>
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="bg-white shadow-sm rounded-lg p-6">
|
|
125
|
+
<h2 class="text-lg font-semibold text-gray-800 mb-4">Output</h2>
|
|
126
|
+
<div id="outputArea" class="output-area bg-gray-100 p-4 rounded border min-h-[300px]">
|
|
127
|
+
Command output will appear here...
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<!-- Commands Reference Page -->
|
|
133
|
+
<div id="commands-page" class="hidden">
|
|
134
|
+
<div class="bg-white shadow-sm rounded-lg p-6">
|
|
135
|
+
<h2 class="text-lg font-semibold text-gray-800 mb-4">Available Commands</h2>
|
|
136
|
+
<div class="prose max-w-none text-sm">
|
|
137
|
+
<h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Basic Astrological Data</h3>
|
|
138
|
+
<ul class="list-disc pl-6 space-y-2">
|
|
139
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">klio [planet]</code> - Displays astrological data for a specific planet</li>
|
|
140
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">klio [planet] --hs koch</code> - Shows planet data with house and house system</li>
|
|
141
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">klio [planet] [planet2] --k</code> - Shows aspects between two planets</li>
|
|
142
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">klio --a</code> - Shows all current aspects</li>
|
|
143
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">klio --s</code> - Shows all planet positions</li>
|
|
144
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">klio --s --hs placidus</code> - Different house systems</li>
|
|
145
|
+
</ul>
|
|
146
|
+
|
|
147
|
+
<h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">House Systems</h3>
|
|
148
|
+
<ul class="list-disc pl-6 space-y-2">
|
|
149
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--hs <system></code> - Shows planet + house position (Placidus, Koch, Porphyry, Regiomontanus, Campanus, Equal, WholeSign, Gauquelin, Vehlow, Topocentric, Alcabitius, Morinus)</li>
|
|
150
|
+
</ul>
|
|
151
|
+
|
|
152
|
+
<h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Date and Time</h3>
|
|
153
|
+
<ul class="list-disc pl-6 space-y-2">
|
|
154
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--d <date></code> - Use a specific date (Format: DD.MM.YYYY or "DD.MM.YYYY HH:MM")</li>
|
|
155
|
+
</ul>
|
|
156
|
+
|
|
157
|
+
<h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Advanced Features</h3>
|
|
158
|
+
<ul class="list-disc pl-6 space-y-2">
|
|
159
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--rx</code> - Shows all retrograde or stationary planets</li>
|
|
160
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--c</code> - Shows all planets on critical degrees</li>
|
|
161
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--el</code> - Shows element distribution of planets</li>
|
|
162
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--af</code> - Shows active aspect figures</li>
|
|
163
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--tr</code> - Shows personal transits based on birth data</li>
|
|
164
|
+
</ul>
|
|
165
|
+
|
|
166
|
+
<h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Past and Future Aspects</h3>
|
|
167
|
+
<ul class="list-disc pl-6 space-y-2">
|
|
168
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--v <count> [planet1] [aspect-type] [planet2]</code> - Shows past aspects</li>
|
|
169
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--z <count> [planet1] [aspect-type] [planet2]</code> - Shows future aspects</li>
|
|
170
|
+
</ul>
|
|
171
|
+
|
|
172
|
+
<h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Configuration</h3>
|
|
173
|
+
<ul class="list-disc pl-6 space-y-2">
|
|
174
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--setup</code> - Setup for a default chart</li>
|
|
175
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--status</code> - Shows the stored configuration data</li>
|
|
176
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--i</code> - Uses birth data from setup</li>
|
|
177
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--person <id> <data></code> - Creates or updates any person</li>
|
|
178
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">--people</code> - Lists all saved persons</li>
|
|
179
|
+
</ul>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<!-- Command History Page -->
|
|
185
|
+
<div id="history-page" class="hidden">
|
|
186
|
+
<div class="bg-white shadow-sm rounded-lg p-6">
|
|
187
|
+
<h2 class="text-lg font-semibold text-gray-800 mb-4">Command History</h2>
|
|
188
|
+
<div id="historyContainer" class="space-y-2 max-h-[500px] overflow-y-auto pr-2"></div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</main>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div id="toast" class="fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-md shadow-lg opacity-0 transform translate-y-4 transition-all duration-300"></div>
|
|
196
|
+
|
|
197
|
+
<script>
|
|
198
|
+
let commandHistory = [];
|
|
199
|
+
const maxHistory = 50;
|
|
200
|
+
|
|
201
|
+
// DOM elements
|
|
202
|
+
const commandInput = document.getElementById('commandInput');
|
|
203
|
+
const historyContainer = document.getElementById('historyContainer');
|
|
204
|
+
const outputArea = document.getElementById('outputArea');
|
|
205
|
+
const toastElement = document.getElementById('toast');
|
|
206
|
+
|
|
207
|
+
// Initialize the app
|
|
208
|
+
function init() {
|
|
209
|
+
loadHistory();
|
|
210
|
+
|
|
211
|
+
// Add event listener for Enter key
|
|
212
|
+
commandInput.addEventListener('keypress', (e) => {
|
|
213
|
+
if (e.key === 'Enter') {
|
|
214
|
+
runCommand();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Focus on command input
|
|
219
|
+
commandInput.focus();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Page navigation function
|
|
223
|
+
function showPage(pageName) {
|
|
224
|
+
// Hide all pages
|
|
225
|
+
document.getElementById('runner-page').classList.add('hidden');
|
|
226
|
+
document.getElementById('commands-page').classList.add('hidden');
|
|
227
|
+
document.getElementById('history-page').classList.add('hidden');
|
|
228
|
+
|
|
229
|
+
// Remove active state from all nav items
|
|
230
|
+
document.getElementById('nav-runner').classList.remove('active');
|
|
231
|
+
document.getElementById('nav-commands').classList.remove('active');
|
|
232
|
+
document.getElementById('nav-history').classList.remove('active');
|
|
233
|
+
|
|
234
|
+
// Show selected page
|
|
235
|
+
if (pageName === 'runner') {
|
|
236
|
+
document.getElementById('runner-page').classList.remove('hidden');
|
|
237
|
+
document.getElementById('nav-runner').classList.add('active');
|
|
238
|
+
document.getElementById('page-title').textContent = 'Command Runner';
|
|
239
|
+
} else if (pageName === 'commands') {
|
|
240
|
+
document.getElementById('commands-page').classList.remove('hidden');
|
|
241
|
+
document.getElementById('nav-commands').classList.add('active');
|
|
242
|
+
document.getElementById('page-title').textContent = 'Commands Reference';
|
|
243
|
+
} else if (pageName === 'history') {
|
|
244
|
+
document.getElementById('history-page').classList.remove('hidden');
|
|
245
|
+
document.getElementById('nav-history').classList.add('active');
|
|
246
|
+
document.getElementById('page-title').textContent = 'Command History';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Load history from localStorage
|
|
251
|
+
function loadHistory() {
|
|
252
|
+
try {
|
|
253
|
+
const savedHistory = localStorage.getItem('astrocliHistory');
|
|
254
|
+
if (savedHistory) {
|
|
255
|
+
commandHistory = JSON.parse(savedHistory);
|
|
256
|
+
renderHistory();
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error('Error loading history:', error);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Save history to localStorage
|
|
264
|
+
function saveHistory() {
|
|
265
|
+
try {
|
|
266
|
+
localStorage.setItem('astrocliHistory', JSON.stringify(commandHistory));
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error('Error saving history:', error);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Add command to history
|
|
273
|
+
function addToHistory(command) {
|
|
274
|
+
// Remove duplicate if exists
|
|
275
|
+
commandHistory = commandHistory.filter(cmd => cmd !== command);
|
|
276
|
+
|
|
277
|
+
// Add new command to beginning
|
|
278
|
+
commandHistory.unshift(command);
|
|
279
|
+
|
|
280
|
+
// Limit history size
|
|
281
|
+
if (commandHistory.length > maxHistory) {
|
|
282
|
+
commandHistory = commandHistory.slice(0, maxHistory);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
saveHistory();
|
|
286
|
+
renderHistory();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Render history
|
|
290
|
+
function renderHistory() {
|
|
291
|
+
if (commandHistory.length === 0) {
|
|
292
|
+
historyContainer.innerHTML = '<div class="text-gray-500 text-sm">No command history yet</div>';
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let html = '<div class="space-y-1">';
|
|
297
|
+
commandHistory.forEach((cmd, index) => {
|
|
298
|
+
html += `<div
|
|
299
|
+
class="command-input px-3 py-2 bg-gray-100 rounded cursor-pointer hover:bg-gray-200 text-sm truncate"
|
|
300
|
+
title="${cmd}"
|
|
301
|
+
onclick="commandInput.value = '${cmd.replace(/'/g, "\\'")}'; showPage('runner'); commandInput.focus()"
|
|
302
|
+
>${cmd}</div>`;
|
|
303
|
+
});
|
|
304
|
+
html += '</div>';
|
|
305
|
+
|
|
306
|
+
historyContainer.innerHTML = html;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Run command
|
|
310
|
+
async function runCommand() {
|
|
311
|
+
const command = commandInput.value.trim();
|
|
312
|
+
|
|
313
|
+
if (!command) {
|
|
314
|
+
showToast('Please enter a command');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
// Show loading state
|
|
320
|
+
outputArea.textContent = 'Running command...\n\n';
|
|
321
|
+
|
|
322
|
+
// Add to history
|
|
323
|
+
addToHistory(command);
|
|
324
|
+
|
|
325
|
+
// Clear input for next command
|
|
326
|
+
commandInput.value = '';
|
|
327
|
+
|
|
328
|
+
// Run the command via API
|
|
329
|
+
const response = await fetch('/api/run-command', {
|
|
330
|
+
method: 'POST',
|
|
331
|
+
headers: {
|
|
332
|
+
'Content-Type': 'application/json'
|
|
333
|
+
},
|
|
334
|
+
body: JSON.stringify({ command })
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const result = await response.json();
|
|
338
|
+
|
|
339
|
+
if (response.ok) {
|
|
340
|
+
// Display the result with better formatting
|
|
341
|
+
if (result.output) {
|
|
342
|
+
const cleanedOutput = result.output
|
|
343
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
344
|
+
.trim();
|
|
345
|
+
outputArea.textContent = cleanedOutput;
|
|
346
|
+
} else {
|
|
347
|
+
outputArea.textContent = 'Command executed successfully';
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
let errorMessage = 'Error: ' + result.error;
|
|
351
|
+
if (result.details) {
|
|
352
|
+
errorMessage += '\n' + result.details;
|
|
353
|
+
}
|
|
354
|
+
outputArea.textContent = errorMessage;
|
|
355
|
+
showToast('Command failed');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error('Error running command:', error);
|
|
360
|
+
outputArea.textContent = 'Error: ' + error.message;
|
|
361
|
+
showToast('Command failed');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Show toast message
|
|
366
|
+
function showToast(message) {
|
|
367
|
+
toastElement.textContent = message;
|
|
368
|
+
toastElement.classList.remove('opacity-0', 'translate-y-4');
|
|
369
|
+
toastElement.classList.add('opacity-100', 'translate-y-0');
|
|
370
|
+
|
|
371
|
+
setTimeout(() => {
|
|
372
|
+
toastElement.classList.add('opacity-0', 'translate-y-4');
|
|
373
|
+
toastElement.classList.remove('opacity-100', 'translate-y-0');
|
|
374
|
+
}, 3000);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Initialize the app when DOM is loaded
|
|
378
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
379
|
+
</script>
|
|
380
|
+
</body>
|
|
381
|
+
</html>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const commandDatabase = require('../database');
|
|
4
|
+
const { exec } = require('child_process');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
// Initialize database
|
|
8
|
+
commandDatabase.initialize().catch(err => {
|
|
9
|
+
console.error('Failed to initialize database:', err);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Get all commands
|
|
13
|
+
router.get('/commands', async (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const commands = await commandDatabase.getAllCommands();
|
|
16
|
+
res.json(commands);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error('Error fetching commands:', err);
|
|
19
|
+
res.status(500).json({ error: 'Failed to fetch commands' });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Search commands
|
|
24
|
+
router.get('/commands/search', async (req, res) => {
|
|
25
|
+
const query = req.query.q || '';
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const results = await commandDatabase.searchCommands(query);
|
|
29
|
+
res.json(results);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error('Error searching commands:', err);
|
|
32
|
+
res.status(500).json({ error: 'Failed to search commands' });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Get specific command by ID
|
|
37
|
+
router.get('/commands/:id', async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const command = await commandDatabase.getCommandById(req.params.id);
|
|
40
|
+
if (command) {
|
|
41
|
+
res.json(command);
|
|
42
|
+
} else {
|
|
43
|
+
res.status(404).json({ error: 'Command not found' });
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error('Error fetching command:', err);
|
|
47
|
+
res.status(500).json({ error: 'Failed to fetch command' });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Get commands by category
|
|
52
|
+
router.get('/commands/category/:category', async (req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
const commands = await commandDatabase.getCommandsByCategory(req.params.category);
|
|
55
|
+
res.json(commands);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error('Error fetching commands by category:', err);
|
|
58
|
+
res.status(500).json({ error: 'Failed to fetch commands by category' });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Save a new command
|
|
63
|
+
router.post('/commands', async (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
const { command, args, result, tags, category } = req.body;
|
|
66
|
+
|
|
67
|
+
if (!command) {
|
|
68
|
+
return res.status(400).json({ error: 'Command is required' });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const commandId = await commandDatabase.saveCommand(command, args, result, tags, category);
|
|
72
|
+
res.status(201).json({ id: commandId, message: 'Command saved successfully' });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error('Error saving command:', err);
|
|
75
|
+
res.status(500).json({ error: 'Failed to save command' });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Health check endpoint
|
|
80
|
+
router.get('/health', (req, res) => {
|
|
81
|
+
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Run command endpoint
|
|
85
|
+
router.post('/run-command', async (req, res) => {
|
|
86
|
+
try {
|
|
87
|
+
const { command } = req.body;
|
|
88
|
+
|
|
89
|
+
if (!command) {
|
|
90
|
+
return res.status(400).json({ error: 'Command is required' });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Execute the command using child_process
|
|
94
|
+
const commandToRun = command.startsWith('klio ') ? command : `klio ${command}`;
|
|
95
|
+
const mainJsPath = path.join(__dirname, '..', '..', '..', 'src', 'main.js');
|
|
96
|
+
|
|
97
|
+
exec(`node ${mainJsPath} ${command}`, {
|
|
98
|
+
cwd: path.join(__dirname, '..', '..', '..'),
|
|
99
|
+
maxBuffer: 1024 * 1024 * 10 // 10MB buffer
|
|
100
|
+
}, (error, stdout, stderr) => {
|
|
101
|
+
if (error) {
|
|
102
|
+
console.error('Command execution error:', error);
|
|
103
|
+
console.error('Stderr:', stderr);
|
|
104
|
+
return res.status(500).json({
|
|
105
|
+
error: 'Command execution failed',
|
|
106
|
+
details: stderr || error.message,
|
|
107
|
+
command: commandToRun
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Save command to history
|
|
112
|
+
commandDatabase.saveCommand(
|
|
113
|
+
command,
|
|
114
|
+
'',
|
|
115
|
+
stdout,
|
|
116
|
+
'',
|
|
117
|
+
'executed'
|
|
118
|
+
).catch(err => {
|
|
119
|
+
console.error('Failed to save command to history:', err);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
res.json({
|
|
123
|
+
output: stdout,
|
|
124
|
+
command: command
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error('Error in run-command endpoint:', err);
|
|
130
|
+
res.status(500).json({ error: 'Failed to execute command' });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
module.exports = router;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const apiRouter = require('./routes/api');
|
|
4
|
+
const commandDatabase = require('./database');
|
|
5
|
+
|
|
6
|
+
class GUIServer {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.app = express();
|
|
9
|
+
this.server = null;
|
|
10
|
+
this.port = 37421; // Less common port
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async initialize() {
|
|
14
|
+
// Initialize database
|
|
15
|
+
await commandDatabase.initialize();
|
|
16
|
+
|
|
17
|
+
// Middleware
|
|
18
|
+
this.app.use(express.json());
|
|
19
|
+
this.app.use(express.urlencoded({ extended: true }));
|
|
20
|
+
|
|
21
|
+
// API routes
|
|
22
|
+
this.app.use('/api', apiRouter);
|
|
23
|
+
|
|
24
|
+
// Serve static files from public directory
|
|
25
|
+
this.app.use(express.static(path.join(__dirname, 'public')));
|
|
26
|
+
|
|
27
|
+
// Serve index.html for all other routes (for SPA behavior)
|
|
28
|
+
this.app.get('*', (req, res) => {
|
|
29
|
+
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Error handling middleware
|
|
33
|
+
this.app.use((err, req, res, next) => {
|
|
34
|
+
console.error('Server error:', err);
|
|
35
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
start(port = this.port) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
this.server = this.app.listen(port, () => {
|
|
42
|
+
console.log(`🌐 GUI Server running on http://localhost:${port}`);
|
|
43
|
+
console.log(`📊 Command history available at http://localhost:${port}`);
|
|
44
|
+
console.log('💡 Press Ctrl+C to stop the server');
|
|
45
|
+
resolve();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.server.on('error', (err) => {
|
|
49
|
+
if (err.code === 'EADDRINUSE') {
|
|
50
|
+
console.error(`Port ${port} is already in use. Trying port ${port + 1}...`);
|
|
51
|
+
this.start(port + 1).then(resolve).catch(reject);
|
|
52
|
+
} else {
|
|
53
|
+
reject(err);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async stop() {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
if (this.server) {
|
|
62
|
+
this.server.close(async () => {
|
|
63
|
+
await commandDatabase.close();
|
|
64
|
+
console.log('GUI Server stopped');
|
|
65
|
+
resolve();
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
resolve();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getPort() {
|
|
74
|
+
if (this.server) {
|
|
75
|
+
const address = this.server.address();
|
|
76
|
+
return address && address.port;
|
|
77
|
+
}
|
|
78
|
+
return this.port;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = new GUIServer();
|