import { Inject, Injectable, Logger } from '@nestjs/common'; import type { Brain } from '@mosaicstack/brain'; import { BRAIN } from '../brain/brain.tokens.js'; import { PluginService } from '../plugin/plugin.service.js'; import { WorkspaceService } from './workspace.service.js'; export interface BootstrapProjectParams { name: string; description?: string; userId: string; teamId?: string; repoUrl?: string; } export interface BootstrapProjectResult { projectId: string; workspacePath: string; } @Injectable() export class ProjectBootstrapService { private readonly logger = new Logger(ProjectBootstrapService.name); constructor( @Inject(BRAIN) private readonly brain: Brain, private readonly workspace: WorkspaceService, private readonly pluginService: PluginService, ) {} /** * Bootstrap a new project: create DB record + workspace directory. * Returns the created project with its workspace path. */ async bootstrap(params: BootstrapProjectParams): Promise { const ownerType: 'user' | 'team' = params.teamId ? 'team' : 'user'; this.logger.log( `Bootstrapping project "${params.name}" for ${ownerType} ${params.teamId ?? params.userId}`, ); // 1. Create DB record const project = await this.brain.projects.create({ name: params.name, description: params.description, ownerId: params.userId, teamId: params.teamId ?? null, ownerType, }); // 2. Create workspace directory (includes docs structure) const workspacePath = await this.workspace.create( { id: project.id, ownerType, userId: params.userId, teamId: params.teamId ?? null, }, params.repoUrl, ); // 3. Create default agent config for the project await this.brain.agents.create({ name: 'default', provider: '', model: '', projectId: project.id, ownerId: params.userId, isSystem: false, status: 'active', }); // 4. Notify plugins so they can set up project-specific resources (e.g. Discord channel) try { for (const plugin of this.pluginService.getPlugins()) { if (plugin.onProjectCreated) { const result = await plugin.onProjectCreated({ id: project.id, name: params.name, description: params.description, }); if (result?.channelId) { await this.brain.projects.update(project.id, { metadata: { discordChannelId: result.channelId }, }); } } } } catch (err) { this.logger.warn( `Plugin project notification failed: ${err instanceof Error ? err.message : String(err)}`, ); } this.logger.log(`Project ${project.id} bootstrapped at ${workspacePath}`); return { projectId: project.id, workspacePath }; } }