char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
5831c1d
main
1import type { Context } from "@earendil-works/chord" ; 2import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context" ; 3import { getOrThrow } from "@earendil-works/pi-durable/env" ; 4import type { Environment } from "@berry/api" ; 5 6async function jj ( env :Environment , cwd :string , args :string [], context :Context ) { 7const streams = { stdout :"" , stderr :"" }; 8const result = getOrThrow ( 9await env . exec ([ "jj" , "--no-pager" , "--color=never" , ...args ], { 10 cwd, 11onOutput :( text , _ , { stream}) => void ( streams [ stream ] += text ), 12}, context ), 13); 14if ( result . exitCode !== 0 ) throw new Error ( streams . stderr . trim ()); 15return streams . stdout . trim (); 16} 17 18/** On either kind of machine: `/` on unix, a drive or a share on windows. */ 19const isAbsolute = ( path :string ) => / ^(?:\/|[A-Za-z]:[\\/]|\\\\) / . test ( path ); 20 21/** The shared repository directory, regardless of which workspace names it. */ 22async function repository ( env :Environment , root :string , context :Context ) :Promise < string > { 23const path = getOrThrow ( await env . joinPath ([ root , ".jj" , "repo" ], context )); 24if ( getOrThrow ( await env . fileInfo ( path , context )). kind === "directory" ) { 25return getOrThrow ( await env . canonicalPath ( path , context )); 26} 27// a secondary workspace's names the repository, relative to its .jj 28const named = getOrThrow ( await env . readTextFile ( path , context )). trim (); 29const resolved = isAbsolute ( named ) 30 ?named 31 :getOrThrow ( await env . joinPath ([ root , ".jj" , named ], context )); 32return getOrThrow ( await env . canonicalPath ( resolved , context )); 33} 34 35/** The last component of a path, on either kind of machine. */ 36const basename = ( path :string ) => path . replace ( / [\\/]+$ / , "" ). split ( / [\\/] / ). at ( - 1 ) ?? "" ; 37 38/** Whether the working directory belongs to a reachable jj workspace. */ 39export async function hasWorkspace ( env :Environment ) :Promise < boolean > { 40try { 41await jj ( env , env . cwd , [ "--ignore-working-copy" , "workspace" , "root" ], BACKGROUND_CONTEXT ); 42return true ; 43} catch { 44return false ; 45} 46} 47 48/** Create or reuse a conversation's workspace without rewriting existing work. */ 49export async function workspace ( env :Environment , name :string , context :Context ) :Promise < string > { 50if ( ! /^[a-zA-Z0-9_-]+$ / . test ( name )) throw new Error ( "Invalid conversation ID for a workspace." ); 51let source :string ; 52try { 53source = await jj ( env , env . cwd , [ "--ignore-working-copy" , "workspace" , "root" ], context ); 54} catch ( error ) { 55throw new Error ( `Cannot create a workspace: cwd must be in a jj repository.\n ${ error } ` ); 56} 57const repo = await repository ( env , source , context ); 58const names = await jj ( env , source , [ 59"--ignore-working-copy" , 60"workspace" , 61"list" , 62"--template" , 63'name ++ "\n"' , 64], context ); 65if ( names . split ( "\n" ). includes ( name )) { 66const target = getOrThrow ( 67await env . canonicalPath ( 68await jj ( 69env , 70source , 71[ "--ignore-working-copy" , "workspace" , "root" , "--name" , name ], 72context , 73), 74context , 75), 76); 77const root = await jj ( env , target , [ "--ignore-working-copy" , "workspace" , "root" ], context ); 78if ( 79getOrThrow ( await env . canonicalPath ( root , context )) !== target || 80await repository ( env , target , context ) !== repo 81) { 82throw new Error ( `Workspace ${ name } no longer belongs to this repository: ${ target } ` ); 83} 84return target ; 85} 86 87const base = env . variables . BERRY_WORKSPACES_DIR ?? "/x/workspaces" ; 88if ( ! isAbsolute ( base )) throw new Error ( "BERRY_WORKSPACES_DIR must be an absolute path." ); 89// the repository is <project>/.jj/repo 90const project = getOrThrow ( await env . joinPath ([ repo , ".." , ".." ], context )); 91const parent = getOrThrow ( await env . joinPath ([ base , basename ( project )], context )); 92const target = getOrThrow ( await env . joinPath ([ parent , name ], context )); 93// empty working-copy commits are mutable staging areas, not shared task bases. 94const parents = await jj ( env , source , [ 95"log" , 96"--no-graph" , 97"--revisions" , 98"heads(ancestors(@-) & ~empty() & ~root())" , 99"--template" , 100'commit_id ++ "\n"' , 101], context ); 102if ( ! parents ) throw new Error ( "Cannot create a workspace: no nonempty ancestor other than root." ); 103getOrThrow ( await env . createDir ( parent , { recursive :true }, context )); 104await jj ( env , source , [ 105"workspace" , 106"add" , 107"--name" , 108name , 109 ...parents . split ( "\n" ). flatMap (( parent ) => [ "--revision" , parent ]), 110target , 111], context ); 112return getOrThrow ( await env . canonicalPath ( target , context )); 113}