merge: integrate origin/dev (5th merge) — resolve @path skill references in split file structure
This commit is contained in:
commit
c71f0aa700
@ -3,6 +3,7 @@ import { join } from "path"
|
|||||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||||
import { resolveSymlink } from "../../shared/file-utils"
|
import { resolveSymlink } from "../../shared/file-utils"
|
||||||
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
||||||
|
import { resolveSkillPathReferences } from "../../shared/skill-path-resolver"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
||||||
import type { SkillMetadata } from "../opencode-skill-loader/types"
|
import type { SkillMetadata } from "../opencode-skill-loader/types"
|
||||||
@ -37,7 +38,8 @@ export function loadPluginSkillsAsCommands(
|
|||||||
const originalDescription = data.description || ""
|
const originalDescription = data.description || ""
|
||||||
const formattedDescription = `(plugin: ${plugin.name} - Skill) ${originalDescription}`
|
const formattedDescription = `(plugin: ${plugin.name} - Skill) ${originalDescription}`
|
||||||
|
|
||||||
const wrappedTemplate = `<skill-instruction>\nBase directory for this skill: ${resolvedPath}/\nFile references (@path) in this skill are relative to this directory.\n\n${body.trim()}\n</skill-instruction>\n\n<user-request>\n$ARGUMENTS\n</user-request>`
|
const resolvedBody = resolveSkillPathReferences(body.trim(), resolvedPath)
|
||||||
|
const wrappedTemplate = `<skill-instruction>\nBase directory for this skill: ${resolvedPath}/\nFile references (@path) in this skill are relative to this directory.\n\n${resolvedBody}\n</skill-instruction>\n\n<user-request>\n$ARGUMENTS\n</user-request>`
|
||||||
|
|
||||||
const definition = {
|
const definition = {
|
||||||
name: namespacedName,
|
name: namespacedName,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import yaml from "js-yaml"
|
|||||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||||
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
||||||
import { resolveSymlink, isMarkdownFile } from "../../shared/file-utils"
|
import { resolveSymlink, isMarkdownFile } from "../../shared/file-utils"
|
||||||
|
import { resolveSkillPathReferences } from "../../shared/skill-path-resolver"
|
||||||
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
||||||
import type { SkillScope, SkillMetadata, LoadedSkill } from "./types"
|
import type { SkillScope, SkillMetadata, LoadedSkill } from "./types"
|
||||||
import type { SkillMcpConfig } from "../skill-mcp-manager/types"
|
import type { SkillMcpConfig } from "../skill-mcp-manager/types"
|
||||||
@ -90,11 +91,12 @@ export async function loadSkillFromPathAsync(
|
|||||||
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
|
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
|
||||||
const formattedDescription = `(${scope} - Skill) ${originalDescription}`
|
const formattedDescription = `(${scope} - Skill) ${originalDescription}`
|
||||||
|
|
||||||
|
const resolvedBody = resolveSkillPathReferences(body.trim(), resolvedPath)
|
||||||
const wrappedTemplate = `<skill-instruction>
|
const wrappedTemplate = `<skill-instruction>
|
||||||
Base directory for this skill: ${resolvedPath}/
|
Base directory for this skill: ${resolvedPath}/
|
||||||
File references (@path) in this skill are relative to this directory.
|
File references (@path) in this skill are relative to this directory.
|
||||||
|
|
||||||
${body.trim()}
|
${resolvedBody}
|
||||||
</skill-instruction>
|
</skill-instruction>
|
||||||
|
|
||||||
<user-request>
|
<user-request>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { promises as fs } from "fs"
|
|||||||
import { basename } from "path"
|
import { 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 { resolveSkillPathReferences } from "../../shared/skill-path-resolver"
|
||||||
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
||||||
import { parseAllowedTools } from "./allowed-tools-parser"
|
import { parseAllowedTools } from "./allowed-tools-parser"
|
||||||
import { loadMcpJsonFromDir, parseSkillMcpConfigFromFrontmatter } from "./skill-mcp-config"
|
import { loadMcpJsonFromDir, parseSkillMcpConfigFromFrontmatter } from "./skill-mcp-config"
|
||||||
@ -30,7 +31,8 @@ export async function loadSkillFromPath(options: {
|
|||||||
const isOpencodeSource = options.scope === "opencode" || options.scope === "opencode-project"
|
const isOpencodeSource = options.scope === "opencode" || options.scope === "opencode-project"
|
||||||
const formattedDescription = `(${options.scope} - Skill) ${originalDescription}`
|
const formattedDescription = `(${options.scope} - Skill) ${originalDescription}`
|
||||||
|
|
||||||
const templateContent = `<skill-instruction>\nBase directory for this skill: ${options.resolvedPath}/\nFile references (@path) in this skill are relative to this directory.\n\n${body.trim()}\n</skill-instruction>\n\n<user-request>\n$ARGUMENTS\n</user-request>`
|
const resolvedBody = resolveSkillPathReferences(body.trim(), options.resolvedPath)
|
||||||
|
const templateContent = `<skill-instruction>\nBase directory for this skill: ${options.resolvedPath}/\nFile references (@path) in this skill are relative to this directory.\n\n${resolvedBody}\n</skill-instruction>\n\n<user-request>\n$ARGUMENTS\n</user-request>`
|
||||||
|
|
||||||
const eagerLoader: LazyContentLoader = {
|
const eagerLoader: LazyContentLoader = {
|
||||||
loaded: true,
|
loaded: true,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { dirname, isAbsolute, resolve } from "path"
|
|||||||
import { homedir } from "os"
|
import { homedir } from "os"
|
||||||
import { parseFrontmatter } from "../../../shared/frontmatter"
|
import { parseFrontmatter } from "../../../shared/frontmatter"
|
||||||
import { sanitizeModelField } from "../../../shared/model-sanitizer"
|
import { sanitizeModelField } from "../../../shared/model-sanitizer"
|
||||||
|
import { resolveSkillPathReferences } from "../../../shared/skill-path-resolver"
|
||||||
import { parseAllowedTools } from "../allowed-tools-parser"
|
import { parseAllowedTools } from "../allowed-tools-parser"
|
||||||
|
|
||||||
function resolveFilePath(from: string, configDir?: string): string {
|
function resolveFilePath(from: string, configDir?: string): string {
|
||||||
@ -66,11 +67,12 @@ export function configEntryToLoadedSkill(
|
|||||||
? dirname(resolveFilePath(entry.from, configDir))
|
? dirname(resolveFilePath(entry.from, configDir))
|
||||||
: configDir || process.cwd()
|
: configDir || process.cwd()
|
||||||
|
|
||||||
|
const resolvedTemplate = resolveSkillPathReferences(template.trim(), resolvedPath)
|
||||||
const wrappedTemplate = `<skill-instruction>
|
const wrappedTemplate = `<skill-instruction>
|
||||||
Base directory for this skill: ${resolvedPath}/
|
Base directory for this skill: ${resolvedPath}/
|
||||||
File references (@path) in this skill are relative to this directory.
|
File references (@path) in this skill are relative to this directory.
|
||||||
|
|
||||||
${template.trim()}
|
${resolvedTemplate}
|
||||||
</skill-instruction>
|
</skill-instruction>
|
||||||
|
|
||||||
<user-request>
|
<user-request>
|
||||||
|
|||||||
104
src/shared/skill-path-resolver.test.ts
Normal file
104
src/shared/skill-path-resolver.test.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { describe, it, expect } from "bun:test"
|
||||||
|
import { resolveSkillPathReferences } from "./skill-path-resolver"
|
||||||
|
|
||||||
|
describe("resolveSkillPathReferences", () => {
|
||||||
|
it("resolves @path references containing a slash to absolute paths", () => {
|
||||||
|
//#given
|
||||||
|
const content = "Run `python3 @scripts/search.py` to search"
|
||||||
|
const basePath = "/home/user/.config/opencode/skills/frontend-ui-ux"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveSkillPathReferences(content, basePath)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(
|
||||||
|
"Run `python3 /home/user/.config/opencode/skills/frontend-ui-ux/scripts/search.py` to search"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("resolves multiple @path references in the same content", () => {
|
||||||
|
//#given
|
||||||
|
const content = "Script: @scripts/search.py\nData: @data/styles.csv"
|
||||||
|
const basePath = "/skills/frontend"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveSkillPathReferences(content, basePath)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(
|
||||||
|
"Script: /skills/frontend/scripts/search.py\nData: /skills/frontend/data/styles.csv"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("resolves directory references with trailing slash", () => {
|
||||||
|
//#given
|
||||||
|
const content = "Data files: @data/"
|
||||||
|
const basePath = "/skills/frontend"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveSkillPathReferences(content, basePath)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe("Data files: /skills/frontend/data/")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not resolve single-segment @references without slash", () => {
|
||||||
|
//#given
|
||||||
|
const content = "@param value @ts-ignore @path"
|
||||||
|
const basePath = "/skills/frontend"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveSkillPathReferences(content, basePath)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe("@param value @ts-ignore @path")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not resolve email addresses", () => {
|
||||||
|
//#given
|
||||||
|
const content = "Contact user@example.com for help"
|
||||||
|
const basePath = "/skills/frontend"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveSkillPathReferences(content, basePath)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe("Contact user@example.com for help")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles deeply nested path references", () => {
|
||||||
|
//#given
|
||||||
|
const content = "@data/stacks/html-tailwind.csv"
|
||||||
|
const basePath = "/skills/frontend"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveSkillPathReferences(content, basePath)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe("/skills/frontend/data/stacks/html-tailwind.csv")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns content unchanged when no @path references exist", () => {
|
||||||
|
//#given
|
||||||
|
const content = "No path references here"
|
||||||
|
const basePath = "/skills/frontend"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveSkillPathReferences(content, basePath)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe("No path references here")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles basePath with trailing slash", () => {
|
||||||
|
//#given
|
||||||
|
const content = "@scripts/search.py"
|
||||||
|
const basePath = "/skills/frontend/"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveSkillPathReferences(content, basePath)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe("/skills/frontend/scripts/search.py")
|
||||||
|
})
|
||||||
|
})
|
||||||
17
src/shared/skill-path-resolver.ts
Normal file
17
src/shared/skill-path-resolver.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { join } from "path"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves @path references in skill content to absolute paths.
|
||||||
|
*
|
||||||
|
* Matches @references that contain at least one slash (e.g., @scripts/search.py, @data/)
|
||||||
|
* to avoid false positives with decorators (@param), JSDoc tags (@ts-ignore), etc.
|
||||||
|
*
|
||||||
|
* Email addresses are excluded since they have alphanumeric characters before @.
|
||||||
|
*/
|
||||||
|
export function resolveSkillPathReferences(content: string, basePath: string): string {
|
||||||
|
const normalizedBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath
|
||||||
|
return content.replace(
|
||||||
|
/(?<![a-zA-Z0-9])@([a-zA-Z0-9_-]+\/[a-zA-Z0-9_.\-\/]*)/g,
|
||||||
|
(_, relativePath: string) => join(normalizedBase, relativePath)
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user