Problem
Users who need file/directory path validation must write custom z.string().refine() logic for existence checks and extension filtering. This is boilerplate that every CLI with file inputs repeats.
Solution
Provide composable file() and directory() wrappers that add filesystem validation to any Standard Schema:
import { defineOption, file, directory } from '@bunli/core'
config: defineOption(
file(z.string(), {
mustExist: true,
extensions: ['.json', '.yaml']
})
)
outDir: defineOption(
directory(z.string(), { mustExist: false })
)
Design
Create packages/core/src/fs-validators.ts:
interface FileOptions {
mustExist?: boolean // default: false
extensions?: string[] // e.g. ['.ts', '.js']
}
interface DirectoryOptions {
mustExist?: boolean // default: false
}
function file<S extends StandardSchemaV1>(schema: S, options?: FileOptions): StandardSchemaV1
function directory<S extends StandardSchemaV1>(schema: S, options?: DirectoryOptions): StandardSchemaV1
Implementation wraps the inner schema by creating a Standard Schema V1-conforming object:
- Validate raw value with inner schema → get string path
- If
mustExist: check fs.statSync(path) — verify it's a file (not directory) or vice versa
- If
extensions: check path.extname() against allowed list (case-insensitive)
- Return validated path or issues
The wrapper implements { '~standard': { version: 1, vendor: 'bunli', validate(value) { ... } } }.
Edge cases
- Relative paths: resolve relative to
process.cwd()
- Symlinks:
fs.statSync follows symlinks (correct default behavior)
- Extension case: compare lowercase (macOS/Windows are case-insensitive)
mustExist: false: only structural validation, no filesystem access
file() + directory path: stat.isFile() must be true → error if directory
directory() + file path: stat.isDirectory() must be true → error if file
- Async compatibility: validate function must work with Standard Schema V1's sync/async contract
Files
- New:
packages/core/src/fs-validators.ts
packages/core/src/index.ts — export file and directory
Testing
- Temp directories with real files
- Valid file passes, nonexistent with
mustExist fails
- Wrong extension fails
- Directory passed to
file() fails (and vice versa)
- Composability:
file(z.string().min(1), { mustExist: true, extensions: ['.json'] })
Independent
No dependencies on other issues in this milestone.
Problem
Users who need file/directory path validation must write custom
z.string().refine()logic for existence checks and extension filtering. This is boilerplate that every CLI with file inputs repeats.Solution
Provide composable
file()anddirectory()wrappers that add filesystem validation to any Standard Schema:Design
Create
packages/core/src/fs-validators.ts:Implementation wraps the inner schema by creating a Standard Schema V1-conforming object:
mustExist: checkfs.statSync(path)— verify it's a file (not directory) or vice versaextensions: checkpath.extname()against allowed list (case-insensitive)The wrapper implements
{ '~standard': { version: 1, vendor: 'bunli', validate(value) { ... } } }.Edge cases
process.cwd()fs.statSyncfollows symlinks (correct default behavior)mustExist: false: only structural validation, no filesystem accessfile()+ directory path:stat.isFile()must be true → error if directorydirectory()+ file path:stat.isDirectory()must be true → error if fileFiles
packages/core/src/fs-validators.tspackages/core/src/index.ts— exportfileanddirectoryTesting
mustExistfailsfile()fails (and vice versa)file(z.string().min(1), { mustExist: true, extensions: ['.json'] })Independent
No dependencies on other issues in this milestone.