mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-01 18:28:38 +00:00
refactor(projects): identify projects by DB projectId instead of folder-derived name
GET /api/projects used to scan ~/.claude/projects/ on every request, derive each project's identity from the encoded folder name, and re-parse JSONL files to build session lists. Using the folder-derived name as the project identifier leaked the Claude CLI's on-disk encoding into every API route, forced every downstream endpoint to re-resolve a real path via JSONL 'cwd' inspection, and made the project list endpoint O(projects x sessions) on disk I/O. This change switches the entire API surface to identify projects by the stable primary key from the 'projects' table and drives the listing straight from the DB: - Add projectsDb.getProjectPathById as the canonical projectId -> path resolver so routes no longer need to touch the filesystem to figure out where a project lives. - Rewrite getProjects so it reads the project list from the 'projects' table and the per-project session list from the 'sessions' table (one SELECT per project). No filesystem scanning happens for this endpoint anymore, which removes the dependency on ~/.claude/projects existing, on Cursor's MD5-hashed chat folders being discoverable, and on Codex's JSONL history being on disk. Per the migration spec each session now exposes 'summary' sourced from sessions.custom_name, 'messageCount' = 0 (message counting is not implemented), and sessionMeta.hasMore is pinned to false since this endpoint doesn't drive session pagination. - Introduce id-based wrappers (getSessionsById, renameProjectById, deleteSessionById, deleteProjectById, getProjectTaskMasterById) so every caller can pass projectId and resolve the real path through the DB. renameProjectById also writes to projects.custom_project_name so the DB-driven getProjects response reflects renames immediately; it keeps project-config.json in sync for any legacy reader that still consults the JSON file. - Migrate every /api/projects/:projectName route in server/index.js, server/routes/taskmaster.js, and server/routes/messages.js to :projectId, and change server/routes/git.js so the 'project' query/body parameter carries a projectId that is resolved through the DB before any git command runs. TaskMaster WebSocket broadcasts emit 'projectId' for the same reason so the frontend can match notifications against its current selection without another lookup. - Delete helpers that existed only to feed the old getProjects path (getCursorSessions, getGeminiCliSessions, getProjectTaskMaster) along with their unused imports (better-sqlite3's Database, applyCustomSessionNames). The legacy folder-name helpers (getSessions, renameProject, deleteSession, deleteProject, extractProjectDirectory) are kept as internal implementation details of the id-based wrappers and of destructive cleanup / conversation search, but they are no longer re-exported. - searchConversations still walks JSONL to produce match snippets (that data doesn't live in the DB), but it now includes the resolved projectId in each result so the sidebar can cross-reference hits with its already loaded project list without a second round-trip. Frontend migration: - Project.name is replaced by Project.projectId in src/types/app.ts, and ProjectSession.__projectName becomes __projectId so session tagging and sidebar state keys stay aligned with the backend identifier. Settings continues to use SettingsProject.name for legacy consumers, but it is populated from projectId by normalizeProjectForSettings. - All places that previously indexed per-project state by project.name (sidebar expanded/starred/loading/deletingProjects sets, additionalSessions map, projectHasMoreOverrides, starredProjects localStorage, command history and draft-input localStorage, TaskMaster caches) now key on projectId so state survives display-name edits and is consistent across the app. - src/utils/api.js renames every endpoint parameter to projectId, the unified messages endpoint takes projectId in its query string, and useSessionStore forwards projectId on fetchFromServer / fetchMore / refreshFromServer. Git panel, file tree, code editor, PRD editor, plugins context, MCP server flows and TaskMaster hooks are all updated to pass projectId. - DEFAULT_PROJECT_FOR_EMPTY_SHELL is updated to carry a 'default' projectId sentinel so the empty-shell placeholder still satisfies the Project contract. Bug fix bundled in: - sessionsDb.setName no longer bumps updated_at when a row already exists. Renaming is a label change, not activity, so there is no reason for it to reset 'last activity' in the sidebar. It also no longer relies on SQLite's CURRENT_TIMESTAMP, which stores a naive 'YYYY-MM-DD HH:MM:SS' value that JavaScript parses as local time and caused renamed sessions to appear shifted backwards by the client's UTC offset. When an INSERT actually happens it now writes ISO-8601 UTC with a 'Z' suffix. - buildSessionsByProviderFromDb normalizes any legacy naive timestamps in the sessions table to ISO-8601 UTC on the way out so rows written before this change also render correctly on the client. Other cleanup: - Removed the filesystem-first project-discovery comment block at the top of server/projects.js and replaced it with a short note that describes the new DB-driven flow and lists the few remaining filesystem-dependent helpers (message reads, search, destructive delete, manual project registration). - server/modules/providers/index.ts is added as a small barrel so the providers module exposes a stable public surface. Made-with: Cursor
This commit is contained in:
@@ -2,7 +2,7 @@ import express from 'express';
|
||||
import { spawn } from 'child_process';
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import { extractProjectDirectory } from '../projects.js';
|
||||
import { getProjectPathById } from '../projects.js';
|
||||
import { queryClaudeSDK } from '../claude-sdk.js';
|
||||
import { spawnCursor } from '../cursor-cli.js';
|
||||
|
||||
@@ -101,14 +101,19 @@ function validateProjectPath(projectPath) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Helper function to get the actual project path from the encoded project name
|
||||
async function getActualProjectPath(projectName) {
|
||||
let projectPath;
|
||||
try {
|
||||
projectPath = await extractProjectDirectory(projectName);
|
||||
} catch (error) {
|
||||
console.error(`Error extracting project directory for ${projectName}:`, error);
|
||||
throw new Error(`Unable to resolve project path for "${projectName}"`);
|
||||
/**
|
||||
* Resolve the absolute project directory for a given DB `projectId`.
|
||||
*
|
||||
* After the projectName → projectId migration, every git endpoint receives
|
||||
* the DB primary key (`project` query/body param). The legacy filesystem
|
||||
* resolver that walked Claude's JSONL history is no longer used here; the
|
||||
* path comes straight from the `projects` table and is then sanity-checked
|
||||
* by `validateProjectPath` before any `git` command runs against it.
|
||||
*/
|
||||
async function getActualProjectPath(projectId) {
|
||||
const projectPath = await getProjectPathById(projectId);
|
||||
if (!projectPath) {
|
||||
throw new Error(`Unable to resolve project path for "${projectId}"`);
|
||||
}
|
||||
return validateProjectPath(projectPath);
|
||||
}
|
||||
@@ -292,7 +297,7 @@ router.get('/status', async (req, res) => {
|
||||
const { project } = req.query;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
return res.status(400).json({ error: 'Project id is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -355,7 +360,7 @@ router.get('/diff', async (req, res) => {
|
||||
const { project, file } = req.query;
|
||||
|
||||
if (!project || !file) {
|
||||
return res.status(400).json({ error: 'Project name and file path are required' });
|
||||
return res.status(400).json({ error: 'Project id and file path are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -438,7 +443,7 @@ router.get('/file-with-diff', async (req, res) => {
|
||||
const { project, file } = req.query;
|
||||
|
||||
if (!project || !file) {
|
||||
return res.status(400).json({ error: 'Project name and file path are required' });
|
||||
return res.status(400).json({ error: 'Project id and file path are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -518,7 +523,7 @@ router.post('/initial-commit', async (req, res) => {
|
||||
const { project } = req.body;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
return res.status(400).json({ error: 'Project id is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -593,7 +598,7 @@ router.post('/revert-local-commit', async (req, res) => {
|
||||
const { project } = req.body;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
return res.status(400).json({ error: 'Project id is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -640,7 +645,7 @@ router.get('/branches', async (req, res) => {
|
||||
const { project } = req.query;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
return res.status(400).json({ error: 'Project id is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -684,7 +689,7 @@ router.post('/checkout', async (req, res) => {
|
||||
const { project, branch } = req.body;
|
||||
|
||||
if (!project || !branch) {
|
||||
return res.status(400).json({ error: 'Project name and branch are required' });
|
||||
return res.status(400).json({ error: 'Project id and branch are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -706,7 +711,7 @@ router.post('/create-branch', async (req, res) => {
|
||||
const { project, branch } = req.body;
|
||||
|
||||
if (!project || !branch) {
|
||||
return res.status(400).json({ error: 'Project name and branch name are required' });
|
||||
return res.status(400).json({ error: 'Project id and branch name are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -728,7 +733,7 @@ router.post('/delete-branch', async (req, res) => {
|
||||
const { project, branch } = req.body;
|
||||
|
||||
if (!project || !branch) {
|
||||
return res.status(400).json({ error: 'Project name and branch name are required' });
|
||||
return res.status(400).json({ error: 'Project id and branch name are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -754,7 +759,7 @@ router.get('/commits', async (req, res) => {
|
||||
const { project, limit = 10 } = req.query;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
return res.status(400).json({ error: 'Project id is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -811,7 +816,7 @@ router.get('/commit-diff', async (req, res) => {
|
||||
const { project, commit } = req.query;
|
||||
|
||||
if (!project || !commit) {
|
||||
return res.status(400).json({ error: 'Project name and commit hash are required' });
|
||||
return res.status(400).json({ error: 'Project id and commit hash are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -843,7 +848,7 @@ router.post('/generate-commit-message', async (req, res) => {
|
||||
const { project, files, provider = 'claude' } = req.body;
|
||||
|
||||
if (!project || !files || files.length === 0) {
|
||||
return res.status(400).json({ error: 'Project name and files are required' });
|
||||
return res.status(400).json({ error: 'Project id and files are required' });
|
||||
}
|
||||
|
||||
// Validate provider
|
||||
@@ -1048,7 +1053,7 @@ router.get('/remote-status', async (req, res) => {
|
||||
const { project } = req.query;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
return res.status(400).json({ error: 'Project id is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1126,7 +1131,7 @@ router.post('/fetch', async (req, res) => {
|
||||
const { project } = req.body;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
return res.status(400).json({ error: 'Project id is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1167,7 +1172,7 @@ router.post('/pull', async (req, res) => {
|
||||
const { project } = req.body;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
return res.status(400).json({ error: 'Project id is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1235,7 +1240,7 @@ router.post('/push', async (req, res) => {
|
||||
const { project } = req.body;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
return res.status(400).json({ error: 'Project id is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1306,7 +1311,7 @@ router.post('/publish', async (req, res) => {
|
||||
const { project, branch } = req.body;
|
||||
|
||||
if (!project || !branch) {
|
||||
return res.status(400).json({ error: 'Project name and branch are required' });
|
||||
return res.status(400).json({ error: 'Project id and branch are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1385,7 +1390,7 @@ router.post('/discard', async (req, res) => {
|
||||
const { project, file } = req.body;
|
||||
|
||||
if (!project || !file) {
|
||||
return res.status(400).json({ error: 'Project name and file path are required' });
|
||||
return res.status(400).json({ error: 'Project id and file path are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1439,7 +1444,7 @@ router.post('/delete-untracked', async (req, res) => {
|
||||
const { project, file } = req.body;
|
||||
|
||||
if (!project || !file) {
|
||||
return res.status(400).json({ error: 'Project name and file path are required' });
|
||||
return res.status(400).json({ error: 'Project id and file path are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
/**
|
||||
* Unified messages endpoint.
|
||||
*
|
||||
* GET /api/sessions/:sessionId/messages?provider=claude&projectName=foo&limit=50&offset=0
|
||||
* GET /api/sessions/:sessionId/messages?provider=claude&projectId=<id>&limit=50&offset=0
|
||||
*
|
||||
* Replaces the four provider-specific session message endpoints with a single route
|
||||
* that delegates to the appropriate adapter via the provider registry.
|
||||
*
|
||||
* After the projectName → projectId migration, Claude history is located via the
|
||||
* DB-backed project path lookup; the route accepts `projectId` (preferred) and
|
||||
* resolves it to the underlying Claude folder name for the downstream adapter.
|
||||
*
|
||||
* @module routes/messages
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { sessionsService } from '../modules/providers/services/sessions.service.js';
|
||||
import { getProjectPathById, claudeFolderNameFromPath } from '../projects.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -21,7 +26,7 @@ const router = express.Router();
|
||||
*
|
||||
* Query params:
|
||||
* provider - 'claude' | 'cursor' | 'codex' | 'gemini' (default: 'claude')
|
||||
* projectName - required for claude provider
|
||||
* projectId - DB primary key of the project (required for claude provider)
|
||||
* projectPath - required for cursor provider (absolute path used for cwdId hash)
|
||||
* limit - page size (omit or null for all)
|
||||
* offset - pagination offset (default: 0)
|
||||
@@ -30,7 +35,7 @@ router.get('/:sessionId/messages', async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const provider = String(req.query.provider || 'claude').trim().toLowerCase();
|
||||
const projectName = req.query.projectName || '';
|
||||
const projectId = req.query.projectId || '';
|
||||
const projectPath = req.query.projectPath || '';
|
||||
const limitParam = req.query.limit;
|
||||
const limit = limitParam !== undefined && limitParam !== null && limitParam !== ''
|
||||
@@ -44,8 +49,20 @@ router.get('/:sessionId/messages', async (req, res) => {
|
||||
return res.status(400).json({ error: `Unknown provider: ${provider}. Available: ${available}` });
|
||||
}
|
||||
|
||||
// The Claude adapter still reads sessions from ~/.claude/projects/<folder>/,
|
||||
// so we translate the caller's projectId into the encoded folder name via
|
||||
// the DB-stored project path before delegating to the adapter.
|
||||
let claudeProjectName = '';
|
||||
if (provider === 'claude' && projectId) {
|
||||
const resolvedPath = await getProjectPathById(projectId);
|
||||
if (!resolvedPath) {
|
||||
return res.status(404).json({ error: 'Project not found' });
|
||||
}
|
||||
claudeProjectName = claudeFolderNameFromPath(resolvedPath);
|
||||
}
|
||||
|
||||
const result = await sessionsService.fetchHistory(provider, sessionId, {
|
||||
projectName,
|
||||
projectName: claudeProjectName,
|
||||
projectPath,
|
||||
limit,
|
||||
offset,
|
||||
|
||||
@@ -13,10 +13,25 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import { spawn } from 'child_process';
|
||||
import { extractProjectDirectory } from '../projects.js';
|
||||
import { getProjectPathById } from '../projects.js';
|
||||
import { detectTaskMasterMCPServer } from '../utils/mcp-detector.js';
|
||||
import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../utils/taskmaster-websocket.js';
|
||||
|
||||
/**
|
||||
* Resolve the absolute project directory from a DB-assigned `projectId`.
|
||||
*
|
||||
* TaskMaster routes used to accept a Claude-encoded folder name (`projectName`)
|
||||
* and derive the path from JSONL history. After the projectId migration the
|
||||
* only identifier we accept is the primary key of the `projects` table, so
|
||||
* every handler calls this helper and 404s when the id is unknown.
|
||||
*/
|
||||
async function resolveProjectPathFromId(projectId) {
|
||||
if (!projectId) {
|
||||
return null;
|
||||
}
|
||||
return getProjectPathById(projectId);
|
||||
}
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
@@ -132,21 +147,22 @@ router.get('/installation-status', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/taskmaster/tasks/:projectName
|
||||
* GET /api/taskmaster/tasks/:projectId
|
||||
* Load actual tasks from .taskmaster/tasks/tasks.json
|
||||
*
|
||||
* `projectId` is the DB primary key of the project; the folder is resolved via
|
||||
* the projects table rather than extracted from Claude JSONL history.
|
||||
*/
|
||||
router.get('/tasks/:projectName', async (req, res) => {
|
||||
router.get('/tasks/:projectId', async (req, res) => {
|
||||
try {
|
||||
const { projectName } = req.params;
|
||||
|
||||
// Get project path
|
||||
let projectPath;
|
||||
try {
|
||||
projectPath = await extractProjectDirectory(projectName);
|
||||
} catch (error) {
|
||||
const { projectId } = req.params;
|
||||
|
||||
// Get project path via the DB; the legacy JSONL-based resolver is gone.
|
||||
const projectPath = await resolveProjectPathFromId(projectId);
|
||||
if (!projectPath) {
|
||||
return res.status(404).json({
|
||||
error: 'Project not found',
|
||||
message: `Project "${projectName}" does not exist`
|
||||
message: `Project "${projectId}" does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -158,7 +174,7 @@ router.get('/tasks/:projectName', async (req, res) => {
|
||||
await fsPromises.access(tasksFilePath);
|
||||
} catch (error) {
|
||||
return res.json({
|
||||
projectName,
|
||||
projectId,
|
||||
tasks: [],
|
||||
message: 'No tasks.json file found'
|
||||
});
|
||||
@@ -213,7 +229,7 @@ router.get('/tasks/:projectName', async (req, res) => {
|
||||
}));
|
||||
|
||||
res.json({
|
||||
projectName,
|
||||
projectId,
|
||||
projectPath,
|
||||
tasks: transformedTasks,
|
||||
currentTag,
|
||||
@@ -247,21 +263,19 @@ router.get('/tasks/:projectName', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/taskmaster/prd/:projectName
|
||||
* GET /api/taskmaster/prd/:projectId
|
||||
* List all PRD files in the project's .taskmaster/docs directory
|
||||
*/
|
||||
router.get('/prd/:projectName', async (req, res) => {
|
||||
router.get('/prd/:projectId', async (req, res) => {
|
||||
try {
|
||||
const { projectName } = req.params;
|
||||
|
||||
// Get project path
|
||||
let projectPath;
|
||||
try {
|
||||
projectPath = await extractProjectDirectory(projectName);
|
||||
} catch (error) {
|
||||
const { projectId } = req.params;
|
||||
|
||||
// projectId → projectPath lookup through the DB (post-migration).
|
||||
const projectPath = await resolveProjectPathFromId(projectId);
|
||||
if (!projectPath) {
|
||||
return res.status(404).json({
|
||||
error: 'Project not found',
|
||||
message: `Project "${projectName}" does not exist`
|
||||
message: `Project "${projectId}" does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -272,7 +286,7 @@ router.get('/prd/:projectName', async (req, res) => {
|
||||
await fsPromises.access(docsPath, fs.constants.R_OK);
|
||||
} catch (error) {
|
||||
return res.json({
|
||||
projectName,
|
||||
projectId,
|
||||
prdFiles: [],
|
||||
message: 'No .taskmaster/docs directory found'
|
||||
});
|
||||
@@ -299,7 +313,7 @@ router.get('/prd/:projectName', async (req, res) => {
|
||||
}
|
||||
|
||||
res.json({
|
||||
projectName,
|
||||
projectId,
|
||||
projectPath,
|
||||
prdFiles: prdFiles.sort((a, b) => new Date(b.modified) - new Date(a.modified)),
|
||||
timestamp: new Date().toISOString()
|
||||
@@ -323,12 +337,12 @@ router.get('/prd/:projectName', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/taskmaster/prd/:projectName
|
||||
* POST /api/taskmaster/prd/:projectId
|
||||
* Create or update a PRD file in the project's .taskmaster/docs directory
|
||||
*/
|
||||
router.post('/prd/:projectName', async (req, res) => {
|
||||
router.post('/prd/:projectId', async (req, res) => {
|
||||
try {
|
||||
const { projectName } = req.params;
|
||||
const { projectId } = req.params;
|
||||
const { fileName, content } = req.body;
|
||||
|
||||
if (!fileName || !content) {
|
||||
@@ -346,14 +360,12 @@ router.post('/prd/:projectName', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Get project path
|
||||
let projectPath;
|
||||
try {
|
||||
projectPath = await extractProjectDirectory(projectName);
|
||||
} catch (error) {
|
||||
// Resolve the project folder through the DB using the projectId param.
|
||||
const projectPath = await resolveProjectPathFromId(projectId);
|
||||
if (!projectPath) {
|
||||
return res.status(404).json({
|
||||
error: 'Project not found',
|
||||
message: `Project "${projectName}" does not exist`
|
||||
message: `Project "${projectId}" does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -379,7 +391,7 @@ router.post('/prd/:projectName', async (req, res) => {
|
||||
const stats = await fsPromises.stat(filePath);
|
||||
|
||||
res.json({
|
||||
projectName,
|
||||
projectId,
|
||||
projectPath,
|
||||
fileName,
|
||||
filePath: path.relative(projectPath, filePath),
|
||||
@@ -408,21 +420,18 @@ router.post('/prd/:projectName', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/taskmaster/prd/:projectName/:fileName
|
||||
* GET /api/taskmaster/prd/:projectId/:fileName
|
||||
* Get content of a specific PRD file
|
||||
*/
|
||||
router.get('/prd/:projectName/:fileName', async (req, res) => {
|
||||
router.get('/prd/:projectId/:fileName', async (req, res) => {
|
||||
try {
|
||||
const { projectName, fileName } = req.params;
|
||||
|
||||
// Get project path
|
||||
let projectPath;
|
||||
try {
|
||||
projectPath = await extractProjectDirectory(projectName);
|
||||
} catch (error) {
|
||||
const { projectId, fileName } = req.params;
|
||||
|
||||
const projectPath = await resolveProjectPathFromId(projectId);
|
||||
if (!projectPath) {
|
||||
return res.status(404).json({
|
||||
error: 'Project not found',
|
||||
message: `Project "${projectName}" does not exist`
|
||||
message: `Project "${projectId}" does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -444,7 +453,7 @@ router.get('/prd/:projectName/:fileName', async (req, res) => {
|
||||
const stats = await fsPromises.stat(filePath);
|
||||
|
||||
res.json({
|
||||
projectName,
|
||||
projectId,
|
||||
projectPath,
|
||||
fileName,
|
||||
filePath: path.relative(projectPath, filePath),
|
||||
@@ -473,21 +482,18 @@ router.get('/prd/:projectName/:fileName', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/taskmaster/init/:projectName
|
||||
* POST /api/taskmaster/init/:projectId
|
||||
* Initialize TaskMaster in a project
|
||||
*/
|
||||
router.post('/init/:projectName', async (req, res) => {
|
||||
router.post('/init/:projectId', async (req, res) => {
|
||||
try {
|
||||
const { projectName } = req.params;
|
||||
|
||||
// Get project path
|
||||
let projectPath;
|
||||
try {
|
||||
projectPath = await extractProjectDirectory(projectName);
|
||||
} catch (error) {
|
||||
const { projectId } = req.params;
|
||||
|
||||
const projectPath = await resolveProjectPathFromId(projectId);
|
||||
if (!projectPath) {
|
||||
return res.status(404).json({
|
||||
error: 'Project not found',
|
||||
message: `Project "${projectName}" does not exist`
|
||||
message: `Project "${projectId}" does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -522,17 +528,19 @@ router.post('/init/:projectName', async (req, res) => {
|
||||
|
||||
initProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
// Broadcast TaskMaster project update via WebSocket
|
||||
// Broadcast TaskMaster project update via WebSocket. The
|
||||
// WebSocket payload keeps using `projectId` so the frontend
|
||||
// can match notifications against the current selection.
|
||||
if (req.app.locals.wss) {
|
||||
broadcastTaskMasterProjectUpdate(
|
||||
req.app.locals.wss,
|
||||
projectName,
|
||||
req.app.locals.wss,
|
||||
projectId,
|
||||
{ hasTaskmaster: true, status: 'initialized' }
|
||||
);
|
||||
}
|
||||
|
||||
res.json({
|
||||
projectName,
|
||||
projectId,
|
||||
projectPath,
|
||||
message: 'TaskMaster initialized successfully',
|
||||
output: stdout,
|
||||
@@ -562,12 +570,12 @@ router.post('/init/:projectName', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/taskmaster/add-task/:projectName
|
||||
* POST /api/taskmaster/add-task/:projectId
|
||||
* Add a new task to the project
|
||||
*/
|
||||
router.post('/add-task/:projectName', async (req, res) => {
|
||||
router.post('/add-task/:projectId', async (req, res) => {
|
||||
try {
|
||||
const { projectName } = req.params;
|
||||
const { projectId } = req.params;
|
||||
const { prompt, title, description, priority = 'medium', dependencies } = req.body;
|
||||
|
||||
if (!prompt && (!title || !description)) {
|
||||
@@ -576,15 +584,12 @@ router.post('/add-task/:projectName', async (req, res) => {
|
||||
message: 'Either "prompt" or both "title" and "description" are required'
|
||||
});
|
||||
}
|
||||
|
||||
// Get project path
|
||||
let projectPath;
|
||||
try {
|
||||
projectPath = await extractProjectDirectory(projectName);
|
||||
} catch (error) {
|
||||
|
||||
const projectPath = await resolveProjectPathFromId(projectId);
|
||||
if (!projectPath) {
|
||||
return res.status(404).json({
|
||||
error: 'Project not found',
|
||||
message: `Project "${projectName}" does not exist`
|
||||
message: `Project "${projectId}" does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -629,16 +634,17 @@ router.post('/add-task/:projectName', async (req, res) => {
|
||||
console.log('Stderr:', stderr);
|
||||
|
||||
if (code === 0) {
|
||||
// Broadcast task update via WebSocket
|
||||
// Broadcast task update via WebSocket using the projectId so
|
||||
// clients subscribed to this project get notified immediately.
|
||||
if (req.app.locals.wss) {
|
||||
broadcastTaskMasterTasksUpdate(
|
||||
req.app.locals.wss,
|
||||
projectName
|
||||
req.app.locals.wss,
|
||||
projectId
|
||||
);
|
||||
}
|
||||
|
||||
res.json({
|
||||
projectName,
|
||||
projectId,
|
||||
projectPath,
|
||||
message: 'Task added successfully',
|
||||
output: stdout,
|
||||
@@ -666,22 +672,19 @@ router.post('/add-task/:projectName', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/taskmaster/update-task/:projectName/:taskId
|
||||
* PUT /api/taskmaster/update-task/:projectId/:taskId
|
||||
* Update a specific task using TaskMaster CLI
|
||||
*/
|
||||
router.put('/update-task/:projectName/:taskId', async (req, res) => {
|
||||
router.put('/update-task/:projectId/:taskId', async (req, res) => {
|
||||
try {
|
||||
const { projectName, taskId } = req.params;
|
||||
const { projectId, taskId } = req.params;
|
||||
const { title, description, status, priority, details } = req.body;
|
||||
|
||||
// Get project path
|
||||
let projectPath;
|
||||
try {
|
||||
projectPath = await extractProjectDirectory(projectName);
|
||||
} catch (error) {
|
||||
|
||||
const projectPath = await resolveProjectPathFromId(projectId);
|
||||
if (!projectPath) {
|
||||
return res.status(404).json({
|
||||
error: 'Project not found',
|
||||
message: `Project "${projectName}" does not exist`
|
||||
message: `Project "${projectId}" does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -707,11 +710,11 @@ router.put('/update-task/:projectName/:taskId', async (req, res) => {
|
||||
if (code === 0) {
|
||||
// Broadcast task update via WebSocket
|
||||
if (req.app.locals.wss) {
|
||||
broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectName);
|
||||
broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectId);
|
||||
}
|
||||
|
||||
res.json({
|
||||
projectName,
|
||||
projectId,
|
||||
projectPath,
|
||||
taskId,
|
||||
message: 'Task status updated successfully',
|
||||
@@ -759,11 +762,11 @@ router.put('/update-task/:projectName/:taskId', async (req, res) => {
|
||||
if (code === 0) {
|
||||
// Broadcast task update via WebSocket
|
||||
if (req.app.locals.wss) {
|
||||
broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectName);
|
||||
broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectId);
|
||||
}
|
||||
|
||||
res.json({
|
||||
projectName,
|
||||
projectId,
|
||||
projectPath,
|
||||
taskId,
|
||||
message: 'Task updated successfully',
|
||||
@@ -793,22 +796,19 @@ router.put('/update-task/:projectName/:taskId', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/taskmaster/parse-prd/:projectName
|
||||
* POST /api/taskmaster/parse-prd/:projectId
|
||||
* Parse a PRD file to generate tasks
|
||||
*/
|
||||
router.post('/parse-prd/:projectName', async (req, res) => {
|
||||
router.post('/parse-prd/:projectId', async (req, res) => {
|
||||
try {
|
||||
const { projectName } = req.params;
|
||||
const { projectId } = req.params;
|
||||
const { fileName = 'prd.txt', numTasks, append = false } = req.body;
|
||||
|
||||
// Get project path
|
||||
let projectPath;
|
||||
try {
|
||||
projectPath = await extractProjectDirectory(projectName);
|
||||
} catch (error) {
|
||||
|
||||
const projectPath = await resolveProjectPathFromId(projectId);
|
||||
if (!projectPath) {
|
||||
return res.status(404).json({
|
||||
error: 'Project not found',
|
||||
message: `Project "${projectName}" does not exist`
|
||||
message: `Project "${projectId}" does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -859,13 +859,13 @@ router.post('/parse-prd/:projectName', async (req, res) => {
|
||||
// Broadcast task update via WebSocket
|
||||
if (req.app.locals.wss) {
|
||||
broadcastTaskMasterTasksUpdate(
|
||||
req.app.locals.wss,
|
||||
projectName
|
||||
req.app.locals.wss,
|
||||
projectId
|
||||
);
|
||||
}
|
||||
|
||||
res.json({
|
||||
projectName,
|
||||
projectId,
|
||||
projectPath,
|
||||
prdFile: fileName,
|
||||
message: 'PRD parsed and tasks generated successfully',
|
||||
@@ -1340,12 +1340,12 @@ Description of the business problem, data sources, and expected insights.
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/taskmaster/apply-template/:projectName
|
||||
* POST /api/taskmaster/apply-template/:projectId
|
||||
* Apply a PRD template to create a new PRD file
|
||||
*/
|
||||
router.post('/apply-template/:projectName', async (req, res) => {
|
||||
router.post('/apply-template/:projectId', async (req, res) => {
|
||||
try {
|
||||
const { projectName } = req.params;
|
||||
const { projectId } = req.params;
|
||||
const { templateId, fileName = 'prd.txt', customizations = {} } = req.body;
|
||||
|
||||
if (!templateId) {
|
||||
@@ -1355,14 +1355,11 @@ router.post('/apply-template/:projectName', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Get project path
|
||||
let projectPath;
|
||||
try {
|
||||
projectPath = await extractProjectDirectory(projectName);
|
||||
} catch (error) {
|
||||
const projectPath = await resolveProjectPathFromId(projectId);
|
||||
if (!projectPath) {
|
||||
return res.status(404).json({
|
||||
error: 'Project not found',
|
||||
message: `Project "${projectName}" does not exist`
|
||||
message: `Project "${projectId}" does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1401,7 +1398,7 @@ router.post('/apply-template/:projectName', async (req, res) => {
|
||||
await fsPromises.writeFile(filePath, content, 'utf8');
|
||||
|
||||
res.json({
|
||||
projectName,
|
||||
projectId,
|
||||
projectPath,
|
||||
templateId,
|
||||
templateName: template.name,
|
||||
|
||||
Reference in New Issue
Block a user