mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-25 01:54:49 +00:00
- Rename packages/opencode → packages/tfcode (directory only) - Rename bin/opencode → bin/tfcode (CLI binary) - Rename .opencode → .tfcode (config directory) - Update package.json name and bin field - Update config directory path references (.tfcode) - Keep internal code references as 'opencode' for easy upstream sync - Keep @opencode-ai/* workspace package names This minimal branding approach allows clean merges from upstream opencode repository while providing tfcode branding for users.
100 lines
2.7 KiB
TypeScript
100 lines
2.7 KiB
TypeScript
import { NamedError } from "@opencode-ai/util/error"
|
|
import matter from "gray-matter"
|
|
import { z } from "zod"
|
|
import { Filesystem } from "../util/filesystem"
|
|
|
|
export namespace ConfigMarkdown {
|
|
export const FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g
|
|
export const SHELL_REGEX = /!`([^`]+)`/g
|
|
|
|
export function files(template: string) {
|
|
return Array.from(template.matchAll(FILE_REGEX))
|
|
}
|
|
|
|
export function shell(template: string) {
|
|
return Array.from(template.matchAll(SHELL_REGEX))
|
|
}
|
|
|
|
// other coding agents like claude code allow invalid yaml in their
|
|
// frontmatter, we need to fallback to a more permissive parser for those cases
|
|
export function fallbackSanitization(content: string): string {
|
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
if (!match) return content
|
|
|
|
const frontmatter = match[1]
|
|
const lines = frontmatter.split(/\r?\n/)
|
|
const result: string[] = []
|
|
|
|
for (const line of lines) {
|
|
// skip comments and empty lines
|
|
if (line.trim().startsWith("#") || line.trim() === "") {
|
|
result.push(line)
|
|
continue
|
|
}
|
|
|
|
// skip lines that are continuations (indented)
|
|
if (line.match(/^\s+/)) {
|
|
result.push(line)
|
|
continue
|
|
}
|
|
|
|
// match key: value pattern
|
|
const kvMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/)
|
|
if (!kvMatch) {
|
|
result.push(line)
|
|
continue
|
|
}
|
|
|
|
const key = kvMatch[1]
|
|
const value = kvMatch[2].trim()
|
|
|
|
// skip if value is empty, already quoted, or uses block scalar
|
|
if (value === "" || value === ">" || value === "|" || value.startsWith('"') || value.startsWith("'")) {
|
|
result.push(line)
|
|
continue
|
|
}
|
|
|
|
// if value contains a colon, convert to block scalar
|
|
if (value.includes(":")) {
|
|
result.push(`${key}: |-`)
|
|
result.push(` ${value}`)
|
|
continue
|
|
}
|
|
|
|
result.push(line)
|
|
}
|
|
|
|
const processed = result.join("\n")
|
|
return content.replace(frontmatter, () => processed)
|
|
}
|
|
|
|
export async function parse(filePath: string) {
|
|
const template = await Filesystem.readText(filePath)
|
|
|
|
try {
|
|
const md = matter(template)
|
|
return md
|
|
} catch {
|
|
try {
|
|
return matter(fallbackSanitization(template))
|
|
} catch (err) {
|
|
throw new FrontmatterError(
|
|
{
|
|
path: filePath,
|
|
message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
|
|
},
|
|
{ cause: err },
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
export const FrontmatterError = NamedError.create(
|
|
"ConfigFrontmatterError",
|
|
z.object({
|
|
path: z.string(),
|
|
message: z.string(),
|
|
}),
|
|
)
|
|
}
|