cskit-cli 1.0.21 → 1.0.22
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/package.json +1 -1
- package/src/commands/init.js +595 -266
- package/src/commands/python.js +170 -0
- package/src/index.js +10 -0
- package/src/lib/github.js +95 -0
- package/src/lib/python-check.js +402 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Python Check Command
|
|
5
|
+
*
|
|
6
|
+
* Diagnose Python installation and provide installation guidance.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const inquirer = require('inquirer');
|
|
11
|
+
const {
|
|
12
|
+
checkAndReport,
|
|
13
|
+
detectPython,
|
|
14
|
+
detectPip,
|
|
15
|
+
getInstallInstructions,
|
|
16
|
+
getPlatform,
|
|
17
|
+
canAutoInstall,
|
|
18
|
+
installPython,
|
|
19
|
+
MIN_PYTHON_VERSION
|
|
20
|
+
} = require('../lib/python-check');
|
|
21
|
+
|
|
22
|
+
async function pythonCommand(options) {
|
|
23
|
+
const python = detectPython();
|
|
24
|
+
|
|
25
|
+
if (options.json) {
|
|
26
|
+
// JSON output for scripting
|
|
27
|
+
const pip = python.found ? detectPip(python.command) : { found: false };
|
|
28
|
+
const autoInstall = canAutoInstall();
|
|
29
|
+
console.log(JSON.stringify({
|
|
30
|
+
platform: getPlatform(),
|
|
31
|
+
python: {
|
|
32
|
+
found: python.found,
|
|
33
|
+
command: python.command,
|
|
34
|
+
version: python.version,
|
|
35
|
+
path: python.path,
|
|
36
|
+
meetsMinimum: python.meetsMinimum,
|
|
37
|
+
minRequired: MIN_PYTHON_VERSION
|
|
38
|
+
},
|
|
39
|
+
pip: {
|
|
40
|
+
found: pip.found,
|
|
41
|
+
version: pip.version
|
|
42
|
+
},
|
|
43
|
+
autoInstall: {
|
|
44
|
+
available: autoInstall.canInstall,
|
|
45
|
+
manager: autoInstall.manager,
|
|
46
|
+
needsSudo: autoInstall.needsSudo
|
|
47
|
+
}
|
|
48
|
+
}, null, 2));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Auto-install Python
|
|
53
|
+
if (options.auto) {
|
|
54
|
+
if (python.found && python.meetsMinimum) {
|
|
55
|
+
console.log(chalk.green(`\n✓ Python ${python.version} already installed.\n`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const autoInfo = canAutoInstall();
|
|
60
|
+
if (!autoInfo.canInstall) {
|
|
61
|
+
console.log(chalk.red('\n✗ No supported package manager found.'));
|
|
62
|
+
console.log(chalk.dim(' Please install Python manually.\n'));
|
|
63
|
+
console.log(chalk.dim(' Run `cskit python --install` for instructions.\n'));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('');
|
|
68
|
+
console.log(chalk.cyan(`Auto-install via ${autoInfo.manager}`));
|
|
69
|
+
if (autoInfo.needsSudo) {
|
|
70
|
+
console.log(chalk.yellow('(requires sudo password)'));
|
|
71
|
+
}
|
|
72
|
+
console.log(chalk.dim(`Command: ${autoInfo.command}`));
|
|
73
|
+
console.log('');
|
|
74
|
+
|
|
75
|
+
const { confirm } = await inquirer.prompt([{
|
|
76
|
+
type: 'confirm',
|
|
77
|
+
name: 'confirm',
|
|
78
|
+
message: 'Proceed with installation?',
|
|
79
|
+
default: true
|
|
80
|
+
}]);
|
|
81
|
+
|
|
82
|
+
if (!confirm) {
|
|
83
|
+
console.log(chalk.dim('\nInstallation cancelled.\n'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log('');
|
|
88
|
+
const result = await installPython((msg) => {
|
|
89
|
+
console.log(chalk.dim(` ${msg}`));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (result.success) {
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(chalk.green(`✓ Python ${result.version} installed successfully!`));
|
|
95
|
+
console.log(chalk.dim(' You may need to restart your terminal for PATH changes.'));
|
|
96
|
+
console.log('');
|
|
97
|
+
} else {
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log(chalk.red(`✗ ${result.error}`));
|
|
100
|
+
console.log('');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (options.install) {
|
|
107
|
+
// Show installation instructions
|
|
108
|
+
const instructions = getInstallInstructions();
|
|
109
|
+
const autoInfo = canAutoInstall();
|
|
110
|
+
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(chalk.bold(`Python Installation Guide (${getPlatform()})`));
|
|
113
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
114
|
+
|
|
115
|
+
if (autoInfo.canInstall) {
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(chalk.green(`Auto-install available: cskit python --auto`));
|
|
118
|
+
console.log(chalk.dim(` Uses: ${autoInfo.manager}`));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(chalk.cyan(`Manual: ${instructions.method}`));
|
|
123
|
+
console.log('');
|
|
124
|
+
|
|
125
|
+
for (const cmd of instructions.commands) {
|
|
126
|
+
if (cmd.startsWith('#')) {
|
|
127
|
+
console.log(chalk.dim(cmd));
|
|
128
|
+
} else if (cmd === '') {
|
|
129
|
+
console.log('');
|
|
130
|
+
} else {
|
|
131
|
+
console.log(chalk.green(` ${cmd}`));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (instructions.notes.length > 0) {
|
|
136
|
+
console.log('');
|
|
137
|
+
console.log(chalk.bold('Notes:'));
|
|
138
|
+
for (const note of instructions.notes) {
|
|
139
|
+
console.log(chalk.dim(` • ${note}`));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log('');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Default: check and report
|
|
148
|
+
const result = checkAndReport();
|
|
149
|
+
|
|
150
|
+
if (result.ok) {
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log(chalk.green('✓ Python environment is ready for CSK.'));
|
|
153
|
+
console.log('');
|
|
154
|
+
} else if (!python.found) {
|
|
155
|
+
const autoInfo = canAutoInstall();
|
|
156
|
+
console.log('');
|
|
157
|
+
if (autoInfo.canInstall) {
|
|
158
|
+
console.log(chalk.cyan(`Auto-install: cskit python --auto (via ${autoInfo.manager})`));
|
|
159
|
+
}
|
|
160
|
+
console.log(chalk.dim('Manual guide: cskit python --install'));
|
|
161
|
+
console.log('');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
} else {
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log(chalk.yellow('Python upgrade recommended.'));
|
|
166
|
+
console.log('');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = pythonCommand;
|
package/src/index.js
CHANGED
|
@@ -16,6 +16,7 @@ const initCommand = require('./commands/init');
|
|
|
16
16
|
const authCommand = require('./commands/auth');
|
|
17
17
|
const statusCommand = require('./commands/status');
|
|
18
18
|
const updateCommand = require('./commands/update');
|
|
19
|
+
const pythonCommand = require('./commands/python');
|
|
19
20
|
|
|
20
21
|
const program = new Command();
|
|
21
22
|
|
|
@@ -67,6 +68,15 @@ program
|
|
|
67
68
|
.description('Update CSK CLI to latest version')
|
|
68
69
|
.action(updateCommand);
|
|
69
70
|
|
|
71
|
+
// Python command - check Python environment
|
|
72
|
+
program
|
|
73
|
+
.command('python')
|
|
74
|
+
.description('Check Python installation status')
|
|
75
|
+
.option('--install', 'Show installation instructions')
|
|
76
|
+
.option('--auto', 'Auto-install Python via package manager')
|
|
77
|
+
.option('--json', 'Output as JSON for scripting')
|
|
78
|
+
.action(pythonCommand);
|
|
79
|
+
|
|
70
80
|
// Parse arguments
|
|
71
81
|
program.parse(process.argv);
|
|
72
82
|
|
package/src/lib/github.js
CHANGED
|
@@ -266,6 +266,100 @@ async function getRepoTreeFromRef(token, ref) {
|
|
|
266
266
|
return data.tree || [];
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Download repository as zip archive from specific ref
|
|
271
|
+
* @param {string} token - GitHub PAT
|
|
272
|
+
* @param {string} ref - Tag or branch name
|
|
273
|
+
* @returns {Promise<Buffer>} - Zip file buffer
|
|
274
|
+
*/
|
|
275
|
+
async function downloadZipFromRef(token, ref) {
|
|
276
|
+
return new Promise((resolve, reject) => {
|
|
277
|
+
const options = {
|
|
278
|
+
hostname: 'api.github.com',
|
|
279
|
+
path: `/repos/${GITHUB_OWNER}/${GITHUB_REPO}/zipball/${ref}`,
|
|
280
|
+
method: 'GET',
|
|
281
|
+
headers: {
|
|
282
|
+
'Authorization': `Bearer ${token}`,
|
|
283
|
+
'Accept': 'application/vnd.github+json',
|
|
284
|
+
'User-Agent': 'csk-cli',
|
|
285
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const req = https.request(options, (res) => {
|
|
290
|
+
// GitHub returns 302 redirect to the actual zip URL
|
|
291
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
292
|
+
const redirectUrl = new URL(res.headers.location);
|
|
293
|
+
|
|
294
|
+
// Choose http or https based on protocol
|
|
295
|
+
const protocol = redirectUrl.protocol === 'https:' ? https : require('http');
|
|
296
|
+
|
|
297
|
+
const redirectOptions = {
|
|
298
|
+
hostname: redirectUrl.hostname,
|
|
299
|
+
path: redirectUrl.pathname + redirectUrl.search,
|
|
300
|
+
method: 'GET',
|
|
301
|
+
headers: {
|
|
302
|
+
'User-Agent': 'csk-cli'
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const redirectReq = protocol.request(redirectOptions, (redirectRes) => {
|
|
307
|
+
// Handle another redirect (GitHub sometimes does this)
|
|
308
|
+
if (redirectRes.statusCode === 302 || redirectRes.statusCode === 301) {
|
|
309
|
+
const secondUrl = new URL(redirectRes.headers.location);
|
|
310
|
+
const secondProtocol = secondUrl.protocol === 'https:' ? https : require('http');
|
|
311
|
+
|
|
312
|
+
const secondOptions = {
|
|
313
|
+
hostname: secondUrl.hostname,
|
|
314
|
+
path: secondUrl.pathname + secondUrl.search,
|
|
315
|
+
method: 'GET',
|
|
316
|
+
headers: { 'User-Agent': 'csk-cli' }
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const secondReq = secondProtocol.request(secondOptions, (secondRes) => {
|
|
320
|
+
if (secondRes.statusCode !== 200) {
|
|
321
|
+
reject(new Error(`Failed to download zip: ${secondRes.statusCode}`));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const chunks = [];
|
|
325
|
+
secondRes.on('data', chunk => chunks.push(chunk));
|
|
326
|
+
secondRes.on('end', () => resolve(Buffer.concat(chunks)));
|
|
327
|
+
});
|
|
328
|
+
secondReq.on('error', reject);
|
|
329
|
+
secondReq.end();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (redirectRes.statusCode !== 200) {
|
|
334
|
+
reject(new Error(`Failed to download zip: ${redirectRes.statusCode}`));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const chunks = [];
|
|
339
|
+
redirectRes.on('data', chunk => chunks.push(chunk));
|
|
340
|
+
redirectRes.on('end', () => resolve(Buffer.concat(chunks)));
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
redirectReq.on('error', reject);
|
|
344
|
+
redirectReq.end();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (res.statusCode !== 200) {
|
|
349
|
+
reject(new Error(`Failed to download zip: ${res.statusCode}`));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const chunks = [];
|
|
354
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
355
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
req.on('error', reject);
|
|
359
|
+
req.end();
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
269
363
|
module.exports = {
|
|
270
364
|
verifyAccess,
|
|
271
365
|
getRepoTree,
|
|
@@ -273,6 +367,7 @@ module.exports = {
|
|
|
273
367
|
getFileContent,
|
|
274
368
|
downloadFile,
|
|
275
369
|
downloadFileFromRef,
|
|
370
|
+
downloadZipFromRef,
|
|
276
371
|
getLatestRelease,
|
|
277
372
|
getAllReleases
|
|
278
373
|
};
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Python Detection and Installation Helper
|
|
5
|
+
*
|
|
6
|
+
* Detects Python availability, version, and provides
|
|
7
|
+
* platform-specific installation guidance.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync, spawnSync } = require('child_process');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const chalk = require('chalk');
|
|
13
|
+
|
|
14
|
+
// Minimum Python version required
|
|
15
|
+
const MIN_PYTHON_VERSION = '3.8';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detect current platform
|
|
19
|
+
* @returns {'windows'|'macos'|'linux'}
|
|
20
|
+
*/
|
|
21
|
+
function getPlatform() {
|
|
22
|
+
const platform = os.platform();
|
|
23
|
+
if (platform === 'win32') return 'windows';
|
|
24
|
+
if (platform === 'darwin') return 'macos';
|
|
25
|
+
return 'linux';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Try to find Python executable
|
|
30
|
+
* @returns {{found: boolean, command: string|null, version: string|null, path: string|null}}
|
|
31
|
+
*/
|
|
32
|
+
function detectPython() {
|
|
33
|
+
const platform = getPlatform();
|
|
34
|
+
|
|
35
|
+
// Commands to try in order of preference
|
|
36
|
+
const commands = platform === 'windows'
|
|
37
|
+
? ['python', 'python3', 'py -3', 'py']
|
|
38
|
+
: ['python3', 'python'];
|
|
39
|
+
|
|
40
|
+
for (const cmd of commands) {
|
|
41
|
+
try {
|
|
42
|
+
// Get version
|
|
43
|
+
const versionResult = spawnSync(
|
|
44
|
+
cmd.split(' ')[0],
|
|
45
|
+
[...cmd.split(' ').slice(1), '--version'],
|
|
46
|
+
{ encoding: 'utf-8', shell: true, timeout: 5000 }
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (versionResult.status === 0) {
|
|
50
|
+
const output = (versionResult.stdout || versionResult.stderr || '').trim();
|
|
51
|
+
const match = output.match(/Python (\d+\.\d+\.\d+)/i);
|
|
52
|
+
|
|
53
|
+
if (match) {
|
|
54
|
+
const version = match[1];
|
|
55
|
+
|
|
56
|
+
// Get path
|
|
57
|
+
let pythonPath = null;
|
|
58
|
+
try {
|
|
59
|
+
const pathCmd = platform === 'windows'
|
|
60
|
+
? `${cmd} -c "import sys; print(sys.executable)"`
|
|
61
|
+
: `${cmd} -c 'import sys; print(sys.executable)'`;
|
|
62
|
+
|
|
63
|
+
pythonPath = execSync(pathCmd, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
64
|
+
} catch (e) {
|
|
65
|
+
// Ignore path detection errors
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
found: true,
|
|
70
|
+
command: cmd,
|
|
71
|
+
version,
|
|
72
|
+
path: pythonPath,
|
|
73
|
+
meetsMinimum: compareVersions(version, MIN_PYTHON_VERSION) >= 0
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// Continue to next command
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { found: false, command: null, version: null, path: null, meetsMinimum: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Compare semantic versions
|
|
87
|
+
* @param {string} v1
|
|
88
|
+
* @param {string} v2
|
|
89
|
+
* @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
|
90
|
+
*/
|
|
91
|
+
function compareVersions(v1, v2) {
|
|
92
|
+
const parts1 = v1.split('.').map(Number);
|
|
93
|
+
const parts2 = v2.split('.').map(Number);
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
96
|
+
const p1 = parts1[i] || 0;
|
|
97
|
+
const p2 = parts2[i] || 0;
|
|
98
|
+
if (p1 < p2) return -1;
|
|
99
|
+
if (p1 > p2) return 1;
|
|
100
|
+
}
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if pip is available
|
|
106
|
+
* @param {string} pythonCmd - Python command to use
|
|
107
|
+
* @returns {{found: boolean, version: string|null}}
|
|
108
|
+
*/
|
|
109
|
+
function detectPip(pythonCmd) {
|
|
110
|
+
try {
|
|
111
|
+
const result = spawnSync(
|
|
112
|
+
pythonCmd.split(' ')[0],
|
|
113
|
+
[...pythonCmd.split(' ').slice(1), '-m', 'pip', '--version'],
|
|
114
|
+
{ encoding: 'utf-8', shell: true, timeout: 5000 }
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (result.status === 0) {
|
|
118
|
+
const match = (result.stdout || '').match(/pip (\d+\.\d+)/);
|
|
119
|
+
return { found: true, version: match ? match[1] : 'unknown' };
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {
|
|
122
|
+
// Ignore
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { found: false, version: null };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get installation instructions for current platform
|
|
130
|
+
* @returns {{method: string, commands: string[], notes: string[]}}
|
|
131
|
+
*/
|
|
132
|
+
function getInstallInstructions() {
|
|
133
|
+
const platform = getPlatform();
|
|
134
|
+
|
|
135
|
+
switch (platform) {
|
|
136
|
+
case 'windows':
|
|
137
|
+
return {
|
|
138
|
+
method: 'Microsoft Store or python.org',
|
|
139
|
+
commands: [
|
|
140
|
+
'# Option 1: Microsoft Store (recommended)',
|
|
141
|
+
'winget install Python.Python.3.12',
|
|
142
|
+
'',
|
|
143
|
+
'# Option 2: Download from python.org',
|
|
144
|
+
'# https://www.python.org/downloads/windows/',
|
|
145
|
+
'',
|
|
146
|
+
'# After install, restart terminal and run:',
|
|
147
|
+
'python --version'
|
|
148
|
+
],
|
|
149
|
+
notes: [
|
|
150
|
+
'Check "Add Python to PATH" during installation',
|
|
151
|
+
'Restart terminal after installation'
|
|
152
|
+
]
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
case 'macos':
|
|
156
|
+
return {
|
|
157
|
+
method: 'Homebrew or python.org',
|
|
158
|
+
commands: [
|
|
159
|
+
'# Option 1: Homebrew (recommended)',
|
|
160
|
+
'brew install python@3.12',
|
|
161
|
+
'',
|
|
162
|
+
'# Option 2: Download from python.org',
|
|
163
|
+
'# https://www.python.org/downloads/macos/',
|
|
164
|
+
'',
|
|
165
|
+
'# Option 3: Xcode Command Line Tools',
|
|
166
|
+
'xcode-select --install'
|
|
167
|
+
],
|
|
168
|
+
notes: [
|
|
169
|
+
'Homebrew: https://brew.sh if not installed',
|
|
170
|
+
'After install: python3 --version'
|
|
171
|
+
]
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
case 'linux':
|
|
175
|
+
return {
|
|
176
|
+
method: 'Package manager',
|
|
177
|
+
commands: [
|
|
178
|
+
'# Debian/Ubuntu:',
|
|
179
|
+
'sudo apt update && sudo apt install python3 python3-pip',
|
|
180
|
+
'',
|
|
181
|
+
'# Fedora:',
|
|
182
|
+
'sudo dnf install python3 python3-pip',
|
|
183
|
+
'',
|
|
184
|
+
'# Arch:',
|
|
185
|
+
'sudo pacman -S python python-pip'
|
|
186
|
+
],
|
|
187
|
+
notes: [
|
|
188
|
+
'Most Linux distros have Python pre-installed',
|
|
189
|
+
'Verify with: python3 --version'
|
|
190
|
+
]
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Display Python status and instructions
|
|
197
|
+
* @returns {{ok: boolean, pythonCmd: string|null}}
|
|
198
|
+
*/
|
|
199
|
+
function checkAndReport() {
|
|
200
|
+
const python = detectPython();
|
|
201
|
+
const platform = getPlatform();
|
|
202
|
+
|
|
203
|
+
console.log('');
|
|
204
|
+
console.log(chalk.bold('Python Environment Check'));
|
|
205
|
+
console.log(chalk.dim('─'.repeat(40)));
|
|
206
|
+
console.log(`Platform: ${chalk.cyan(platform)}`);
|
|
207
|
+
|
|
208
|
+
if (python.found) {
|
|
209
|
+
const versionColor = python.meetsMinimum ? chalk.green : chalk.yellow;
|
|
210
|
+
console.log(`Python: ${versionColor(python.version)} (${python.command})`);
|
|
211
|
+
|
|
212
|
+
if (python.path) {
|
|
213
|
+
console.log(`Path: ${chalk.dim(python.path)}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!python.meetsMinimum) {
|
|
217
|
+
console.log('');
|
|
218
|
+
console.log(chalk.yellow(`⚠ Python ${MIN_PYTHON_VERSION}+ required, found ${python.version}`));
|
|
219
|
+
console.log(chalk.dim(' Some features may not work correctly'));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check pip
|
|
223
|
+
const pip = detectPip(python.command);
|
|
224
|
+
if (pip.found) {
|
|
225
|
+
console.log(`pip: ${chalk.green(pip.version)}`);
|
|
226
|
+
} else {
|
|
227
|
+
console.log(`pip: ${chalk.yellow('not found')}`);
|
|
228
|
+
console.log(chalk.dim(` Install: ${python.command} -m ensurepip --upgrade`));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log(chalk.dim('─'.repeat(40)));
|
|
232
|
+
return { ok: python.meetsMinimum, pythonCmd: python.command };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Python not found
|
|
236
|
+
console.log(`Python: ${chalk.red('not found')}`);
|
|
237
|
+
console.log('');
|
|
238
|
+
console.log(chalk.yellow('Python is required for advanced CSK features:'));
|
|
239
|
+
console.log(chalk.dim(' - Data fetching and analysis'));
|
|
240
|
+
console.log(chalk.dim(' - Chart generation'));
|
|
241
|
+
console.log(chalk.dim(' - MCP server tools'));
|
|
242
|
+
console.log('');
|
|
243
|
+
|
|
244
|
+
const instructions = getInstallInstructions();
|
|
245
|
+
console.log(chalk.bold(`Install via ${instructions.method}:`));
|
|
246
|
+
console.log('');
|
|
247
|
+
|
|
248
|
+
for (const cmd of instructions.commands) {
|
|
249
|
+
if (cmd.startsWith('#')) {
|
|
250
|
+
console.log(chalk.dim(cmd));
|
|
251
|
+
} else if (cmd === '') {
|
|
252
|
+
console.log('');
|
|
253
|
+
} else {
|
|
254
|
+
console.log(chalk.cyan(` ${cmd}`));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (instructions.notes.length > 0) {
|
|
259
|
+
console.log('');
|
|
260
|
+
console.log(chalk.dim('Notes:'));
|
|
261
|
+
for (const note of instructions.notes) {
|
|
262
|
+
console.log(chalk.dim(` • ${note}`));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log(chalk.dim('─'.repeat(40)));
|
|
267
|
+
return { ok: false, pythonCmd: null };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Quick check - returns Python command or null
|
|
272
|
+
* @returns {string|null}
|
|
273
|
+
*/
|
|
274
|
+
function quickCheck() {
|
|
275
|
+
const python = detectPython();
|
|
276
|
+
return python.found && python.meetsMinimum ? python.command : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Check if a package manager is available
|
|
281
|
+
* @returns {{available: boolean, manager: string|null, installCmd: string|null}}
|
|
282
|
+
*/
|
|
283
|
+
function detectPackageManager() {
|
|
284
|
+
const platform = getPlatform();
|
|
285
|
+
|
|
286
|
+
const managers = {
|
|
287
|
+
windows: [
|
|
288
|
+
{ name: 'winget', check: 'winget --version', install: 'winget install Python.Python.3.12 --accept-package-agreements --accept-source-agreements' },
|
|
289
|
+
{ name: 'choco', check: 'choco --version', install: 'choco install python3 -y' },
|
|
290
|
+
{ name: 'scoop', check: 'scoop --version', install: 'scoop install python' }
|
|
291
|
+
],
|
|
292
|
+
macos: [
|
|
293
|
+
{ name: 'brew', check: 'brew --version', install: 'brew install python@3.12' }
|
|
294
|
+
],
|
|
295
|
+
linux: [
|
|
296
|
+
{ name: 'apt', check: 'apt --version', install: 'sudo apt update && sudo apt install -y python3 python3-pip python3-venv' },
|
|
297
|
+
{ name: 'dnf', check: 'dnf --version', install: 'sudo dnf install -y python3 python3-pip' },
|
|
298
|
+
{ name: 'pacman', check: 'pacman --version', install: 'sudo pacman -S --noconfirm python python-pip' },
|
|
299
|
+
{ name: 'apk', check: 'apk --version', install: 'apk add python3 py3-pip' }
|
|
300
|
+
]
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const platformManagers = managers[platform] || [];
|
|
304
|
+
|
|
305
|
+
for (const mgr of platformManagers) {
|
|
306
|
+
try {
|
|
307
|
+
const result = spawnSync(mgr.check.split(' ')[0], mgr.check.split(' ').slice(1), {
|
|
308
|
+
encoding: 'utf-8',
|
|
309
|
+
shell: true,
|
|
310
|
+
timeout: 5000,
|
|
311
|
+
stdio: 'pipe'
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (result.status === 0) {
|
|
315
|
+
return {
|
|
316
|
+
available: true,
|
|
317
|
+
manager: mgr.name,
|
|
318
|
+
installCmd: mgr.install
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
} catch (e) {
|
|
322
|
+
// Continue to next manager
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { available: false, manager: null, installCmd: null };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Attempt to install Python using detected package manager
|
|
331
|
+
* @param {function} onProgress - Callback for progress updates
|
|
332
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
333
|
+
*/
|
|
334
|
+
async function installPython(onProgress = console.log) {
|
|
335
|
+
const pkgMgr = detectPackageManager();
|
|
336
|
+
|
|
337
|
+
if (!pkgMgr.available) {
|
|
338
|
+
return {
|
|
339
|
+
success: false,
|
|
340
|
+
error: 'No supported package manager found. Please install Python manually.'
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
onProgress(`Installing Python via ${pkgMgr.manager}...`);
|
|
345
|
+
onProgress(`Command: ${pkgMgr.installCmd}`);
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
// Execute install command
|
|
349
|
+
execSync(pkgMgr.installCmd, {
|
|
350
|
+
stdio: 'inherit', // Show output to user
|
|
351
|
+
shell: true,
|
|
352
|
+
timeout: 300000 // 5 minute timeout
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Verify installation
|
|
356
|
+
const python = detectPython();
|
|
357
|
+
if (python.found) {
|
|
358
|
+
onProgress(`Python ${python.version} installed successfully!`);
|
|
359
|
+
return { success: true, version: python.version, command: python.command };
|
|
360
|
+
} else {
|
|
361
|
+
return {
|
|
362
|
+
success: false,
|
|
363
|
+
error: 'Installation completed but Python not detected. You may need to restart your terminal.'
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
} catch (error) {
|
|
367
|
+
return {
|
|
368
|
+
success: false,
|
|
369
|
+
error: `Installation failed: ${error.message}`
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Check if auto-install is possible
|
|
376
|
+
* @returns {{canInstall: boolean, manager: string|null, command: string|null, needsSudo: boolean}}
|
|
377
|
+
*/
|
|
378
|
+
function canAutoInstall() {
|
|
379
|
+
const pkgMgr = detectPackageManager();
|
|
380
|
+
const platform = getPlatform();
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
canInstall: pkgMgr.available,
|
|
384
|
+
manager: pkgMgr.manager,
|
|
385
|
+
command: pkgMgr.installCmd,
|
|
386
|
+
needsSudo: platform === 'linux' && pkgMgr.installCmd?.includes('sudo')
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
module.exports = {
|
|
391
|
+
getPlatform,
|
|
392
|
+
detectPython,
|
|
393
|
+
detectPip,
|
|
394
|
+
compareVersions,
|
|
395
|
+
getInstallInstructions,
|
|
396
|
+
checkAndReport,
|
|
397
|
+
quickCheck,
|
|
398
|
+
detectPackageManager,
|
|
399
|
+
installPython,
|
|
400
|
+
canAutoInstall,
|
|
401
|
+
MIN_PYTHON_VERSION
|
|
402
|
+
};
|