Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-mcp

Model Context Protocol server for Pudu language documentation and compiler tools

0.1.1Apache-2.01

InstallClose

Workspace.pudu

Pudu81 lines3.3 KB

GitHub ↗
1/** @Services.Workspace.Guard — confines paths and hosts inline source */2module PuduLangMcp.Services.Workspace34import Std.Env as Env5import Std.Fs as Fs6import Std.Io as Io7import Std.Option as Option8import Std.Path as Path9import PuduLangMcp.Constants.Server as Server10import PuduLangMcp.Domain.Code.SourceFile as SourceFile11import PuduLangMcp.Errors.ToolError as ToolError1213/** @Services.Workspace.Workspace — the directory path arguments stay inside */14export type Workspace = { root: Str }1516/** @Services.Workspace.Target — the file one command reads */17export type Target = { directory: Str, file: Str, source: Str, scratch: Bool }1819/// The prefix of every scratch directory.20const SCRATCH_PREFIX: Str = "pudu-mcp-"2122/// The file inline source is written to when it declares no module.23const DEFAULT_FILE: Str = "Main.pudu"2425/// The workspace named by `PUDU_MCP_ROOT`, else the current directory.26export fn fromEnvironment() -> Workspace {27  at(Env.variableOr(Server.ENV_WORKSPACE, "."))28}2930/// The workspace rooted at a directory, canonicalized when it exists.31export fn at(root: Str) -> Workspace {32  match Fs.canonical(root) {33    case Ok(real) => Workspace{root: real}34    case Err(_) => Workspace{root: root}35  }36}3738/// The real location of `path` inside the workspace, or why it is refused.39export fn resolve(workspace: &Workspace, path: Str) -> Result[Str, ToolError.ToolFailure] {40  match Fs.resolveInside(workspace.root, path) {41    case Ok(real) => Ok(real)42    case Err(_) => Err(ToolError.OutsideWorkspace(path))43  }44}4546/// A path relative to the workspace root when it is inside it.47export fn relative(workspace: &Workspace, path: Str) -> Str {48  if Path.isInside(workspace.root, path) { Option.unwrapOr(Path.relativeTo(workspace.root, path), path) } else { path }49}5051/// Runs `use` on the file `source` or `path` names; scratch files are removed afterwards.52export fn withTarget[T](workspace: &Workspace, source: Option[Str], path: Option[Str], use: fn(Target) -> T) -> Result[T, ToolError.ToolFailure] {53  match (source, path) {54    case (_, Some(given)) => {55      let real = resolve(workspace, given) ?56      if !real.endsWith(".pudu") { return Err(ToolError.OutsideWorkspace(given)) }57      let text = match Io.read(real) {58        case Ok(content) => content59        case Err(_) => { return Err(ToolError.OutsideWorkspace(given)) }60      }61      Ok(use(Target{directory: workspace.root, file: real, source: text, scratch: false}))62    }63    case (Some(text), None) => {64      let directory = match Fs.temporaryDirectoryIn(Env.temporaryDirectory(), SCRATCH_PREFIX) {65        case Ok(made) => made66        case Err(_) => { return Err(ToolError.Unavailable("A scratch directory for the source could not be created.")) }67      }68      let placed = Option.unwrapOr(Option.map(SourceFile.moduleNameOf(text), |name: Str| SourceFile.fileOf(name)), DEFAULT_FILE)69      let file = Path.join(directory, placed)70      let written = Io.makeDirectory(Path.directoryOf(file)) == Ok(()) && Io.write(file, text) == Ok(())71      let answer = if written { Some(use(Target{directory: directory, file: file, source: text, scratch: true})) } else { None }72      let _removed = Fs.removeTree(directory)73      match answer {74        case Some(value) => Ok(value)75        case None => Err(ToolError.Unavailable("The source could not be written to a scratch directory."))76      }77    }78    case _ => Err(ToolError.ExactlyOne(["source", "path"]))79  }80}81