klio 1.4.8 → 1.4.9
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 +4 -2
- package/package.json +1 -1
- package/src/astrology/astrologyService.js +51 -2
- package/src/cli/cli.js +70 -2
- package/src/utils/chartGenerator.js +129 -0
- package/src/utils/downloadsFolder.js +70 -0
package/README.md
CHANGED
|
@@ -97,8 +97,10 @@ It's possible to analyze a csv with a column of either ISO date time or unix tim
|
|
|
97
97
|
|
|
98
98
|
- **Show house and sign distribution of the datetime column**: `klio [planet] --csv <file-path>`
|
|
99
99
|
- **Show aspect type distribution between two planets:** `klio [planet1] [planet2] --csv <file-path> --a`
|
|
100
|
+
- **Filter CSV data by column value:** `klio [planet] --csv <file-path> --filter "column:value"` (e.g., `--filter "Item:coffee"`)
|
|
101
|
+
- **Creating a bar chart**: Create a bar chart and save the image to the downloads' folder. The image shows the aspect distribution of your csv datetime values: `klio moon sun --csv /home/user/Downloads/coffee.csv --filter "Item:cookie" --a --title "Eaten cookies during sun-moon aspects"`
|
|
100
102
|
|
|
101
|
-
The command also returns a Chi-Square.
|
|
103
|
+
- The command also returns a Chi-Square.
|
|
102
104
|
|
|
103
105
|
### Adding different charts
|
|
104
106
|
|
|
@@ -109,7 +111,7 @@ The command also returns a Chi-Square.
|
|
|
109
111
|
- `--people` - Lists all saved persons
|
|
110
112
|
- `--delete-person <id>` - Deletes a person
|
|
111
113
|
|
|
112
|
-
Then, instead using `--i` for the commands from above you can use `--wp <id>`
|
|
114
|
+
Then, instead using `--i` for the commands from above you can use `--wp <id>` or `--wp john`
|
|
113
115
|
|
|
114
116
|
### Advanced Features
|
|
115
117
|
|
package/package.json
CHANGED
|
@@ -1900,7 +1900,39 @@ function analyzeSignDistributionSignificance(signDistribution) {
|
|
|
1900
1900
|
};
|
|
1901
1901
|
}
|
|
1902
1902
|
|
|
1903
|
-
|
|
1903
|
+
// Helper function to parse filter criteria
|
|
1904
|
+
function parseFilterCriteria(filterString) {
|
|
1905
|
+
if (!filterString) return null;
|
|
1906
|
+
|
|
1907
|
+
const parts = filterString.split(':');
|
|
1908
|
+
if (parts.length !== 2) {
|
|
1909
|
+
console.warn('Invalid filter format. Expected "column:value"');
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
return {
|
|
1914
|
+
column: parts[0].trim(),
|
|
1915
|
+
value: parts[1].trim()
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// Helper function to apply filter to records
|
|
1920
|
+
function applyFilter(records, filterCriteria) {
|
|
1921
|
+
if (!filterCriteria || !records || records.length === 0) return records;
|
|
1922
|
+
|
|
1923
|
+
const parsedCriteria = typeof filterCriteria === 'string'
|
|
1924
|
+
? parseFilterCriteria(filterCriteria)
|
|
1925
|
+
: filterCriteria;
|
|
1926
|
+
|
|
1927
|
+
if (!parsedCriteria) return records;
|
|
1928
|
+
|
|
1929
|
+
return records.filter(record => {
|
|
1930
|
+
const recordValue = record[parsedCriteria.column];
|
|
1931
|
+
return recordValue && recordValue.toString() === parsedCriteria.value;
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'koch', analyzeAspects = false, partnerPlanet = null, filterCriteria = null) {
|
|
1904
1936
|
return new Promise(async (resolve, reject) => {
|
|
1905
1937
|
const results = [];
|
|
1906
1938
|
let pendingOperations = 0;
|
|
@@ -1967,8 +1999,11 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
|
|
|
1967
1999
|
skip_empty_lines: true
|
|
1968
2000
|
});
|
|
1969
2001
|
|
|
2002
|
+
// Apply filter if specified
|
|
2003
|
+
const filteredRecords = filterCriteria ? applyFilter(records, filterCriteria) : records;
|
|
2004
|
+
|
|
1970
2005
|
// Process each row
|
|
1971
|
-
for (const data of
|
|
2006
|
+
for (const data of filteredRecords) {
|
|
1972
2007
|
// Look for a column with ISO-Datetime values or Unix-Timestamps
|
|
1973
2008
|
const datetimeColumns = Object.keys(data).filter(key => {
|
|
1974
2009
|
const value = data[key];
|
|
@@ -2107,6 +2142,20 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
|
|
|
2107
2142
|
fs.createReadStream(filePath)
|
|
2108
2143
|
.pipe(csvParser())
|
|
2109
2144
|
.on('data', (data) => {
|
|
2145
|
+
// Apply filter if specified
|
|
2146
|
+
if (filterCriteria) {
|
|
2147
|
+
const parsedCriteria = typeof filterCriteria === 'string'
|
|
2148
|
+
? parseFilterCriteria(filterCriteria)
|
|
2149
|
+
: filterCriteria;
|
|
2150
|
+
|
|
2151
|
+
if (parsedCriteria) {
|
|
2152
|
+
const recordValue = data[parsedCriteria.column];
|
|
2153
|
+
if (!recordValue || recordValue.toString() !== parsedCriteria.value) {
|
|
2154
|
+
return; // Skip this record
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2110
2159
|
// Look for a column with ISO-Datetime values or Unix-Timestamps
|
|
2111
2160
|
const datetimeColumns = Object.keys(data).filter(key => {
|
|
2112
2161
|
const value = data[key];
|
package/src/cli/cli.js
CHANGED
|
@@ -11,6 +11,16 @@ const swisseph = require('swisseph');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
|
|
14
|
+
// Import chart generation and downloads folder utilities
|
|
15
|
+
let chartGenerator = null;
|
|
16
|
+
let downloadsFolder = null;
|
|
17
|
+
try {
|
|
18
|
+
chartGenerator = require('../utils/chartGenerator');
|
|
19
|
+
downloadsFolder = require('../utils/downloadsFolder');
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.debug('Chart generation utilities not available');
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
// Wikidata service import
|
|
15
25
|
let wikidataService = null;
|
|
16
26
|
try {
|
|
@@ -265,6 +275,8 @@ program
|
|
|
265
275
|
.option('--v <count>', 'Shows past aspects between two planets (Format: --v <count> planet1 aspectType planet2)')
|
|
266
276
|
.option('--z <count>', 'Shows future aspects between two planets (Format: --z <count> planet1 aspectType planet2)')
|
|
267
277
|
.option('--csv <filepath>', 'Analyzes a CSV file with ISO-Datetime values or Unix timestamps')
|
|
278
|
+
.option('--filter <column:value>', 'Filters CSV data by column:value (e.g., --filter "Item:coffee")')
|
|
279
|
+
.option('--title <title>', 'Title for the chart image (generates PNG image when provided)')
|
|
268
280
|
.option('--in [count]', 'Shows next planet ingress (entering new sign). Optional count for multiple ingresses')
|
|
269
281
|
.option('--wiki <occupation>', 'Fetches people from Wikidata by occupation and checks for specific aspects (Format: planet1 aspectType planet2 --wiki <occupation> [limit])')
|
|
270
282
|
.option('--gui', 'Launches the web interface for command history (port 37421)')
|
|
@@ -1495,8 +1507,9 @@ program
|
|
|
1495
1507
|
const planet = planetArg ? planetArg.toLowerCase() : 'moon';
|
|
1496
1508
|
const houseSystem = actualOptions.hs ? actualOptions.hs.toLowerCase() : 'koch';
|
|
1497
1509
|
const analyzeAspects = actualOptions.a || false;
|
|
1510
|
+
const filterCriteria = actualOptions.filter;
|
|
1498
1511
|
|
|
1499
|
-
analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2)
|
|
1512
|
+
analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria)
|
|
1500
1513
|
.then(({ results, houseDistribution, signDistribution, aspectStatistics }) => {
|
|
1501
1514
|
if (results.length === 0) {
|
|
1502
1515
|
console.log('No valid ISO-Datetime values found in the CSV file.');
|
|
@@ -1614,6 +1627,60 @@ program
|
|
|
1614
1627
|
// Also show total number of records and house system
|
|
1615
1628
|
console.log(`\nIn Total: ${results.length}`);
|
|
1616
1629
|
console.log(`House system: ${houseSystem.charAt(0).toUpperCase() + houseSystem.slice(1)}`);
|
|
1630
|
+
|
|
1631
|
+
// Generate chart image if --title option is provided
|
|
1632
|
+
if (actualOptions.title && chartGenerator && downloadsFolder) {
|
|
1633
|
+
try {
|
|
1634
|
+
// Get the downloads folder path
|
|
1635
|
+
const downloadsPath = downloadsFolder.getValidDownloadsFolder();
|
|
1636
|
+
|
|
1637
|
+
// Create a safe filename from the title
|
|
1638
|
+
const safeTitle = actualOptions.title
|
|
1639
|
+
.replace(/[^a-zA-Z0-9\s\-_.]/g, '')
|
|
1640
|
+
.substring(0, 50)
|
|
1641
|
+
.trim();
|
|
1642
|
+
|
|
1643
|
+
const timestamp = new Date().toISOString()
|
|
1644
|
+
.replace(/[:.]/g, '-')
|
|
1645
|
+
.substring(0, 19);
|
|
1646
|
+
|
|
1647
|
+
const filename = `${timestamp}_${safeTitle}.png`;
|
|
1648
|
+
const outputPath = path.join(downloadsPath, filename);
|
|
1649
|
+
|
|
1650
|
+
console.log(`\n📊 Generating chart image: ${filename}`);
|
|
1651
|
+
console.log(`💾 Saving to: ${outputPath}`);
|
|
1652
|
+
|
|
1653
|
+
// Calculate statistics for the chart
|
|
1654
|
+
let chartStats = null;
|
|
1655
|
+
if (analyzeAspects && aspectStatistics) {
|
|
1656
|
+
const { analyzeAspectDistributionSignificance } = require('../astrology/astrologyService');
|
|
1657
|
+
chartStats = analyzeAspectDistributionSignificance(aspectStatistics);
|
|
1658
|
+
} else if (houseDistribution) {
|
|
1659
|
+
const { analyzeHouseDistributionSignificance } = require('../astrology/astrologyService');
|
|
1660
|
+
chartStats = analyzeHouseDistributionSignificance(houseDistribution);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// Generate the chart
|
|
1664
|
+
const chartData = {
|
|
1665
|
+
houseDistribution: houseDistribution || { counts: {}, total: results.length },
|
|
1666
|
+
aspectStatistics: aspectStatistics || { types: [], counts: {}, total: 0 },
|
|
1667
|
+
results: results,
|
|
1668
|
+
statistics: chartStats
|
|
1669
|
+
};
|
|
1670
|
+
|
|
1671
|
+
return chartGenerator.generateBarChart(
|
|
1672
|
+
chartData,
|
|
1673
|
+
actualOptions.title,
|
|
1674
|
+
outputPath,
|
|
1675
|
+
analyzeAspects // Show aspects if --a option was used
|
|
1676
|
+
).then(imagePath => {
|
|
1677
|
+
console.log(`✅ Chart image successfully saved to: ${imagePath}`);
|
|
1678
|
+
console.log(`📁 You can find it in your Downloads folder`);
|
|
1679
|
+
});
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
console.error('❌ Error generating chart image:', error.message);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1617
1684
|
}
|
|
1618
1685
|
})
|
|
1619
1686
|
.catch(error => {
|
|
@@ -2509,8 +2576,9 @@ program
|
|
|
2509
2576
|
const planet = planetArg ? planetArg.toLowerCase() : 'moon';
|
|
2510
2577
|
const houseSystem = actualOptions.hs ? actualOptions.hs.toLowerCase() : 'koch';
|
|
2511
2578
|
const analyzeAspects = actualOptions.a || false;
|
|
2579
|
+
const filterCriteria = actualOptions.filter;
|
|
2512
2580
|
|
|
2513
|
-
analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2)
|
|
2581
|
+
analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria)
|
|
2514
2582
|
.then(({ results, houseDistribution, signDistribution, aspectStatistics }) => {
|
|
2515
2583
|
if (results.length === 0) {
|
|
2516
2584
|
console.log('No valid ISO-Datetime values found in the CSV file.');
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a bar chart SVG from CSV analysis data
|
|
6
|
+
* @param {Object} data - CSV analysis data
|
|
7
|
+
* @param {string} title - Chart title
|
|
8
|
+
* @param {string} outputPath - Output file path
|
|
9
|
+
* @param {boolean} showAspects - Whether to show aspects or house distribution
|
|
10
|
+
* @returns {Promise<string>} Path to the generated image
|
|
11
|
+
*/
|
|
12
|
+
async function generateBarChart(data, title, outputPath, showAspects = false) {
|
|
13
|
+
// SVG dimensions
|
|
14
|
+
const width = 1200;
|
|
15
|
+
const height = 800;
|
|
16
|
+
const padding = 60;
|
|
17
|
+
const chartWidth = width - 2 * padding;
|
|
18
|
+
const chartHeight = height - 2 * padding;
|
|
19
|
+
const titleHeight = 80;
|
|
20
|
+
const legendHeight = 100;
|
|
21
|
+
|
|
22
|
+
// Colors
|
|
23
|
+
const barColors = ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac'];
|
|
24
|
+
|
|
25
|
+
// Prepare data for charting
|
|
26
|
+
let chartData, labels, maxValue, dataType;
|
|
27
|
+
|
|
28
|
+
if (showAspects && data.aspectStatistics) {
|
|
29
|
+
// Aspect statistics chart
|
|
30
|
+
dataType = 'Aspects';
|
|
31
|
+
chartData = data.aspectStatistics.types.map((type, index) => ({
|
|
32
|
+
label: type,
|
|
33
|
+
value: data.aspectStatistics.counts[type] || 0,
|
|
34
|
+
color: barColors[index % barColors.length]
|
|
35
|
+
}));
|
|
36
|
+
maxValue = Math.max(...chartData.map(d => d.value), 10);
|
|
37
|
+
} else {
|
|
38
|
+
// House distribution chart (default)
|
|
39
|
+
dataType = 'House Distribution';
|
|
40
|
+
chartData = [];
|
|
41
|
+
|
|
42
|
+
// Ensure houseDistribution exists and has counts
|
|
43
|
+
const houseDist = data.houseDistribution || { counts: {} };
|
|
44
|
+
|
|
45
|
+
for (let i = 1; i <= 12; i++) {
|
|
46
|
+
chartData.push({
|
|
47
|
+
label: `House ${i}`,
|
|
48
|
+
value: houseDist.counts[i] || 0,
|
|
49
|
+
color: barColors[(i-1) % barColors.length]
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
maxValue = Math.max(...chartData.map(d => d.value), 10);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Calculate bar dimensions
|
|
57
|
+
const barCount = chartData.length;
|
|
58
|
+
const barWidth = chartWidth / barCount / 1.5;
|
|
59
|
+
const maxBarHeight = chartHeight - titleHeight - legendHeight;
|
|
60
|
+
const yScale = maxBarHeight / maxValue;
|
|
61
|
+
|
|
62
|
+
// Generate SVG
|
|
63
|
+
const svgContent = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
64
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
65
|
+
<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
|
|
66
|
+
<!-- Background -->
|
|
67
|
+
<rect width="100%" height="100%" fill="#f8f9fa"/>
|
|
68
|
+
|
|
69
|
+
<!-- Title -->
|
|
70
|
+
<text x="${width/2}" y="${padding + titleHeight/2 + 10}" font-family="Arial" font-size="24" font-weight="bold" text-anchor="middle" fill="#333333">${title}</text>
|
|
71
|
+
|
|
72
|
+
<!-- Statistical Subtitle -->
|
|
73
|
+
${data.statistics ? `
|
|
74
|
+
<text x="${width/2}" y="${padding + titleHeight/2 + 40}" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle" fill="#666666">
|
|
75
|
+
χ²(${data.statistics.degreesOfFreedom}) = ${data.statistics.chiSquare}, p ${data.statistics.isSignificant ? '< 0.05' : '≥ 0.05'}, Cramer's V = ${data.statistics.effectSize}
|
|
76
|
+
</text>
|
|
77
|
+
` : ''}
|
|
78
|
+
|
|
79
|
+
<!-- Y-axis grid lines -->
|
|
80
|
+
${Array.from({length: 6}).map((_, i) => {
|
|
81
|
+
const y = height - padding - (i * maxBarHeight / 5);
|
|
82
|
+
return `<line x1="${padding}" y1="${y}" x2="${width - padding}" y2="${y}" stroke="#e0e0e0" stroke-width="1"/>`;
|
|
83
|
+
}).join('')}
|
|
84
|
+
|
|
85
|
+
<!-- Y-axis labels -->
|
|
86
|
+
${Array.from({length: 6}).map((_, i) => {
|
|
87
|
+
const y = height - padding - (i * maxBarHeight / 5);
|
|
88
|
+
const value = Math.round(maxValue * (i / 5));
|
|
89
|
+
return `<text x="${padding - 10}" y="${y + 4}" font-family="Arial" font-size="14" text-anchor="end" fill="#666666">${value}</text>`;
|
|
90
|
+
}).join('')}
|
|
91
|
+
|
|
92
|
+
<!-- X-axis -->
|
|
93
|
+
<line x1="${padding}" y1="${height - padding}" x2="${width - padding}" y2="${height - padding}" stroke="#666666" stroke-width="2"/>
|
|
94
|
+
|
|
95
|
+
<!-- Bars -->
|
|
96
|
+
${chartData.map((d, index) => {
|
|
97
|
+
const barHeight = d.value * yScale;
|
|
98
|
+
const x = padding + (index * (barWidth * 1.5)) + barWidth / 2;
|
|
99
|
+
const y = height - padding - barHeight;
|
|
100
|
+
|
|
101
|
+
return `
|
|
102
|
+
<rect x="${x - barWidth / 2}" y="${y}" width="${barWidth}" height="${barHeight}" fill="${d.color}"/>
|
|
103
|
+
${barHeight > 20 ? `<text x="${x}" y="${y + 20}" font-family="Arial" font-size="14" font-weight="bold" text-anchor="middle" fill="white">${d.value}</text>` : ''}
|
|
104
|
+
`;
|
|
105
|
+
}).join('')}
|
|
106
|
+
|
|
107
|
+
<!-- X-axis labels -->
|
|
108
|
+
${chartData.map((d, index) => {
|
|
109
|
+
const x = padding + (index * (barWidth * 1.5)) + barWidth / 2;
|
|
110
|
+
const y = height - padding + 25;
|
|
111
|
+
return `<text x="${x}" y="${y}" font-family="Arial" font-size="14" text-anchor="middle" fill="#666666">${d.label}</text>`;
|
|
112
|
+
}).join('')}
|
|
113
|
+
</svg>`;
|
|
114
|
+
|
|
115
|
+
// Save the SVG file
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
fs.writeFile(outputPath, svgContent, 'utf8', (error) => {
|
|
118
|
+
if (error) {
|
|
119
|
+
reject(error);
|
|
120
|
+
} else {
|
|
121
|
+
resolve(outputPath);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
generateBarChart
|
|
129
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Gets the default downloads folder path for the current operating system
|
|
6
|
+
* @returns {string} Path to the downloads folder
|
|
7
|
+
*/
|
|
8
|
+
function getDownloadsFolder() {
|
|
9
|
+
const homeDir = os.homedir();
|
|
10
|
+
|
|
11
|
+
switch (process.platform) {
|
|
12
|
+
case 'win32':
|
|
13
|
+
// Windows: %USERPROFILE%\Downloads
|
|
14
|
+
return path.join(homeDir, 'Downloads');
|
|
15
|
+
|
|
16
|
+
case 'darwin':
|
|
17
|
+
// macOS: ~/Downloads
|
|
18
|
+
return path.join(homeDir, 'Downloads');
|
|
19
|
+
|
|
20
|
+
case 'linux':
|
|
21
|
+
// Linux: Check common locations
|
|
22
|
+
const xdgDownload = process.env.XDG_DOWNLOAD_DIR;
|
|
23
|
+
if (xdgDownload) {
|
|
24
|
+
return xdgDownload;
|
|
25
|
+
}
|
|
26
|
+
// Fallback to ~/Downloads
|
|
27
|
+
return path.join(homeDir, 'Downloads');
|
|
28
|
+
|
|
29
|
+
default:
|
|
30
|
+
// Fallback for other platforms
|
|
31
|
+
return path.join(homeDir, 'Downloads');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Ensures the downloads folder exists and is writable
|
|
37
|
+
* @returns {string} Valid downloads folder path
|
|
38
|
+
*/
|
|
39
|
+
function getValidDownloadsFolder() {
|
|
40
|
+
const fs = require('fs');
|
|
41
|
+
const downloadsFolder = getDownloadsFolder();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Check if folder exists
|
|
45
|
+
if (!fs.existsSync(downloadsFolder)) {
|
|
46
|
+
// Try to create it
|
|
47
|
+
fs.mkdirSync(downloadsFolder, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if folder is writable
|
|
51
|
+
const testFile = path.join(downloadsFolder, '.write_test_' + Date.now());
|
|
52
|
+
try {
|
|
53
|
+
fs.writeFileSync(testFile, 'test');
|
|
54
|
+
fs.unlinkSync(testFile);
|
|
55
|
+
return downloadsFolder;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// Fallback to current directory if downloads folder is not writable
|
|
58
|
+
console.warn(`⚠️ Downloads folder not writable: ${downloadsFolder}, using current directory`);
|
|
59
|
+
return process.cwd();
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn(`⚠️ Could not access downloads folder: ${error.message}, using current directory`);
|
|
63
|
+
return process.cwd();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
getDownloadsFolder,
|
|
69
|
+
getValidDownloadsFolder
|
|
70
|
+
};
|