#!/usr/bin/env runts -- --allow-env --allow-run import { execCommand, execCommandShell, existsPath, joinPath, log, readFileToString, resolveFilename, } from "https://global.hatter.ink/script/get/@16/deno-commons-mod.ts"; import { parseArgs } from "jsr:@std/cli/parse-args"; import { writeStringToFile } from "../libraries/deno-commons-mod.ts"; const PYTHON_CONFIG_FILE = "~/.config/python-config.json"; const PYTHON_VENV_DEFAULT_BASE_DIR = "~/.venv/"; interface PythonConfig { default_version?: string; default_profile?: string; python_venv_base_path?: string; versions: Map; profiles?: Map; } interface PythonVersion { version: string; path: string; // Python path dir or Python binary file comment?: string; } interface PythonVenv { version: string; path: string; comment?: string; } async function loadPythonConfig(): Promise { const pythonConfigFile = resolveFilename(PYTHON_CONFIG_FILE); const pythonConfigJson = await readFileToString(pythonConfigFile); if (!pythonConfigJson) { throw `Could not read python config file: ${pythonConfigFile}`; } return JSON.parse(pythonConfigJson) as PythonConfig; } async function savePythonConfig(pythonConfig: PythonConfig): Promise { const pythonConfigFile = resolveFilename(PYTHON_CONFIG_FILE); await writeStringToFile( pythonConfigFile, JSON.stringify(pythonConfig, null, 2), ); } async function findVirtualEnv(pythonVenvVersion: string): Promise { const pythonConfig = await loadPythonConfig(); if (!pythonConfig.profiles) { throw "No Python venvs configured"; } const pythonVenv = pythonConfig.profiles[pythonVenvVersion]; if (!pythonVenv) { throw `Python venv not found: ${pythonVenvVersion}`; } return pythonVenv; } async function newVirtualEnv(pythonVersion: string | null, pythonVenv: string) { const pythonConfig = await loadPythonConfig(); const selectedPythonVersion = pythonVersion || pythonConfig.default_version || null; if (!selectedPythonVersion) { throw `No Python version assigned.`; } if (!selectedPythonVersion) { throw `No Python venv assigned.`; } const pythonVersionProfile = pythonConfig.versions[pythonVersion]; if (!pythonVersionProfile) { throw `Python version: ${pythonVersion} not found`; } log.success(`Found Python version: ${pythonVersion}`); const pythonVenvProfile = pythonConfig.profiles[pythonVenv]; if (pythonVenvProfile) { throw `Python venv already exists: ${pythonVenv}`; } const pythonVenvBaseDir = resolveFilename( pythonConfig.python_venv_base_path || PYTHON_VENV_DEFAULT_BASE_DIR, ); if (!existsPath(pythonVenvBaseDir)) { log.info(`Make python venv base dir: ${pythonVenvBaseDir}`); Deno.mkdirSync(pythonVenvBaseDir); } const pythonVenvDir = joinPath(pythonVenvBaseDir, pythonVenv); if (existsPath(pythonVenvDir)) { throw `Python venv: ${pythonVenvDir} already exists`; } // python3 -m venv myenv const python3Cmd = pythonVersionProfile.path; log.success(`Found Python: ${python3Cmd}`); const pythonVenvArgs = ["-m", "venv", pythonVenvDir]; log.info( `Create Python venv, python: ${python3Cmd}, args: ${pythonVenvArgs}`, ); await execCommandShell(python3Cmd, pythonVenvArgs); } async function isFile(path: string): Promise { const fileInfo = await Deno.stat(path); return fileInfo?.isFile; } function handleHelp(_args: string[]) { const help = []; help.push( `python.ts - Python version and virtual environment management tool python.ts python - management python version [alias: py] python.ts add-python - add python version [alias: py] python.ts venv - management python virtual environment`, ); // source <(cat ~/.venv-python-3.13.5/bin/activate) // source <(python.ts venv test1) console.log(help.join("\n")); } async function handlePython(args: string[]) { const flags = parseArgs(Deno.args, { boolean: ["help"], string: ["version"], alias: { V: "version", }, }); if (flags.help) { console.log("Help massage for python"); return; } if (!flags.version) { const pythonConfig = await loadPythonConfig(); if (!pythonConfig.versions) { log.error("No Python versions configured"); return; } const versions = []; let maxVersionLength = 0; for (let version in pythonConfig.versions) { versions.push(version); if (version.length > maxVersionLength) { maxVersionLength = version.length; } } versions.sort(); console.log(`Found ${versions.length} Python version(s)`); for (let version in pythonConfig.versions) { const pythonVersion = pythonConfig.versions[version]; const versionPadding = " ".repeat( maxVersionLength - version.length, ); console.log( `- Python ${version} ${versionPadding}: ${pythonVersion.path} [version: ${ pythonVersion.version || "unknown" }]`, ); } return; } const pythonVirtualEnv = await findVirtualEnv(flags.version); // TODO } async function getPythonVersion(pythonBinPath: string): Promise { const pythonVersion = await execCommand(pythonBinPath, ["--version"]); const pythonVersionLower = pythonVersion.assertSuccess() .getStdoutAsStringThenTrim().toLowerCase(); if (!pythonVersionLower.startsWith("python ")) { throw new Error(`Invalid Python version: ${pythonVersionLower}`); } return pythonVersionLower.substring("python ".length).trim(); } async function handleAddPython(args: string[]) { const flags = parseArgs(Deno.args, { boolean: ["help"], string: ["path"], }); if (flags.help) { console.log("Help massage for python add-python"); return; } if (!flags.path) { throw new Error("Path is empty"); } const path = flags.path as string; let pythonPath = path; if (!path.includes("/")) { const whichPath = await execCommand("which", [path]); pythonPath = whichPath.assertSuccess().getStdoutAsStringThenTrim(); } log.info(`Python path: ${pythonPath}`); const isPythonPathFile = await isFile(pythonPath); let pythonBinPath = ""; if (!isPythonPathFile) { const pythonPath1 = joinPath(pythonPath, "bin", "python3"); if (await isFile(pythonPath1)) { pythonBinPath = pythonPath1; } else { const pythonPath2 = joinPath(pythonPath, "bin", "python"); if (await isFile(pythonPath2)) { pythonBinPath = pythonPath2; } else { throw new Error( `Python bin path not found, python path: ${pythonPath}`, ); } } } else { pythonBinPath = pythonPath; } const pythonVersion = await getPythonVersion(pythonBinPath); log.info(`Python version: ${pythonVersion}`); const pythonConfig = await loadPythonConfig(); if (!pythonConfig.versions) { pythonConfig.versions = {}; } if (pythonConfig.versions[pythonVersion]) { throw new Error(`Python version ${pythonVersion} exists`); } const pyVersion: PythonVersion = { version: pythonVersion, path: pythonBinPath, comment: `Python v${pythonVersion}`, }; log.info("Found Python version", pyVersion); pythonConfig.versions[pythonVersion] = pyVersion; log.info("Save to Python config"); await savePythonConfig(pythonConfig); } async function handleVenv(args: string[]) { const flags = parseArgs(Deno.args, { boolean: ["help"], string: ["version"], alias: { V: "version", }, }); if (flags.help) { console.log("Help massage for venv"); return; } if (!flags.version) { log.warn("Version missing"); return; } const pythonVirtualEnv = await findVirtualEnv(flags.version); // TODO } async function main() { const args = parseArgs(Deno.args); const [subcommand, ...remainingArgs] = args._; if (!subcommand) { log.warn("Subcommand not found, `python.ts help` for help message"); return; } switch (subcommand) { case "help": handleHelp(remainingArgs); break; case "py": case "python": await handlePython(remainingArgs); break; case "add-py": case "add-python": await handleAddPython(remainingArgs); break; case "venv": await handleVenv(remainingArgs); break; default: log.error(`Unknown subcommand: ${subcommand}`); break; } } main().catch((e) => log.error(e));