Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ packages/code/
packages/format/
├── index.ts # Public API
├── formatter.ts # Formatter definitions (biome, prettier, etc.)
└── config/ # Config loading (dora.json, opencode.json)
└── config/ # Config loading (.please/config.json or .please/config.yml)
```

### packages/lsp
Expand All @@ -91,6 +91,7 @@ packages/lsp/
├── index.ts # Public API
├── client.ts # LSP client implementation
├── server.ts # LSP server definitions
├── config.ts # LSP config from unified config file
└── language.ts # Language detection
```

Expand Down Expand Up @@ -133,6 +134,50 @@ Claude/MCP Client <-> StdioTransport <-> McpServer <-> Providers

biome, prettier, gofmt, mix (Elixir), zig fmt, clang-format, ktlint (Kotlin), ruff, air (R), uv format, rubocop, standardrb, htmlbeautifier, dart, ocamlformat, terraform, latexindent, gleam, prisma

### Configuration

Configuration is loaded from `.please/config.json` or `.please/config.yml` in the project root.

```yaml
# .please/config.yml

# Shared settings
language: en
ignore_patterns:
- node_modules
- dist

# Formatter settings
formatter:
biome:
command: ["biome", "format", "--write", "$FILE"]
extensions: [".ts", ".tsx", ".js", ".jsx"]
prettier:
disabled: true # Disable a built-in formatter

# LSP settings
lsp:
typescript:
enabled: true
vue:
enabled: false # Disable a specific LSP server
pyright:
root: "./backend" # Custom root path
```

**Formatter Config:**
- `disabled: true` - Disable a built-in formatter
- `command: [...]` - Override command (use `$FILE` placeholder)
- `extensions: [...]` - Override file extensions
- `environment: {...}` - Environment variables

**LSP Config:**
- `enabled: true/false` - Enable/disable specific LSP servers
- `root: "path"` - Custom project root path
- `command: [...]` - Custom spawn command

Set `lsp: false` or `formatter: false` to globally disable.

### Name Path Convention (JetBrains)

