import { Router } from 'express'; import { requireAuth } from '../../auth-connector/source/server/auth'; import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; const router = Router(); router.use( requireAuth ); interface FileNode { name: string; path: string; type: 'file' | 'directory'; children?: FileNode[]; } function safeResolve( root: string, filePath: string ): string | null { const resolvedRoot = path.resolve( root ); const full = path.resolve( path.join( resolvedRoot, filePath ) ); if ( !full.startsWith( resolvedRoot + path.sep ) && full !== resolvedRoot ) return null; return full; } function buildTree( absDir: string, rootDir: string ): FileNode[] { return fs.readdirSync( absDir ).filter( name => name !== '.roject' ).map( name => { const abs = path.join( absDir, name ); const rel = path.relative( rootDir, abs ).replace( /\\/g, '/' ); if ( fs.statSync( abs ).isDirectory() ) { return { name, path: rel, type: 'directory' as const, children: buildTree( abs, rootDir ) }; } return { name, path: rel, type: 'file' as const }; } ); } router.get( '/tree', ( req, res ) => { const root = req.query.root as string; if ( !root ) { res.status( 400 ).json( { error: 'root required' } ); return; } const resolvedRoot = path.resolve( root ); if ( !fs.existsSync( resolvedRoot ) ) { res.status( 404 ).json( { error: 'Directory not found' } ); return; } res.json( buildTree( resolvedRoot, resolvedRoot ) ); } ); router.get( '/read', ( req, res ) => { const root = req.query.root as string; const filePath = req.query.path as string; if ( !root || !filePath ) { res.status( 400 ).json( { error: 'root and path required' } ); return; } const full = safeResolve( root, filePath ); if ( !full || !fs.existsSync( full ) ) { res.status( 404 ).json( { error: 'Not found' } ); return; } res.type( 'text/plain' ).send( fs.readFileSync( full, 'utf8' ) ); } ); router.put( '/write', ( req, res ) => { const root = req.query.root as string; const filePath = req.query.path as string; if ( !root || !filePath ) { res.status( 400 ).json( { error: 'root and path required' } ); return; } if ( typeof req.body !== 'string' ) { res.status( 400 ).json( { error: 'Content must be text' } ); return; } const full = safeResolve( root, filePath ); if ( !full ) { res.status( 403 ).json( { error: 'Invalid path' } ); return; } fs.mkdirSync( path.dirname( full ), { recursive: true } ); fs.writeFileSync( full, req.body, 'utf8' ); res.json( { ok: true } ); } ); router.post( '/create-file', ( req, res ) => { const { root, path: filePath } = req.body as { root: string; path: string }; if ( !root || !filePath ) { res.status( 400 ).json( { error: 'root and path required' } ); return; } const full = safeResolve( root, filePath ); if ( !full || fs.existsSync( full ) ) { res.status( 409 ).json( { error: 'Already exists or invalid path' } ); return; } fs.mkdirSync( path.dirname( full ), { recursive: true } ); fs.writeFileSync( full, '', 'utf8' ); res.json( { ok: true } ); } ); router.post( '/create-directory', ( req, res ) => { const { root, path: dirPath } = req.body as { root: string; path: string }; if ( !root || !dirPath ) { res.status( 400 ).json( { error: 'root and path required' } ); return; } const full = safeResolve( root, dirPath ); if ( !full || fs.existsSync( full ) ) { res.status( 409 ).json( { error: 'Already exists or invalid path' } ); return; } fs.mkdirSync( full, { recursive: true } ); res.json( { ok: true } ); } ); router.post( '/rename', ( req, res ) => { const { root, path: oldPath, newName } = req.body as { root: string; path: string; newName: string }; if ( !root || !oldPath || !newName ) { res.status( 400 ).json( { error: 'root, path, and newName required' } ); return; } if ( newName.includes( '/' ) || newName.includes( '\\' ) ) { res.status( 400 ).json( { error: 'Invalid name' } ); return; } const full = safeResolve( root, oldPath ); if ( !full || !fs.existsSync( full ) ) { res.status( 404 ).json( { error: 'Not found' } ); return; } const resolvedRoot = path.resolve( root ); const newFull = path.join( path.dirname( full ), newName ); if ( !newFull.startsWith( resolvedRoot + path.sep ) ) { res.status( 403 ).json( { error: 'Invalid path' } ); return; } if ( fs.existsSync( newFull ) ) { res.status( 409 ).json( { error: 'Already exists' } ); return; } fs.renameSync( full, newFull ); res.json( { ok: true } ); } ); router.post( '/delete', ( req, res ) => { const { root, path: targetPath } = req.body as { root: string; path: string }; if ( !root || !targetPath ) { res.status( 400 ).json( { error: 'root and path required' } ); return; } const full = safeResolve( root, targetPath ); if ( !full || !fs.existsSync( full ) ) { res.status( 404 ).json( { error: 'Not found' } ); return; } fs.rmSync( full, { recursive: true, force: true } ); res.json( { ok: true } ); } ); // ── Rojo helpers ───────────────────────────────────────────────────────────── function pickRandom( arr: T[] ): T { return arr[ Math.floor( Math.random() * arr.length ) ]; } function capitalize( s: string ): string { return s[ 0 ].toUpperCase() + s.slice( 1 ); } const ROJO_ADJECTIVES = [ "funky", "cool", "groovy", "wild", "sharp", "slick", "swift", "bold", "bright", "calm", "daring", "eager", "fierce", "jolly", "lively", "quirky", "smart", "vivid", "zesty", "sleek", "crisp", "nifty", "rad", "sunny", "sassy", "snappy", "peppy", "zippy", "breezy", "mellow", ]; const ROJO_ROLES = [ "boss", "doctor", "player", "teacher", "hunter", "builder", "maker", "dancer", "singer", "painter", "writer", "coder", "pilot", "chef", "sailor", "rider", "climber", "dreamer", "wanderer", "scout", "keeper", "helper", "guide", "mentor", "runner", "seeker", "ranger", "scholar", ]; const ROJO_NAMES = [ "Sofia", "Elena", "Lucas", "Mia", "Noah", "Emma", "Leon", "Lena", "Felix", "Anna", "Max", "Clara", "Julian", "Sara", "Lars", "Nina", "Tom", "Lara", "Erik", "Ida", "Hugo", "Vera", "Otto", "Maja", "Kai", "Maya", "Arjun", "Zara", "Aisha", "Omar", "Nala", "Ravi", "Yuki", "Kenji", "Amara", "Diego", "Mateo", "Jae", "Sora", "Kira", "Rio", "Zion", "Nova", "Leila", "Cyrus", "Noa", "Bao", "Mila", ]; function generateLocalRojoName(): { display: string; slug: string } { const adj = pickRandom( ROJO_ADJECTIVES ); const role = pickRandom( ROJO_ROLES ); const name = pickRandom( ROJO_NAMES ); return { display: `${ capitalize( adj ) } ${ capitalize( role ) } ${ name }`, slug: `${ adj }-${ role }-${ name.toLowerCase() }`, }; } function scanLocalRojoFiles( dir: string, rootDir: string, out: string[] = [] ): string[] { if ( !fs.existsSync( dir ) ) return out; for ( const entry of fs.readdirSync( dir, { withFileTypes: true } ) ) { const full = path.join( dir, entry.name ); if ( entry.isDirectory() ) { scanLocalRojoFiles( full, rootDir, out ); } else if ( entry.name.endsWith( '.rojo' ) ) { out.push( path.relative( rootDir, full ).replace( /\\/g, '/' ) ); } } return out; } // ── Local rojo list ────────────────────────────────────────────────────────── router.get( '/rojos', ( req, res ) => { const root = req.query.root as string; if ( !root ) { res.status( 400 ).json( { error: 'root required' } ); return; } const resolvedRoot = path.resolve( root ); const rojoDir = path.join( resolvedRoot, 'workspace', 'rojos' ); const files = scanLocalRojoFiles( rojoDir, resolvedRoot ); const rojos = files.map( filePath => { let id = '', name = filePath, description = ''; const full = safeResolve( root, filePath ); if ( full && fs.existsSync( full ) ) { try { const s = JSON.parse( fs.readFileSync( full, 'utf8' ) ); id = s.id || ''; name = s.name || name; description = s.description || ''; } catch {} } return { id, name, description, path: filePath }; } ); res.json( rojos ); } ); // ── Local rojo create ──────────────────────────────────────────────────────── router.post( '/rojos/create', ( req, res ) => { const { root, parentDir: parentDirArg } = req.body as { root: string; parentDir?: string }; if ( !root ) { res.status( 400 ).json( { error: 'root required' } ); return; } const dir = parentDirArg || 'workspace/rojos'; const { display, slug } = generateLocalRojoName(); const id = crypto.randomUUID(); const filePath = `${ dir }/${ slug }.rojo`; const full = safeResolve( root, filePath ); if ( !full ) { res.status( 403 ).json( { error: 'Invalid path' } ); return; } const defaults = JSON.stringify( { id, name: display, description: '', systemPrompt: '', endpoint: { type: 'external', url: '', model: '', apiKey: '', tunnelId: '' }, appearance: { colors: [], layers: [] }, }, null, 2 ); fs.mkdirSync( path.dirname( full ), { recursive: true } ); fs.writeFileSync( full, defaults, 'utf8' ); res.json( { id, path: filePath, name: display } ); } ); export default router;