
Workspace.pudu
Pudu81 lines3.3 KB
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 }181920const SCRATCH_PREFIX: Str = "pudu-mcp-"212223const DEFAULT_FILE: Str = "Main.pudu"242526export fn fromEnvironment() -> Workspace {27 at(Env.variableOr(Server.ENV_WORKSPACE, "."))28}293031export 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}373839export 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}454647export 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}505152export 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