refactor(ui+skills): split import.ts + primitives.tsx god files

Two more > 400 LOC modules split along responsibility seams. Zero
behavior change — public APIs re-export from the original file paths,
so every consumer import stays working.

| Original                              | Split into                    | LOC each |
|---------------------------------------|-------------------------------|----------|
| src/main/skills/import.ts (458)       | import (orchestrator) /       | 132      |
|                                       | discovery (findSkillMdFiles,  | 153      |
|                                       | buildLocalInventory)          |          |
|                                       | materialize                   | 24       |
|                                       | import-local                  | 147      |
|                                       | import-url                    | 85       |
| src/renderer/components/ui/           | primitives (barrel re-export) | 11       |
| primitives.tsx (419)                  | Card / Eyebrow / Badge /      | 9–59     |
|                                       | Spinner / Panel / Field /     | each     |
|                                       | Input / Select / TextArea /   |          |
|                                       | ActionButton / SectionButton  |          |

`createSkillImporter` still returns `{ importFromSource, importFromGitHub,
importFromLocal, importFromUrl, cleanupSkillFiles, parseGitHubUrl,
findSkillMdFiles, buildLocalInventory }` — every existing consumer
resolves unchanged.

`primitives.tsx` is now a pure barrel re-exporting the 11 primitives.
Consumers can migrate to direct imports (`../ui/Card`) opportunistically
but the barrel path keeps working.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-17 16:35:30 -04:00
1 parent dbdec86cdd
commit 00b0e0aaed
17 files changed
+836 -786

No files matched your search

