mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-01 10:18:37 +00:00
refactor: move project deletion to module
This commit is contained in:
@@ -4,3 +4,5 @@ export {
|
||||
getProjectsWithSessions,
|
||||
writeSnapshot,
|
||||
} from './services/projects-with-sessions-fetch.service.js';
|
||||
export { updateProjectDisplayName } from './services/project-management.service.js';
|
||||
export { deleteOrArchiveProject, deleteSessionJsonlFilesForProjectPath } from './services/project-delete.service.js';
|
||||
|
||||
@@ -5,6 +5,8 @@ import { startCloneProject } from '@/modules/projects/services/project-clone.ser
|
||||
import { getProjectTaskMaster } from '@/modules/projects/services/projects-has-taskmaster.service.js';
|
||||
import { AppError, asyncHandler } from '@/shared/utils.js';
|
||||
import { getProjectsWithSessions } from '@/modules/projects/services/projects-with-sessions-fetch.service.js';
|
||||
import { deleteOrArchiveProject } from '@/modules/projects/services/project-delete.service.js';
|
||||
import { applyLegacyStarredProjectIds, toggleProjectStar } from '@/modules/projects/services/project-star.service.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -92,6 +94,20 @@ router.post(
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* One-time (or idempotent) migration: apply legacy `localStorage` starred projectIds to the DB, then clear client storage.
|
||||
*/
|
||||
router.post(
|
||||
'/migrate-legacy-stars',
|
||||
asyncHandler(async (req, res) => {
|
||||
const projectIds = Array.isArray((req.body as { projectIds?: unknown })?.projectIds)
|
||||
? ((req.body as { projectIds: unknown[] }).projectIds as unknown[]).map((x) => String(x))
|
||||
: [];
|
||||
const { updated } = applyLegacyStarredProjectIds(projectIds);
|
||||
res.json({ success: true, updated });
|
||||
}),
|
||||
);
|
||||
|
||||
router.get('/clone-progress', async (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
@@ -177,4 +193,27 @@ router.put('/:projectId/rename', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post(
|
||||
'/:projectId/toggle-star',
|
||||
asyncHandler(async (req, res) => {
|
||||
const projectId = typeof req.params.projectId === 'string' ? req.params.projectId : '';
|
||||
const { isStarred } = toggleProjectStar(projectId);
|
||||
res.json({ success: true, isStarred });
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* - `force` not set / false: archive project in DB only (`isArchived` = 1; hidden from active list).
|
||||
* - `force=true`: remove DB row, delete session rows for that path, remove all `*.jsonl` under the Claude project dir.
|
||||
*/
|
||||
router.delete(
|
||||
'/:projectId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const projectId = typeof req.params.projectId === 'string' ? req.params.projectId : '';
|
||||
const force = req.query.force === 'true';
|
||||
await deleteOrArchiveProject(projectId, force);
|
||||
res.json({ success: true });
|
||||
}),
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
75
server/modules/projects/services/project-delete.service.ts
Normal file
75
server/modules/projects/services/project-delete.service.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { projectsDb, sessionsDb } from '@/modules/database/index.js';
|
||||
import { AppError } from '@/shared/utils.js';
|
||||
|
||||
function uniqueJsonlPathsFromSessions(
|
||||
sessions: Array<{ jsonl_path: string | null }>,
|
||||
): string[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
|
||||
for (const row of sessions) {
|
||||
const raw = row.jsonl_path?.trim();
|
||||
if (!raw) {
|
||||
continue;
|
||||
}
|
||||
const absolute = path.isAbsolute(raw) ? path.normalize(raw) : path.resolve(raw);
|
||||
if (seen.has(absolute)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(absolute);
|
||||
result.push(absolute);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function unlinkJsonlIfExists(filePath: string): Promise<void> {
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
if (code === 'ENOENT') {
|
||||
return;
|
||||
}
|
||||
console.warn(`[project-delete] Failed to remove ${filePath}:`, (error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all session rows for the project path and removes each distinct `jsonl_path` file on disk.
|
||||
*/
|
||||
export async function deleteSessionJsonlFilesForProjectPath(projectPath: string): Promise<void> {
|
||||
const sessions = sessionsDb.getSessionsByProjectPath(projectPath);
|
||||
const paths = uniqueJsonlPathsFromSessions(sessions);
|
||||
|
||||
for (const filePath of paths) {
|
||||
await unlinkJsonlIfExists(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* - **Soft delete** (`force` false): set `isArchived` on the `projects` row (hide from the active list; DB only).
|
||||
* - **Force** (`force` true): for each session row for that `project_path`, delete the file at `jsonl_path`
|
||||
* (when set), then remove session rows and the `projects` row.
|
||||
*/
|
||||
export async function deleteOrArchiveProject(projectId: string, force: boolean): Promise<void> {
|
||||
const row = projectsDb.getProjectById(projectId);
|
||||
if (!row) {
|
||||
throw new AppError(`Unknown projectId: ${projectId}`, {
|
||||
code: 'PROJECT_NOT_FOUND',
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
if (!force) {
|
||||
projectsDb.updateProjectIsArchivedById(projectId, true);
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteSessionJsonlFilesForProjectPath(row.project_path);
|
||||
sessionsDb.deleteSessionsByProjectPath(row.project_path);
|
||||
projectsDb.deleteProjectById(projectId);
|
||||
}
|
||||
Reference in New Issue
Block a user