#!/usr/bin/env runts -- --allow-env --allow-run import { execCommand, execCommandShell, existsPath, joinPath, log, ProcessBar, 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 addVirtualEnv(pythonVersion: string | null, pythonVenv: string) { const pythonConfig = await loadPythonConfig(); if (!pythonVersion) { throw `No Python version assigned.`; } if (!pythonVenv) { throw `No Python venv assigned.`; } const pythonVersionProfile = pythonConfig.versions[pythonVersion]; if (!pythonVersionProfile) { throw `Python version: ${pythonVersion} not found`; } const realPythonVersion = pythonVersionProfile.version || pythonVersion; log.success( `Found Python version: ${realPythonVersion}`, ); const pythonVenvProfile = pythonConfig.profiles && 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 (!await existsPath(pythonVenvBaseDir)) { log.info(`Make python venv base dir: ${pythonVenvBaseDir}`); Deno.mkdirSync(pythonVenvBaseDir); } const pythonVenvLeafDir = `${pythonVenv}_python_${realPythonVersion}`; const pythonVenvDir = joinPath(pythonVenvBaseDir, pythonVenvLeafDir); if (await 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, pythonVenvArgs]); await execCommandShell(python3Cmd, pythonVenvArgs); const newPythonVenvProfile: PythonVenv = { version: realPythonVersion, path: pythonVenvDir, comment: `Python venv:${pythonVenv}, version: ${realPythonVersion}`, }; if (!pythonConfig.profiles) { pythonConfig.profiles = {}; } pythonConfig.profiles[pythonVenv] = newPythonVenvProfile; await savePythonConfig(pythonConfig); } 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 python.ts add-venv - add python virtual environment python.ts remove-venv - remove 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(args, { boolean: ["help"], string: ["name"], alias: { n: "name", }, }); if (flags.help) { console.log(`Help massage for venv python.ts venv [--name|-n filter-name]`); return; } let pythonConfig = await loadPythonConfig(); if (!pythonConfig.profiles) { console.warn("No Python virtual environments found"); return; } let totalProfilesCount = 0; let filterProfilesCount = 0; Object.entries(pythonConfig.profiles).forEach( ([venv, pythonVenvProfile]) => { totalProfilesCount++; if (flags.name && venv.indexOf(flags.name) < 0) { return; } filterProfilesCount++; console.log("-", venv, pythonVenvProfile); }, ); if (totalProfilesCount !== filterProfilesCount) { console.log( `\nFilter profiles with '${flags.name}': ${filterProfilesCount} of ${totalProfilesCount}`, ); } console.log( "\nNOTE: use --name|-n filter virtual environments, use `source <(python.rs active[-venv] venv-name)`", ); } async function handleAddVenv(args: string[]) { const flags = parseArgs(args, { boolean: ["help"], string: ["version", "venv"], alias: { V: "version", }, }); if (args.length === 0 || flags.help) { console.log(`Help massage for add-venv python.ts add-venv --version 3.10 --venv test-env`); return; } if (!flags.version) { log.error("Version missing"); return; } if (!flags.venv) { log.error("Venv is missing"); return; } await addVirtualEnv(flags.version, flags.venv); } async function handleRemoveVenv(args: string[]) { const flags = parseArgs(args, { boolean: ["help"], string: ["venv"], }); if (args.length === 0 || flags.help) { console.log(`Help massage for remove-venv python.ts remove-venv --venv test-env`); return; } if (!flags.venv) { log.error("Venv is missing"); return; } const pythonConfig = await loadPythonConfig(); const pythonVenvProfile = pythonConfig.profiles && pythonConfig.profiles[flags.venv]; if (!pythonVenvProfile) { throw `Python venv not exists: ${flags.venv}`; } console.log( `Pending remove virtual environment [PLEASE CONFIRM]: `, pythonVenvProfile, ); const yesOrNo = prompt("Please confirm (yes/no):"); if (yesOrNo === "yes" || yesOrNo === "y") { // DO CONFIRM YES // TODO const path = pythonVenvProfile.path; log.info(`Remove path: ${path}`); await new ProcessBar(`Removing path ${path}`).call(async () => { await Deno.remove(path, { recursive: true }); }); delete pythonConfig.profiles[flags.venv]; await savePythonConfig(pythonConfig); log.success(`Remove virtual environment: ${flags.venv}`); } else { console.log( `Your input '${yesOrNo}', skip remove the virtual environment`, ); } } async function main() { const args = Deno.args; if (args.length === 0) { log.warn("No subcommand assigned"); handleHelp([]); return; } const subcommand = args[0]; const remainingArgs = args.slice(1); 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; case "add-venv": await handleAddVenv(remainingArgs); break; case "rm-venv": case "remove-venv": await handleRemoveVenv(remainingArgs); break; default: log.error(`Unknown subcommand: ${subcommand}`); break; } } main().catch((e) => log.error(e));