ready for app-registry refactor

This commit is contained in:
2026-02-21 17:28:10 +00:00
parent 6d97edb6ab
commit fe7874dd65
21 changed files with 281 additions and 246 deletions
+3
View File
@@ -0,0 +1,3 @@
export * from './workspaces';
export * from './types';
export * from './utils';
+3
View File
@@ -0,0 +1,3 @@
export type KeyMapping = { file: string; dir?: string };
export type ResolveDirs = { wsDir: string; screensDir: string };
@@ -1,58 +1,58 @@
import { mkdir, readdir, rename, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { createRouter } from '../../create-router';
import { getUserWorkspacesDir, getUserHomepageWorkspaceDir, getUserStateFile } from '@@/data-path';
import type { KeyMapping, ResolveDirs } from './types';
export const workspacesRouter = createRouter();
const RESERVED_DIRS = new Set(['screens']);
type KeyMapping = { file: string; dir?: string };
type ResolveDirs = { wsDir: string; homepageDir: string };
function workspaceDir(dirs: ResolveDirs, id: string) {
return id === 'ws-homepage' ? dirs.homepageDir : join(dirs.wsDir, id);
}
function resolveKey(dirs: ResolveDirs, key: string): KeyMapping | null {
export function resolveKey(dirs: ResolveDirs, key: string): KeyMapping | null {
if (key === 'workspaces') return { file: join(dirs.wsDir, 'index.json') };
if (key === 'ws-terminals-default') return { file: join(dirs.wsDir, 'default-terminals.json') };
if (key === 'ws-host-terminals-default') return { file: join(dirs.wsDir, 'default-host-terminals.json') };
// screens/{name} → screens/{name}/layout.json
const screensMatch = key.match(/^screens\/(.+)$/);
if (screensMatch) {
const name = screensMatch[1]!;
const dir = join(dirs.screensDir, name);
return { file: join(dir, 'layout.json'), dir };
}
const layoutMatch = key.match(/^ws-layout-(.+)$/);
if (layoutMatch) {
const id = layoutMatch[1]!;
const dir = workspaceDir(dirs, id);
const dir = join(dirs.wsDir, id);
return { file: join(dir, 'layout.json'), dir };
}
const terminalsMatch = key.match(/^ws-terminals-(.+)$/);
if (terminalsMatch) {
const id = terminalsMatch[1]!;
const dir = workspaceDir(dirs, id);
const dir = join(dirs.wsDir, id);
return { file: join(dir, 'terminals.json'), dir };
}
const hostTerminalsMatch = key.match(/^ws-host-terminals-(.+)$/);
if (hostTerminalsMatch) {
const id = hostTerminalsMatch[1]!;
const dir = workspaceDir(dirs, id);
const dir = join(dirs.wsDir, id);
return { file: join(dir, 'host-terminals.json'), dir };
}
return null;
}
async function readJsonFile(path: string): Promise<unknown | null> {
export 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) {
export async function writeJsonFile(path: string, data: unknown) {
await Bun.write(path, JSON.stringify(data, null, 2));
}
async function migrateFromState(email: string, dirs: ResolveDirs) {
export async function migrateFromState(email: string, dirs: ResolveDirs) {
const stateFile = getUserStateFile(email);
const file = Bun.file(stateFile);
if (!(await file.exists())) return;
@@ -66,7 +66,7 @@ async function migrateFromState(email: string, dirs: ResolveDirs) {
await mkdir(dirs.wsDir, { recursive: true });
for (const key of wsKeys) {
const migratedKey = key === 'ws-layout-workspaces' ? 'ws-layout-ws-homepage' : key;
const migratedKey = key === 'ws-layout-workspaces' ? 'screens/homepage' : key;
const mapping = resolveKey(dirs, migratedKey);
if (!mapping) continue;
if (mapping.dir) await mkdir(mapping.dir, { recursive: true });
@@ -78,26 +78,40 @@ async function migrateFromState(email: string, dirs: ResolveDirs) {
await Bun.write(stateFile, JSON.stringify(cleaned, null, 2));
}
async function migrateListLayout(dirs: ResolveDirs) {
const oldFile = join(dirs.wsDir, 'list-layout.json');
if (!(await Bun.file(oldFile).exists())) return;
await mkdir(dirs.homepageDir, { recursive: true });
await rename(oldFile, join(dirs.homepageDir, 'layout.json'));
}
async function migrateHomepageFromWorkspaces(dirs: ResolveDirs) {
const oldDir = join(dirs.wsDir, 'ws-homepage');
const layoutFile = join(oldDir, 'layout.json');
export async function migrateHomepageToScreens(dirs: ResolveDirs, email: string) {
// Migrate from old ws-homepage/ dir to workspaces/screens/homepage/
const oldHomepageDir = getUserHomepageWorkspaceDir(email);
const layoutFile = join(oldHomepageDir, 'layout.json');
if (!(await Bun.file(layoutFile).exists())) return;
await mkdir(dirs.homepageDir, { recursive: true });
const targetDir = join(dirs.screensDir, 'homepage');
await mkdir(targetDir, { recursive: true });
for (const name of ['layout.json', 'terminals.json', 'host-terminals.json']) {
const src = join(oldDir, name);
const src = join(oldHomepageDir, name);
if (await Bun.file(src).exists()) {
await rename(src, join(dirs.homepageDir, name));
await rename(src, join(targetDir, name));
}
}
const remaining = await readdir(oldDir);
if (remaining.length === 0) await rm(oldDir, { recursive: true, force: true });
const remaining = await readdir(oldHomepageDir);
if (remaining.length === 0) await rm(oldHomepageDir, { recursive: true, force: true });
// Also migrate from workspaces/ws-homepage/ if it exists
const oldWsHomepageDir = join(dirs.wsDir, 'ws-homepage');
const oldWsLayout = join(oldWsHomepageDir, 'layout.json');
if (!(await Bun.file(oldWsLayout).exists())) return;
for (const name of ['layout.json', 'terminals.json', 'host-terminals.json']) {
const src = join(oldWsHomepageDir, name);
const target = join(targetDir, name);
if (await Bun.file(src).exists() && !(await Bun.file(target).exists())) {
await rename(src, target);
}
}
const wsRemaining = await readdir(oldWsHomepageDir);
if (wsRemaining.length === 0) await rm(oldWsHomepageDir, { recursive: true, force: true });
}
async function readWorkspaceDir(dirPath: string, id: string, result: Record<string, unknown>) {
@@ -111,7 +125,22 @@ async function readWorkspaceDir(dirPath: string, id: string, result: Record<stri
if (hostTerminals !== null) result[`ws-host-terminals-${id}`] = hostTerminals;
}
async function readAllWorkspacesState(dirs: ResolveDirs): Promise<Record<string, unknown>> {
async function readScreensDir(screensDir: string, result: Record<string, unknown>) {
let entries: import('node:fs').Dirent[] = [];
try {
entries = await readdir(screensDir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const layout = await readJsonFile(join(screensDir, entry.name, 'layout.json'));
if (layout !== null) result[`screens/${entry.name}`] = layout;
}
}
export async function readAllWorkspacesState(dirs: ResolveDirs): Promise<Record<string, unknown>> {
const result: Record<string, unknown> = {};
const indexData = await readJsonFile(join(dirs.wsDir, 'index.json'));
@@ -123,8 +152,8 @@ async function readAllWorkspacesState(dirs: ResolveDirs): Promise<Record<string,
const defaultHostTerminals = await readJsonFile(join(dirs.wsDir, 'default-host-terminals.json'));
if (defaultHostTerminals !== null) result['ws-host-terminals-default'] = defaultHostTerminals;
// Read ws-homepage from its own dir
await readWorkspaceDir(dirs.homepageDir, 'ws-homepage', result);
// Read screens
await readScreensDir(dirs.screensDir, result);
// Read per-workspace subdirs
let entries: import('node:fs').Dirent[] = [];
@@ -135,63 +164,16 @@ async function readAllWorkspacesState(dirs: ResolveDirs): Promise<Record<string,
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (!entry.isDirectory() || RESERVED_DIRS.has(entry.name)) continue;
await readWorkspaceDir(join(dirs.wsDir, entry.name), entry.name, result);
}
return result;
}
function getDirs(email: string): ResolveDirs {
return { wsDir: getUserWorkspacesDir(email), homepageDir: getUserHomepageWorkspaceDir(email) };
export function getDirs(email: string): ResolveDirs {
return {
wsDir: getUserWorkspacesDir(email),
screensDir: join(getUserWorkspacesDir(email), 'screens'),
};
}
// GET /workspaces-state
workspacesRouter.get('/workspaces-state', async (ctx) => {
const email = ctx.get('user').email;
const dirs = getDirs(email);
const dirFile = Bun.file(join(dirs.wsDir, 'index.json'));
if (!(await dirFile.exists())) {
await migrateFromState(email, dirs);
}
await migrateListLayout(dirs);
await migrateHomepageFromWorkspaces(dirs);
const state = await readAllWorkspacesState(dirs);
return ctx.json(state);
});
// PATCH /workspaces-state
workspacesRouter.patch('/workspaces-state', async (ctx) => {
const email = ctx.get('user').email;
const body = ctx.get('body') as Record<string, unknown>;
const dirs = getDirs(email);
await mkdir(dirs.wsDir, { recursive: true });
for (const [key, value] of Object.entries(body)) {
const mapping = resolveKey(dirs, 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 readAllWorkspacesState(dirs);
return ctx.json(state);
});
+62
View File
@@ -0,0 +1,62 @@
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';
export const workspacesRouter = createRouter();
// GET /workspaces
workspacesRouter.get('/', async (ctx) => {
const email = ctx.get('user').email;
const dirs = getDirs(email);
const dirFile = Bun.file(join(dirs.wsDir, 'index.json'));
if (!(await dirFile.exists())) {
await migrateFromState(email, dirs);
}
await migrateHomepageToScreens(dirs, email);
const state = await readAllWorkspacesState(dirs);
return ctx.json(state);
});
// PATCH /workspaces
workspacesRouter.patch('/', async (ctx) => {
const email = ctx.get('user').email;
const body = ctx.get('body') as Record<string, unknown>;
const dirs = getDirs(email);
await mkdir(dirs.wsDir, { recursive: true });
for (const [key, value] of Object.entries(body)) {
const mapping = resolveKey(dirs, 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 readAllWorkspacesState(dirs);
return ctx.json(state);
});
+6 -2
View File
@@ -26,9 +26,13 @@ export const getArchivedSessionDir = (email: string, sessionId: string) =>
export const getHomeDir = (email: string) => join(DATA_PATH, email, 'home');
export const getUserSettingsFile = (email: string) => join(DATA_PATH, email, 'settings.json');
export const getUserSettingsDir = (email: string) => join(DATA_PATH, email, 'settings');
export const getUserStateFile = (email: string) => join(DATA_PATH, email, 'state.json');
export const getUserSettingsFile = (email: string) => join(DATA_PATH, email, 'settings', 'settings.json');
export const getUserStateDir = (email: string) => join(DATA_PATH, email, 'state');
export const getUserStateFile = (email: string) => join(DATA_PATH, email, 'state', 'state.json');
export const getUserWorkspacesDir = (email: string) => join(DATA_PATH, email, 'workspaces');
+2 -2
View File
@@ -14,7 +14,7 @@ import { sessionsRouter } from './api/sessions/sessions';
import { scrapeRouter } from './api/scrape/scrape';
import { uploadRouter } from './api/upload/upload';
import { settingsRouter } from './api/settings/settings';
import { workspacesRouter } from './api/settings/workspaces';
import { workspacesRouter } from './api/workspaces';
import { projectsRouter } from './api/settings/projects';
import { taskLogsRouter } from './api/task-logs/task-logs';
import { router as fileBrowserRouter } from './api/file-browser/router';
@@ -54,7 +54,7 @@ protectedRouter.route('/', sessionsRouter);
protectedRouter.route('/scrape', scrapeRouter);
protectedRouter.route('/upload', uploadRouter);
protectedRouter.route('/user', settingsRouter);
protectedRouter.route('/user', workspacesRouter);
protectedRouter.route('/workspaces', workspacesRouter);
protectedRouter.route('/user', projectsRouter);
protectedRouter.route('/task-logs', taskLogsRouter);
protectedRouter.route('/file-browser', fileBrowserRouter);