refactor: extract shared utilities (isMarkdownFile, isPlainObject, resolveSymlink) (#33)
This commit is contained in:
@@ -3,6 +3,7 @@ import { homedir } from "os"
|
|||||||
import { join, basename } from "path"
|
import { join, basename } from "path"
|
||||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||||
|
import { isMarkdownFile } from "../../shared/file-utils"
|
||||||
import type { AgentScope, AgentFrontmatter, LoadedAgent } from "./types"
|
import type { AgentScope, AgentFrontmatter, LoadedAgent } from "./types"
|
||||||
|
|
||||||
function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefined {
|
function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefined {
|
||||||
@@ -18,10 +19,6 @@ function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefine
|
|||||||
return result
|
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[] {
|
function loadAgentsFromDir(agentsDir: string, scope: AgentScope): LoadedAgent[] {
|
||||||
if (!existsSync(agentsDir)) {
|
if (!existsSync(agentsDir)) {
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -3,12 +3,9 @@ import { homedir } from "os"
|
|||||||
import { join, basename } from "path"
|
import { join, basename } from "path"
|
||||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||||
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
||||||
|
import { isMarkdownFile } from "../../shared/file-utils"
|
||||||
import type { CommandScope, CommandDefinition, CommandFrontmatter, LoadedCommand } from "./types"
|
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[] {
|
function loadCommandsFromDir(commandsDir: string, scope: CommandScope): LoadedCommand[] {
|
||||||
if (!existsSync(commandsDir)) {
|
if (!existsSync(commandsDir)) {
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { existsSync, readdirSync, readFileSync, lstatSync, readlinkSync } from "fs"
|
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||||
import { homedir } from "os"
|
import { homedir } from "os"
|
||||||
import { join, resolve } from "path"
|
import { join } from "path"
|
||||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||||
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
||||||
|
import { resolveSymlink } from "../../shared/file-utils"
|
||||||
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
||||||
import type { SkillScope, SkillMetadata, LoadedSkillAsCommand } from "./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
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
||||||
|
|
||||||
let resolvedPath = skillPath
|
const resolvedPath = resolveSymlink(skillPath)
|
||||||
try {
|
|
||||||
if (lstatSync(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
||||||
resolvedPath = resolve(skillPath, "..", readlinkSync(skillPath))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const skillMdPath = join(resolvedPath, "SKILL.md")
|
const skillMdPath = join(resolvedPath, "SKILL.md")
|
||||||
if (!existsSync(skillMdPath)) continue
|
if (!existsSync(skillMdPath)) continue
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
||||||
const MAX_DEPTH = 50;
|
const MAX_DEPTH = 50;
|
||||||
|
|
||||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
return (
|
return (
|
||||||
typeof value === "object" &&
|
typeof value === "object" &&
|
||||||
value !== null &&
|
value !== null &&
|
||||||
|
|||||||
26
src/shared/file-utils.ts
Normal file
26
src/shared/file-utils.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,3 +8,4 @@ export * from "./tool-name"
|
|||||||
export * from "./pattern-matcher"
|
export * from "./pattern-matcher"
|
||||||
export * from "./hook-disabled"
|
export * from "./hook-disabled"
|
||||||
export * from "./deep-merge"
|
export * from "./deep-merge"
|
||||||
|
export * from "./file-utils"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isPlainObject } from "./deep-merge"
|
||||||
|
|
||||||
export function camelToSnake(str: string): string {
|
export function camelToSnake(str: string): string {
|
||||||
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
|
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())
|
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(
|
export function objectToSnakeCase(
|
||||||
obj: Record<string, unknown>,
|
obj: Record<string, unknown>,
|
||||||
deep: boolean = true
|
deep: boolean = true
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { tool } from "@opencode-ai/plugin"
|
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 { homedir } from "os"
|
||||||
import { join, resolve, basename } from "path"
|
import { join, basename } from "path"
|
||||||
import { z } from "zod/v4"
|
import { z } from "zod/v4"
|
||||||
import { parseFrontmatter, resolveCommandsInText } from "../../shared"
|
import { parseFrontmatter, resolveCommandsInText } from "../../shared"
|
||||||
|
import { resolveSymlink } from "../../shared/file-utils"
|
||||||
import { SkillFrontmatterSchema } from "./types"
|
import { SkillFrontmatterSchema } from "./types"
|
||||||
import type { SkillScope, SkillMetadata, SkillInfo, LoadedSkill, SkillFrontmatter } from "./types"
|
import type { SkillScope, SkillMetadata, SkillInfo, LoadedSkill, SkillFrontmatter } from "./types"
|
||||||
|
|
||||||
@@ -37,15 +38,7 @@ function discoverSkillsFromDir(
|
|||||||
const skillPath = join(skillsDir, entry.name)
|
const skillPath = join(skillsDir, entry.name)
|
||||||
|
|
||||||
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
||||||
let resolvedPath = skillPath
|
const resolvedPath = resolveSymlink(skillPath)
|
||||||
try {
|
|
||||||
const stats = lstatSync(skillPath, { throwIfNoEntry: false })
|
|
||||||
if (stats?.isSymbolicLink()) {
|
|
||||||
resolvedPath = resolve(skillPath, "..", readlinkSync(skillPath))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const skillMdPath = join(resolvedPath, "SKILL.md")
|
const skillMdPath = join(resolvedPath, "SKILL.md")
|
||||||
if (!existsSync(skillMdPath)) continue
|
if (!existsSync(skillMdPath)) continue
|
||||||
@@ -83,18 +76,6 @@ const skillListForDescription = availableSkills
|
|||||||
.map((s) => `- ${s.name}: ${s.description} (${s.scope})`)
|
.map((s) => `- ${s.name}: ${s.description} (${s.scope})`)
|
||||||
.join("\n")
|
.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> {
|
async function parseSkillMd(skillPath: string): Promise<SkillInfo | null> {
|
||||||
const resolvedPath = resolveSymlink(skillPath)
|
const resolvedPath = resolveSymlink(skillPath)
|
||||||
const skillMdPath = join(resolvedPath, "SKILL.md")
|
const skillMdPath = join(resolvedPath, "SKILL.md")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync } from "fs"
|
|||||||
import { homedir } from "os"
|
import { homedir } from "os"
|
||||||
import { join, basename, dirname } from "path"
|
import { join, basename, dirname } from "path"
|
||||||
import { parseFrontmatter, resolveCommandsInText, resolveFileReferencesInText, sanitizeModelField } from "../../shared"
|
import { parseFrontmatter, resolveCommandsInText, resolveFileReferencesInText, sanitizeModelField } from "../../shared"
|
||||||
|
import { isMarkdownFile } from "../../shared/file-utils"
|
||||||
import type { CommandScope, CommandMetadata, CommandInfo } from "./types"
|
import type { CommandScope, CommandMetadata, CommandInfo } from "./types"
|
||||||
|
|
||||||
function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): CommandInfo[] {
|
function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): CommandInfo[] {
|
||||||
@@ -14,9 +15,7 @@ function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): Comm
|
|||||||
const commands: CommandInfo[] = []
|
const commands: CommandInfo[] = []
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.name.startsWith(".")) continue
|
if (!isMarkdownFile(entry)) continue
|
||||||
if (!entry.name.endsWith(".md")) continue
|
|
||||||
if (!entry.isFile()) continue
|
|
||||||
|
|
||||||
const commandPath = join(commandsDir, entry.name)
|
const commandPath = join(commandsDir, entry.name)
|
||||||
const commandName = basename(entry.name, ".md")
|
const commandName = basename(entry.name, ".md")
|
||||||
|
|||||||
Reference in New Issue
Block a user