refactor: extract shared utilities (isMarkdownFile, isPlainObject, resolveSymlink) (#33)

This commit is contained in:
Junho Yeo
2025-12-13 14:23:04 +09:00
committed by GitHub
parent 60d9513d3a
commit 1323443c85
9 changed files with 42 additions and 49 deletions

View File

@@ -3,6 +3,7 @@ import { homedir } from "os"
import { join, basename } from "path"
import type { AgentConfig } from "@opencode-ai/sdk"
import { parseFrontmatter } from "../../shared/frontmatter"
import { isMarkdownFile } from "../../shared/file-utils"
import type { AgentScope, AgentFrontmatter, LoadedAgent } from "./types"
function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefined {
@@ -18,10 +19,6 @@ function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefine
return result
}
function isMarkdownFile(entry: { name: string; isFile: () => boolean }): boolean {
return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile()
}
function loadAgentsFromDir(agentsDir: string, scope: AgentScope): LoadedAgent[] {
if (!existsSync(agentsDir)) {
return []

View File

@@ -3,12 +3,9 @@ import { homedir } from "os"
import { join, basename } from "path"
import { parseFrontmatter } from "../../shared/frontmatter"
import { sanitizeModelField } from "../../shared/model-sanitizer"
import { isMarkdownFile } from "../../shared/file-utils"
import type { CommandScope, CommandDefinition, CommandFrontmatter, LoadedCommand } from "./types"
function isMarkdownFile(entry: { name: string; isFile: () => boolean }): boolean {
return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile()
}
function loadCommandsFromDir(commandsDir: string, scope: CommandScope): LoadedCommand[] {
if (!existsSync(commandsDir)) {
return []

View File

@@ -1,8 +1,9 @@
import { existsSync, readdirSync, readFileSync, lstatSync, readlinkSync } from "fs"
import { existsSync, readdirSync, readFileSync } from "fs"
import { homedir } from "os"
import { join, resolve } from "path"
import { join } from "path"
import { parseFrontmatter } from "../../shared/frontmatter"
import { sanitizeModelField } from "../../shared/model-sanitizer"
import { resolveSymlink } from "../../shared/file-utils"
import type { CommandDefinition } from "../claude-code-command-loader/types"
import type { SkillScope, SkillMetadata, LoadedSkillAsCommand } from "./types"
@@ -21,14 +22,7 @@ function loadSkillsFromDir(skillsDir: string, scope: SkillScope): LoadedSkillAsC
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
let resolvedPath = skillPath
try {
if (lstatSync(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
resolvedPath = resolve(skillPath, "..", readlinkSync(skillPath))
}
} catch {
continue
}
const resolvedPath = resolveSymlink(skillPath)
const skillMdPath = join(resolvedPath, "SKILL.md")
if (!existsSync(skillMdPath)) continue

View File

@@ -1,7 +1,7 @@
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
const MAX_DEPTH = 50;
function isPlainObject(value: unknown): value is Record<string, unknown> {
export function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&

26
src/shared/file-utils.ts Normal file
View File

@@ -0,0 +1,26 @@
import { lstatSync, readlinkSync } from "fs"
import { resolve } from "path"
export function isMarkdownFile(entry: { name: string; isFile: () => boolean }): boolean {
return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile()
}
export function isSymbolicLink(filePath: string): boolean {
try {
return lstatSync(filePath, { throwIfNoEntry: false })?.isSymbolicLink() ?? false
} catch {
return false
}
}
export function resolveSymlink(filePath: string): string {
try {
const stats = lstatSync(filePath, { throwIfNoEntry: false })
if (stats?.isSymbolicLink()) {
return resolve(filePath, "..", readlinkSync(filePath))
}
return filePath
} catch {
return filePath
}
}

View File

@@ -8,3 +8,4 @@ export * from "./tool-name"
export * from "./pattern-matcher"
export * from "./hook-disabled"
export * from "./deep-merge"
export * from "./file-utils"

View File

@@ -1,3 +1,5 @@
import { isPlainObject } from "./deep-merge"
export function camelToSnake(str: string): string {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
}
@@ -6,10 +8,6 @@ export function snakeToCamel(str: string): string {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
export function objectToSnakeCase(
obj: Record<string, unknown>,
deep: boolean = true

View File

@@ -1,9 +1,10 @@
import { tool } from "@opencode-ai/plugin"
import { existsSync, readdirSync, lstatSync, readlinkSync, readFileSync } from "fs"
import { existsSync, readdirSync, readFileSync } from "fs"
import { homedir } from "os"
import { join, resolve, basename } from "path"
import { join, basename } from "path"
import { z } from "zod/v4"
import { parseFrontmatter, resolveCommandsInText } from "../../shared"
import { resolveSymlink } from "../../shared/file-utils"
import { SkillFrontmatterSchema } from "./types"
import type { SkillScope, SkillMetadata, SkillInfo, LoadedSkill, SkillFrontmatter } from "./types"
@@ -37,15 +38,7 @@ function discoverSkillsFromDir(
const skillPath = join(skillsDir, entry.name)
if (entry.isDirectory() || entry.isSymbolicLink()) {
let resolvedPath = skillPath
try {
const stats = lstatSync(skillPath, { throwIfNoEntry: false })
if (stats?.isSymbolicLink()) {
resolvedPath = resolve(skillPath, "..", readlinkSync(skillPath))
}
} catch {
continue
}
const resolvedPath = resolveSymlink(skillPath)
const skillMdPath = join(resolvedPath, "SKILL.md")
if (!existsSync(skillMdPath)) continue
@@ -83,18 +76,6 @@ const skillListForDescription = availableSkills
.map((s) => `- ${s.name}: ${s.description} (${s.scope})`)
.join("\n")
function resolveSymlink(skillPath: string): string {
try {
const stats = lstatSync(skillPath, { throwIfNoEntry: false })
if (stats?.isSymbolicLink()) {
return resolve(skillPath, "..", readlinkSync(skillPath))
}
return skillPath
} catch {
return skillPath
}
}
async function parseSkillMd(skillPath: string): Promise<SkillInfo | null> {
const resolvedPath = resolveSymlink(skillPath)
const skillMdPath = join(resolvedPath, "SKILL.md")

View File

@@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync } from "fs"
import { homedir } from "os"
import { join, basename, dirname } from "path"
import { parseFrontmatter, resolveCommandsInText, resolveFileReferencesInText, sanitizeModelField } from "../../shared"
import { isMarkdownFile } from "../../shared/file-utils"
import type { CommandScope, CommandMetadata, CommandInfo } from "./types"
function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): CommandInfo[] {
@@ -14,9 +15,7 @@ function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): Comm
const commands: CommandInfo[] = []
for (const entry of entries) {
if (entry.name.startsWith(".")) continue
if (!entry.name.endsWith(".md")) continue
if (!entry.isFile()) continue
if (!isMarkdownFile(entry)) continue
const commandPath = join(commandsDir, entry.name)
const commandName = basename(entry.name, ".md")