Skip to content

Commit 1323443

Browse files
authored
refactor: extract shared utilities (isMarkdownFile, isPlainObject, resolveSymlink) (#33)
1 parent 60d9513 commit 1323443

File tree

9 files changed

+42
-49
lines changed

9 files changed

+42
-49
lines changed

src/features/claude-code-agent-loader/loader.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { homedir } from "os"
33
import { join, basename } from "path"
44
import type { AgentConfig } from "@opencode-ai/sdk"
55
import { parseFrontmatter } from "../../shared/frontmatter"
6+
import { isMarkdownFile } from "../../shared/file-utils"
67
import type { AgentScope, AgentFrontmatter, LoadedAgent } from "./types"
78

89
function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefined {
@@ -18,10 +19,6 @@ function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefine
1819
return result
1920
}
2021

21-
function isMarkdownFile(entry: { name: string; isFile: () => boolean }): boolean {
22-
return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile()
23-
}
24-
2522
function loadAgentsFromDir(agentsDir: string, scope: AgentScope): LoadedAgent[] {
2623
if (!existsSync(agentsDir)) {
2724
return []

src/features/claude-code-command-loader/loader.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@ import { homedir } from "os"
33
import { join, basename } from "path"
44
import { parseFrontmatter } from "../../shared/frontmatter"
55
import { sanitizeModelField } from "../../shared/model-sanitizer"
6+
import { isMarkdownFile } from "../../shared/file-utils"
67
import type { CommandScope, CommandDefinition, CommandFrontmatter, LoadedCommand } from "./types"
78

8-
function isMarkdownFile(entry: { name: string; isFile: () => boolean }): boolean {
9-
return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile()
10-
}
11-
129
function loadCommandsFromDir(commandsDir: string, scope: CommandScope): LoadedCommand[] {
1310
if (!existsSync(commandsDir)) {
1411
return []

src/features/claude-code-skill-loader/loader.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { existsSync, readdirSync, readFileSync, lstatSync, readlinkSync } from "fs"
1+
import { existsSync, readdirSync, readFileSync } from "fs"
22
import { homedir } from "os"
3-
import { join, resolve } from "path"
3+
import { join } from "path"
44
import { parseFrontmatter } from "../../shared/frontmatter"
55
import { sanitizeModelField } from "../../shared/model-sanitizer"
6+
import { resolveSymlink } from "../../shared/file-utils"
67
import type { CommandDefinition } from "../claude-code-command-loader/types"
78
import type { SkillScope, SkillMetadata, LoadedSkillAsCommand } from "./types"
89

@@ -21,14 +22,7 @@ function loadSkillsFromDir(skillsDir: string, scope: SkillScope): LoadedSkillAsC
2122

2223
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
2324

24-
let resolvedPath = skillPath
25-
try {
26-
if (lstatSync(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
27-
resolvedPath = resolve(skillPath, "..", readlinkSync(skillPath))
28-
}
29-
} catch {
30-
continue
31-
}
25+
const resolvedPath = resolveSymlink(skillPath)
3226

3327
const skillMdPath = join(resolvedPath, "SKILL.md")
3428
if (!existsSync(skillMdPath)) continue

src/shared/deep-merge.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
22
const MAX_DEPTH = 50;
33

4-
function isPlainObject(value: unknown): value is Record<string, unknown> {
4+
export function isPlainObject(value: unknown): value is Record<string, unknown> {
55
return (
66
typeof value === "object" &&
77
value !== null &&

src/shared/file-utils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { lstatSync, readlinkSync } from "fs"
2+
import { resolve } from "path"
3+
4+
export function isMarkdownFile(entry: { name: string; isFile: () => boolean }): boolean {
5+
return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile()
6+
}
7+
8+
export function isSymbolicLink(filePath: string): boolean {
9+
try {
10+
return lstatSync(filePath, { throwIfNoEntry: false })?.isSymbolicLink() ?? false
11+
} catch {
12+
return false
13+
}
14+
}
15+
16+
export function resolveSymlink(filePath: string): string {
17+
try {
18+
const stats = lstatSync(filePath, { throwIfNoEntry: false })
19+
if (stats?.isSymbolicLink()) {
20+
return resolve(filePath, "..", readlinkSync(filePath))
21+
}
22+
return filePath
23+
} catch {
24+
return filePath
25+
}
26+
}

src/shared/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from "./tool-name"
88
export * from "./pattern-matcher"
99
export * from "./hook-disabled"
1010
export * from "./deep-merge"
11+
export * from "./file-utils"

src/shared/snake-case.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { isPlainObject } from "./deep-merge"
2+
13
export function camelToSnake(str: string): string {
24
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
35
}
@@ -6,10 +8,6 @@ export function snakeToCamel(str: string): string {
68
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
79
}
810

9-
function isPlainObject(value: unknown): value is Record<string, unknown> {
10-
return typeof value === "object" && value !== null && !Array.isArray(value)
11-
}
12-
1311
export function objectToSnakeCase(
1412
obj: Record<string, unknown>,
1513
deep: boolean = true

src/tools/skill/tools.ts

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { tool } from "@opencode-ai/plugin"
2-
import { existsSync, readdirSync, lstatSync, readlinkSync, readFileSync } from "fs"
2+
import { existsSync, readdirSync, readFileSync } from "fs"
33
import { homedir } from "os"
4-
import { join, resolve, basename } from "path"
4+
import { join, basename } from "path"
55
import { z } from "zod/v4"
66
import { parseFrontmatter, resolveCommandsInText } from "../../shared"
7+
import { resolveSymlink } from "../../shared/file-utils"
78
import { SkillFrontmatterSchema } from "./types"
89
import type { SkillScope, SkillMetadata, SkillInfo, LoadedSkill, SkillFrontmatter } from "./types"
910

@@ -37,15 +38,7 @@ function discoverSkillsFromDir(
3738
const skillPath = join(skillsDir, entry.name)
3839

3940
if (entry.isDirectory() || entry.isSymbolicLink()) {
40-
let resolvedPath = skillPath
41-
try {
42-
const stats = lstatSync(skillPath, { throwIfNoEntry: false })
43-
if (stats?.isSymbolicLink()) {
44-
resolvedPath = resolve(skillPath, "..", readlinkSync(skillPath))
45-
}
46-
} catch {
47-
continue
48-
}
41+
const resolvedPath = resolveSymlink(skillPath)
4942

5043
const skillMdPath = join(resolvedPath, "SKILL.md")
5144
if (!existsSync(skillMdPath)) continue
@@ -83,18 +76,6 @@ const skillListForDescription = availableSkills
8376
.map((s) => `- ${s.name}: ${s.description} (${s.scope})`)
8477
.join("\n")
8578

86-
function resolveSymlink(skillPath: string): string {
87-
try {
88-
const stats = lstatSync(skillPath, { throwIfNoEntry: false })
89-
if (stats?.isSymbolicLink()) {
90-
return resolve(skillPath, "..", readlinkSync(skillPath))
91-
}
92-
return skillPath
93-
} catch {
94-
return skillPath
95-
}
96-
}
97-
9879
async function parseSkillMd(skillPath: string): Promise<SkillInfo | null> {
9980
const resolvedPath = resolveSymlink(skillPath)
10081
const skillMdPath = join(resolvedPath, "SKILL.md")

src/tools/slashcommand/tools.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync } from "fs"
33
import { homedir } from "os"
44
import { join, basename, dirname } from "path"
55
import { parseFrontmatter, resolveCommandsInText, resolveFileReferencesInText, sanitizeModelField } from "../../shared"
6+
import { isMarkdownFile } from "../../shared/file-utils"
67
import type { CommandScope, CommandMetadata, CommandInfo } from "./types"
78

89
function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): CommandInfo[] {
@@ -14,9 +15,7 @@ function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): Comm
1415
const commands: CommandInfo[] = []
1516

1617
for (const entry of entries) {
17-
if (entry.name.startsWith(".")) continue
18-
if (!entry.name.endsWith(".md")) continue
19-
if (!entry.isFile()) continue
18+
if (!isMarkdownFile(entry)) continue
2019

2120
const commandPath = join(commandsDir, entry.name)
2221
const commandName = basename(entry.name, ".md")

0 commit comments

Comments
 (0)