mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 12:00:15 +00:00
fix(coding-agent): apply config overrides to auto-discovery
This commit is contained in:
parent
ea93e2f3da
commit
b270e7b585
6 changed files with 443 additions and 380 deletions
|
|
@ -2,12 +2,8 @@
|
||||||
* TUI config selector for `pi config` command
|
* TUI config selector for `pi config` command
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
||||||
import { basename, join, relative } from "node:path";
|
|
||||||
import { ProcessTerminal, TUI } from "@mariozechner/pi-tui";
|
import { ProcessTerminal, TUI } from "@mariozechner/pi-tui";
|
||||||
import { minimatch } from "minimatch";
|
import type { ResolvedPaths } from "../core/package-manager.js";
|
||||||
import { CONFIG_DIR_NAME } from "../config.js";
|
|
||||||
import type { PathMetadata, ResolvedPaths, ResolvedResource } from "../core/package-manager.js";
|
|
||||||
import type { SettingsManager } from "../core/settings-manager.js";
|
import type { SettingsManager } from "../core/settings-manager.js";
|
||||||
import { ConfigSelectorComponent } from "../modes/interactive/components/config-selector.js";
|
import { ConfigSelectorComponent } from "../modes/interactive/components/config-selector.js";
|
||||||
import { initTheme, stopThemeWatcher } from "../modes/interactive/theme/theme.js";
|
import { initTheme, stopThemeWatcher } from "../modes/interactive/theme/theme.js";
|
||||||
|
|
@ -19,359 +15,17 @@ export interface ConfigSelectorOptions {
|
||||||
agentDir: string;
|
agentDir: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceType = "extensions" | "skills" | "prompts" | "themes";
|
|
||||||
|
|
||||||
const FILE_PATTERNS: Record<ResourceType, RegExp> = {
|
|
||||||
extensions: /\.(ts|js)$/,
|
|
||||||
skills: /\.md$/,
|
|
||||||
prompts: /\.md$/,
|
|
||||||
themes: /\.json$/,
|
|
||||||
};
|
|
||||||
|
|
||||||
function collectFiles(dir: string, pattern: RegExp): string[] {
|
|
||||||
const files: string[] = [];
|
|
||||||
if (!existsSync(dir)) return files;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const entries = readdirSync(dir, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.name.startsWith(".")) continue;
|
|
||||||
if (entry.name === "node_modules") continue;
|
|
||||||
|
|
||||||
const fullPath = join(dir, entry.name);
|
|
||||||
let isDir = entry.isDirectory();
|
|
||||||
let isFile = entry.isFile();
|
|
||||||
|
|
||||||
if (entry.isSymbolicLink()) {
|
|
||||||
try {
|
|
||||||
const stats = statSync(fullPath);
|
|
||||||
isDir = stats.isDirectory();
|
|
||||||
isFile = stats.isFile();
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDir) {
|
|
||||||
files.push(...collectFiles(fullPath, pattern));
|
|
||||||
} else if (isFile && pattern.test(entry.name)) {
|
|
||||||
files.push(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore errors
|
|
||||||
}
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect skill entries from a directory.
|
|
||||||
* Matches the behavior of loadSkillsFromDirInternal in skills.ts:
|
|
||||||
* - Direct .md files in the root directory
|
|
||||||
* - Subdirectories containing SKILL.md (returns the directory path)
|
|
||||||
* - Recursively checks subdirectories that don't have SKILL.md
|
|
||||||
*/
|
|
||||||
function collectSkillEntries(dir: string, isRoot = true): string[] {
|
|
||||||
const entries: string[] = [];
|
|
||||||
if (!existsSync(dir)) return entries;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const dirEntries = readdirSync(dir, { withFileTypes: true });
|
|
||||||
for (const entry of dirEntries) {
|
|
||||||
if (entry.name.startsWith(".")) continue;
|
|
||||||
if (entry.name === "node_modules") continue;
|
|
||||||
|
|
||||||
const fullPath = join(dir, entry.name);
|
|
||||||
let isDir = entry.isDirectory();
|
|
||||||
let isFile = entry.isFile();
|
|
||||||
|
|
||||||
if (entry.isSymbolicLink()) {
|
|
||||||
try {
|
|
||||||
const stats = statSync(fullPath);
|
|
||||||
isDir = stats.isDirectory();
|
|
||||||
isFile = stats.isFile();
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDir) {
|
|
||||||
// Check for SKILL.md in subdirectory
|
|
||||||
const skillMd = join(fullPath, "SKILL.md");
|
|
||||||
if (existsSync(skillMd)) {
|
|
||||||
// This is a skill directory, add it
|
|
||||||
entries.push(fullPath);
|
|
||||||
} else {
|
|
||||||
// Recurse into subdirectory to find skills
|
|
||||||
entries.push(...collectSkillEntries(fullPath, false));
|
|
||||||
}
|
|
||||||
} else if (isFile && entry.name.endsWith(".md")) {
|
|
||||||
// Only include direct .md files at root level, or SKILL.md anywhere
|
|
||||||
if (isRoot || entry.name === "SKILL.md") {
|
|
||||||
entries.push(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore errors
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectExtensionEntries(dir: string): string[] {
|
|
||||||
const entries: string[] = [];
|
|
||||||
if (!existsSync(dir)) return entries;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const dirEntries = readdirSync(dir, { withFileTypes: true });
|
|
||||||
for (const entry of dirEntries) {
|
|
||||||
if (entry.name.startsWith(".")) continue;
|
|
||||||
if (entry.name === "node_modules") continue;
|
|
||||||
|
|
||||||
const fullPath = join(dir, entry.name);
|
|
||||||
let isDir = entry.isDirectory();
|
|
||||||
let isFile = entry.isFile();
|
|
||||||
|
|
||||||
if (entry.isSymbolicLink()) {
|
|
||||||
try {
|
|
||||||
const stats = statSync(fullPath);
|
|
||||||
isDir = stats.isDirectory();
|
|
||||||
isFile = stats.isFile();
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFile && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
|
|
||||||
entries.push(fullPath);
|
|
||||||
} else if (isDir) {
|
|
||||||
// Check for index.ts/js or package.json with pi field
|
|
||||||
const indexTs = join(fullPath, "index.ts");
|
|
||||||
const indexJs = join(fullPath, "index.js");
|
|
||||||
if (existsSync(indexTs)) {
|
|
||||||
entries.push(indexTs);
|
|
||||||
} else if (existsSync(indexJs)) {
|
|
||||||
entries.push(indexJs);
|
|
||||||
}
|
|
||||||
// Skip subdirectories that don't have an entry point
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore errors
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeExactPattern(pattern: string): string {
|
|
||||||
if (pattern.startsWith("./") || pattern.startsWith(".\\")) {
|
|
||||||
return pattern.slice(2);
|
|
||||||
}
|
|
||||||
return pattern;
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchesAnyPattern(filePath: string, patterns: string[], baseDir: string): boolean {
|
|
||||||
const rel = relative(baseDir, filePath);
|
|
||||||
const name = basename(filePath);
|
|
||||||
return patterns.some(
|
|
||||||
(pattern) => minimatch(rel, pattern) || minimatch(name, pattern) || minimatch(filePath, pattern),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchesAnyExactPattern(filePath: string, patterns: string[], baseDir: string): boolean {
|
|
||||||
if (patterns.length === 0) return false;
|
|
||||||
const rel = relative(baseDir, filePath);
|
|
||||||
return patterns.some((pattern) => {
|
|
||||||
const normalized = normalizeExactPattern(pattern);
|
|
||||||
return normalized === rel || normalized === filePath;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isEnabledByOverrides(filePath: string, patterns: string[], baseDir: string): boolean {
|
|
||||||
const overrides = patterns.filter(
|
|
||||||
(pattern) => pattern.startsWith("!") || pattern.startsWith("+") || pattern.startsWith("-"),
|
|
||||||
);
|
|
||||||
const excludes = overrides.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
|
|
||||||
const forceIncludes = overrides.filter((pattern) => pattern.startsWith("+")).map((pattern) => pattern.slice(1));
|
|
||||||
const forceExcludes = overrides.filter((pattern) => pattern.startsWith("-")).map((pattern) => pattern.slice(1));
|
|
||||||
|
|
||||||
let enabled = true;
|
|
||||||
if (excludes.length > 0 && matchesAnyPattern(filePath, excludes, baseDir)) {
|
|
||||||
enabled = false;
|
|
||||||
}
|
|
||||||
if (forceIncludes.length > 0 && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) {
|
|
||||||
enabled = true;
|
|
||||||
}
|
|
||||||
if (forceExcludes.length > 0 && matchesAnyExactPattern(filePath, forceExcludes, baseDir)) {
|
|
||||||
enabled = false;
|
|
||||||
}
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge auto-discovered resources into resolved paths.
|
|
||||||
* Auto-discovered resources are enabled by default unless explicitly disabled via settings.
|
|
||||||
*/
|
|
||||||
function mergeAutoDiscoveredResources(
|
|
||||||
resolvedPaths: ResolvedPaths,
|
|
||||||
settingsManager: SettingsManager,
|
|
||||||
cwd: string,
|
|
||||||
agentDir: string,
|
|
||||||
): ResolvedPaths {
|
|
||||||
const result: ResolvedPaths = {
|
|
||||||
extensions: [...resolvedPaths.extensions],
|
|
||||||
skills: [...resolvedPaths.skills],
|
|
||||||
prompts: [...resolvedPaths.prompts],
|
|
||||||
themes: [...resolvedPaths.themes],
|
|
||||||
};
|
|
||||||
|
|
||||||
const existingPaths = {
|
|
||||||
extensions: new Set(resolvedPaths.extensions.map((r) => r.path)),
|
|
||||||
skills: new Set(resolvedPaths.skills.map((r) => r.path)),
|
|
||||||
prompts: new Set(resolvedPaths.prompts.map((r) => r.path)),
|
|
||||||
themes: new Set(resolvedPaths.themes.map((r) => r.path)),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get override patterns from settings
|
|
||||||
const globalSettings = settingsManager.getGlobalSettings();
|
|
||||||
const projectSettings = settingsManager.getProjectSettings();
|
|
||||||
|
|
||||||
const userOverrides = {
|
|
||||||
extensions: globalSettings.extensions ?? [],
|
|
||||||
skills: globalSettings.skills ?? [],
|
|
||||||
prompts: globalSettings.prompts ?? [],
|
|
||||||
themes: globalSettings.themes ?? [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const projectOverrides = {
|
|
||||||
extensions: projectSettings.extensions ?? [],
|
|
||||||
skills: projectSettings.skills ?? [],
|
|
||||||
prompts: projectSettings.prompts ?? [],
|
|
||||||
themes: projectSettings.themes ?? [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const addResources = (
|
|
||||||
target: ResolvedResource[],
|
|
||||||
existing: Set<string>,
|
|
||||||
paths: string[],
|
|
||||||
metadata: PathMetadata,
|
|
||||||
overrides: string[],
|
|
||||||
baseDir: string,
|
|
||||||
) => {
|
|
||||||
for (const path of paths) {
|
|
||||||
if (!existing.has(path)) {
|
|
||||||
const enabled = isEnabledByOverrides(path, overrides, baseDir);
|
|
||||||
target.push({ path, enabled, metadata });
|
|
||||||
existing.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const userBaseDir = agentDir;
|
|
||||||
const projectBaseDir = join(cwd, CONFIG_DIR_NAME);
|
|
||||||
|
|
||||||
// User scope auto-discovery
|
|
||||||
const userExtDir = join(agentDir, "extensions");
|
|
||||||
const userSkillsDir = join(agentDir, "skills");
|
|
||||||
const userPromptsDir = join(agentDir, "prompts");
|
|
||||||
const userThemesDir = join(agentDir, "themes");
|
|
||||||
|
|
||||||
addResources(
|
|
||||||
result.extensions,
|
|
||||||
existingPaths.extensions,
|
|
||||||
collectExtensionEntries(userExtDir),
|
|
||||||
{ source: "auto", scope: "user", origin: "top-level" },
|
|
||||||
userOverrides.extensions,
|
|
||||||
userBaseDir,
|
|
||||||
);
|
|
||||||
addResources(
|
|
||||||
result.skills,
|
|
||||||
existingPaths.skills,
|
|
||||||
collectSkillEntries(userSkillsDir),
|
|
||||||
{ source: "auto", scope: "user", origin: "top-level" },
|
|
||||||
userOverrides.skills,
|
|
||||||
userBaseDir,
|
|
||||||
);
|
|
||||||
addResources(
|
|
||||||
result.prompts,
|
|
||||||
existingPaths.prompts,
|
|
||||||
collectFiles(userPromptsDir, FILE_PATTERNS.prompts),
|
|
||||||
{ source: "auto", scope: "user", origin: "top-level" },
|
|
||||||
userOverrides.prompts,
|
|
||||||
userBaseDir,
|
|
||||||
);
|
|
||||||
addResources(
|
|
||||||
result.themes,
|
|
||||||
existingPaths.themes,
|
|
||||||
collectFiles(userThemesDir, FILE_PATTERNS.themes),
|
|
||||||
{ source: "auto", scope: "user", origin: "top-level" },
|
|
||||||
userOverrides.themes,
|
|
||||||
userBaseDir,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Project scope auto-discovery
|
|
||||||
const projectExtDir = join(cwd, CONFIG_DIR_NAME, "extensions");
|
|
||||||
const projectSkillsDir = join(cwd, CONFIG_DIR_NAME, "skills");
|
|
||||||
const projectPromptsDir = join(cwd, CONFIG_DIR_NAME, "prompts");
|
|
||||||
const projectThemesDir = join(cwd, CONFIG_DIR_NAME, "themes");
|
|
||||||
|
|
||||||
addResources(
|
|
||||||
result.extensions,
|
|
||||||
existingPaths.extensions,
|
|
||||||
collectExtensionEntries(projectExtDir),
|
|
||||||
{ source: "auto", scope: "project", origin: "top-level" },
|
|
||||||
projectOverrides.extensions,
|
|
||||||
projectBaseDir,
|
|
||||||
);
|
|
||||||
addResources(
|
|
||||||
result.skills,
|
|
||||||
existingPaths.skills,
|
|
||||||
collectSkillEntries(projectSkillsDir),
|
|
||||||
{ source: "auto", scope: "project", origin: "top-level" },
|
|
||||||
projectOverrides.skills,
|
|
||||||
projectBaseDir,
|
|
||||||
);
|
|
||||||
addResources(
|
|
||||||
result.prompts,
|
|
||||||
existingPaths.prompts,
|
|
||||||
collectFiles(projectPromptsDir, FILE_PATTERNS.prompts),
|
|
||||||
{ source: "auto", scope: "project", origin: "top-level" },
|
|
||||||
projectOverrides.prompts,
|
|
||||||
projectBaseDir,
|
|
||||||
);
|
|
||||||
addResources(
|
|
||||||
result.themes,
|
|
||||||
existingPaths.themes,
|
|
||||||
collectFiles(projectThemesDir, FILE_PATTERNS.themes),
|
|
||||||
{ source: "auto", scope: "project", origin: "top-level" },
|
|
||||||
projectOverrides.themes,
|
|
||||||
projectBaseDir,
|
|
||||||
);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Show TUI config selector and return when closed */
|
/** Show TUI config selector and return when closed */
|
||||||
export async function selectConfig(options: ConfigSelectorOptions): Promise<void> {
|
export async function selectConfig(options: ConfigSelectorOptions): Promise<void> {
|
||||||
// Initialize theme before showing TUI
|
// Initialize theme before showing TUI
|
||||||
initTheme(options.settingsManager.getTheme(), true);
|
initTheme(options.settingsManager.getTheme(), true);
|
||||||
|
|
||||||
// Merge auto-discovered resources with package manager results
|
|
||||||
const allPaths = mergeAutoDiscoveredResources(
|
|
||||||
options.resolvedPaths,
|
|
||||||
options.settingsManager,
|
|
||||||
options.cwd,
|
|
||||||
options.agentDir,
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const ui = new TUI(new ProcessTerminal());
|
const ui = new TUI(new ProcessTerminal());
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
|
|
||||||
const selector = new ConfigSelectorComponent(
|
const selector = new ConfigSelectorComponent(
|
||||||
allPaths,
|
options.resolvedPaths,
|
||||||
options.settingsManager,
|
options.settingsManager,
|
||||||
options.cwd,
|
options.cwd,
|
||||||
options.agentDir,
|
options.agentDir,
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,192 @@ function collectSkillEntries(dir: string): string[] {
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectAutoSkillEntries(dir: string, isRoot = true): string[] {
|
||||||
|
const entries: string[] = [];
|
||||||
|
if (!existsSync(dir)) return entries;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dirEntries = readdirSync(dir, { withFileTypes: true });
|
||||||
|
for (const entry of dirEntries) {
|
||||||
|
if (entry.name.startsWith(".")) continue;
|
||||||
|
if (entry.name === "node_modules") continue;
|
||||||
|
|
||||||
|
const fullPath = join(dir, entry.name);
|
||||||
|
let isDir = entry.isDirectory();
|
||||||
|
let isFile = entry.isFile();
|
||||||
|
|
||||||
|
if (entry.isSymbolicLink()) {
|
||||||
|
try {
|
||||||
|
const stats = statSync(fullPath);
|
||||||
|
isDir = stats.isDirectory();
|
||||||
|
isFile = stats.isFile();
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDir) {
|
||||||
|
const skillMd = join(fullPath, "SKILL.md");
|
||||||
|
if (existsSync(skillMd)) {
|
||||||
|
entries.push(fullPath);
|
||||||
|
} else {
|
||||||
|
entries.push(...collectAutoSkillEntries(fullPath, false));
|
||||||
|
}
|
||||||
|
} else if (isFile && entry.name.endsWith(".md")) {
|
||||||
|
if (isRoot || entry.name === "SKILL.md") {
|
||||||
|
entries.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAutoPromptEntries(dir: string): string[] {
|
||||||
|
const entries: string[] = [];
|
||||||
|
if (!existsSync(dir)) return entries;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dirEntries = readdirSync(dir, { withFileTypes: true });
|
||||||
|
for (const entry of dirEntries) {
|
||||||
|
if (entry.name.startsWith(".")) continue;
|
||||||
|
if (entry.name === "node_modules") continue;
|
||||||
|
|
||||||
|
const fullPath = join(dir, entry.name);
|
||||||
|
let isFile = entry.isFile();
|
||||||
|
if (entry.isSymbolicLink()) {
|
||||||
|
try {
|
||||||
|
isFile = statSync(fullPath).isFile();
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFile && entry.name.endsWith(".md")) {
|
||||||
|
entries.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAutoThemeEntries(dir: string): string[] {
|
||||||
|
const entries: string[] = [];
|
||||||
|
if (!existsSync(dir)) return entries;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dirEntries = readdirSync(dir, { withFileTypes: true });
|
||||||
|
for (const entry of dirEntries) {
|
||||||
|
if (entry.name.startsWith(".")) continue;
|
||||||
|
if (entry.name === "node_modules") continue;
|
||||||
|
|
||||||
|
const fullPath = join(dir, entry.name);
|
||||||
|
let isFile = entry.isFile();
|
||||||
|
if (entry.isSymbolicLink()) {
|
||||||
|
try {
|
||||||
|
isFile = statSync(fullPath).isFile();
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFile && entry.name.endsWith(".json")) {
|
||||||
|
entries.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPiManifestFile(packageJsonPath: string): PiManifest | null {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(packageJsonPath, "utf-8");
|
||||||
|
const pkg = JSON.parse(content) as { pi?: PiManifest };
|
||||||
|
return pkg.pi ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveExtensionEntries(dir: string): string[] | null {
|
||||||
|
const packageJsonPath = join(dir, "package.json");
|
||||||
|
if (existsSync(packageJsonPath)) {
|
||||||
|
const manifest = readPiManifestFile(packageJsonPath);
|
||||||
|
if (manifest?.extensions?.length) {
|
||||||
|
const entries: string[] = [];
|
||||||
|
for (const extPath of manifest.extensions) {
|
||||||
|
const resolvedExtPath = resolve(dir, extPath);
|
||||||
|
if (existsSync(resolvedExtPath)) {
|
||||||
|
entries.push(resolvedExtPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (entries.length > 0) {
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexTs = join(dir, "index.ts");
|
||||||
|
const indexJs = join(dir, "index.js");
|
||||||
|
if (existsSync(indexTs)) {
|
||||||
|
return [indexTs];
|
||||||
|
}
|
||||||
|
if (existsSync(indexJs)) {
|
||||||
|
return [indexJs];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAutoExtensionEntries(dir: string): string[] {
|
||||||
|
const entries: string[] = [];
|
||||||
|
if (!existsSync(dir)) return entries;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dirEntries = readdirSync(dir, { withFileTypes: true });
|
||||||
|
for (const entry of dirEntries) {
|
||||||
|
if (entry.name.startsWith(".")) continue;
|
||||||
|
if (entry.name === "node_modules") continue;
|
||||||
|
|
||||||
|
const fullPath = join(dir, entry.name);
|
||||||
|
let isDir = entry.isDirectory();
|
||||||
|
let isFile = entry.isFile();
|
||||||
|
|
||||||
|
if (entry.isSymbolicLink()) {
|
||||||
|
try {
|
||||||
|
const stats = statSync(fullPath);
|
||||||
|
isDir = stats.isDirectory();
|
||||||
|
isFile = stats.isFile();
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFile && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
|
||||||
|
entries.push(fullPath);
|
||||||
|
} else if (isDir) {
|
||||||
|
const resolvedEntries = resolveExtensionEntries(fullPath);
|
||||||
|
if (resolvedEntries) {
|
||||||
|
entries.push(...resolvedEntries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
function matchesAnyPattern(filePath: string, patterns: string[], baseDir: string): boolean {
|
function matchesAnyPattern(filePath: string, patterns: string[], baseDir: string): boolean {
|
||||||
const rel = relative(baseDir, filePath);
|
const rel = relative(baseDir, filePath);
|
||||||
const name = basename(filePath);
|
const name = basename(filePath);
|
||||||
|
|
@ -235,6 +421,29 @@ function matchesAnyExactPattern(filePath: string, patterns: string[], baseDir: s
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOverridePatterns(entries: string[]): string[] {
|
||||||
|
return entries.filter((pattern) => pattern.startsWith("!") || pattern.startsWith("+") || pattern.startsWith("-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEnabledByOverrides(filePath: string, patterns: string[], baseDir: string): boolean {
|
||||||
|
const overrides = getOverridePatterns(patterns);
|
||||||
|
const excludes = overrides.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
|
||||||
|
const forceIncludes = overrides.filter((pattern) => pattern.startsWith("+")).map((pattern) => pattern.slice(1));
|
||||||
|
const forceExcludes = overrides.filter((pattern) => pattern.startsWith("-")).map((pattern) => pattern.slice(1));
|
||||||
|
|
||||||
|
let enabled = true;
|
||||||
|
if (excludes.length > 0 && matchesAnyPattern(filePath, excludes, baseDir)) {
|
||||||
|
enabled = false;
|
||||||
|
}
|
||||||
|
if (forceIncludes.length > 0 && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) {
|
||||||
|
enabled = true;
|
||||||
|
}
|
||||||
|
if (forceExcludes.length > 0 && matchesAnyExactPattern(filePath, forceExcludes, baseDir)) {
|
||||||
|
enabled = false;
|
||||||
|
}
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply patterns to paths and return a Set of enabled paths.
|
* Apply patterns to paths and return a Set of enabled paths.
|
||||||
* Pattern types:
|
* Pattern types:
|
||||||
|
|
@ -395,6 +604,8 @@ export class DefaultPackageManager implements PackageManager {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.addAutoDiscoveredResources(accumulator, globalSettings, projectSettings, globalBaseDir, projectBaseDir);
|
||||||
|
|
||||||
return this.toResolvedPaths(accumulator);
|
return this.toResolvedPaths(accumulator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1048,6 +1259,125 @@ export class DefaultPackageManager implements PackageManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addAutoDiscoveredResources(
|
||||||
|
accumulator: ResourceAccumulator,
|
||||||
|
globalSettings: ReturnType<SettingsManager["getGlobalSettings"]>,
|
||||||
|
projectSettings: ReturnType<SettingsManager["getProjectSettings"]>,
|
||||||
|
globalBaseDir: string,
|
||||||
|
projectBaseDir: string,
|
||||||
|
): void {
|
||||||
|
const userMetadata: PathMetadata = {
|
||||||
|
source: "auto",
|
||||||
|
scope: "user",
|
||||||
|
origin: "top-level",
|
||||||
|
baseDir: globalBaseDir,
|
||||||
|
};
|
||||||
|
const projectMetadata: PathMetadata = {
|
||||||
|
source: "auto",
|
||||||
|
scope: "project",
|
||||||
|
origin: "top-level",
|
||||||
|
baseDir: projectBaseDir,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userOverrides = {
|
||||||
|
extensions: (globalSettings.extensions ?? []) as string[],
|
||||||
|
skills: (globalSettings.skills ?? []) as string[],
|
||||||
|
prompts: (globalSettings.prompts ?? []) as string[],
|
||||||
|
themes: (globalSettings.themes ?? []) as string[],
|
||||||
|
};
|
||||||
|
const projectOverrides = {
|
||||||
|
extensions: (projectSettings.extensions ?? []) as string[],
|
||||||
|
skills: (projectSettings.skills ?? []) as string[],
|
||||||
|
prompts: (projectSettings.prompts ?? []) as string[],
|
||||||
|
themes: (projectSettings.themes ?? []) as string[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const userDirs = {
|
||||||
|
extensions: join(globalBaseDir, "extensions"),
|
||||||
|
skills: join(globalBaseDir, "skills"),
|
||||||
|
prompts: join(globalBaseDir, "prompts"),
|
||||||
|
themes: join(globalBaseDir, "themes"),
|
||||||
|
};
|
||||||
|
const projectDirs = {
|
||||||
|
extensions: join(projectBaseDir, "extensions"),
|
||||||
|
skills: join(projectBaseDir, "skills"),
|
||||||
|
prompts: join(projectBaseDir, "prompts"),
|
||||||
|
themes: join(projectBaseDir, "themes"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const addResources = (
|
||||||
|
resourceType: ResourceType,
|
||||||
|
paths: string[],
|
||||||
|
metadata: PathMetadata,
|
||||||
|
overrides: string[],
|
||||||
|
baseDir: string,
|
||||||
|
) => {
|
||||||
|
const target = this.getTargetMap(accumulator, resourceType);
|
||||||
|
for (const path of paths) {
|
||||||
|
const enabled = isEnabledByOverrides(path, overrides, baseDir);
|
||||||
|
this.addResource(target, path, metadata, enabled);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addResources(
|
||||||
|
"extensions",
|
||||||
|
collectAutoExtensionEntries(userDirs.extensions),
|
||||||
|
userMetadata,
|
||||||
|
userOverrides.extensions,
|
||||||
|
globalBaseDir,
|
||||||
|
);
|
||||||
|
addResources(
|
||||||
|
"skills",
|
||||||
|
collectAutoSkillEntries(userDirs.skills),
|
||||||
|
userMetadata,
|
||||||
|
userOverrides.skills,
|
||||||
|
globalBaseDir,
|
||||||
|
);
|
||||||
|
addResources(
|
||||||
|
"prompts",
|
||||||
|
collectAutoPromptEntries(userDirs.prompts),
|
||||||
|
userMetadata,
|
||||||
|
userOverrides.prompts,
|
||||||
|
globalBaseDir,
|
||||||
|
);
|
||||||
|
addResources(
|
||||||
|
"themes",
|
||||||
|
collectAutoThemeEntries(userDirs.themes),
|
||||||
|
userMetadata,
|
||||||
|
userOverrides.themes,
|
||||||
|
globalBaseDir,
|
||||||
|
);
|
||||||
|
|
||||||
|
addResources(
|
||||||
|
"extensions",
|
||||||
|
collectAutoExtensionEntries(projectDirs.extensions),
|
||||||
|
projectMetadata,
|
||||||
|
projectOverrides.extensions,
|
||||||
|
projectBaseDir,
|
||||||
|
);
|
||||||
|
addResources(
|
||||||
|
"skills",
|
||||||
|
collectAutoSkillEntries(projectDirs.skills),
|
||||||
|
projectMetadata,
|
||||||
|
projectOverrides.skills,
|
||||||
|
projectBaseDir,
|
||||||
|
);
|
||||||
|
addResources(
|
||||||
|
"prompts",
|
||||||
|
collectAutoPromptEntries(projectDirs.prompts),
|
||||||
|
projectMetadata,
|
||||||
|
projectOverrides.prompts,
|
||||||
|
projectBaseDir,
|
||||||
|
);
|
||||||
|
addResources(
|
||||||
|
"themes",
|
||||||
|
collectAutoThemeEntries(projectDirs.themes),
|
||||||
|
projectMetadata,
|
||||||
|
projectOverrides.themes,
|
||||||
|
projectBaseDir,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private collectFilesFromPaths(paths: string[], resourceType: ResourceType): string[] {
|
private collectFilesFromPaths(paths: string[], resourceType: ResourceType): string[] {
|
||||||
const files: string[] = [];
|
const files: string[] = [];
|
||||||
for (const p of paths) {
|
for (const p of paths) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
import { basename, isAbsolute, join, resolve } from "path";
|
import { basename, isAbsolute, join, resolve, sep } from "path";
|
||||||
import { CONFIG_DIR_NAME, getPromptsDir } from "../config.js";
|
import { CONFIG_DIR_NAME, getPromptsDir } from "../config.js";
|
||||||
import { parseFrontmatter } from "../utils/frontmatter.js";
|
import { parseFrontmatter } from "../utils/frontmatter.js";
|
||||||
|
|
||||||
|
|
@ -181,6 +181,8 @@ export interface LoadPromptTemplatesOptions {
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
/** Explicit prompt template paths (files or directories) */
|
/** Explicit prompt template paths (files or directories) */
|
||||||
promptPaths?: string[];
|
promptPaths?: string[];
|
||||||
|
/** Include default prompt directories. Default: true */
|
||||||
|
includeDefaults?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePath(input: string): string {
|
function normalizePath(input: string): string {
|
||||||
|
|
@ -211,9 +213,11 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): P
|
||||||
const resolvedCwd = options.cwd ?? process.cwd();
|
const resolvedCwd = options.cwd ?? process.cwd();
|
||||||
const resolvedAgentDir = options.agentDir ?? getPromptsDir();
|
const resolvedAgentDir = options.agentDir ?? getPromptsDir();
|
||||||
const promptPaths = options.promptPaths ?? [];
|
const promptPaths = options.promptPaths ?? [];
|
||||||
|
const includeDefaults = options.includeDefaults ?? true;
|
||||||
|
|
||||||
const templates: PromptTemplate[] = [];
|
const templates: PromptTemplate[] = [];
|
||||||
|
|
||||||
|
if (includeDefaults) {
|
||||||
// 1. Load global templates from agentDir/prompts/
|
// 1. Load global templates from agentDir/prompts/
|
||||||
// Note: if agentDir is provided, it should be the agent dir, not the prompts dir
|
// Note: if agentDir is provided, it should be the agent dir, not the prompts dir
|
||||||
const globalPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir;
|
const globalPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir;
|
||||||
|
|
@ -222,6 +226,31 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): P
|
||||||
// 2. Load project templates from cwd/{CONFIG_DIR_NAME}/prompts/
|
// 2. Load project templates from cwd/{CONFIG_DIR_NAME}/prompts/
|
||||||
const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts");
|
const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts");
|
||||||
templates.push(...loadTemplatesFromDir(projectPromptsDir, "project", "(project)"));
|
templates.push(...loadTemplatesFromDir(projectPromptsDir, "project", "(project)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir;
|
||||||
|
const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts");
|
||||||
|
|
||||||
|
const isUnderPath = (target: string, root: string): boolean => {
|
||||||
|
const normalizedRoot = resolve(root);
|
||||||
|
if (target === normalizedRoot) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;
|
||||||
|
return target.startsWith(prefix);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSourceInfo = (resolvedPath: string): { source: string; label: string } => {
|
||||||
|
if (!includeDefaults) {
|
||||||
|
if (isUnderPath(resolvedPath, userPromptsDir)) {
|
||||||
|
return { source: "user", label: "(user)" };
|
||||||
|
}
|
||||||
|
if (isUnderPath(resolvedPath, projectPromptsDir)) {
|
||||||
|
return { source: "project", label: "(project)" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { source: "path", label: buildPathSourceLabel(resolvedPath) };
|
||||||
|
};
|
||||||
|
|
||||||
// 3. Load explicit prompt paths
|
// 3. Load explicit prompt paths
|
||||||
for (const rawPath of promptPaths) {
|
for (const rawPath of promptPaths) {
|
||||||
|
|
@ -232,10 +261,11 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): P
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stats = statSync(resolvedPath);
|
const stats = statSync(resolvedPath);
|
||||||
|
const { source, label } = getSourceInfo(resolvedPath);
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
templates.push(...loadTemplatesFromDir(resolvedPath, "path", buildPathSourceLabel(resolvedPath)));
|
templates.push(...loadTemplatesFromDir(resolvedPath, source, label));
|
||||||
} else if (stats.isFile() && resolvedPath.endsWith(".md")) {
|
} else if (stats.isFile() && resolvedPath.endsWith(".md")) {
|
||||||
const template = loadTemplateFromFile(resolvedPath, "path", buildPathSourceLabel(resolvedPath));
|
const template = loadTemplateFromFile(resolvedPath, source, label);
|
||||||
if (template) {
|
if (template) {
|
||||||
templates.push(template);
|
templates.push(template);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,7 @@ import type { ResourceDiagnostic } from "./diagnostics.js";
|
||||||
export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.js";
|
export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.js";
|
||||||
|
|
||||||
import { createEventBus, type EventBus } from "./event-bus.js";
|
import { createEventBus, type EventBus } from "./event-bus.js";
|
||||||
import {
|
import { createExtensionRuntime, loadExtensionFromFactory, loadExtensions } from "./extensions/loader.js";
|
||||||
createExtensionRuntime,
|
|
||||||
discoverAndLoadExtensions,
|
|
||||||
loadExtensionFromFactory,
|
|
||||||
loadExtensions,
|
|
||||||
} from "./extensions/loader.js";
|
|
||||||
import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./extensions/types.js";
|
import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./extensions/types.js";
|
||||||
import { DefaultPackageManager, type PathMetadata } from "./package-manager.js";
|
import { DefaultPackageManager, type PathMetadata } from "./package-manager.js";
|
||||||
import type { PromptTemplate } from "./prompt-templates.js";
|
import type { PromptTemplate } from "./prompt-templates.js";
|
||||||
|
|
@ -312,12 +307,7 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||||
? cliEnabledExtensions
|
? cliEnabledExtensions
|
||||||
: this.mergePaths(enabledExtensions, cliEnabledExtensions);
|
: this.mergePaths(enabledExtensions, cliEnabledExtensions);
|
||||||
|
|
||||||
let extensionsResult: LoadExtensionsResult;
|
const extensionsResult = await loadExtensions(extensionPaths, this.cwd, this.eventBus);
|
||||||
if (this.noExtensions) {
|
|
||||||
extensionsResult = await loadExtensions(extensionPaths, this.cwd, this.eventBus);
|
|
||||||
} else {
|
|
||||||
extensionsResult = await discoverAndLoadExtensions(extensionPaths, this.cwd, this.agentDir, this.eventBus);
|
|
||||||
}
|
|
||||||
const inlineExtensions = await this.loadExtensionFactories(extensionsResult.runtime);
|
const inlineExtensions = await this.loadExtensionFactories(extensionsResult.runtime);
|
||||||
extensionsResult.extensions.push(...inlineExtensions.extensions);
|
extensionsResult.extensions.push(...inlineExtensions.extensions);
|
||||||
extensionsResult.errors.push(...inlineExtensions.errors);
|
extensionsResult.errors.push(...inlineExtensions.errors);
|
||||||
|
|
@ -346,6 +336,7 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||||
cwd: this.cwd,
|
cwd: this.cwd,
|
||||||
agentDir: this.agentDir,
|
agentDir: this.agentDir,
|
||||||
skillPaths,
|
skillPaths,
|
||||||
|
includeDefaults: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const resolvedSkills = this.skillsOverride ? this.skillsOverride(skillsResult) : skillsResult;
|
const resolvedSkills = this.skillsOverride ? this.skillsOverride(skillsResult) : skillsResult;
|
||||||
|
|
@ -367,6 +358,7 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||||
cwd: this.cwd,
|
cwd: this.cwd,
|
||||||
agentDir: this.agentDir,
|
agentDir: this.agentDir,
|
||||||
promptPaths,
|
promptPaths,
|
||||||
|
includeDefaults: false,
|
||||||
});
|
});
|
||||||
promptsResult = this.dedupePrompts(allPrompts);
|
promptsResult = this.dedupePrompts(allPrompts);
|
||||||
}
|
}
|
||||||
|
|
@ -385,7 +377,7 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||||
if (this.noThemes && themePaths.length === 0) {
|
if (this.noThemes && themePaths.length === 0) {
|
||||||
themesResult = { themes: [], diagnostics: [] };
|
themesResult = { themes: [], diagnostics: [] };
|
||||||
} else {
|
} else {
|
||||||
const loaded = this.loadThemes(themePaths);
|
const loaded = this.loadThemes(themePaths, false);
|
||||||
const deduped = this.dedupeThemes(loaded.themes);
|
const deduped = this.dedupeThemes(loaded.themes);
|
||||||
themesResult = { themes: deduped.themes, diagnostics: [...loaded.diagnostics, ...deduped.diagnostics] };
|
themesResult = { themes: deduped.themes, diagnostics: [...loaded.diagnostics, ...deduped.diagnostics] };
|
||||||
}
|
}
|
||||||
|
|
@ -447,14 +439,22 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||||
return resolve(this.cwd, expanded);
|
return resolve(this.cwd, expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadThemes(paths: string[]): { themes: Theme[]; diagnostics: ResourceDiagnostic[] } {
|
private loadThemes(
|
||||||
|
paths: string[],
|
||||||
|
includeDefaults: boolean = true,
|
||||||
|
): {
|
||||||
|
themes: Theme[];
|
||||||
|
diagnostics: ResourceDiagnostic[];
|
||||||
|
} {
|
||||||
const themes: Theme[] = [];
|
const themes: Theme[] = [];
|
||||||
const diagnostics: ResourceDiagnostic[] = [];
|
const diagnostics: ResourceDiagnostic[] = [];
|
||||||
|
if (includeDefaults) {
|
||||||
const defaultDirs = [join(this.agentDir, "themes"), join(this.cwd, CONFIG_DIR_NAME, "themes")];
|
const defaultDirs = [join(this.agentDir, "themes"), join(this.cwd, CONFIG_DIR_NAME, "themes")];
|
||||||
|
|
||||||
for (const dir of defaultDirs) {
|
for (const dir of defaultDirs) {
|
||||||
this.loadThemesFromDir(dir, themes, diagnostics);
|
this.loadThemesFromDir(dir, themes, diagnostics);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const p of paths) {
|
for (const p of paths) {
|
||||||
const resolved = resolve(this.cwd, p);
|
const resolved = resolve(this.cwd, p);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
|
||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
import { basename, dirname, isAbsolute, join, resolve } from "path";
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "path";
|
||||||
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
||||||
import { parseFrontmatter } from "../utils/frontmatter.js";
|
import { parseFrontmatter } from "../utils/frontmatter.js";
|
||||||
import type { ResourceDiagnostic } from "./diagnostics.js";
|
import type { ResourceDiagnostic } from "./diagnostics.js";
|
||||||
|
|
@ -296,6 +296,8 @@ export interface LoadSkillsOptions {
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
/** Explicit skill paths (files or directories) */
|
/** Explicit skill paths (files or directories) */
|
||||||
skillPaths?: string[];
|
skillPaths?: string[];
|
||||||
|
/** Include default skills directories. Default: true */
|
||||||
|
includeDefaults?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePath(input: string): string {
|
function normalizePath(input: string): string {
|
||||||
|
|
@ -316,7 +318,7 @@ function resolveSkillPath(p: string, cwd: string): string {
|
||||||
* Returns skills and any validation diagnostics.
|
* Returns skills and any validation diagnostics.
|
||||||
*/
|
*/
|
||||||
export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
|
export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
|
||||||
const { cwd = process.cwd(), agentDir, skillPaths = [] } = options;
|
const { cwd = process.cwd(), agentDir, skillPaths = [], includeDefaults = true } = options;
|
||||||
|
|
||||||
// Resolve agentDir - if not provided, use default from config
|
// Resolve agentDir - if not provided, use default from config
|
||||||
const resolvedAgentDir = agentDir ?? getAgentDir();
|
const resolvedAgentDir = agentDir ?? getAgentDir();
|
||||||
|
|
@ -362,8 +364,30 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (includeDefaults) {
|
||||||
addSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, "skills"), "user", true));
|
addSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, "skills"), "user", true));
|
||||||
addSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, "skills"), "project", true));
|
addSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, "skills"), "project", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
const userSkillsDir = join(resolvedAgentDir, "skills");
|
||||||
|
const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, "skills");
|
||||||
|
|
||||||
|
const isUnderPath = (target: string, root: string): boolean => {
|
||||||
|
const normalizedRoot = resolve(root);
|
||||||
|
if (target === normalizedRoot) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;
|
||||||
|
return target.startsWith(prefix);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSource = (resolvedPath: string): "user" | "project" | "path" => {
|
||||||
|
if (!includeDefaults) {
|
||||||
|
if (isUnderPath(resolvedPath, userSkillsDir)) return "user";
|
||||||
|
if (isUnderPath(resolvedPath, projectSkillsDir)) return "project";
|
||||||
|
}
|
||||||
|
return "path";
|
||||||
|
};
|
||||||
|
|
||||||
for (const rawPath of skillPaths) {
|
for (const rawPath of skillPaths) {
|
||||||
const resolvedPath = resolveSkillPath(rawPath, cwd);
|
const resolvedPath = resolveSkillPath(rawPath, cwd);
|
||||||
|
|
@ -374,10 +398,11 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stats = statSync(resolvedPath);
|
const stats = statSync(resolvedPath);
|
||||||
|
const source = getSource(resolvedPath);
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
addSkills(loadSkillsFromDirInternal(resolvedPath, "path", true));
|
addSkills(loadSkillsFromDirInternal(resolvedPath, source, true));
|
||||||
} else if (stats.isFile() && resolvedPath.endsWith(".md")) {
|
} else if (stats.isFile() && resolvedPath.endsWith(".md")) {
|
||||||
const result = loadSkillFromFile(resolvedPath, "path");
|
const result = loadSkillFromFile(resolvedPath, source);
|
||||||
if (result.skill) {
|
if (result.skill) {
|
||||||
addSkills({ skills: [result.skill], diagnostics: result.diagnostics });
|
addSkills({ skills: [result.skill], diagnostics: result.diagnostics });
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,30 @@ Content`,
|
||||||
const result = await packageManager.resolve();
|
const result = await packageManager.resolve();
|
||||||
expect(result.extensions.some((r) => r.path === extPath && r.enabled)).toBe(true);
|
expect(result.extensions.some((r) => r.path === extPath && r.enabled)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should auto-discover user prompts with overrides", async () => {
|
||||||
|
const promptsDir = join(agentDir, "prompts");
|
||||||
|
mkdirSync(promptsDir, { recursive: true });
|
||||||
|
const promptPath = join(promptsDir, "auto.md");
|
||||||
|
writeFileSync(promptPath, "Auto prompt");
|
||||||
|
|
||||||
|
settingsManager.setPromptTemplatePaths(["!prompts/auto.md"]);
|
||||||
|
|
||||||
|
const result = await packageManager.resolve();
|
||||||
|
expect(result.prompts.some((r) => r.path === promptPath && !r.enabled)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should auto-discover project prompts with overrides", async () => {
|
||||||
|
const promptsDir = join(tempDir, ".pi", "prompts");
|
||||||
|
mkdirSync(promptsDir, { recursive: true });
|
||||||
|
const promptPath = join(promptsDir, "is.md");
|
||||||
|
writeFileSync(promptPath, "Is prompt");
|
||||||
|
|
||||||
|
settingsManager.setProjectPromptTemplatePaths(["!prompts/is.md"]);
|
||||||
|
|
||||||
|
const result = await packageManager.resolve();
|
||||||
|
expect(result.prompts.some((r) => r.path === promptPath && !r.enabled)).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveExtensionSources", () => {
|
describe("resolveExtensionSources", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue