cc-haha/src/server/api/project-rules.ts
小橙子 5c612bd967
feat(settings): add Project Rules (CLAUDE.md) management UI (#72)
* feat(settings): add Project Rules (CLAUDE.md) management UI

Add a new "Project Rules" tab to the desktop settings page that lets users
view, create, and open their CLAUDE.md files directly from the UI. This
provides a visual entry point for managing project-specific instructions
that are auto-injected into every conversation.

- New settings tab "Project Rules" with i18n across all 5 locales
- ProjectRulesSettings page showing project-level and user-level CLAUDE.md
- Server API: GET/POST /api/project-rules for file status and creation
- "Open" button launches the file in the system editor
- "Create" button scaffolds a template CLAUDE.md if it doesn't exist

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: resolve TypeScript errors in ProjectRulesSettings

- Remove unused 'query' variable
- Fix translation function type to use TranslationKey

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: 你的姓名 <you@example.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 01:49:43 +08:00

98 lines
2.8 KiB
TypeScript

/**
* Project Rules (CLAUDE.md) REST API
*
* GET /api/project-rules — Get CLAUDE.md file paths and existence status
* POST /api/project-rules/create — Create a CLAUDE.md file if it doesn't exist
*/
import * as path from 'path'
import * as fs from 'fs/promises'
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
import { getCwd } from '../../utils/cwd.js'
export async function handleProjectRulesApi(
req: Request,
url: URL,
segments: string[],
): Promise<Response> {
const sub = segments[2]
if (req.method === 'GET' && !sub) {
return await getProjectRules(url)
}
if (req.method === 'POST' && sub === 'create') {
return await createProjectRulesFile(req, url)
}
return Response.json(
{ error: 'Not Found', message: `Unknown project-rules endpoint` },
{ status: 404 },
)
}
async function getProjectRules(url: URL): Promise<Response> {
const cwd = url.searchParams.get('cwd') || getCwd()
const userFile = path.join(getClaudeConfigHomeDir(), 'CLAUDE.md')
const projectFile = path.join(cwd, '.claude', 'CLAUDE.md')
const [userExists, projectExists] = await Promise.all([
fileExists(userFile),
fileExists(projectFile),
])
return Response.json({
projectFile: { path: projectFile, exists: projectExists },
userFile: { path: userFile, exists: userExists },
})
}
async function createProjectRulesFile(req: Request, url: URL): Promise<Response> {
let body: { scope?: string; cwd?: string }
try {
body = await req.json() as { scope?: string; cwd?: string }
} catch {
return Response.json({ error: 'Invalid JSON body' }, { status: 400 })
}
const scope = body.scope
const cwd = body.cwd || url.searchParams.get('cwd') || getCwd()
let filePath: string
if (scope === 'project') {
filePath = path.join(cwd, '.claude', 'CLAUDE.md')
} else if (scope === 'user') {
filePath = path.join(getClaudeConfigHomeDir(), 'CLAUDE.md')
} else {
return Response.json({ error: 'Invalid scope, must be "project" or "user"' }, { status: 400 })
}
// Don't overwrite existing files
if (await fileExists(filePath)) {
return Response.json({ ok: true, path: filePath, created: false })
}
// Create directory if needed
const dir = path.dirname(filePath)
await fs.mkdir(dir, { recursive: true })
// Create file with template content
const template = scope === 'project'
? '# Project Rules\n\n<!-- Add project-specific instructions here. These are loaded into every conversation. -->\n'
: '# User Rules\n\n<!-- Add global instructions here. These apply to all projects. -->\n'
await fs.writeFile(filePath, template, 'utf-8')
return Response.json({ ok: true, path: filePath, created: true })
}
async function fileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}