Symbols are identified by "name paths" - paths in the symbol tree within a source file:
Expand Down
7 changes: 4 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/format/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"ai-coding"
],
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./config": "./src/config/index.ts"
},
"main": "src/index.ts",
"files": [
Expand Down
116 changes: 104 additions & 12 deletions packages/format/src/config/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,41 @@ import path from 'node:path'
import YAML from 'yaml'
import { ConfigSchema, defaultConfig } from './schema'

/**
* Error thrown when configuration validation fails
*/
export class ConfigValidationError extends Error {
constructor(
message: string,
public readonly filePath: string,
public readonly issues: Array<{ path: string[], message: string }>,
) {
super(message)
this.name = 'ConfigValidationError'
}
}

/**
* Error thrown when configuration file cannot be read or parsed
*/
export class ConfigLoadError extends Error {
constructor(
message: string,
public readonly filePath: string,
public readonly cause?: unknown,
) {
super(message)
this.name = 'ConfigLoadError'
}
}

/**
* Configuration file names to search for (in order of priority)
*/
const CONFIG_FILES = [
'opencode.json',
'dora.json',
'.please/config.json',
'.please/config.yml',
'.please/config.yaml',
'.dora/config.yml',
'.dora/config.yaml',
]

/**
Expand Down Expand Up @@ -50,45 +75,87 @@ function parseConfigContent(content: string, filePath: string): unknown {
if (ext === '.yml' || ext === '.yaml') {
return YAML.parse(content)
}
// Try JSON first, then YAML
// Try JSON first, then YAML (only catch syntax errors)
try {
return JSON.parse(content)
}
catch {
return YAML.parse(content)
catch (err) {
if (err instanceof SyntaxError) {
// JSON syntax error - try YAML as fallback
return YAML.parse(content)
}
// Re-throw unexpected errors (memory, stack overflow, etc.)
throw err
}
}

/**
* Load configuration from a specific file
* @throws {ConfigLoadError} If file cannot be read or parsed
* @throws {ConfigValidationError} If configuration fails validation
*/
export async function loadConfigFromFile(filePath: string): Promise<Config> {
const file = Bun.file(filePath)
if (!(await file.exists())) {
return defaultConfig
}

const content = await file.text()
const parsed = parseConfigContent(content, filePath)
const result = ConfigSchema.safeParse(parsed)
// Read file with error context
let content: string
try {
content = await file.text()
}
catch (err) {
throw new ConfigLoadError(
`Failed to read configuration file: ${err instanceof Error ? err.message : String(err)}`,
filePath,
err,
)
}

// Parse file with error context
let parsed: unknown
try {
parsed = parseConfigContent(content, filePath)
}
catch (err) {
throw new ConfigLoadError(
`Failed to parse configuration file: ${err instanceof Error ? err.message : String(err)}`,
filePath,
err,
)
}

// Validate configuration - throw on validation errors
const result = ConfigSchema.safeParse(parsed)
if (!result.success) {
console.error(`[config] Invalid configuration in ${filePath}:`, result.error.message)
return defaultConfig
const issues = result.error.issues.map(i => ({
path: i.path.map(String),
message: i.message,
}))
const issueMessages = issues.map(i => ` - ${i.path.join('.')}: ${i.message}`).join('\n')
throw new ConfigValidationError(
`Invalid configuration in ${filePath}:\n${issueMessages}`,
filePath,
issues,
)
}

return result.data
}

/**
* Load configuration by searching from projectDir upward
* @throws {ConfigLoadError} If file cannot be read or parsed
* @throws {ConfigValidationError} If configuration fails validation
*/
export async function loadConfig(projectDir: string): Promise<Config> {
const configPath = await findConfigFile(projectDir)
if (!configPath) {
return defaultConfig
}

// Use stderr to avoid interfering with JSON output on stdout
console.error(`[config] Loading configuration from ${configPath}`)
return loadConfigFromFile(configPath)
}
Expand All @@ -99,6 +166,15 @@ export async function loadConfig(projectDir: string): Promise<Config> {
export function mergeConfig(base: Config, source: Partial<Config>): Config {
const merged: Config = { ...base }

// Merge shared settings
if (source.language !== undefined) {
merged.language = source.language
}
if (source.ignore_patterns !== undefined) {
merged.ignore_patterns = source.ignore_patterns
}

// Merge formatter config
if (source.formatter !== undefined) {
if (source.formatter === false) {
merged.formatter = false
Expand All @@ -114,5 +190,21 @@ export function mergeConfig(base: Config, source: Partial<Config>): Config {
}
}

// Merge LSP config
if (source.lsp !== undefined) {
if (source.lsp === false) {
merged.lsp = false
}
else if (base.lsp === false) {
merged.lsp = source.lsp
}
else {
merged.lsp = {
...(base.lsp ?? {}),
...source.lsp,
}
}
}

return merged
}
32 changes: 30 additions & 2 deletions packages/format/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { z } from 'zod'

/**
* Formatter configuration schema
* Compatible with opencode.json and .please/config.yml
* Compatible with .please/config.json and .please/config.yml
*/
export const FormatterItemSchema = z.object({
/** Disable this specific formatter */
Expand All @@ -25,11 +25,38 @@ export const FormatterConfigSchema = z.union([
export type FormatterConfig = z.infer<typeof FormatterConfigSchema>

/**
* Dora configuration schema
* LSP server configuration schema
*/
export const LspItemSchema = z.object({
/** Enable/disable this LSP server */
enabled: z.boolean().optional(),
/** Custom project root path for this server (must be non-empty if provided) */
root: z.string().min(1, 'root path cannot be empty').optional(),
/** Custom command to spawn the server (must have at least one element if provided) */
command: z.array(z.string()).min(1, 'command must have at least one element').optional(),
})

export type LspItem = z.infer<typeof LspItemSchema>

export const LspConfigSchema = z.union([
z.literal(false),
z.record(z.string(), LspItemSchema),
])

export type LspConfig = z.infer<typeof LspConfigSchema>

/**
* Unified configuration schema
*/
export const ConfigSchema = z.object({
/** Language for messages (en or ko) */
language: z.enum(['en', 'ko']).optional(),
/** Patterns to ignore */
ignore_patterns: z.array(z.string()).optional(),
/** Formatter configuration */
formatter: FormatterConfigSchema.optional(),
/** LSP configuration */
lsp: LspConfigSchema.optional(),
})

export type Config = z.infer<typeof ConfigSchema>
Expand All @@ -39,4 +66,5 @@ export type Config = z.infer<typeof ConfigSchema>
*/
export const defaultConfig: Config = {
formatter: {},
lsp: {},
}
Loading