klio 1.4.9 → 1.5.1

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.
@@ -0,0 +1,1197 @@
1
+ // CLI Service - Modular version of CLI functionality that can be imported
2
+ // This allows the GUI to run commands without spawning child processes
3
+
4
+ const { Command } = require('commander');
5
+ const { planets, signs } = require('../astrology/astrologyConstants');
6
+ const { showRetrogradePlanets } = require('../astrology/retrogradeService');
7
+ const { getCurrentTimeInTimezone, showAspectFigures, analyzeElementDistribution, getTimezoneOffset, calculateJulianDayUTC, calculateHouses, getAstrologicalData, getPlanetHouse, showPlanetAspects, calculatePlanetAspects, getAllActiveAspects, showAllActiveAspects, getBirthDataFromConfig, getPersonDataFromConfig, detectAspectFigures, calculatePersonalTransits, showPersonalTransitAspects, showCombinedAnalysis, calculatePersonalTransitAspects, determineAspectPhase, getAspectAngle, getFutureAspects, getPastAspects, analyzeCSVWithDatetime, analyzeHouseDistributionSignificance, analyzeAspectDistributionSignificance, analyzeSignDistributionSignificance, calculateAspectStatistics, calculatePlanetComboAspects, showPlanetComboAspects, getCriticalPlanets, getHouseSystemCode, calculateNextPlanetIngress, calculateAstrologicalAngles, longitudeToSignDegree, calculateTransitFrequency } = require('../astrology/astrologyService');
8
+ const { performSetup, showConfigStatus, loadConfig, setAIModel, askAIModel, setPerson1, setPerson2, setPerson, getPersonData, listPeople, deletePerson } = require('../config/configService');
9
+ const { parseAppleHealthXML } = require('../health/healthService');
10
+ const { analyzeStepsByPlanetSign, analyzeStressByPlanetAspects, analyzePlanetAspectsForSleep, analyzeLateNightAspects, analyzeAllNighterAspects } = require('../health/healthAnalysis');
11
+ const { getFileCreationDate, parseDateToComponents } = require('../utils/fileUtils');
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+
15
+ // Import chart generation and downloads folder utilities
16
+ let chartGenerator = null;
17
+ let downloadsFolder = null;
18
+ try {
19
+ chartGenerator = require('../utils/chartGenerator');
20
+ downloadsFolder = require('../utils/downloadsFolder');
21
+ } catch (error) {
22
+ console.debug('Chart generation utilities not available');
23
+ }
24
+
25
+ // Wikidata service import
26
+ let wikidataService = null;
27
+ try {
28
+ wikidataService = require('../wikidata/wikidataService');
29
+ } catch (error) {
30
+ // Wikidata service not available
31
+ console.debug('Wikidata service not available');
32
+ }
33
+
34
+ function getAspectTypeFullName(aspectType) {
35
+ const aspectNames = {
36
+ c: 'Conjunctions',
37
+ conjunction: 'Conjunctions',
38
+ o: 'Oppositions',
39
+ opposition: 'Oppositions',
40
+ s: 'Squares',
41
+ square: 'Squares',
42
+ t: 'Trines',
43
+ trine: 'Trines',
44
+ se: 'Sextiles',
45
+ sextile: 'Sextiles'
46
+ };
47
+
48
+ const key = aspectType ? aspectType.toLowerCase() : '';
49
+ return aspectNames[key] || aspectType;
50
+ }
51
+
52
+ function splitCommandArgs(commandString) {
53
+ const args = [];
54
+ let current = '';
55
+ let quoteChar = null;
56
+ let escaped = false;
57
+
58
+ for (let i = 0; i < commandString.length; i++) {
59
+ const char = commandString[i];
60
+
61
+ if (escaped) {
62
+ current += char;
63
+ escaped = false;
64
+ continue;
65
+ }
66
+
67
+ if (char === '\\') {
68
+ escaped = true;
69
+ continue;
70
+ }
71
+
72
+ if (quoteChar) {
73
+ if (char === quoteChar) {
74
+ quoteChar = null;
75
+ } else {
76
+ current += char;
77
+ }
78
+ continue;
79
+ }
80
+
81
+ if (char === '"' || char === "'") {
82
+ quoteChar = char;
83
+ continue;
84
+ }
85
+
86
+ if (/\s/.test(char)) {
87
+ if (current.length) {
88
+ args.push(current);
89
+ current = '';
90
+ }
91
+ continue;
92
+ }
93
+
94
+ current += char;
95
+ }
96
+
97
+ if (current.length) {
98
+ args.push(current);
99
+ }
100
+
101
+ return args;
102
+ }
103
+
104
+ class CLIService {
105
+ constructor() {
106
+ this.program = new Command();
107
+ this.setupProgram();
108
+ }
109
+
110
+ setupProgram() {
111
+ this.program
112
+ .name('klio')
113
+ .description('A simple CLI for astrological data')
114
+ .version('1.0.0');
115
+
116
+ this.program
117
+ .argument('[planet]', 'First planet for astrological calculations')
118
+ .argument('[planet2]', 'Second planet for aspect calculations')
119
+ .allowExcessArguments(true)
120
+ .option('--hs <system>', 'House system (Placidus, Koch, Porphyry, Regiomontanus, Campanus, Equal, WholeSign, Gauquelin, Vehlow, Topocentric, Alcabitius, Morinus) - shows planet + house position')
121
+ .option('--hl <system>', 'House list (Placidus, Koch, Porphyry, Regiomontanus, Campanus, Equal, WholeSign, Gauquelin, Vehlow, Topocentric, Alcabitius, Morinus) - shows only house table')
122
+ .option('--d <date>', 'Use a specific date for calculations (Format: DD.MM.YYYY or "DD.MM.YYYY HH:MM")')
123
+ .option('--a', 'Shows all aspects of the planet')
124
+ .option('--k', 'Shows aspects between any planet combinations')
125
+ .option('--s', 'Shows all planet positions in a table with signs, degrees and houses')
126
+ .option('--apple <filepath>', 'Analyzes Apple Health Export XML file')
127
+ .option('--hypothesis <type>', 'Specifies the hypothesis to test (e.g. "sleep-moon", "steps-moon", "stress-moon")')
128
+ .option('--sleep', 'Analyzes sleep patterns with moon aspects')
129
+ .option('--steps', 'Analyzes step patterns with moon signs')
130
+ .option('--stress', 'Analyzes stress patterns with moon aspects based on HRV')
131
+ .option('--night', 'Analyzes late night sleep patterns (after 2am) and common aspects')
132
+ .option('--night-all', 'Analyzes all-nighter sleep patterns (04:00 and 06:00 starts)')
133
+ .option('--setup', 'Configures location and birth data for personalized calculations')
134
+ .option('--ai <model>', 'Sets a specific AI model (e.g. "google/gemma-3n-e4b")')
135
+ .option('--system <prompt>', 'Sets a custom system prompt for all AI requests')
136
+ .option('--rx', 'Shows all retrograde or stationary planets')
137
+ .option('--c', 'Shows all planets on critical degrees')
138
+ .option('--status', 'Shows the stored configuration data')
139
+ .option('--i', 'Uses the birth data from setup for calculations (short form)')
140
+ .option('--p1', 'Uses the data from person 1 for calculations')
141
+ .option('--p2', 'Uses the data from person 2 for calculations')
142
+ .option('--p3', 'Uses the data from person 3 for calculations')
143
+ .option('--p4', 'Uses the data from person 4 for calculations')
144
+ .option('--p5', 'Uses the data from person 5 for calculations')
145
+ .option('--p6', 'Uses the data from person 6 for calculations')
146
+ .option('--with-person <id>', 'Uses the data from a specific person ID for calculations')
147
+ .option('--wp <id>', 'Uses the data from a specific person ID for calculations (short form)')
148
+ .option('--up <id>', 'Uses the data from a specific person ID for calculations (short form)')
149
+ .option('--person1 <data>', 'Creates or updates person 1 (Format: "Location, Country, DD.MM.YYYY HH:MM")')
150
+ .option('--person2 <data>', 'Creates or updates person 2 (Format: "Location, Country, DD.MM.YYYY HH:MM")')
151
+ .option('--person <id> <data>', 'Creates or updates any person (Format: "Location, Country, DD.MM.YYYY HH:MM")')
152
+ .option('--people', 'Lists all saved persons')
153
+ .option('--delete-person <id>', 'Deletes a person')
154
+ .option('--t <days>', 'Time limit (e.g. 7d, 30d, 90d, 14d)')
155
+ .option('--p <prompt>', 'Asks a question to the AI model (e.g. "Which careers suit this position?")')
156
+ .option('--el', 'Shows the element distribution of planets in a horizontal chart')
157
+ .option('--af', 'Shows active aspect figures like T-squares, Grand Trines, etc.')
158
+ .option('--tra', 'Shows personal transits based on birth data')
159
+ .option('--tr', 'Shows personal transit aspects (Transit → Radix) (short form)')
160
+ .option('--v <count>', 'Shows past aspects between two planets (Format: --v <count> planet1 aspectType planet2)')
161
+ .option('--z <count>', 'Shows future aspects between two planets (Format: --z <count> planet1 aspectType planet2)')
162
+ .option('--csv <filepath>', 'Analyzes a CSV file with ISO-Datetime values or Unix timestamps')
163
+ .option('--filter <column:value>', 'Filters CSV data by column:value (e.g., --filter "Item:coffee")')
164
+ .option('--title <title>', 'Title for the chart image (generates PNG image when provided)')
165
+ .option('--in [count]', 'Shows next planet ingress (entering new sign). Optional count for multiple ingresses')
166
+ .option('--wiki <occupation>', 'Fetches people from Wikidata by occupation and checks for specific aspects (Format: planet1 aspectType planet2 --wiki <occupation> [limit])')
167
+ .option('--gui', 'Launches the web interface for command history (port 37421)')
168
+ .option('--gui-port <port>', 'Specify custom port for GUI server')
169
+ .option('--o', 'Shows how often this transit occurs in the sky (frequency calculation)')
170
+ .description('Shows astrological data for a planet')
171
+ .action(this.handleCommand.bind(this));
172
+ }
173
+
174
+ // Helper function to check if person data should be used (including new options)
175
+ shouldUsePersonData(options) {
176
+ return options.i || options.p1 || options.p2 || options.withPerson || options.wp || options.up;
177
+ }
178
+
179
+ // Helper function to determine which person data should be used
180
+ getPersonDataToUse(options) {
181
+ const userId = options._userId;
182
+
183
+ // Check for direct person ID usage (e.g., --with-person john or --wp john or --up john)
184
+ if (options.withPerson) {
185
+ const personData = getPersonDataFromConfig(options.withPerson, userId);
186
+ if (!personData) {
187
+ throw new Error(`Person "${options.withPerson}" not found.`);
188
+ }
189
+ return { source: options.withPerson, data: personData };
190
+ }
191
+ if (options.wp) {
192
+ const personData = getPersonDataFromConfig(options.wp, userId);
193
+ if (!personData) {
194
+ throw new Error(`Person "${options.wp}" not found.`);
195
+ }
196
+ return { source: options.wp, data: personData };
197
+ }
198
+ if (options.up) {
199
+ const personData = getPersonDataFromConfig(options.up, userId);
200
+ if (!personData) {
201
+ throw new Error(`Person "${options.up}" not found.`);
202
+ }
203
+ return { source: options.up, data: personData };
204
+ }
205
+
206
+ // Dynamic person names (e.g., --p3, --p4, etc.)
207
+ for (const [key, value] of Object.entries(options)) {
208
+ if (key.startsWith('p') && key.length > 1 && !isNaN(key.substring(1))) {
209
+ const personId = key.substring(1); // Extract the number
210
+ return { source: key, data: getPersonDataFromConfig(personId, userId) };
211
+ }
212
+ }
213
+
214
+ if (options.p1) {
215
+ return { source: 'p1', data: getPersonDataFromConfig('p1', userId) };
216
+ } else if (options.p2) {
217
+ return { source: 'p2', data: getPersonDataFromConfig('p2', userId) };
218
+ } else if (options.p3) {
219
+ return { source: 'p3', data: getPersonDataFromConfig('p3', userId) };
220
+ } else if (options.p4) {
221
+ return { source: 'p4', data: getPersonDataFromConfig('p4', userId) };
222
+ } else if (options.p5) {
223
+ return { source: 'p5', data: getPersonDataFromConfig('p5', userId) };
224
+ } else if (options.p6) {
225
+ return { source: 'p6', data: getPersonDataFromConfig('p6', userId) };
226
+ } else if (options.i) {
227
+ return { source: 'birth', data: getPersonDataFromConfig('birth', userId) };
228
+ }
229
+ return { source: null, data: null };
230
+ }
231
+
232
+ // Helper function to check if birth data should be used (for backward compatibility only)
233
+ shouldUseBirthData(options) {
234
+ return options.i;
235
+ }
236
+
237
+ // Function to calculate the Julian Day in UTC
238
+ getJulianDay(customDate = null, useBirthData = false) {
239
+ let timezoneOffsetMinutes = 0;
240
+ const config = loadConfig();
241
+
242
+ if (customDate) {
243
+ // Calculate the timezone offset
244
+ if (useBirthData && config && config.birthData && config.birthData.location) {
245
+ timezoneOffsetMinutes = getTimezoneOffset(customDate, config.birthData.location.timezone || 'Europe/Zurich');
246
+ } else if (config && config.currentLocation && config.currentLocation.timezone) {
247
+ timezoneOffsetMinutes = getTimezoneOffset(customDate, config.currentLocation.timezone);
248
+ } else {
249
+ timezoneOffsetMinutes = -new Date().getTimezoneOffset();
250
+ }
251
+
252
+ return calculateJulianDayUTC(customDate, timezoneOffsetMinutes);
253
+ } else {
254
+ // Use the current time in the configured timezone
255
+ let now;
256
+ let timezone = 'Europe/Zurich'; // Default
257
+
258
+ if (config && config.currentLocation && config.currentLocation.timezone) {
259
+ timezone = config.currentLocation.timezone;
260
+ try {
261
+ const moment = require('moment-timezone');
262
+ now = moment().tz(timezone);
263
+ console.log(`Using timezone: ${timezone}`);
264
+ } catch (error) {
265
+ console.log('Could not load moment-timezone, using local time');
266
+ now = require('moment')();
267
+ }
268
+ } else {
269
+ now = require('moment')();
270
+ console.log('No timezone configuration found, using local system time');
271
+ }
272
+
273
+ timezoneOffsetMinutes = now.utcOffset();
274
+ return calculateJulianDayUTC({
275
+ year: now.year(),
276
+ month: now.month() + 1,
277
+ day: now.date(),
278
+ hour: now.hours(),
279
+ minute: now.minutes()
280
+ }, timezoneOffsetMinutes);
281
+ }
282
+ }
283
+
284
+ // Main command handler
285
+ async handleCommand(planetArg, planet2Arg, options, userId = null) {
286
+ // If planet2Arg is an object, it contains the options (commander behavior)
287
+ let planet2 = typeof planet2Arg === 'string' ? planet2Arg : null;
288
+ // Fix: Check for null explicitly since typeof null === 'object' in JavaScript
289
+ let actualOptions = (typeof planet2Arg === 'object' && planet2Arg !== null) ? planet2Arg : options;
290
+
291
+ // Ensure actualOptions is an object
292
+ if (!actualOptions) {
293
+ actualOptions = {};
294
+ }
295
+
296
+ // Store userId in the options for use in config functions
297
+ actualOptions._userId = userId;
298
+
299
+ // Capture output for GUI use
300
+ const output = [];
301
+ const originalConsoleLog = console.log;
302
+ const originalConsoleError = console.error;
303
+
304
+ // Override console methods to capture output
305
+ const captureOnly = process.env.KLIO_CAPTURE_ONLY === '1';
306
+
307
+ console.log = (...args) => {
308
+ if (!captureOnly) {
309
+ originalConsoleLog(...args);
310
+ }
311
+ output.push(args.join(' '));
312
+ };
313
+
314
+ console.error = (...args) => {
315
+ if (!captureOnly) {
316
+ originalConsoleError(...args);
317
+ }
318
+ output.push(args.join(' '));
319
+ };
320
+
321
+ try {
322
+ // Handle GUI launch if --gui flag is specified
323
+ if (options.gui) {
324
+ throw new Error('GUI mode cannot be launched through CLI service');
325
+ }
326
+
327
+ // Show critical planets if --c option is specified (no planet required)
328
+ if (actualOptions.c) {
329
+ // Use person data if --p1, --p2 or --i option is specified
330
+ let customDate = null;
331
+ let personSource = null;
332
+ const personDataToUse = this.getPersonDataToUse(actualOptions);
333
+
334
+ if (personDataToUse.data) {
335
+ customDate = personDataToUse.data;
336
+ personSource = personDataToUse.source;
337
+
338
+ if (personSource === 'p1') {
339
+ console.log(`Using Person 1 data: ${customDate.day}.${customDate.month}.${customDate.year} ${customDate.hour}:${customDate.minute.toString().padStart(2, '0')}`);
340
+ } else if (personSource === 'p2') {
341
+ console.log(`Using Person 2 data: ${customDate.day}.${customDate.month}.${customDate.year} ${customDate.hour}:${customDate.minute.toString().padStart(2, '0')}`);
342
+ } else if (personSource.startsWith('p')) {
343
+ const personId = personSource.substring(1);
344
+ console.log(`Using Person ${personId} data: ${customDate.day}.${customDate.month}.${customDate.year} ${customDate.hour}:${customDate.minute.toString().padStart(2, '0')}`);
345
+ } else {
346
+ console.log(`Using birth data: ${customDate.day}.${customDate.month}.${customDate.year} ${customDate.hour}:${customDate.minute.toString().padStart(2, '0')}`);
347
+ }
348
+
349
+ if (customDate.location) {
350
+ console.log(`Location: ${customDate.location.name}, ${customDate.location.country}`);
351
+ }
352
+ }
353
+
354
+ // Use custom date if specified (overrides person data)
355
+ if (actualOptions.d) {
356
+ // Try to parse the date as DD.MM.YYYY or DD.MM.YYYY HH:MM
357
+ const dateRegex = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?$/;
358
+ const match = actualOptions.d.match(dateRegex);
359
+
360
+ if (match) {
361
+ const day = parseInt(match[1], 10);
362
+ const month = parseInt(match[2], 10);
363
+ const year = parseInt(match[3], 10);
364
+ const hour = match[4] ? parseInt(match[4], 10) : 12; // Default: 12 o'clock
365
+ const minute = match[5] ? parseInt(match[5], 10) : 0; // Default: 0 minutes
366
+
367
+ // Check if the date is valid
368
+ const date = new Date(year, month - 1, day, hour, minute);
369
+ if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
370
+ customDate = {
371
+ day: day,
372
+ month: month,
373
+ year: year,
374
+ hour: hour,
375
+ minute: minute
376
+ };
377
+ console.log(`Using custom date: ${day}.${month}.${year} ${hour}:${minute.toString().padStart(2, '0')}`);
378
+ } else {
379
+ console.error('Invalid date:', actualOptions.d);
380
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
381
+ return { success: false, output: output.join('\n') };
382
+ }
383
+ } else {
384
+ console.error('Invalid date:', actualOptions.d);
385
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
386
+ return { success: false, output: output.join('\n') };
387
+ }
388
+ }
389
+
390
+ // If no custom date is specified, use current date
391
+ if (!customDate) {
392
+ const currentTime = getCurrentTimeInTimezone();
393
+ customDate = {
394
+ day: currentTime.day,
395
+ month: currentTime.month,
396
+ year: currentTime.year,
397
+ hour: currentTime.hour,
398
+ minute: currentTime.minute
399
+ };
400
+ }
401
+
402
+ const criticalPlanets = getCriticalPlanets(customDate);
403
+
404
+ if (criticalPlanets.length === 0) {
405
+ console.log('No planets on critical degrees found.');
406
+ return { success: true, output: output.join('\n') };
407
+ }
408
+
409
+ console.log('================================================================================================================');
410
+ console.log('| Planet | Sign | Degree | Type | Interpretation |');
411
+ console.log('================================================================================================================');
412
+
413
+ criticalPlanets.forEach(planet => {
414
+ const planetName = planet.name.charAt(0).toUpperCase() + planet.name.slice(1);
415
+ const sign = planet.sign.padEnd(10, ' ');
416
+ const degree = planet.degree.padEnd(5, ' ');
417
+ const criticalType = planet.criticalType.padEnd(20, ' ');
418
+ const interpretation = planet.interpretation.padEnd(46, ' ');
419
+ console.log(`| ${planetName.padEnd(8)} | ${sign} | ${degree} | ${criticalType} | ${interpretation} |`);
420
+ });
421
+
422
+ console.log('================================================================================================================');
423
+ if (this.shouldUseBirthData(actualOptions)) {
424
+ console.log('\nThis analysis is based on your birth chart.');
425
+ } else {
426
+ console.log('\nThis analysis is based on the current planet position.');
427
+ }
428
+
429
+ return { success: true, output: output.join('\n') };
430
+ }
431
+
432
+ // Show retrograde planets if --rx option is specified (no planet required)
433
+ if (actualOptions.rx) {
434
+ await showRetrogradePlanets(this.shouldUseBirthData(actualOptions));
435
+ return { success: true, output: output.join('\n') };
436
+ }
437
+
438
+ // Show element distribution if --el option is specified (no planet required)
439
+ if (actualOptions.el) {
440
+ // Use person data if --p1, --p2 or --i option is specified
441
+ let customDate = null;
442
+ let useBirthData = false;
443
+ let personSource = null;
444
+
445
+ const personDataToUse = this.getPersonDataToUse(actualOptions);
446
+ if (personDataToUse.data) {
447
+ customDate = personDataToUse.data;
448
+ personSource = personDataToUse.source;
449
+ useBirthData = personSource === 'birth'; // Only true for birth data
450
+
451
+ if (personSource === 'p1') {
452
+ console.log(`Using Person 1 data: ${customDate.day}.${customDate.month}.${customDate.year} ${customDate.hour}:${customDate.minute.toString().padStart(2, '0')}`);
453
+ } else if (personSource === 'p2') {
454
+ console.log(`Using Person 2 data: ${customDate.day}.${customDate.month}.${customDate.year} ${customDate.hour}:${customDate.minute.toString().padStart(2, '0')}`);
455
+ } else if (personSource.startsWith('p')) {
456
+ const personId = personSource.substring(1);
457
+ console.log(`Using Person ${personId} data: ${customDate.day}.${customDate.month}.${customDate.year} ${customDate.hour}:${customDate.minute.toString().padStart(2, '0')}`);
458
+ } else {
459
+ console.log(`Using birth data: ${customDate.day}.${customDate.month}.${customDate.year} ${customDate.hour}:${customDate.minute.toString().padStart(2, '0')}`);
460
+ }
461
+
462
+ if (customDate.location) {
463
+ console.log(`Location: ${customDate.location.name}, ${customDate.location.country}`);
464
+ }
465
+ }
466
+ // Use custom date if specified (overrides --p1, --p2 and --i options)
467
+ if (actualOptions.d) {
468
+ // Try to parse the date as DD.MM.YYYY or DD.MM.YYYY HH:MM
469
+ const dateRegex = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?$/;
470
+ const match = actualOptions.d.match(dateRegex);
471
+
472
+ if (match) {
473
+ const day = parseInt(match[1], 10);
474
+ const month = parseInt(match[2], 10);
475
+ const year = parseInt(match[3], 10);
476
+ const hour = match[4] ? parseInt(match[4], 10) : 12; // Default: 12 o'clock
477
+ const minute = match[5] ? parseInt(match[5], 10) : 0; // Default: 0 minutes
478
+
479
+ // Check if the date is valid
480
+ const date = new Date(year, month - 1, day, hour, minute);
481
+ if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
482
+ customDate = {
483
+ day: day,
484
+ month: month,
485
+ year: year,
486
+ hour: hour,
487
+ minute: minute
488
+ };
489
+ console.log(`Using custom date: ${day}.${month}.${year} ${hour}:${minute.toString().padStart(2, '0')}`);
490
+ } else {
491
+ console.error('Invalid date:', actualOptions.d);
492
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
493
+ return { success: false, output: output.join('\n') };
494
+ }
495
+ } else {
496
+ console.error('Invalid date:', actualOptions.d);
497
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
498
+ return { success: false, output: output.join('\n') };
499
+ }
500
+ }
501
+
502
+ // If no custom date is specified, use current date
503
+ if (!customDate) {
504
+ const currentTime = getCurrentTimeInTimezone();
505
+ customDate = {
506
+ day: currentTime.day,
507
+ month: currentTime.month,
508
+ year: currentTime.year,
509
+ hour: currentTime.hour,
510
+ minute: currentTime.minute
511
+ };
512
+ }
513
+
514
+ // Analyze and show element distribution
515
+ analyzeElementDistribution(customDate, useBirthData);
516
+
517
+ return { success: true, output: output.join('\n') };
518
+ }
519
+
520
+ // Show personal transits if --tra option is specified (no planet required)
521
+ if (actualOptions.tra) {
522
+ // Person data is required for transits
523
+ const personDataToUse = this.getPersonDataToUse(actualOptions);
524
+ if (!personDataToUse.data) {
525
+ console.error('Error: Personal transits require person data. Please run --setup or create a person with --person1/--person2.');
526
+ return { success: false, output: output.join('\n') };
527
+ }
528
+
529
+ const birthData = personDataToUse.data;
530
+
531
+ // Use custom date if specified
532
+ let transitDate = null;
533
+ if (actualOptions.d) {
534
+ // Try to parse the date as DD.MM.YYYY or DD.MM.YYYY HH:MM
535
+ const dateRegex = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?$/;
536
+ const match = actualOptions.d.match(dateRegex);
537
+
538
+ if (match) {
539
+ const day = parseInt(match[1], 10);
540
+ const month = parseInt(match[2], 10);
541
+ const year = parseInt(match[3], 10);
542
+ const hour = match[4] ? parseInt(match[4], 10) : 12; // Default: 12 o'clock
543
+ const minute = match[5] ? parseInt(match[5], 10) : 0; // Default: 0 minutes
544
+
545
+ // Check if the date is valid
546
+ const date = new Date(year, month - 1, day, hour, minute);
547
+ if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
548
+ transitDate = {
549
+ day: day,
550
+ month: month,
551
+ year: year,
552
+ hour: hour,
553
+ minute: minute
554
+ };
555
+ console.log(`Using transit date: ${day}.${month}.${year} ${hour}:${minute.toString().padStart(2, '0')}`);
556
+ } else {
557
+ console.error('Invalid date:', actualOptions.d);
558
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
559
+ return { success: false, output: output.join('\n') };
560
+ }
561
+ } else {
562
+ console.error('Invalid date:', actualOptions.d);
563
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
564
+ return { success: false, output: output.join('\n') };
565
+ }
566
+ }
567
+
568
+ // Calculate personal transits
569
+ const transitData = await calculatePersonalTransits(transitDate, birthData);
570
+
571
+ if (!transitData) {
572
+ console.error('Error calculating personal transits.');
573
+ return { success: false, output: output.join('\n') };
574
+ }
575
+
576
+ // Show person data
577
+ if (personDataToUse.source === 'p1') {
578
+ console.log(`Person 1 data: ${birthData.day}.${birthData.month}.${birthData.year} ${birthData.hour}:${birthData.minute.toString().padStart(2, '0')}`);
579
+ } else if (personDataToUse.source === 'p2') {
580
+ console.log(`Person 2 data: ${birthData.day}.${birthData.month}.${birthData.year} ${birthData.hour}:${birthData.minute.toString().padStart(2, '0')}`);
581
+ } else {
582
+ console.log(`Birth data: ${birthData.day}.${birthData.month}.${birthData.year} ${birthData.hour}:${birthData.minute.toString().padStart(2, '0')}`);
583
+ }
584
+ if (birthData.location) {
585
+ console.log(`Location: ${birthData.location.name}, ${birthData.location.country}`);
586
+ }
587
+
588
+ // Show transit date
589
+ const transitDateDisplay = transitDate || getCurrentTimeInTimezone();
590
+ console.log(`Transit date: ${transitDateDisplay.day}.${transitDateDisplay.month}.${transitDateDisplay.year} ${transitDateDisplay.hour}:${transitDateDisplay.minute.toString().padStart(2, '0')}`);
591
+
592
+ console.log('\nPersonal transits (House System: Koch, based on birth ASC):');
593
+ console.log('================================================================================');
594
+ console.log('| Planet | Transit Position | Birth Position | Transit House |');
595
+ console.log('================================================================================');
596
+
597
+ // Show all planets with their transit and birth positions
598
+ for (const [name, data] of Object.entries(transitData.planets)) {
599
+ const planetNameFormatted = name.charAt(0).toUpperCase() + name.slice(1);
600
+ const transitPos = `${data.sign} ${data.degreeInSign}°`;
601
+ const birthPos = `${data.birthPosition.sign} ${data.birthPosition.degreeInSign}°`;
602
+ const transitHouse = data.transitHouse;
603
+
604
+ console.log(`| ${planetNameFormatted.padEnd(11)} | ${transitPos.padEnd(22)} | ${birthPos.padEnd(22)} | ${transitHouse.toString().padEnd(11)} |`);
605
+ }
606
+
607
+ console.log('================================================================================');
608
+
609
+ return { success: true, output: output.join('\n') };
610
+ }
611
+
612
+ // Show configuration status if --status option is specified (no planet required)
613
+ if (actualOptions.status) {
614
+ showConfigStatus(userId);
615
+ return { success: true, output: output.join('\n') };
616
+ }
617
+
618
+ // Perform setup if --setup option is specified (no planet required)
619
+ if (actualOptions.setup) {
620
+ await performSetup(userId);
621
+ return { success: true, output: output.join('\n') };
622
+ }
623
+
624
+ // Create/update person 1 if --person1 option is specified (no planet required)
625
+ if (actualOptions.person1) {
626
+ await setPerson1(actualOptions.person1, userId);
627
+ return { success: true, output: output.join('\n') };
628
+ }
629
+
630
+ // Create/update person 2 if --person2 option is specified (no planet required)
631
+ if (actualOptions.person2) {
632
+ await setPerson2(actualOptions.person2, userId);
633
+ return { success: true, output: output.join('\n') };
634
+ }
635
+
636
+ // List people if --people option is specified (no planet required)
637
+ if (actualOptions.people) {
638
+ listPeople(userId);
639
+ return { success: true, output: output.join('\n') };
640
+ }
641
+
642
+ // Set AI model if --ai option is specified (no planet required)
643
+ if (actualOptions.ai) {
644
+ setAIModel(actualOptions.ai, userId);
645
+ return { success: true, output: output.join('\n') };
646
+ }
647
+
648
+ // Set system prompt if --system option is specified (no planet required)
649
+ if (actualOptions.system) {
650
+ const { setSystemPrompt } = require('../config/configService');
651
+ setSystemPrompt(actualOptions.system, userId);
652
+ return { success: true, output: output.join('\n') };
653
+ }
654
+
655
+ // Delete person if --delete-person option is specified (no planet required)
656
+ if (actualOptions.deletePerson) {
657
+ deletePerson(actualOptions.deletePerson, userId);
658
+ return { success: true, output: output.join('\n') };
659
+ }
660
+
661
+ // Create/update any person if --person option is specified (no planet required)
662
+ if (actualOptions.person) {
663
+ // The person option expects both id and data, but we need to handle the parsing
664
+ // For now, we'll assume the format is "id data" and split on first space
665
+ const personParts = actualOptions.person.split(' ');
666
+ const personId = personParts[0];
667
+ const personData = personParts.slice(1).join(' ');
668
+
669
+ if (personId && personData) {
670
+ const { setPerson } = require('../config/configService');
671
+ await setPerson(personId, personData, userId);
672
+ } else {
673
+ console.error('Invalid format for --person option. Expected: --person <id> "Location, Country, DD.MM.YYYY HH:MM"');
674
+ }
675
+ return { success: true, output: output.join('\n') };
676
+ }
677
+
678
+ // Show aspects if --a option is specified
679
+ if (actualOptions.a) {
680
+ // Use person data if --p1, --p2 or --i option is specified
681
+ let customDate = null;
682
+ let useBirthData = false;
683
+ let personSource = null;
684
+
685
+ const personDataToUse = this.getPersonDataToUse(actualOptions);
686
+ if (personDataToUse.data) {
687
+ customDate = personDataToUse.data;
688
+ personSource = personDataToUse.source;
689
+ useBirthData = personSource === 'birth'; // Only true for birth data
690
+
691
+ if (personSource === 'p1') {
692
+ console.log(`Using Person 1 data: ${customDate.day}.${customDate.month}.${customDate.year} ${customDate.hour}:${customDate.minute.toString().padStart(2, '0')}`);
693
+ } else if (personSource === 'p2') {
694
+ console.log(`Using Person 2 data: ${customDate.day}.${customDate.month}.${customDate.year} ${customDate.hour}:${customDate.minute.toString().padStart(2, '0')}`);
695
+ } else if (personSource.startsWith('p')) {
696
+ const personId = personSource.substring(1);
697
+ console.log(`Using Person ${personId} data: ${customDate.day}.${customDate.month}.${customDate.year} ${customDate.hour}:${customDate.minute.toString().padStart(2, '0')}`);
698
+ } else {
699
+ console.log(`Using birth data: ${customDate.day}.${customDate.month}.${customDate.year} ${customDate.hour}:${customDate.minute.toString().padStart(2, '0')}`);
700
+ }
701
+
702
+ if (customDate.location) {
703
+ console.log(`Location: ${customDate.location.name}, ${customDate.location.country}`);
704
+ }
705
+ }
706
+ // Use custom date if specified (overrides --p1, --p2 and --i options)
707
+ if (actualOptions.d) {
708
+ // Try to parse the date as DD.MM.YYYY or DD.MM.YYYY HH:MM
709
+ const dateRegex = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?$/;
710
+ const match = actualOptions.d.match(dateRegex);
711
+
712
+ if (match) {
713
+ const day = parseInt(match[1], 10);
714
+ const month = parseInt(match[2], 10);
715
+ const year = parseInt(match[3], 10);
716
+ const hour = match[4] ? parseInt(match[4], 10) : 12; // Default: 12 o'clock
717
+ const minute = match[5] ? parseInt(match[5], 10) : 0; // Default: 0 minutes
718
+
719
+ // Check if the date is valid
720
+ const date = new Date(year, month - 1, day, hour, minute);
721
+ if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
722
+ customDate = {
723
+ day: day,
724
+ month: month,
725
+ year: year,
726
+ hour: hour,
727
+ minute: minute
728
+ };
729
+ console.log(`Using custom date: ${day}.${month}.${year} ${hour}:${minute.toString().padStart(2, '0')}`);
730
+ } else {
731
+ console.error('Invalid date:', actualOptions.d);
732
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
733
+ return { success: false, output: output.join('\n') };
734
+ }
735
+ } else {
736
+ console.error('Invalid date:', actualOptions.d);
737
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
738
+ return { success: false, output: output.join('\n') };
739
+ }
740
+ }
741
+
742
+ // If no custom date is specified, use current date
743
+ if (!customDate) {
744
+ const currentTime = getCurrentTimeInTimezone();
745
+ customDate = {
746
+ day: currentTime.day,
747
+ month: currentTime.month,
748
+ year: currentTime.year,
749
+ hour: currentTime.hour,
750
+ minute: currentTime.minute
751
+ };
752
+ }
753
+
754
+ // If no planet is specified, show all active aspects for all planets
755
+ if (!planetArg) {
756
+ showAllActiveAspects(customDate, useBirthData);
757
+
758
+ return { success: true, output: output.join('\n') };
759
+ }
760
+
761
+ // Show aspects for the specific planet (with Huber orbs)
762
+ showPlanetAspects(planetArg, customDate, useBirthData, true);
763
+
764
+ return { success: true, output: output.join('\n') };
765
+ }
766
+
767
+ // Show table view of all planet positions if --s option is specified
768
+ if (actualOptions.s) {
769
+ // Use person data for house calculation if --p1, --p2 or --i option is specified
770
+ let customDate = null;
771
+ let birthData = null;
772
+ let houseSystem = actualOptions.hs ? actualOptions.hs.toLowerCase() : 'koch';
773
+ let personSource = null;
774
+ let useNatalPositions = false;
775
+
776
+ // Check if person data should be used for house calculation
777
+ const personDataToUse = this.getPersonDataToUse(actualOptions);
778
+ if (personDataToUse.data) {
779
+ birthData = personDataToUse.data;
780
+ personSource = personDataToUse.source;
781
+
782
+ if (personSource === 'p1') {
783
+ console.log(`Using Person 1 data for house calculation: ${birthData.day}.${birthData.month}.${birthData.year} ${birthData.hour}:${birthData.minute.toString().padStart(2, '0')}`);
784
+ } else if (personSource === 'p2') {
785
+ console.log(`Using Person 2 data for house calculation: ${birthData.day}.${birthData.month}.${birthData.year} ${birthData.hour}:${birthData.minute.toString().padStart(2, '0')}`);
786
+ } else if (personSource.startsWith('p')) {
787
+ const personId = personSource.substring(1);
788
+ console.log(`Using Person ${personId} data for house calculation: ${birthData.day}.${birthData.month}.${birthData.year} ${birthData.hour}:${birthData.minute.toString().padStart(2, '0')}`);
789
+ } else {
790
+ console.log(`Using birth data for house calculation: ${birthData.day}.${birthData.month}.${birthData.year} ${birthData.hour}:${birthData.minute.toString().padStart(2, '0')}`);
791
+ }
792
+
793
+ if (birthData.location) {
794
+ console.log(`Location: ${birthData.location.name}, ${birthData.location.country}`);
795
+ }
796
+ } else {
797
+ console.log('No person data found. Using current location for house calculation.');
798
+ }
799
+
800
+ // Use custom date if specified (for planet positions)
801
+ if (actualOptions.d) {
802
+ // Try to parse the date as DD.MM.YYYY or DD.MM.YYYY HH:MM
803
+ const dateRegex = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?$/;
804
+ const match = actualOptions.d.match(dateRegex);
805
+
806
+ if (match) {
807
+ const day = parseInt(match[1], 10);
808
+ const month = parseInt(match[2], 10);
809
+ const year = parseInt(match[3], 10);
810
+ const hour = match[4] ? parseInt(match[4], 10) : 12; // Default: 12 o'clock
811
+ const minute = match[5] ? parseInt(match[5], 10) : 0; // Default: 0 minutes
812
+
813
+ // Check if the date is valid
814
+ const date = new Date(year, month - 1, day, hour, minute);
815
+ if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
816
+ customDate = {
817
+ day: day,
818
+ month: month,
819
+ year: year,
820
+ hour: hour,
821
+ minute: minute
822
+ };
823
+ console.log(`Using custom date: ${day}.${month}.${year} ${hour}:${minute.toString().padStart(2, '0')}`);
824
+ } else {
825
+ console.error('Invalid date:', actualOptions.d);
826
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
827
+ return { success: false, output: output.join('\n') };
828
+ }
829
+ } else {
830
+ console.error('Invalid date:', actualOptions.d);
831
+ console.error('Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
832
+ return { success: false, output: output.join('\n') };
833
+ }
834
+ }
835
+
836
+ // If no custom date is specified, use current date
837
+ if (!customDate) {
838
+ const currentTime = getCurrentTimeInTimezone();
839
+ customDate = {
840
+ day: currentTime.day,
841
+ month: currentTime.month,
842
+ year: currentTime.year,
843
+ hour: currentTime.hour,
844
+ minute: currentTime.minute
845
+ };
846
+ }
847
+
848
+ // Check if we should use natal positions (when --i is used without custom date)
849
+ if (this.shouldUseBirthData(actualOptions) && !actualOptions.d) {
850
+ useNatalPositions = true;
851
+ // customDate is already set to birth data from personDataToUse
852
+ console.log('Using natal positions from birth data.');
853
+ }
854
+
855
+ const config = loadConfig();
856
+
857
+ // Function to calculate Julian Day in UTC for planet positions
858
+ const getJulianDayUTCForPlanets = () => {
859
+ let timezoneOffsetMinutes = 0;
860
+
861
+ if (config && config.currentLocation && config.currentLocation.timezone) {
862
+ timezoneOffsetMinutes = getTimezoneOffset(customDate, config.currentLocation.timezone);
863
+ } else {
864
+ timezoneOffsetMinutes = -new Date().getTimezoneOffset();
865
+ }
866
+
867
+ return calculateJulianDayUTC(customDate, timezoneOffsetMinutes);
868
+ };
869
+
870
+ // Function to calculate Julian Day in UTC for house calculation
871
+ const getJulianDayUTCForHouses = () => {
872
+ let timezoneOffsetMinutes = 0;
873
+
874
+ if (birthData && birthData.location) {
875
+ timezoneOffsetMinutes = getTimezoneOffset(birthData, birthData.location.timezone || 'Europe/Zurich');
876
+ } else if (config && config.currentLocation && config.currentLocation.timezone) {
877
+ timezoneOffsetMinutes = getTimezoneOffset(customDate, config.currentLocation.timezone);
878
+ } else {
879
+ timezoneOffsetMinutes = -new Date().getTimezoneOffset();
880
+ }
881
+
882
+ return calculateJulianDayUTC(birthData ? birthData : customDate, timezoneOffsetMinutes);
883
+ };
884
+
885
+ // Calculate houses if house system is specified
886
+ let houses = null;
887
+ if (houseSystem) {
888
+ try {
889
+ const julianDayForHouses = getJulianDayUTCForHouses();
890
+ const useBirthLocation = birthData !== null;
891
+
892
+ houses = await calculateHouses(julianDayForHouses, getHouseSystemCode(houseSystem), useBirthLocation);
893
+ } catch (error) {
894
+ console.error('Error calculating houses:', error);
895
+ }
896
+ }
897
+
898
+ // Show table view of all planet positions
899
+ console.log('Planet positions:');
900
+ console.log('================================================================================');
901
+ console.log('| Planet | Sign | Deg | House | Dignity | Element |');
902
+ console.log('================================================================================');
903
+
904
+ // Calculate positions of all planets
905
+ for (const [name, planetId] of Object.entries(planets)) {
906
+ const data = getAstrologicalData(name, customDate);
907
+
908
+ // Determine the house if houses were calculated
909
+ let house = 'N/A';
910
+ if (houses) {
911
+ const planetLongitude = parseFloat(data.degreeInSign) + (signs.indexOf(data.sign) * 30);
912
+ house = getPlanetHouse(planetLongitude, houses.house);
913
+ }
914
+
915
+ const planetNameFormatted = name.charAt(0).toUpperCase() + name.slice(1);
916
+ const signFormatted = data.sign.padEnd(10, ' ');
917
+ const degreeFormatted = data.degreeInSign.padEnd(5, ' ');
918
+ const houseFormatted = house.toString().padEnd(4, ' ');
919
+ const dignityFormatted = data.dignity.padEnd(12, ' ');
920
+ const elementFormatted = data.element.padEnd(9, ' ');
921
+
922
+ console.log(`| ${planetNameFormatted.padEnd(11)} | ${signFormatted} | ${degreeFormatted}° | ${houseFormatted} | ${dignityFormatted} | ${elementFormatted} |`);
923
+ }
924
+
925
+ console.log('================================================================================');
926
+
927
+ // Show astrological angles if houses were calculated
928
+ if (houses) {
929
+ console.log('\nAstrological Angles:');
930
+ console.log('================================================================================');
931
+ console.log('| Angle | Sign | Degree |');
932
+ console.log('================================================================================');
933
+
934
+ const angles = calculateAstrologicalAngles(houses.house);
935
+
936
+ // ASC (Ascendant)
937
+ const ascData = longitudeToSignDegree(angles.asc);
938
+ console.log(`| ASC | ${ascData.sign.padEnd(10)} | ${ascData.degree.padEnd(6)}° |`);
939
+
940
+ // MC (Medium Coeli/Midheaven)
941
+ const mcData = longitudeToSignDegree(angles.mc);
942
+ console.log(`| MC | ${mcData.sign.padEnd(10)} | ${mcData.degree.padEnd(6)}° |`);
943
+
944
+ // DESC (Descendant)
945
+ const descData = longitudeToSignDegree(angles.desc);
946
+ console.log(`| DESC | ${descData.sign.padEnd(10)} | ${descData.degree.padEnd(6)}° |`);
947
+
948
+ // IC (Imum Coeli)
949
+ const icData = longitudeToSignDegree(angles.ic);
950
+ console.log(`| IC | ${icData.sign.padEnd(10)} | ${icData.degree.padEnd(6)}° |`);
951
+
952
+ console.log('================================================================================');
953
+ }
954
+
955
+ if (birthData) {
956
+ if (useNatalPositions) {
957
+ console.log('\nThis analysis shows your natal planet positions.');
958
+ console.log('The houses are based on your birth ASC (natal chart).');
959
+ } else {
960
+ console.log('\nThis analysis shows current planet positions in your birth houses.');
961
+ console.log('The houses are based on your birth ASC (personal transits).');
962
+ }
963
+ } else {
964
+ console.log('\nThis analysis is based on the current planet position.');
965
+ }
966
+
967
+ if (houses) {
968
+ console.log(`House system: ${houseSystem.charAt(0).toUpperCase() + houseSystem.slice(1)}`);
969
+ }
970
+
971
+ return { success: true, output: output.join('\n') };
972
+ }
973
+
974
+ if (actualOptions.v || actualOptions.z) {
975
+ const positionalArgs = actualOptions._positionalArgs || [];
976
+ let planet1 = positionalArgs[0] ? positionalArgs[0].toLowerCase() : null;
977
+ let aspectType = positionalArgs[1] ? positionalArgs[1].toLowerCase() : null;
978
+ let planet2 = positionalArgs[2] ? positionalArgs[2].toLowerCase() : null;
979
+ const countValue = actualOptions.v || actualOptions.z;
980
+ const count = parseInt(countValue, 10);
981
+
982
+ if (!planet1 || !aspectType || !planet2) {
983
+ const rawArgs = actualOptions._args || [];
984
+ const optionFlag = actualOptions.v ? '--v' : '--z';
985
+ const optionIndex = rawArgs.findIndex(arg => arg === optionFlag);
986
+ if (optionIndex !== -1 && rawArgs.length >= optionIndex + 5) {
987
+ planet1 = rawArgs[optionIndex + 2] ? rawArgs[optionIndex + 2].toLowerCase() : null;
988
+ aspectType = rawArgs[optionIndex + 3] ? rawArgs[optionIndex + 3].toLowerCase() : null;
989
+ planet2 = rawArgs[optionIndex + 4] ? rawArgs[optionIndex + 4].toLowerCase() : null;
990
+ }
991
+ }
992
+
993
+ if (!planet1 || !aspectType || !planet2) {
994
+ console.error('Error: Past/future aspects require two planets and an aspect type.');
995
+ console.error('Format: --v <count> planet1 aspectType planet2');
996
+ console.error('Example: --v 3 saturn c neptune');
997
+ return { success: false, output: output.join('\n') };
998
+ }
999
+
1000
+ if (!Number.isFinite(count) || count <= 0) {
1001
+ console.error('Error: Please provide a valid count for --v/--z.');
1002
+ return { success: false, output: output.join('\n') };
1003
+ }
1004
+
1005
+ if (!planets[planet1] || !planets[planet2]) {
1006
+ console.error('Error: Invalid planets.');
1007
+ console.error('Available planets:', Object.keys(planets).join(', '));
1008
+ return { success: false, output: output.join('\n') };
1009
+ }
1010
+
1011
+ const targetAngle = getAspectAngle(aspectType);
1012
+ if (targetAngle === null) {
1013
+ console.error('Error: Invalid aspect type.');
1014
+ console.error('Available aspect types: c, o, s, t, se (conjunction, opposition, square, trine, sextile)');
1015
+ return { success: false, output: output.join('\n') };
1016
+ }
1017
+
1018
+ const aspectTypeFull = getAspectTypeFullName(aspectType);
1019
+ const aspects = actualOptions.v
1020
+ ? getPastAspects(planet1, planet2, aspectType, count)
1021
+ : getFutureAspects(planet1, planet2, aspectType, count);
1022
+ const heading = actualOptions.v ? 'Last' : 'Next';
1023
+
1024
+ console.log(`${heading} ${count} ${aspectTypeFull} between ${planet1} and ${planet2}:`);
1025
+ console.log('================================================================================');
1026
+ console.log('| Date | Planet 1 Position | Planet 2 Position |');
1027
+ console.log('================================================================================');
1028
+
1029
+ if (aspects.length === 0) {
1030
+ console.log(actualOptions.v ? 'No past aspects found.' : 'No future aspects found.');
1031
+ } else {
1032
+ aspects.forEach(aspect => {
1033
+ const dateStr = `${String(aspect.date.day).padStart(2, '0')}.${String(aspect.date.month).padStart(2, '0')}.${aspect.date.year}`;
1034
+ const planet1Pos = `${aspect.planet1Position.padEnd(22, ' ')}`;
1035
+ const planet2Pos = `${aspect.planet2Position.padEnd(22, ' ')}`;
1036
+ console.log(`| ${dateStr.padEnd(18)} | ${planet1Pos} | ${planet2Pos} |`);
1037
+ });
1038
+ }
1039
+
1040
+ console.log('================================================================================');
1041
+ console.log(`Found: ${aspects.length} exact ${aspectTypeFull}`);
1042
+ return { success: true, output: output.join('\n') };
1043
+ }
1044
+
1045
+ // Show normal planet info as default
1046
+ const planet = planetArg ? planetArg.toLowerCase() : 'moon';
1047
+ const data = getAstrologicalData(planet, null);
1048
+
1049
+ console.log(`\nAstrological data for ${data.planet}:`);
1050
+ console.log(`Sign: ${data.sign}`);
1051
+ console.log(`Degree in sign: ${data.degreeInSign}°`);
1052
+ console.log(`Dignity: ${data.dignity}`);
1053
+ console.log(`Element: ${data.element}`);
1054
+ console.log(`Decan: ${data.decan}\n`);
1055
+
1056
+ return { success: true, output: output.join('\n') };
1057
+
1058
+ } catch (error) {
1059
+ console.error('Error executing command:', error.message);
1060
+ return { success: false, output: output.join('\n') };
1061
+ } finally {
1062
+ // Restore original console methods
1063
+ console.log = originalConsoleLog;
1064
+ console.error = originalConsoleError;
1065
+ }
1066
+ }
1067
+
1068
+ // Method to execute a command programmatically
1069
+ async executeCommand(commandString, userId = null) {
1070
+ // Parse the command string into arguments
1071
+ const args = splitCommandArgs(commandString);
1072
+
1073
+ if (args.includes('--help') || args.includes('-h')) {
1074
+ const helpText = [
1075
+ 'Usage: [planet] [options]',
1076
+ '',
1077
+ 'Options:',
1078
+ ' --help, -h Show this help',
1079
+ ' --c Critical degrees',
1080
+ ' --rx Retrograde planets',
1081
+ ' --el Element distribution',
1082
+ ' --s All planet positions',
1083
+ ' --status Show config',
1084
+ ' --people List people',
1085
+ ' --setup Configure via GUI',
1086
+ ' --i Use birth data',
1087
+ ' --hs <system> House system',
1088
+ ' --hl <system> House list',
1089
+ ' --d <date> Specific date (DD.MM.YYYY or "DD.MM.YYYY HH:MM")',
1090
+ ' --a Show aspects for planet',
1091
+ ' --k Show aspects between planets',
1092
+ ' --af Show active aspect figures',
1093
+ ' --tr Show personal transit aspects',
1094
+ ' --v <count> Past aspects (planet1 aspectType planet2)',
1095
+ ' --z <count> Future aspects (planet1 aspectType planet2)',
1096
+ ' --in [count] Next planet ingress',
1097
+ ' --o Transit frequency'
1098
+ ].join('\n');
1099
+ return { success: true, output: helpText };
1100
+ }
1101
+
1102
+ // Parse options manually since Commander.js doesn't easily allow capturing output
1103
+ const options = {};
1104
+ const positionalArgs = [];
1105
+ const supportedOptions = new Set();
1106
+ for (const option of this.program.options) {
1107
+ if (option.long) {
1108
+ supportedOptions.add(option.long.replace(/^--/, ''));
1109
+ }
1110
+ if (option.short) {
1111
+ supportedOptions.add(option.short.replace(/^-/, ''));
1112
+ }
1113
+ }
1114
+
1115
+ for (let i = 0; i < args.length; i++) {
1116
+ const arg = args[i];
1117
+
1118
+ if (arg.startsWith('--')) {
1119
+ // Handle options
1120
+ const optionName = arg.substring(2);
1121
+
1122
+ if (!supportedOptions.has(optionName)) {
1123
+ return { success: false, output: `Unknown option: --${optionName}` };
1124
+ }
1125
+
1126
+ // Special handling for --v option which requires multiple arguments
1127
+ if (optionName === 'v') {
1128
+ // --v requires: count, planet1, aspectType, planet2
1129
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
1130
+ options[optionName] = args[i + 1]; // This is the count
1131
+
1132
+ // Now we need to extract the next 3 arguments: planet1, aspectType, planet2
1133
+ if (i + 2 < args.length && !args[i + 2].startsWith('--')) {
1134
+ options['vPlanet1'] = args[i + 2];
1135
+ }
1136
+ if (i + 3 < args.length && !args[i + 3].startsWith('--')) {
1137
+ options['vAspectType'] = args[i + 3];
1138
+ }
1139
+ if (i + 4 < args.length && !args[i + 4].startsWith('--')) {
1140
+ options['vPlanet2'] = args[i + 4];
1141
+ }
1142
+
1143
+ i += 4; // Skip the next 4 arguments (count + 3 planets/aspects)
1144
+ } else {
1145
+ options[optionName] = true;
1146
+ }
1147
+ }
1148
+ // Special handling for --z option which requires multiple arguments
1149
+ else if (optionName === 'z') {
1150
+ // --z requires: count, planet1, aspectType, planet2
1151
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
1152
+ options[optionName] = args[i + 1]; // This is the count
1153
+
1154
+ // Now we need to extract the next 3 arguments: planet1, aspectType, planet2
1155
+ if (i + 2 < args.length && !args[i + 2].startsWith('--')) {
1156
+ options['zPlanet1'] = args[i + 2];
1157
+ }
1158
+ if (i + 3 < args.length && !args[i + 3].startsWith('--')) {
1159
+ options['zAspectType'] = args[i + 3];
1160
+ }
1161
+ if (i + 4 < args.length && !args[i + 4].startsWith('--')) {
1162
+ options['zPlanet2'] = args[i + 4];
1163
+ }
1164
+
1165
+ i += 4; // Skip the next 4 arguments (count + 3 planets/aspects)
1166
+ } else {
1167
+ options[optionName] = true;
1168
+ }
1169
+ }
1170
+ // Handle other options with values
1171
+ else if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
1172
+ options[optionName] = args[i + 1];
1173
+ i++; // Skip the next argument as it's the value
1174
+ } else {
1175
+ options[optionName] = true;
1176
+ }
1177
+ } else if (arg.startsWith('-')) {
1178
+ // Handle short options (we'll skip these for simplicity)
1179
+ // This is a simplified parser for the GUI use case
1180
+ } else {
1181
+ // Handle positional arguments
1182
+ positionalArgs.push(arg);
1183
+ }
1184
+ }
1185
+
1186
+ // Create a new instance to handle the command
1187
+ const tempService = new CLIService();
1188
+ const planetArg = positionalArgs[0] || null;
1189
+ const planet2Arg = positionalArgs[1] || null;
1190
+ options._positionalArgs = positionalArgs;
1191
+ options._args = args;
1192
+
1193
+ return tempService.handleCommand(planetArg, planet2Arg, options, userId);
1194
+ }
1195
+ }
1196
+
1197
+ module.exports = new CLIService();