import { Effect, Context, Layer } from "effect"; import { execSync } from "child_process"; import * as fs from "fs/promises"; import * as path from "path"; /** * Directory Management Service * * Handles creation and management of user home directories, * Claude Code configurations, and workspace setup for isolated * multi-user environments. * * Directory Structure: * /home/{username}/ * ├── .claude/ # Claude Code configuration * │ ├── config.json # User-specific Claude settings * │ ├── sessions/ # Session storage * │ └── cache/ # Cache directory * ├── workspace/ # User's workspace directory * └── .bashrc # Shell configuration */ export interface DirectorySetupOptions { username: string; homeDir: string; uid: number; gid: number; claudeConfig?: ClaudeConfig; } export interface ClaudeConfig { apiKey?: string; model?: string; maxTokens?: number; temperature?: number; } export interface DirectoryError { readonly _tag: "DirectoryError"; readonly message: string; readonly cause?: unknown; } const DirectoryError = (message: string, cause?: unknown): DirectoryError => ({ _tag: "DirectoryError", message, cause, }); export class DirectoryService extends Context.Tag("DirectoryService")< DirectoryService, { /** * Set up complete home directory structure for a user */ readonly setupHomeDirectory: ( options: DirectorySetupOptions ) => Effect.Effect; /** * Create Claude Code configuration directory */ readonly createClaudeConfig: ( homeDir: string, config: ClaudeConfig, uid: number, gid: number ) => Effect.Effect; /** * Create user workspace directory */ readonly createWorkspace: ( homeDir: string, uid: number, gid: number ) => Effect.Effect; /** * Set proper ownership and permissions */ readonly setOwnership: ( dirPath: string, uid: number, gid: number, recursive?: boolean ) => Effect.Effect; /** * Clean up user directories */ readonly cleanupDirectories: ( homeDir: string ) => Effect.Effect; } >() { static Live = Layer.succeed(this, { setupHomeDirectory: (options: DirectorySetupOptions) => Effect.gen(function* () { const { username, homeDir, uid, gid, claudeConfig } = options; // Ensure home directory exists (should be created by useradd) yield* Effect.tryPromise({ try: async () => { await fs.access(homeDir); }, catch: (error) => DirectoryError( `Home directory ${homeDir} does not exist. Create user first.`, error ), }); // Create .claude directory const claudeDir = path.join(homeDir, ".claude"); yield* Effect.tryPromise({ try: async () => { await fs.mkdir(claudeDir, { recursive: true, mode: 0o755 }); }, catch: (error) => DirectoryError(`Failed to create .claude directory`, error), }); // Create subdirectories const subdirs = ["sessions", "cache", "workspace"]; for (const subdir of subdirs) { const subdirPath = subdir === "workspace" ? path.join(homeDir, subdir) : path.join(claudeDir, subdir); yield* Effect.tryPromise({ try: async () => { await fs.mkdir(subdirPath, { recursive: true, mode: 0o755 }); }, catch: (error) => DirectoryError(`Failed to create ${subdir} directory`, error), }); } // Create Claude config file if (claudeConfig) { const configPath = path.join(claudeDir, "config.json"); const configContent = JSON.stringify( { apiKey: claudeConfig.apiKey || "", model: claudeConfig.model || "claude-sonnet-4-20250514", maxTokens: claudeConfig.maxTokens || 8192, temperature: claudeConfig.temperature || 1.0, username, createdAt: new Date().toISOString(), }, null, 2 ); yield* Effect.tryPromise({ try: async () => { await fs.writeFile(configPath, configContent, { mode: 0o600, }); }, catch: (error) => DirectoryError("Failed to create Claude config file", error), }); } // Create .bashrc with helpful aliases const bashrcPath = path.join(homeDir, ".bashrc"); const bashrcContent = `# MyAIDev Method User Environment # Generated on ${new Date().toISOString()} # Helpful aliases alias ll='ls -alh' alias workspace='cd ~/workspace' alias claude='cd ~/.claude' # Environment export PATH="$HOME/.local/bin:$PATH" export CLAUDE_CONFIG_DIR="$HOME/.claude" # Workspace directory export WORKSPACE="$HOME/workspace" # Welcome message echo "Welcome to MyAIDev Method, ${username}!" echo "Your workspace is in: ~/workspace" `; yield* Effect.tryPromise({ try: async () => { await fs.writeFile(bashrcPath, bashrcContent, { mode: 0o644 }); }, catch: (error) => DirectoryError("Failed to create .bashrc file", error), }); // Set ownership of all created files/directories yield* Effect.tryPromise({ try: async () => { execSync(`sudo chown -R ${uid}:${gid} ${homeDir}`, { stdio: "pipe", }); }, catch: (error) => DirectoryError( `Failed to set ownership for ${homeDir}`, error ), }); // Set proper permissions yield* Effect.tryPromise({ try: async () => { execSync(`sudo chmod 755 ${homeDir}`, { stdio: "pipe" }); execSync(`sudo chmod 700 ${claudeDir}`, { stdio: "pipe" }); execSync(`sudo chmod 755 ${path.join(homeDir, "workspace")}`, { stdio: "pipe", }); }, catch: (error) => DirectoryError("Failed to set directory permissions", error), }); }), createClaudeConfig: ( homeDir: string, config: ClaudeConfig, uid: number, gid: number ) => Effect.gen(function* () { const claudeDir = path.join(homeDir, ".claude"); const configPath = path.join(claudeDir, "config.json"); // Ensure .claude directory exists yield* Effect.tryPromise({ try: async () => { await fs.mkdir(claudeDir, { recursive: true, mode: 0o700 }); }, catch: (error) => DirectoryError("Failed to create .claude directory", error), }); // Write config file const configContent = JSON.stringify(config, null, 2); yield* Effect.tryPromise({ try: async () => { await fs.writeFile(configPath, configContent, { mode: 0o600 }); }, catch: (error) => DirectoryError("Failed to write Claude config", error), }); // Set ownership yield* Effect.tryPromise({ try: async () => { execSync(`sudo chown -R ${uid}:${gid} ${claudeDir}`, { stdio: "pipe", }); }, catch: (error) => DirectoryError("Failed to set config ownership", error), }); }), createWorkspace: (homeDir: string, uid: number, gid: number) => Effect.gen(function* () { const workspaceDir = path.join(homeDir, "workspace"); // Create workspace directory yield* Effect.tryPromise({ try: async () => { await fs.mkdir(workspaceDir, { recursive: true, mode: 0o755 }); }, catch: (error) => DirectoryError("Failed to create workspace directory", error), }); // Create a README file const readmePath = path.join(workspaceDir, "README.md"); const readmeContent = `# Welcome to Your MyAIDev Method Workspace This is your personal workspace for AI-assisted development with Claude Code. ## Directory Structure - Use this workspace to create and manage your projects - Claude Code will have access to files in this directory - Your work is isolated from other users ## Getting Started 1. Create a new project directory 2. Initialize your project (git init, npm init, etc.) 3. Start working with Claude Code assistance Happy coding! `; yield* Effect.tryPromise({ try: async () => { await fs.writeFile(readmePath, readmeContent, { mode: 0o644 }); }, catch: (error) => DirectoryError("Failed to create workspace README", error), }); // Set ownership yield* Effect.tryPromise({ try: async () => { execSync(`sudo chown -R ${uid}:${gid} ${workspaceDir}`, { stdio: "pipe", }); }, catch: (error) => DirectoryError("Failed to set workspace ownership", error), }); }), setOwnership: ( dirPath: string, uid: number, gid: number, recursive: boolean = false ) => Effect.tryPromise({ try: async () => { const recursiveFlag = recursive ? "-R" : ""; execSync(`sudo chown ${recursiveFlag} ${uid}:${gid} ${dirPath}`, { stdio: "pipe", }); }, catch: (error) => DirectoryError(`Failed to set ownership for ${dirPath}`, error), }), cleanupDirectories: (homeDir: string) => Effect.tryPromise({ try: async () => { // Remove home directory and all contents execSync(`sudo rm -rf ${homeDir}`, { stdio: "pipe" }); }, catch: (error) => DirectoryError(`Failed to cleanup directories for ${homeDir}`, error), }), }); }