Download src/server/services/doctorService.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 14.2 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/server/services/doctorService.ts
- Command line
-
hf download hf://chenbhao/codev/src/server/services/doctorService.ts
-
curl -L -o doctorService.ts https://huggingface.co/chenbhao/codev/resolve/main/src/server/services/doctorService.ts
14.2 kB
| import * as fs from 'node:fs/promises' | |
| import * as os from 'node:os' | |
| import * as path from 'node:path' | |
| import type { Dirent } from 'node:fs' | |
| import { getClaudeConfigHomeDir } from '../../utils/envUtils.js' | |
| import { diagnosticsService } from './diagnosticsService.js' | |
| export type DoctorItemKind = 'json' | 'jsonl' | 'directory' | |
| export type DoctorItemStatus = 'ok' | 'missing' | 'invalid_json' | 'invalid_jsonl' | 'unreadable' | |
| export type DoctorSkipReason = 'protected' | |
| export type DoctorReportItem = { | |
| id: string | |
| label: string | |
| kind: DoctorItemKind | |
| scope: 'user' | 'project' | |
| path: string | |
| protected: boolean | |
| exists: boolean | |
| status: DoctorItemStatus | |
| bytes: number | |
| entryCount?: number | |
| lineCount?: number | |
| invalidLineCount?: number | |
| error?: string | |
| } | |
| export type DoctorProtectedSkip = { | |
| id: string | |
| path: string | |
| reason: DoctorSkipReason | |
| } | |
| export type DoctorReport = { | |
| generatedAt: string | |
| items: DoctorReportItem[] | |
| protectedSkips: DoctorProtectedSkip[] | |
| summary: { | |
| total: number | |
| protectedCount: number | |
| missingCount: number | |
| invalidCount: number | |
| } | |
| } | |
| export type DoctorRepairOperation = { | |
| id: string | |
| path: string | |
| action: 'would_repair' | |
| } | |
| export type DoctorRepairSkip = { | |
| id: string | |
| path: string | |
| reason: DoctorSkipReason | |
| } | |
| export type DoctorRepairResult = { | |
| dryRun: true | |
| mutated: false | |
| operations: DoctorRepairOperation[] | |
| skips: DoctorRepairSkip[] | |
| summary: { | |
| operationCount: number | |
| skipCount: number | |
| } | |
| } | |
| type DoctorServiceOptions = { | |
| configDir?: string | |
| homeDir?: string | |
| projectRoot?: string | |
| } | |
| type DoctorTarget = { | |
| id: string | |
| label: string | |
| kind: DoctorItemKind | |
| scope: 'user' | 'project' | |
| filePath: string | |
| protected: true | |
| } | |
| export class DoctorService { | |
| private readonly configDir: string | |
| private readonly homeDir: string | |
| private readonly projectRoot?: string | |
| private readonly usesConfigDirOverride: boolean | |
| constructor(options: DoctorServiceOptions = {}) { | |
| this.configDir = options.configDir || getClaudeConfigHomeDir() | |
| this.homeDir = options.homeDir || inferHomeDir(this.configDir) | |
| this.projectRoot = options.projectRoot | |
| this.usesConfigDirOverride = Boolean(options.configDir || process.env.CLAUDE_CONFIG_DIR) | |
| } | |
| async getReport(): Promise<DoctorReport> { | |
| const targets = await this.buildTargets() | |
| const items = await Promise.all(targets.map((target) => this.inspectTarget(target))) | |
| const protectedSkips = items | |
| .filter((item) => item.protected) | |
| .map((item) => ({ | |
| id: item.id, | |
| path: item.path, | |
| reason: 'protected' as const, | |
| })) | |
| return { | |
| generatedAt: new Date().toISOString(), | |
| items, | |
| protectedSkips, | |
| summary: { | |
| total: items.length, | |
| protectedCount: protectedSkips.length, | |
| missingCount: items.filter((item) => item.status === 'missing').length, | |
| invalidCount: items.filter((item) => | |
| item.status === 'invalid_json' || | |
| item.status === 'invalid_jsonl' || | |
| item.status === 'unreadable' | |
| ).length, | |
| }, | |
| } | |
| } | |
| async repair(targetIds?: string[]): Promise<DoctorRepairResult> { | |
| const report = await this.getReport() | |
| const selectedIds = targetIds?.length ? new Set(targetIds) : null | |
| const selectedItems = selectedIds | |
| ? report.items.filter((item) => selectedIds.has(item.id)) | |
| : report.items | |
| const operations: DoctorRepairOperation[] = [] | |
| const skips = selectedItems.map((item) => { | |
| if (!item.protected && item.status !== 'ok') { | |
| operations.push({ | |
| id: item.id, | |
| path: item.path, | |
| action: 'would_repair', | |
| }) | |
| } | |
| return { | |
| id: item.id, | |
| path: item.path, | |
| reason: 'protected' as const, | |
| } | |
| }) | |
| return { | |
| dryRun: true, | |
| mutated: false, | |
| operations, | |
| skips, | |
| summary: { | |
| operationCount: operations.length, | |
| skipCount: skips.length, | |
| }, | |
| } | |
| } | |
| private async buildTargets(): Promise<DoctorTarget[]> { | |
| const targets: DoctorTarget[] = [ | |
| this.jsonTarget('user-settings', 'User settings', 'user', path.join(this.configDir, 'settings.json')), | |
| this.jsonTarget( | |
| 'cc-haha-providers', | |
| 'Managed providers', | |
| 'user', | |
| path.join(this.configDir, 'cc-haha', 'providers.json'), | |
| ), | |
| this.jsonTarget( | |
| 'cc-haha-settings', | |
| 'Managed provider settings', | |
| 'user', | |
| path.join(this.configDir, 'cc-haha', 'settings.json'), | |
| ), | |
| this.jsonTarget('adapters', 'Adapters config', 'user', path.join(this.configDir, 'adapters.json')), | |
| this.jsonTarget( | |
| 'adapter-sessions', | |
| 'Adapter sessions', | |
| 'user', | |
| path.join(this.configDir, 'adapter-sessions.json'), | |
| ), | |
| this.directoryTarget('user-skills', 'User skills', 'user', path.join(this.configDir, 'skills')), | |
| this.directoryTarget('teams', 'Teams', 'user', path.join(this.configDir, 'teams')), | |
| this.directoryTarget('plugins', 'Plugins', 'user', path.join(this.configDir, 'plugins')), | |
| this.directoryTarget( | |
| 'cowork-plugins', | |
| 'Cowork plugins', | |
| 'user', | |
| path.join(this.configDir, 'cowork_plugins'), | |
| ), | |
| this.jsonTarget('user-mcp', 'User MCP config', 'user', this.getUserMcpConfigPath()), | |
| this.jsonTarget('oauth', 'OAuth tokens', 'user', path.join(this.configDir, 'cc-haha', 'oauth.json')), | |
| this.jsonTarget( | |
| 'openai-oauth', | |
| 'OpenAI OAuth tokens', | |
| 'user', | |
| path.join(this.configDir, 'cc-haha', 'openai-oauth.json'), | |
| ), | |
| ] | |
| if (this.projectRoot) { | |
| targets.push( | |
| this.jsonTarget( | |
| 'project-settings', | |
| 'Project settings', | |
| 'project', | |
| path.join(this.projectRoot, '.claude', 'settings.json'), | |
| ), | |
| this.directoryTarget( | |
| 'project-skills', | |
| 'Project skills', | |
| 'project', | |
| path.join(this.projectRoot, '.claude', 'skills'), | |
| ), | |
| this.jsonTarget( | |
| 'project-mcp', | |
| 'Project MCP config', | |
| 'project', | |
| path.join(this.projectRoot, '.mcp.json'), | |
| ), | |
| ) | |
| } | |
| const sessionFiles = await this.listJsonlFiles(path.join(this.configDir, 'projects')) | |
| for (const filePath of sessionFiles) { | |
| const relativePath = toPosix(path.relative(this.configDir, filePath)) | |
| targets.push( | |
| this.jsonlTarget( | |
| `session-jsonl:${relativePath}`, | |
| 'Session transcript', | |
| 'user', | |
| filePath, | |
| ), | |
| ) | |
| } | |
| return targets | |
| } | |
| private async inspectTarget(target: DoctorTarget): Promise<DoctorReportItem> { | |
| switch (target.kind) { | |
| case 'json': | |
| return this.inspectJsonTarget(target) | |
| case 'jsonl': | |
| return this.inspectJsonlTarget(target) | |
| case 'directory': | |
| return this.inspectDirectoryTarget(target) | |
| default: | |
| return this.inspectMissingTarget(target) | |
| } | |
| } | |
| private async inspectJsonTarget(target: DoctorTarget): Promise<DoctorReportItem> { | |
| const exists = await this.pathExists(target.filePath) | |
| if (!exists) return this.inspectMissingTarget(target) | |
| let raw: string | |
| try { | |
| raw = await fs.readFile(target.filePath, 'utf-8') | |
| } catch (error) { | |
| return this.inspectUnreadableTarget(target, error) | |
| } | |
| const bytes = Buffer.byteLength(raw, 'utf-8') | |
| if (!raw.trim()) { | |
| return { | |
| ...this.baseItem(target), | |
| exists: true, | |
| status: 'invalid_json', | |
| bytes, | |
| error: 'Empty JSON file', | |
| } | |
| } | |
| try { | |
| const parsed = JSON.parse(raw) | |
| return { | |
| ...this.baseItem(target), | |
| exists: true, | |
| status: 'ok', | |
| bytes, | |
| entryCount: countJsonEntries(parsed), | |
| } | |
| } catch (error) { | |
| return { | |
| ...this.baseItem(target), | |
| exists: true, | |
| status: 'invalid_json', | |
| bytes, | |
| error: this.sanitizeText(error instanceof Error ? error.message : String(error)), | |
| } | |
| } | |
| } | |
| private async inspectJsonlTarget(target: DoctorTarget): Promise<DoctorReportItem> { | |
| const exists = await this.pathExists(target.filePath) | |
| if (!exists) return this.inspectMissingTarget(target) | |
| let raw: string | |
| try { | |
| raw = await fs.readFile(target.filePath, 'utf-8') | |
| } catch (error) { | |
| return this.inspectUnreadableTarget(target, error) | |
| } | |
| const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0) | |
| let invalidLineCount = 0 | |
| for (const line of lines) { | |
| try { | |
| JSON.parse(line) | |
| } catch { | |
| invalidLineCount += 1 | |
| } | |
| } | |
| return { | |
| ...this.baseItem(target), | |
| exists: true, | |
| status: invalidLineCount > 0 ? 'invalid_jsonl' : 'ok', | |
| bytes: Buffer.byteLength(raw, 'utf-8'), | |
| lineCount: lines.length, | |
| invalidLineCount, | |
| } | |
| } | |
| private async inspectDirectoryTarget(target: DoctorTarget): Promise<DoctorReportItem> { | |
| const exists = await this.pathExists(target.filePath) | |
| if (!exists) return this.inspectMissingTarget(target) | |
| let entries: Dirent[] | |
| try { | |
| entries = await fs.readdir(target.filePath, { withFileTypes: true }) | |
| } catch (error) { | |
| return this.inspectUnreadableTarget(target, error) | |
| } | |
| return { | |
| ...this.baseItem(target), | |
| exists: true, | |
| status: 'ok', | |
| bytes: 0, | |
| entryCount: countVisibleEntries(entries), | |
| } | |
| } | |
| private inspectMissingTarget(target: DoctorTarget): DoctorReportItem { | |
| return { | |
| ...this.baseItem(target), | |
| exists: false, | |
| status: 'missing', | |
| bytes: 0, | |
| } | |
| } | |
| private inspectUnreadableTarget(target: DoctorTarget, error: unknown): DoctorReportItem { | |
| return { | |
| ...this.baseItem(target), | |
| exists: true, | |
| status: 'unreadable', | |
| bytes: 0, | |
| error: this.sanitizeText(error instanceof Error ? error.message : String(error)), | |
| } | |
| } | |
| private baseItem(target: DoctorTarget): Omit<DoctorReportItem, 'exists' | 'status' | 'bytes'> { | |
| return { | |
| id: target.id, | |
| label: target.label, | |
| kind: target.kind, | |
| scope: target.scope, | |
| path: this.sanitizePath(target.filePath), | |
| protected: target.protected, | |
| } | |
| } | |
| private sanitizePath(filePath: string): string { | |
| if (this.projectRoot && isWithinRoot(filePath, this.projectRoot)) { | |
| return this.withAlias('<project>', filePath, this.projectRoot) | |
| } | |
| if (isWithinRoot(filePath, this.configDir)) { | |
| return this.withAlias('~/.claude', filePath, this.configDir) | |
| } | |
| if (isWithinRoot(filePath, this.homeDir)) { | |
| return this.withAlias('~', filePath, this.homeDir) | |
| } | |
| return this.sanitizeText(filePath) | |
| } | |
| private sanitizeText(value: string): string { | |
| let sanitized = diagnosticsService.sanitizeString(value) | |
| const replacements: Array<[string | undefined, string]> = [ | |
| [this.projectRoot, '<project>'], | |
| [this.configDir, '~/.claude'], | |
| [this.homeDir, '~'], | |
| ] | |
| for (const [from, to] of replacements) { | |
| if (!from) continue | |
| sanitized = sanitized.split(from).join(to) | |
| } | |
| return sanitized | |
| } | |
| private getUserMcpConfigPath(): string { | |
| if (this.usesConfigDirOverride) { | |
| return path.join(this.configDir, '.claude.json') | |
| } | |
| return path.join(this.homeDir, '.claude.json') | |
| } | |
| private async listJsonlFiles(rootDir: string): Promise<string[]> { | |
| const exists = await this.pathExists(rootDir) | |
| if (!exists) return [] | |
| const results: string[] = [] | |
| const stack = [rootDir] | |
| while (stack.length > 0) { | |
| const current = stack.pop() | |
| if (!current) continue | |
| let entries: Dirent[] | |
| try { | |
| entries = await fs.readdir(current, { withFileTypes: true }) | |
| } catch { | |
| continue | |
| } | |
| for (const entry of entries) { | |
| const fullPath = path.join(current, entry.name) | |
| if (entry.isDirectory()) { | |
| stack.push(fullPath) | |
| continue | |
| } | |
| if (entry.isFile() && fullPath.endsWith('.jsonl')) { | |
| results.push(fullPath) | |
| } | |
| } | |
| } | |
| results.sort((left, right) => left.localeCompare(right)) | |
| return results | |
| } | |
| private async pathExists(filePath: string): Promise<boolean> { | |
| try { | |
| await fs.access(filePath) | |
| return true | |
| } catch { | |
| return false | |
| } | |
| } | |
| private jsonTarget( | |
| id: string, | |
| label: string, | |
| scope: 'user' | 'project', | |
| filePath: string, | |
| ): DoctorTarget { | |
| return { id, label, kind: 'json', scope, filePath, protected: true } | |
| } | |
| private jsonlTarget( | |
| id: string, | |
| label: string, | |
| scope: 'user' | 'project', | |
| filePath: string, | |
| ): DoctorTarget { | |
| return { id, label, kind: 'jsonl', scope, filePath, protected: true } | |
| } | |
| private directoryTarget( | |
| id: string, | |
| label: string, | |
| scope: 'user' | 'project', | |
| filePath: string, | |
| ): DoctorTarget { | |
| return { id, label, kind: 'directory', scope, filePath, protected: true } | |
| } | |
| private withAlias(alias: string, filePath: string, root: string): string { | |
| const relativePath = path.relative(root, filePath) | |
| if (!relativePath) return alias | |
| return `${alias}/${toPosix(relativePath)}` | |
| } | |
| } | |
| function countJsonEntries(value: unknown): number { | |
| if (Array.isArray(value)) return value.length | |
| if (value && typeof value === 'object') return Object.keys(value as Record<string, unknown>).length | |
| if (value === null || value === undefined) return 0 | |
| return 1 | |
| } | |
| function countVisibleEntries(entries: Dirent[]): number { | |
| return entries.filter((entry) => entry.name !== '.DS_Store').length | |
| } | |
| function inferHomeDir(configDir: string): string { | |
| if (path.basename(configDir) === '.claude') { | |
| return path.dirname(configDir) | |
| } | |
| return os.homedir() | |
| } | |
| function isWithinRoot(filePath: string, root: string): boolean { | |
| const relativePath = path.relative(root, filePath) | |
| return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) | |
| } | |
| function toPosix(value: string): string { | |
| return value.split(path.sep).join('/') | |
| } | |