Files
dsh-slopo/lib/paths.js
T
dsh-slopo agent 844f12faa3 Add path resolution and slopo config rendering
Every path the plugin owns is under $DSH_HOME. Only two directories in the
container this runs in are persistent volumes ($DSH_HOME and the workspace
root); $HOME/.local -- where 'uv tool install' puts slopo and its CPython by
default -- is on the container's writable layer and dies on recreation, not
merely on restart.

renderSlopoConfig writes from a closed allow-list because slopo treats an
unknown config key as a hard error, and quotes every scalar because a path
can contain a colon or a leading dash. Verified against slopo 0.8.0 itself:
it reads the generated file back with no complaint and round-trips all 19
keys. The embedding API key is deliberately absent -- slopo reads
SLOPO_EMBEDDING_API_KEY from the environment ahead of the YAML value, so the
secret never lands on disk.
2026-09-22 06:06:44 +01:00

80 lines
3.1 KiB
JavaScript

/**
* 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/<name>` 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')
}