+153
View File
@@ -0,0 +1,153 @@
import { readdirSync, realpathSync, type Dirent } from "node:fs";
import { join, resolve, relative } from "node:path";
import type { FileInventoryEntry } from "../skill-types.js";
import { classifyInventoryKind } from "../skill-format.js";
export const DEFAULT_SKILL_WALK_MAX_DEPTH = 8;
export const DEFAULT_SKILL_WALK_MAX_FILES = 256;
const BLOCKED_PATH_PREFIXES_UNIX = ["/etc", "/var", "/proc", "/sys", "/dev", "/boot", "/sbin", "/usr/sbin"];
const BLOCKED_PATH_PREFIXES_WIN = ["C:\\Windows", "C:\\Program Files", "C:\\ProgramData"];
export interface DiscoveryLogger {
warn: (...args: unknown[]) => void;
}
export function assertNotSystemPath(realPath: string): void {
const normalized = realPath.replace(/\\/g, "/");
const blocked = process.platform === "win32" ? BLOCKED_PATH_PREFIXES_WIN : BLOCKED_PATH_PREFIXES_UNIX;
for (const prefix of blocked) {
const normalizedPrefix = prefix.replace(/\\/g, "/");
if (normalized === normalizedPrefix || normalized.startsWith(normalizedPrefix + "/")) {
throw new Error(`Importing skills from system directory is not allowed: ${realPath}`);
}
}
}
export function findSkillMdFiles(
rootDir: string,
logger: DiscoveryLogger,
opts?: { maxDepth?: number; maxFiles?: number },
): string[] {
const maxDepth = opts?.maxDepth ?? DEFAULT_SKILL_WALK_MAX_DEPTH;
const maxFiles = opts?.maxFiles ?? DEFAULT_SKILL_WALK_MAX_FILES;
const found: string[] = [];
const visitedRealPaths = new Set<string>();
let filesSeen = 0;
function walk(dir: string, depth: number) {
if (depth > maxDepth) {
throw new Error(
`Skill directory too deep, refusing to walk beyond ${maxDepth} levels`,
);
}
let realDir: string;
try {
realDir = realpathSync(dir);
} catch (err) {
logger.warn(`[skill-import] realpathSync failed for ${dir}:`, err instanceof Error ? err.message : String(err));
return;
}
if (visitedRealPaths.has(realDir)) {
return;
}
visitedRealPaths.add(realDir);
let entries: Dirent[];
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch (err) {
logger.warn(`[skill-import] readdirSync failed for ${dir}:`, err instanceof Error ? err.message : String(err));
return;
}
for (const entry of entries) {
filesSeen++;
if (filesSeen > maxFiles) {
throw new Error(
`Skill directory contains too many files, refusing after ${maxFiles} entries`,
);
}
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
walk(fullPath, depth + 1);
} else if (entry.name === "SKILL.md") {
found.push(fullPath);
}
}
}
walk(rootDir, 0);
return found;
}
export function buildLocalInventory(
skillDir: string,
skillMdPath: string,
logger: DiscoveryLogger,
opts?: { maxDepth?: number; maxFiles?: number },
): FileInventoryEntry[] {
const maxDepth = opts?.maxDepth ?? DEFAULT_SKILL_WALK_MAX_DEPTH;
const maxFiles = opts?.maxFiles ?? DEFAULT_SKILL_WALK_MAX_FILES;
const inventory: FileInventoryEntry[] = [
{ path: "SKILL.md", kind: "skill" },
];
const visitedRealPaths = new Set<string>();
let filesSeen = 0;
function walk(dir: string, depth: number) {
if (depth > maxDepth) {
throw new Error(
`Skill directory too deep, refusing to walk beyond ${maxDepth} levels`,
);
}
let realDir: string;
try {
realDir = realpathSync(dir);
} catch (err) {
logger.warn(`[skill-import] realpathSync failed for ${dir}:`, err instanceof Error ? err.message : String(err));
return;
}
if (visitedRealPaths.has(realDir)) {
return;
}
visitedRealPaths.add(realDir);
let entries: Dirent[];
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch (err) {
logger.warn(`[skill-import] readdirSync failed for ${dir}:`, err instanceof Error ? err.message : String(err));
return;
}
for (const entry of entries) {
filesSeen++;
if (filesSeen > maxFiles) {
throw new Error(
`Skill directory contains too many files, refusing after ${maxFiles} entries`,
);
}
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
walk(fullPath, depth + 1);
continue;
}
if (!entry.isFile()) continue;
const resolvedPath = resolve(fullPath);
if (resolvedPath === skillMdPath) continue;
const relativePath = relative(skillDir, resolvedPath).replace(/\\/g, "/");
inventory.push({
path: relativePath,
kind: classifyInventoryKind(relativePath),
});
}
}
try {
walk(skillDir, 0);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (msg.startsWith("Skill directory too deep") || msg.startsWith("Skill directory contains too many files")) {
throw err;
}
}
return inventory;
}
+147
View File
@@ -0,0 +1,147 @@
import { randomUUID } from "node:crypto";
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
import { resolve, basename, dirname } from "node:path";
import type { AppDatabase } from "../database";
import type { FileInventoryEntry, SkillImportResult } from "../skill-types.js";
import { parseFrontmatterMarkdown, deriveTrustLevel } from "../skill-format.js";
import {
deriveCanonicalSkillKey,
normalizeSkillSlug,
hashSkillValue,
} from "../skill-keys.js";
import {
assertNotSystemPath,
findSkillMdFiles,
buildLocalInventory,
} from "./discovery";
import { materializeSkillFiles } from "./materialize";
export interface LocalImportContext {
db: AppDatabase;
logger: {
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
};
skillFilesDir: (companyId: string, slug: string) => string;
}
export async function importFromLocal(
ctx: LocalImportContext,
companyId: string,
localPath: string,
): Promise<SkillImportResult[]> {
const { db, logger, skillFilesDir } = ctx;
const resolvedPath = resolve(localPath.replace(/^~/, process.env.HOME ?? process.env.USERPROFILE ?? ""));
if (!existsSync(resolvedPath)) {
throw new Error(`Local path does not exist: ${resolvedPath}`);
}
// Resolve symlinks to prevent traversal attacks (agent importing /tmp/evil-link → /etc/shadow)
const realPath = realpathSync(resolvedPath);
assertNotSystemPath(realPath);
const stat = statSync(resolvedPath);
const results: SkillImportResult[] = [];
const nowTs = new Date().toISOString();
if (stat.isFile()) {
const markdown = readFileSync(resolvedPath, "utf-8");
const parsed = parseFrontmatterMarkdown(markdown);
const rawSlug =
normalizeSkillSlug(parsed.frontmatter.slug as string | undefined) ??
normalizeSkillSlug(basename(resolvedPath, ".md")) ??
"skill";
const inventory: FileInventoryEntry[] = [
{ path: "SKILL.md", kind: "skill" },
];
const trustLevel = deriveTrustLevel(inventory);
const contentHash = hashSkillValue(markdown);
const key = deriveCanonicalSkillKey(companyId, {
slug: rawSlug,
sourceType: "local_path",
sourceLocator: resolvedPath,
metadata: null,
});
const skillId = randomUUID();
await materializeSkillFiles(skillFilesDir(companyId, rawSlug), markdown, inventory);
const { isUpdate } = db.upsertCompanySkill({
id: skillId,
companyId,
key,
slug: rawSlug,
name: (typeof parsed.frontmatter.name === "string" ? parsed.frontmatter.name : null) ?? rawSlug,
description: typeof parsed.frontmatter.description === "string" ? parsed.frontmatter.description : null,
sourceType: "local_path",
sourceLocator: resolvedPath,
sourceRef: contentHash,
trustLevel,
compatibility: "compatible",
fileInventory: JSON.stringify(inventory),
metadata: null,
createdAt: nowTs,
updatedAt: nowTs,
});
results.push({ skillId, key, name: rawSlug, isUpdate });
logger.info(`Skill ${isUpdate ? "updated" : "imported"} from local file: ${key}`);
return results;
}
// Directory: walk to find SKILL.md files
const skillMdFiles = findSkillMdFiles(resolvedPath, logger);
if (skillMdFiles.length === 0) {
throw new Error(`No SKILL.md files found in ${resolvedPath}`);
}
for (const skillMdPath of skillMdFiles) {
const skillDir = dirname(skillMdPath);
const markdown = readFileSync(skillMdPath, "utf-8");
const parsed = parseFrontmatterMarkdown(markdown);
const rawSlug =
normalizeSkillSlug(parsed.frontmatter.slug as string | undefined) ??
normalizeSkillSlug(basename(skillDir)) ??
"skill";
const inventory = buildLocalInventory(skillDir, skillMdPath, logger);
const trustLevel = deriveTrustLevel(inventory);
const contentHash = hashSkillValue(markdown);
const key = deriveCanonicalSkillKey(companyId, {
slug: rawSlug,
sourceType: "local_path",
sourceLocator: skillDir,
metadata: null,
});
const skillId = randomUUID();
await materializeSkillFiles(skillFilesDir(companyId, rawSlug), markdown, inventory);
const { isUpdate } = db.upsertCompanySkill({
id: skillId,
companyId,
key,
slug: rawSlug,
name: (typeof parsed.frontmatter.name === "string" ? parsed.frontmatter.name : null) ?? rawSlug,
description: typeof parsed.frontmatter.description === "string" ? parsed.frontmatter.description : null,
sourceType: "local_path",
sourceLocator: skillDir,
sourceRef: contentHash,
trustLevel,
compatibility: "compatible",
fileInventory: JSON.stringify(inventory),
metadata: null,
createdAt: nowTs,
updatedAt: nowTs,
});
results.push({ skillId, key, name: rawSlug, isUpdate });
logger.info(`Skill ${isUpdate ? "updated" : "imported"} from local path: ${key}`);
}
return results;
}
+85
View File
@@ -0,0 +1,85 @@
import { randomUUID } from "node:crypto";
import type { AppDatabase } from "../database";
import type { FileInventoryEntry, SkillImportResult } from "../skill-types.js";
import { parseFrontmatterMarkdown } from "../skill-format.js";
import { deriveCanonicalSkillKey, normalizeSkillSlug } from "../skill-keys.js";
import { materializeSkillFiles } from "./materialize";
export interface UrlImportContext {
db: AppDatabase;
logger: {
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
};
skillFilesDir: (companyId: string, slug: string) => string;
}
export async function importFromUrl(
ctx: UrlImportContext,
companyId: string,
url: string,
safeFetch: (target: string) => Promise<Response>,
): Promise<SkillImportResult[]> {
const { db, logger, skillFilesDir } = ctx;
const response = await safeFetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch skill from URL: ${response.status} ${url}`);
}
const markdown = await response.text();
const parsed = parseFrontmatterMarkdown(markdown);
const nowTs = new Date().toISOString();
let urlSlug: string | null = null;
try {
const urlObj = new URL(url);
const pathParts = urlObj.pathname.split("/").filter(Boolean);
const lastPart = pathParts[pathParts.length - 1] ?? "";
urlSlug = normalizeSkillSlug(lastPart.replace(/\.md$/i, ""));
} catch (err) {
logger.warn(`[skill-import] Unable to parse skill URL for slug derivation (${url}):`, err instanceof Error ? err.message : String(err));
}
const rawSlug =
normalizeSkillSlug(parsed.frontmatter.slug as string | undefined) ??
urlSlug ??
"skill";
const inventory: FileInventoryEntry[] = [
{ path: "SKILL.md", kind: "skill" },
];
const trustLevel = "markdown_only" as const;
const key = deriveCanonicalSkillKey(companyId, {
slug: rawSlug,
sourceType: "url",
sourceLocator: url,
metadata: null,
});
const skillId = randomUUID();
await materializeSkillFiles(skillFilesDir(companyId, rawSlug), markdown, inventory);
const { isUpdate } = db.upsertCompanySkill({
id: skillId,
companyId,
key,
slug: rawSlug,
name: (typeof parsed.frontmatter.name === "string" ? parsed.frontmatter.name : null) ?? rawSlug,
description: typeof parsed.frontmatter.description === "string" ? parsed.frontmatter.description : null,
sourceType: "url",
sourceLocator: url,
sourceRef: null,
trustLevel,
compatibility: "compatible",
fileInventory: JSON.stringify(inventory),
metadata: null,
createdAt: nowTs,
updatedAt: nowTs,
});
const results: SkillImportResult[] = [
{ skillId, key, name: rawSlug, isUpdate },
];
logger.info(`Skill ${isUpdate ? "updated" : "imported"} from URL: ${key}`);
return results;
}
+41 -367
View File
@@ -1,40 +1,15 @@
import { randomUUID } from "node:crypto";
import { existsSync, readdirSync, readFileSync, realpathSync, statSync, type Dirent } from "node:fs";
import { mkdir, writeFile, rm } from "node:fs/promises";
import { join, resolve, basename, dirname, relative } from "node:path";
import type { AppDatabase } from "../database";
import type { DesktopResult } from "@shared/contracts";
import type { FileInventoryEntry, SkillImportResult } from "../skill-types.js";
import {
parseFrontmatterMarkdown,
classifyInventoryKind,
deriveTrustLevel,
} from "../skill-format.js";
import {
deriveCanonicalSkillKey,
normalizeSkillSlug,
hashSkillValue,
parseSkillImportSource,
} from "../skill-keys.js";
import type { SkillImportResult } from "../skill-types.js";
import { parseSkillImportSource } from "../skill-keys.js";
import { createGitHubImporter } from "./import-github";
import { MAX_SKILL_BYTES } from "../skill-github.js";
const BLOCKED_PATH_PREFIXES_UNIX = ["/etc", "/var", "/proc", "/sys", "/dev", "/boot", "/sbin", "/usr/sbin"];
const BLOCKED_PATH_PREFIXES_WIN = ["C:\\Windows", "C:\\Program Files", "C:\\ProgramData"];
const DEFAULT_SKILL_WALK_MAX_DEPTH = 8;
const DEFAULT_SKILL_WALK_MAX_FILES = 256;
function assertNotSystemPath(realPath: string): void {
const normalized = realPath.replace(/\\/g, "/");
const blocked = process.platform === "win32" ? BLOCKED_PATH_PREFIXES_WIN : BLOCKED_PATH_PREFIXES_UNIX;
for (const prefix of blocked) {
const normalizedPrefix = prefix.replace(/\\/g, "/");
if (normalized === normalizedPrefix || normalized.startsWith(normalizedPrefix + "/")) {
throw new Error(`Importing skills from system directory is not allowed: ${realPath}`);
}
}
}
import {
findSkillMdFiles as findSkillMdFilesImpl,
buildLocalInventory as buildLocalInventoryImpl,
} from "./discovery";
import { cleanupSkillFilesDir } from "./materialize";
import { importFromLocal as importFromLocalImpl } from "./import-local";
import { importFromUrl as importFromUrlImpl } from "./import-url";
interface ImportDeps {
db: AppDatabase;
@@ -57,29 +32,40 @@ export function createSkillImporter({ db, profileDir: _profileDir, logger, skill
}
const ghImporter = createGitHubImporter({ db, logger, skillFilesDir });
async function materializeSkillFiles(
companyId: string,
slug: string,
markdown: string,
_inventory: FileInventoryEntry[],
): Promise<string> {
if (markdown.length * 2 > MAX_SKILL_BYTES) {
throw new Error(
`SKILL.md exceeds ${MAX_SKILL_BYTES}-byte limit`,
);
}
const dir = skillFilesDir(companyId, slug);
await mkdir(dir, { recursive: true });
await writeFile(join(dir, "SKILL.md"), markdown, "utf-8");
return dir;
}
const ctx = { db, logger, skillFilesDir };
async function cleanupSkillFiles(companyId: string, slug: string): Promise<void> {
const dir = skillFilesDir(companyId, slug);
if (existsSync(dir)) {
await rm(dir, { recursive: true, force: true });
}
await cleanupSkillFilesDir(skillFilesDir(companyId, slug));
}
async function importFromLocal(
companyId: string,
localPath: string,
): Promise<SkillImportResult[]> {
return importFromLocalImpl(ctx, companyId, localPath);
}
async function importFromUrl(
companyId: string,
url: string,
safeFetch: (target: string) => Promise<Response>,
): Promise<SkillImportResult[]> {
return importFromUrlImpl(ctx, companyId, url, safeFetch);
}
function findSkillMdFiles(
rootDir: string,
opts?: { maxDepth?: number; maxFiles?: number },
): string[] {
return findSkillMdFilesImpl(rootDir, logger, opts);
}
function buildLocalInventory(
skillDir: string,
skillMdPath: string,
opts?: { maxDepth?: number; maxFiles?: number },
) {
return buildLocalInventoryImpl(skillDir, skillMdPath, logger, opts);
}
async function importFromSource(
@@ -133,318 +119,6 @@ export function createSkillImporter({ db, profileDir: _profileDir, logger, skill
}
}
async function importFromLocal(
companyId: string,
localPath: string,
): Promise<SkillImportResult[]> {
const resolvedPath = resolve(localPath.replace(/^~/, process.env.HOME ?? process.env.USERPROFILE ?? ""));
if (!existsSync(resolvedPath)) {
throw new Error(`Local path does not exist: ${resolvedPath}`);
}
// Resolve symlinks to prevent traversal attacks (agent importing /tmp/evil-link → /etc/shadow)
const realPath = realpathSync(resolvedPath);
assertNotSystemPath(realPath);
const stat = statSync(resolvedPath);
const results: SkillImportResult[] = [];
const nowTs = new Date().toISOString();
if (stat.isFile()) {
const markdown = readFileSync(resolvedPath, "utf-8");
const parsed = parseFrontmatterMarkdown(markdown);
const rawSlug =
normalizeSkillSlug(parsed.frontmatter.slug as string | undefined) ??
normalizeSkillSlug(basename(resolvedPath, ".md")) ??
"skill";
const inventory: FileInventoryEntry[] = [
{ path: "SKILL.md", kind: "skill" },
];
const trustLevel = deriveTrustLevel(inventory);
const contentHash = hashSkillValue(markdown);
const key = deriveCanonicalSkillKey(companyId, {
slug: rawSlug,
sourceType: "local_path",
sourceLocator: resolvedPath,
metadata: null,
});
const skillId = randomUUID();
await materializeSkillFiles(companyId, rawSlug, markdown, inventory);
const { isUpdate } = db.upsertCompanySkill({
id: skillId,
companyId,
key,
slug: rawSlug,
name: (typeof parsed.frontmatter.name === "string" ? parsed.frontmatter.name : null) ?? rawSlug,
description: typeof parsed.frontmatter.description === "string" ? parsed.frontmatter.description : null,
sourceType: "local_path",
sourceLocator: resolvedPath,
sourceRef: contentHash,
trustLevel,
compatibility: "compatible",
fileInventory: JSON.stringify(inventory),
metadata: null,
createdAt: nowTs,
updatedAt: nowTs,
});
results.push({ skillId, key, name: rawSlug, isUpdate });
logger.info(`Skill ${isUpdate ? "updated" : "imported"} from local file: ${key}`);
return results;
}
// Directory: walk to find SKILL.md files
const skillMdFiles = findSkillMdFiles(resolvedPath);
if (skillMdFiles.length === 0) {
throw new Error(`No SKILL.md files found in ${resolvedPath}`);
}
for (const skillMdPath of skillMdFiles) {
const skillDir = dirname(skillMdPath);
const markdown = readFileSync(skillMdPath, "utf-8");
const parsed = parseFrontmatterMarkdown(markdown);
const rawSlug =
normalizeSkillSlug(parsed.frontmatter.slug as string | undefined) ??
normalizeSkillSlug(basename(skillDir)) ??
"skill";
const inventory = buildLocalInventory(skillDir, skillMdPath);
const trustLevel = deriveTrustLevel(inventory);
const contentHash = hashSkillValue(markdown);
const key = deriveCanonicalSkillKey(companyId, {
slug: rawSlug,
sourceType: "local_path",
sourceLocator: skillDir,
metadata: null,
});
const skillId = randomUUID();
await materializeSkillFiles(companyId, rawSlug, markdown, inventory);
const { isUpdate } = db.upsertCompanySkill({
id: skillId,
companyId,
key,
slug: rawSlug,
name: (typeof parsed.frontmatter.name === "string" ? parsed.frontmatter.name : null) ?? rawSlug,
description: typeof parsed.frontmatter.description === "string" ? parsed.frontmatter.description : null,
sourceType: "local_path",
sourceLocator: skillDir,
sourceRef: contentHash,
trustLevel,
compatibility: "compatible",
fileInventory: JSON.stringify(inventory),
metadata: null,
createdAt: nowTs,
updatedAt: nowTs,
});
results.push({ skillId, key, name: rawSlug, isUpdate });
logger.info(`Skill ${isUpdate ? "updated" : "imported"} from local path: ${key}`);
}
return results;
}
async function importFromUrl(
companyId: string,
url: string,
safeFetch: (target: string) => Promise<Response>,
): Promise<SkillImportResult[]> {
const response = await safeFetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch skill from URL: ${response.status} ${url}`);
}
const markdown = await response.text();
const parsed = parseFrontmatterMarkdown(markdown);
const nowTs = new Date().toISOString();
let urlSlug: string | null = null;
try {
const urlObj = new URL(url);
const pathParts = urlObj.pathname.split("/").filter(Boolean);
const lastPart = pathParts[pathParts.length - 1] ?? "";
urlSlug = normalizeSkillSlug(lastPart.replace(/\.md$/i, ""));
} catch (err) {
logger.warn(`[skill-import] Unable to parse skill URL for slug derivation (${url}):`, err instanceof Error ? err.message : String(err));
}
const rawSlug =
normalizeSkillSlug(parsed.frontmatter.slug as string | undefined) ??
urlSlug ??
"skill";
const inventory: FileInventoryEntry[] = [
{ path: "SKILL.md", kind: "skill" },
];
const trustLevel = "markdown_only" as const;
const key = deriveCanonicalSkillKey(companyId, {
slug: rawSlug,
sourceType: "url",
sourceLocator: url,
metadata: null,
});
const skillId = randomUUID();
await materializeSkillFiles(companyId, rawSlug, markdown, inventory);
const { isUpdate } = db.upsertCompanySkill({
id: skillId,
companyId,
key,
slug: rawSlug,
name: (typeof parsed.frontmatter.name === "string" ? parsed.frontmatter.name : null) ?? rawSlug,
description: typeof parsed.frontmatter.description === "string" ? parsed.frontmatter.description : null,
sourceType: "url",
sourceLocator: url,
sourceRef: null,
trustLevel,
compatibility: "compatible",
fileInventory: JSON.stringify(inventory),
metadata: null,
createdAt: nowTs,
updatedAt: nowTs,
});
const results: SkillImportResult[] = [
{ skillId, key, name: rawSlug, isUpdate },
];
logger.info(`Skill ${isUpdate ? "updated" : "imported"} from URL: ${key}`);
return results;
}
function findSkillMdFiles(
rootDir: string,
opts?: { maxDepth?: number; maxFiles?: number },
): string[] {
const maxDepth = opts?.maxDepth ?? DEFAULT_SKILL_WALK_MAX_DEPTH;
const maxFiles = opts?.maxFiles ?? DEFAULT_SKILL_WALK_MAX_FILES;
const found: string[] = [];
const visitedRealPaths = new Set<string>();
let filesSeen = 0;
function walk(dir: string, depth: number) {
if (depth > maxDepth) {
throw new Error(
`Skill directory too deep, refusing to walk beyond ${maxDepth} levels`,
);
}
let realDir: string;
try {
realDir = realpathSync(dir);
} catch (err) {
logger.warn(`[skill-import] realpathSync failed for ${dir}:`, err instanceof Error ? err.message : String(err));
return;
}
if (visitedRealPaths.has(realDir)) {
return;
}
visitedRealPaths.add(realDir);
let entries: Dirent[];
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch (err) {
logger.warn(`[skill-import] readdirSync failed for ${dir}:`, err instanceof Error ? err.message : String(err));
return;
}
for (const entry of entries) {
filesSeen++;
if (filesSeen > maxFiles) {
throw new Error(
`Skill directory contains too many files, refusing after ${maxFiles} entries`,
);
}
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
walk(fullPath, depth + 1);
} else if (entry.name === "SKILL.md") {
found.push(fullPath);
}
}
}
walk(rootDir, 0);
return found;
}
function buildLocalInventory(
skillDir: string,
skillMdPath: string,
opts?: { maxDepth?: number; maxFiles?: number },
): FileInventoryEntry[] {
const maxDepth = opts?.maxDepth ?? DEFAULT_SKILL_WALK_MAX_DEPTH;
const maxFiles = opts?.maxFiles ?? DEFAULT_SKILL_WALK_MAX_FILES;
const inventory: FileInventoryEntry[] = [
{ path: "SKILL.md", kind: "skill" },
];
const visitedRealPaths = new Set<string>();
let filesSeen = 0;
function walk(dir: string, depth: number) {
if (depth > maxDepth) {
throw new Error(
`Skill directory too deep, refusing to walk beyond ${maxDepth} levels`,
);
}
let realDir: string;
try {
realDir = realpathSync(dir);
} catch (err) {
logger.warn(`[skill-import] realpathSync failed for ${dir}:`, err instanceof Error ? err.message : String(err));
return;
}
if (visitedRealPaths.has(realDir)) {
return;
}
visitedRealPaths.add(realDir);
let entries: Dirent[];
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch (err) {
logger.warn(`[skill-import] readdirSync failed for ${dir}:`, err instanceof Error ? err.message : String(err));
return;
}
for (const entry of entries) {
filesSeen++;
if (filesSeen > maxFiles) {
throw new Error(
`Skill directory contains too many files, refusing after ${maxFiles} entries`,
);
}
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
walk(fullPath, depth + 1);
continue;
}
if (!entry.isFile()) continue;
const resolvedPath = resolve(fullPath);
if (resolvedPath === skillMdPath) continue;
const relativePath = relative(skillDir, resolvedPath).replace(/\\/g, "/");
inventory.push({
path: relativePath,
kind: classifyInventoryKind(relativePath),
});
}
}
try {
walk(skillDir, 0);
} catch (err) {
// Re-throw limit errors so caller can surface them; silently ignore others.
const msg = err instanceof Error ? err.message : String(err);
if (msg.startsWith("Skill directory too deep") || msg.startsWith("Skill directory contains too many files")) {
throw err;
}
}
return inventory;
}
return {
importFromSource,
importFromGitHub: ghImporter.importFromGitHub,
+24
View File
@@ -0,0 +1,24 @@
import { existsSync } from "node:fs";
import { mkdir, writeFile, rm } from "node:fs/promises";
import { join } from "node:path";
import type { FileInventoryEntry } from "../skill-types.js";
import { MAX_SKILL_BYTES } from "../skill-github.js";
export async function materializeSkillFiles(
dir: string,
markdown: string,
_inventory: FileInventoryEntry[],
): Promise<string> {
if (markdown.length * 2 > MAX_SKILL_BYTES) {
throw new Error(`SKILL.md exceeds ${MAX_SKILL_BYTES}-byte limit`);
}
await mkdir(dir, { recursive: true });
await writeFile(join(dir, "SKILL.md"), markdown, "utf-8");
return dir;
}
export async function cleanupSkillFilesDir(dir: string): Promise<void> {
if (existsSync(dir)) {
await rm(dir, { recursive: true, force: true });
}
}