All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
117 lines
3.7 KiB
TypeScript
117 lines
3.7 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
export interface WorkspaceProject {
|
|
id: string;
|
|
ownerType: 'user' | 'team';
|
|
userId: string;
|
|
teamId: string | null;
|
|
}
|
|
|
|
@Injectable()
|
|
export class WorkspaceService {
|
|
private readonly logger = new Logger(WorkspaceService.name);
|
|
private readonly mosaicRoot: string;
|
|
|
|
constructor() {
|
|
this.mosaicRoot = process.env['MOSAIC_ROOT'] ?? '/opt/mosaic';
|
|
}
|
|
|
|
/**
|
|
* Resolve the workspace path for a project.
|
|
* Solo: $MOSAIC_ROOT/.workspaces/users/<userId>/<projectId>/
|
|
* Team: $MOSAIC_ROOT/.workspaces/teams/<teamId>/<projectId>/
|
|
*/
|
|
resolvePath(project: WorkspaceProject): string {
|
|
if (project.ownerType === 'team' && project.teamId) {
|
|
return path.join(this.mosaicRoot, '.workspaces', 'teams', project.teamId, project.id);
|
|
}
|
|
return path.join(this.mosaicRoot, '.workspaces', 'users', project.userId, project.id);
|
|
}
|
|
|
|
/**
|
|
* Create a workspace directory and initialize it as a git repo.
|
|
* If repoUrl is provided, clone instead of init.
|
|
*/
|
|
async create(project: WorkspaceProject, repoUrl?: string): Promise<string> {
|
|
const workspacePath = this.resolvePath(project);
|
|
|
|
// Create directory
|
|
await fs.mkdir(workspacePath, { recursive: true });
|
|
|
|
if (repoUrl) {
|
|
// Clone existing repo
|
|
await execFileAsync('git', ['clone', repoUrl, '.'], { cwd: workspacePath });
|
|
this.logger.log(`Cloned ${repoUrl} into workspace ${workspacePath}`);
|
|
} else {
|
|
// Init new git repo
|
|
await execFileAsync('git', ['init'], { cwd: workspacePath });
|
|
await execFileAsync('git', ['commit', '--allow-empty', '-m', 'Initial workspace commit'], {
|
|
cwd: workspacePath,
|
|
env: {
|
|
...process.env,
|
|
GIT_AUTHOR_NAME: 'Mosaic',
|
|
GIT_AUTHOR_EMAIL: 'mosaic@localhost',
|
|
GIT_COMMITTER_NAME: 'Mosaic',
|
|
GIT_COMMITTER_EMAIL: 'mosaic@localhost',
|
|
},
|
|
});
|
|
this.logger.log(`Initialized git workspace at ${workspacePath}`);
|
|
}
|
|
|
|
// Create standard docs structure
|
|
await fs.mkdir(path.join(workspacePath, 'docs', 'plans'), { recursive: true });
|
|
await fs.mkdir(path.join(workspacePath, 'docs', 'reports'), { recursive: true });
|
|
this.logger.log(`Created docs structure at ${workspacePath}`);
|
|
|
|
return workspacePath;
|
|
}
|
|
|
|
/**
|
|
* Delete a workspace directory recursively.
|
|
*/
|
|
async delete(project: WorkspaceProject): Promise<void> {
|
|
const workspacePath = this.resolvePath(project);
|
|
try {
|
|
await fs.rm(workspacePath, { recursive: true, force: true });
|
|
this.logger.log(`Deleted workspace at ${workspacePath}`);
|
|
} catch (err) {
|
|
this.logger.warn(`Failed to delete workspace at ${workspacePath}: ${err}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check whether the workspace directory exists.
|
|
*/
|
|
async exists(project: WorkspaceProject): Promise<boolean> {
|
|
const workspacePath = this.resolvePath(project);
|
|
try {
|
|
await fs.access(workspacePath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create the base user workspace directory (call on user registration).
|
|
*/
|
|
async createUserRoot(userId: string): Promise<void> {
|
|
const userRoot = path.join(this.mosaicRoot, '.workspaces', 'users', userId);
|
|
await fs.mkdir(userRoot, { recursive: true });
|
|
}
|
|
|
|
/**
|
|
* Create the base team workspace directory (call on team creation).
|
|
*/
|
|
async createTeamRoot(teamId: string): Promise<void> {
|
|
const teamRoot = path.join(this.mosaicRoot, '.workspaces', 'teams', teamId);
|
|
await fs.mkdir(teamRoot, { recursive: true });
|
|
}
|
|
}
|