/** * Where this plugin keeps things on disk. * * Every path here is deliberately under `$DSH_HOME`, because `$DSH_HOME` is the * one directory that survives the container this harness usually runs in. The * uv-managed slopo install and the CPython it needs would otherwise live in * `~/.local`, which is on the container's writable layer and is lost whenever * the container is recreated rather than merely restarted. * * Relative paths are never used: slopo resolves every relative path against the * *process working directory*, not against the config file that named it, so a * relative path in a config invoked as `--config /elsewhere/x.yaml` still lands * in whatever directory the caller happened to be in. */ import { createHash } from 'node:crypto' import { homedir } from 'node:os' import { basename, join, resolve } from 'node:path' /** Plugin name, used for the `$DSH_HOME/plugin-data/` directory. */ export const PLUGIN_NAME = 'dsh-slopo' /** The dsh home directory: `$DSH_HOME`, else `~/.dsh`. */ export function resolveDshHome(env = process.env) { const configured = typeof env.DSH_HOME === 'string' ? env.DSH_HOME.trim() : '' return configured === '' ? join(homedir(), '.dsh') : resolve(configured) } /** * A stable, filesystem-safe key for one workspace. * * The basename is for a human reading the directory listing; the hash is what * actually identifies it, so two checkouts both named `app` cannot collide. */ export function workspaceKey(cwd) { const absolute = resolve(cwd) const name = basename(absolute) .replace(/[^A-Za-z0-9._-]+/g, '-') .replace(/^[-.]+|-+$/g, '') || 'workspace' const hash = createHash('sha256').update(absolute).digest('hex').slice(0, 8) return `${name}-${hash}` } /** Per-workspace state: the slopo config, index database, ignore file and log. */ export function workspaceStateDir(dshHome, cwd) { return join(dshHome, 'plugin-data', PLUGIN_NAME, 'workspaces', workspaceKey(cwd)) } /** Root of this plugin's own data, independent of any workspace. */ export function pluginDataDir(dshHome) { return join(dshHome, 'plugin-data', PLUGIN_NAME) } /** * The directory `uv tool install` writes the slopo venv and shim into. * * Redirected here from its `~/.local/share/uv/tools` default for the durability * reason in the file header. */ export function managedToolDir(dshHome) { return join(pluginDataDir(dshHome), 'uv-tools') } /** The directory uv keeps its downloaded CPython builds in. */ export function managedPythonDir(dshHome) { return join(pluginDataDir(dshHome), 'uv-python') } /** The slopo executable inside {@link managedToolDir}. */ export function managedSlopoPath(dshHome) { return join(managedToolDir(dshHome), 'bin', process.platform === 'win32' ? 'slopo.exe' : 'slopo') } /** Slopo's own default install location, for machines that already have one. */ export function userLocalSlopoPath(env = process.env) { const home = typeof env.HOME === 'string' && env.HOME !== '' ? env.HOME : homedir() return join(home, '.local', 'bin', process.platform === 'win32' ? 'slopo.exe' : 'slopo') }