Projects
This commit is contained in:
@@ -1,128 +0,0 @@
|
||||
import { mkdir, readdir, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { createRouter } from '../../create-router';
|
||||
import { getUserProjectsDir } from '@@/data-path';
|
||||
|
||||
export const projectsRouter = createRouter();
|
||||
|
||||
type KeyMapping = { file: string; dir?: string };
|
||||
|
||||
function resolveKey(projDir: string, key: string): KeyMapping | null {
|
||||
if (key === 'projects') return { file: join(projDir, 'index.json') };
|
||||
|
||||
const layoutMatch = key.match(/^proj-layout-(.+)$/);
|
||||
if (layoutMatch) {
|
||||
const id = layoutMatch[1]!;
|
||||
const dir = join(projDir, id);
|
||||
return { file: join(dir, 'layout.json'), dir };
|
||||
}
|
||||
|
||||
const terminalsMatch = key.match(/^proj-terminals-(.+)$/);
|
||||
if (terminalsMatch) {
|
||||
const id = terminalsMatch[1]!;
|
||||
const dir = join(projDir, id);
|
||||
return { file: join(dir, 'terminals.json'), dir };
|
||||
}
|
||||
|
||||
const hostTerminalsMatch = key.match(/^proj-host-terminals-(.+)$/);
|
||||
if (hostTerminalsMatch) {
|
||||
const id = hostTerminalsMatch[1]!;
|
||||
const dir = join(projDir, id);
|
||||
return { file: join(dir, 'host-terminals.json'), dir };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function readJsonFile(path: string): Promise<unknown | null> {
|
||||
const file = Bun.file(path);
|
||||
if (await file.exists()) return file.json();
|
||||
return null;
|
||||
}
|
||||
|
||||
async function writeJsonFile(path: string, data: unknown) {
|
||||
await Bun.write(path, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
async function readProjectDir(dirPath: string, id: string, result: Record<string, unknown>) {
|
||||
const layout = await readJsonFile(join(dirPath, 'layout.json'));
|
||||
if (layout !== null) result[`proj-layout-${id}`] = layout;
|
||||
|
||||
const terminals = await readJsonFile(join(dirPath, 'terminals.json'));
|
||||
if (terminals !== null) result[`proj-terminals-${id}`] = terminals;
|
||||
|
||||
const hostTerminals = await readJsonFile(join(dirPath, 'host-terminals.json'));
|
||||
if (hostTerminals !== null) result[`proj-host-terminals-${id}`] = hostTerminals;
|
||||
}
|
||||
|
||||
async function readAllProjectsState(projDir: string): Promise<Record<string, unknown>> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
const indexData = await readJsonFile(join(projDir, 'index.json'));
|
||||
if (indexData !== null) result['projects'] = indexData;
|
||||
|
||||
let entries: import('node:fs').Dirent[] = [];
|
||||
try {
|
||||
entries = await readdir(projDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
await readProjectDir(join(projDir, entry.name), entry.name, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// GET /projects-state
|
||||
projectsRouter.get('/projects-state', async (ctx) => {
|
||||
const email = ctx.get('user').email;
|
||||
const projDir = getUserProjectsDir(email);
|
||||
|
||||
const state = await readAllProjectsState(projDir);
|
||||
return ctx.json(state);
|
||||
});
|
||||
|
||||
// PATCH /projects-state
|
||||
projectsRouter.patch('/projects-state', async (ctx) => {
|
||||
const email = ctx.get('user').email;
|
||||
const body = ctx.get('body') as Record<string, unknown>;
|
||||
const projDir = getUserProjectsDir(email);
|
||||
|
||||
await mkdir(projDir, { recursive: true });
|
||||
|
||||
for (const [key, value] of Object.entries(body)) {
|
||||
// When writing the projects list, resolve cwds and create project dirs
|
||||
if (key === 'projects' && Array.isArray(value)) {
|
||||
for (const project of value) {
|
||||
const projectFilesDir = join(projDir, project.id, 'files');
|
||||
project.cwd = projectFilesDir;
|
||||
await mkdir(projectFilesDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
const mapping = resolveKey(projDir, key);
|
||||
if (!mapping) continue;
|
||||
|
||||
if (value === null) {
|
||||
try {
|
||||
await rm(mapping.file, { force: true });
|
||||
if (mapping.dir) {
|
||||
const remaining = await readdir(mapping.dir);
|
||||
if (remaining.length === 0) await rm(mapping.dir, { recursive: true, force: true });
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mapping.dir) await mkdir(mapping.dir, { recursive: true });
|
||||
await writeJsonFile(mapping.file, value);
|
||||
}
|
||||
|
||||
const state = await readAllProjectsState(projDir);
|
||||
return ctx.json(state);
|
||||
});
|
||||
@@ -288,6 +288,7 @@ const containerHome = '/home/officer';
|
||||
const resolveCwd = (home: string, cwd?: string) => {
|
||||
if (!cwd || cwd === '~') return home;
|
||||
if (cwd.startsWith('~/')) return join(home, cwd.slice(2));
|
||||
if (cwd.startsWith('/')) return join(home, cwd.slice(1));
|
||||
return home;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export type KeyMapping = { file: string; dir?: string };
|
||||
|
||||
export type ResolveDirs = { wsDir: string; screensDir: string };
|
||||
export type ResolveDirs = { wsDir: string; screensDir: string; projDir: string };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mkdir, readdir, rename, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { getUserWorkspacesDir, getUserHomepageWorkspaceDir, getUserStateFile } from '@@/data-path';
|
||||
import { getUserWorkspacesDir, getUserHomepageWorkspaceDir, getUserStateFile, getUserProjectsDir } from '@@/data-path';
|
||||
import type { KeyMapping, ResolveDirs } from './types';
|
||||
|
||||
const RESERVED_DIRS = new Set(['screens']);
|
||||
@@ -39,6 +39,35 @@ export function resolveKey(dirs: ResolveDirs, key: string): KeyMapping | null {
|
||||
return { file: join(dir, 'host-terminals.json'), dir };
|
||||
}
|
||||
|
||||
// Project keys — stored in {projDir}/{slug}/.officerdev/
|
||||
const projMetaMatch = key.match(/^proj-meta-(.+)$/);
|
||||
if (projMetaMatch) {
|
||||
const slug = projMetaMatch[1]!;
|
||||
const dir = join(dirs.projDir, slug, '.officerdev');
|
||||
return { file: join(dir, 'meta.json'), dir };
|
||||
}
|
||||
|
||||
const projLayoutMatch = key.match(/^proj-layout-(.+)$/);
|
||||
if (projLayoutMatch) {
|
||||
const slug = projLayoutMatch[1]!;
|
||||
const dir = join(dirs.projDir, slug, '.officerdev');
|
||||
return { file: join(dir, 'layout.json'), dir };
|
||||
}
|
||||
|
||||
const projTerminalsMatch = key.match(/^proj-terminals-(.+)$/);
|
||||
if (projTerminalsMatch) {
|
||||
const slug = projTerminalsMatch[1]!;
|
||||
const dir = join(dirs.projDir, slug, '.officerdev');
|
||||
return { file: join(dir, 'terminals.json'), dir };
|
||||
}
|
||||
|
||||
const projHostTerminalsMatch = key.match(/^proj-host-terminals-(.+)$/);
|
||||
if (projHostTerminalsMatch) {
|
||||
const slug = projHostTerminalsMatch[1]!;
|
||||
const dir = join(dirs.projDir, slug, '.officerdev');
|
||||
return { file: join(dir, 'host-terminals.json'), dir };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -125,6 +154,23 @@ async function readWorkspaceDir(dirPath: string, id: string, result: Record<stri
|
||||
if (hostTerminals !== null) result[`ws-host-terminals-${id}`] = hostTerminals;
|
||||
}
|
||||
|
||||
async function readProjectDir(projectPath: string, slug: string, result: Record<string, unknown>): Promise<unknown | null> {
|
||||
const officerdevDir = join(projectPath, '.officerdev');
|
||||
const meta = await readJsonFile(join(officerdevDir, 'meta.json'));
|
||||
if (!meta) return null;
|
||||
|
||||
const layout = await readJsonFile(join(officerdevDir, 'layout.json'));
|
||||
if (layout !== null) result[`proj-layout-${slug}`] = layout;
|
||||
|
||||
const terminals = await readJsonFile(join(officerdevDir, 'terminals.json'));
|
||||
if (terminals !== null) result[`proj-terminals-${slug}`] = terminals;
|
||||
|
||||
const hostTerminals = await readJsonFile(join(officerdevDir, 'host-terminals.json'));
|
||||
if (hostTerminals !== null) result[`proj-host-terminals-${slug}`] = hostTerminals;
|
||||
|
||||
return { ...(meta as object), id: slug, cwd: `/Projects/${slug}` };
|
||||
}
|
||||
|
||||
async function readScreensDir(screensDir: string, result: Record<string, unknown>) {
|
||||
let entries: import('node:fs').Dirent[] = [];
|
||||
try {
|
||||
@@ -168,6 +214,23 @@ export async function readAllWorkspacesState(dirs: ResolveDirs): Promise<Record<
|
||||
await readWorkspaceDir(join(dirs.wsDir, entry.name), entry.name, result);
|
||||
}
|
||||
|
||||
// Read project data — scan directories containing .officerdev/meta.json
|
||||
const projects: unknown[] = [];
|
||||
let projEntries: import('node:fs').Dirent[] = [];
|
||||
try {
|
||||
projEntries = await readdir(dirs.projDir, { withFileTypes: true });
|
||||
} catch {
|
||||
result['projects'] = projects;
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const entry of projEntries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const meta = await readProjectDir(join(dirs.projDir, entry.name), entry.name, result);
|
||||
if (meta) projects.push(meta);
|
||||
}
|
||||
result['projects'] = projects;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -175,5 +238,6 @@ export function getDirs(email: string): ResolveDirs {
|
||||
return {
|
||||
wsDir: getUserWorkspacesDir(email),
|
||||
screensDir: join(getUserWorkspacesDir(email), 'screens'),
|
||||
projDir: getUserProjectsDir(email),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { mkdir, readdir, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { createRouter } from '@@/create-router';
|
||||
import {
|
||||
getDirs,
|
||||
migrateFromState,
|
||||
migrateHomepageToScreens,
|
||||
readAllWorkspacesState,
|
||||
resolveKey,
|
||||
writeJsonFile,
|
||||
} from './utils';
|
||||
import { getDirs, migrateFromState, migrateHomepageToScreens, readAllWorkspacesState, resolveKey, writeJsonFile } from './utils';
|
||||
|
||||
export const workspacesRouter = createRouter();
|
||||
|
||||
@@ -37,6 +30,30 @@ workspacesRouter.patch('/', async (ctx) => {
|
||||
await mkdir(dirs.wsDir, { recursive: true });
|
||||
|
||||
for (const [key, value] of Object.entries(body)) {
|
||||
// Handle proj-meta-{slug}: create/update/delete project
|
||||
const projMetaMatch = key.match(/^proj-meta-(.+)$/);
|
||||
if (projMetaMatch) {
|
||||
const slug = projMetaMatch[1]!;
|
||||
const projectDir = join(dirs.projDir, slug);
|
||||
|
||||
if (value === null) {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
const officerdevDir = join(projectDir, '.officerdev');
|
||||
const metaFile = join(officerdevDir, 'meta.json');
|
||||
const isNew = !(await Bun.file(metaFile).exists());
|
||||
await mkdir(officerdevDir, { recursive: true });
|
||||
await writeJsonFile(metaFile, value);
|
||||
|
||||
if (isNew) {
|
||||
const proc = Bun.spawn(['git', 'init', projectDir]);
|
||||
await proc.exited;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const mapping = resolveKey(dirs, key);
|
||||
if (!mapping) continue;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user