feat(FN-2233): merge fusion/fn-2233
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { writeFileSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { writeFileSync, mkdirSync, rmSync, existsSync, readFileSync } from "node:fs";
|
||||
import { execSync } from "node:child_process";
|
||||
import { join } from "node:path";
|
||||
import { join, resolve } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { AgentStore } from "@fusion/core";
|
||||
import { runAgentImport } from "./agent-import.js";
|
||||
@@ -105,6 +105,82 @@ function createHierarchyCompanyDirectory(basePath: string): string {
|
||||
return basePath;
|
||||
}
|
||||
|
||||
function makeSkillManifest(options: {
|
||||
name: string;
|
||||
description?: string;
|
||||
slug?: string;
|
||||
version?: string;
|
||||
license?: string;
|
||||
authors?: string[];
|
||||
tags?: string[];
|
||||
instructionBody?: string;
|
||||
}): string {
|
||||
const lines = ["---"];
|
||||
lines.push(`name: ${options.name}`);
|
||||
if (options.description) lines.push(`description: ${options.description}`);
|
||||
if (options.slug) lines.push(`slug: ${options.slug}`);
|
||||
if (options.version) lines.push(`version: ${options.version}`);
|
||||
if (options.license) lines.push(`license: ${options.license}`);
|
||||
if (options.authors && options.authors.length > 0) {
|
||||
lines.push("authors:");
|
||||
for (const author of options.authors) {
|
||||
lines.push(` - ${author}`);
|
||||
}
|
||||
}
|
||||
if (options.tags && options.tags.length > 0) {
|
||||
lines.push("tags:");
|
||||
for (const tag of options.tags) {
|
||||
lines.push(` - ${tag}`);
|
||||
}
|
||||
}
|
||||
lines.push("---");
|
||||
if (options.instructionBody) {
|
||||
lines.push(options.instructionBody);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function createCompanyDirectoryWithSkills(basePath: string, skills: Array<{
|
||||
name: string;
|
||||
description?: string;
|
||||
instructionBody?: string;
|
||||
}>): string {
|
||||
// Create base company structure
|
||||
mkdirSync(basePath, { recursive: true });
|
||||
writeFileSync(
|
||||
join(basePath, "COMPANY.md"),
|
||||
"---\nname: Example Company\nslug: example-company\n---\nCompany description",
|
||||
);
|
||||
|
||||
const agentDir = join(basePath, "agents", "ceo");
|
||||
mkdirSync(agentDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(agentDir, "AGENTS.md"),
|
||||
makeAgentManifest({
|
||||
name: "CEO",
|
||||
title: "Chief Executive",
|
||||
body: "Lead the company",
|
||||
}),
|
||||
);
|
||||
|
||||
// Create skills
|
||||
const skillsDir = join(basePath, "skills");
|
||||
for (const skill of skills) {
|
||||
const skillDir = join(skillsDir, skill.name.toLowerCase().replace(/\s+/g, "-"));
|
||||
mkdirSync(skillDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(skillDir, "SKILL.md"),
|
||||
makeSkillManifest({
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
instructionBody: skill.instructionBody,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return basePath;
|
||||
}
|
||||
|
||||
describe("agent-import", () => {
|
||||
const tmpDir = join(tmpdir(), `fn-agent-import-test-${process.pid}`);
|
||||
let createAgentMock: ReturnType<typeof vi.fn>;
|
||||
@@ -313,4 +389,283 @@ describe("agent-import", () => {
|
||||
exitSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe("skill import", () => {
|
||||
const projectDir = join(tmpDir, "test-project");
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
beforeEach(() => {
|
||||
mkdirSync(projectDir, { recursive: true });
|
||||
// Create .fusion directory with fusion.db to make it detectable as a project
|
||||
mkdirSync(join(projectDir, ".fusion"), { recursive: true });
|
||||
writeFileSync(join(projectDir, ".fusion", "fusion.db"), "");
|
||||
// Change to project directory so project auto-detection works
|
||||
process.chdir(projectDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(originalCwd);
|
||||
});
|
||||
|
||||
it("imports skills from directory package to skills/imported directory", async () => {
|
||||
const companyDir = createCompanyDirectoryWithSkills(
|
||||
join(tmpDir, "company-with-skills"),
|
||||
[
|
||||
{ name: "Code Review", description: "Review code changes", instructionBody: "Review all PRs carefully" },
|
||||
{ name: "Strategy", instructionBody: "Plan the roadmap" },
|
||||
],
|
||||
);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await runAgentImport(companyDir);
|
||||
|
||||
// Verify skill files were created
|
||||
const skillDir = join(projectDir, "skills", "imported", "example-company");
|
||||
expect(existsSync(join(skillDir, "code-review", "SKILL.md"))).toBe(true);
|
||||
expect(existsSync(join(skillDir, "strategy", "SKILL.md"))).toBe(true);
|
||||
|
||||
// Verify output includes skill results
|
||||
const output = logSpy.mock.calls.flat().join(" ");
|
||||
expect(output).toContain("Skills:");
|
||||
expect(output).toContain("2 imported");
|
||||
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("generates skill markdown with required frontmatter keys", async () => {
|
||||
const companyDir = createCompanyDirectoryWithSkills(
|
||||
join(tmpDir, "company-frontmatter-test"),
|
||||
[{ name: "Test Skill", instructionBody: "Test instructions" }],
|
||||
);
|
||||
|
||||
await runAgentImport(companyDir);
|
||||
|
||||
const skillContent = readFileSync(
|
||||
join(projectDir, "skills", "imported", "example-company", "test-skill", "SKILL.md"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
// Check required frontmatter keys
|
||||
expect(skillContent).toContain("name: Test Skill");
|
||||
expect(skillContent).toContain("schema: agentcompanies/v1");
|
||||
expect(skillContent).toContain("kind: skill");
|
||||
// Check body
|
||||
expect(skillContent).toContain("Test instructions");
|
||||
});
|
||||
|
||||
it("includes optional frontmatter fields when present", async () => {
|
||||
const companyDir = join(tmpDir, "company-optional-frontmatter");
|
||||
mkdirSync(companyDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(companyDir, "COMPANY.md"),
|
||||
"---\nname: Test Co\nslug: test-co\n---",
|
||||
);
|
||||
mkdirSync(join(companyDir, "agents", "test"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(companyDir, "agents", "test", "AGENTS.md"),
|
||||
makeAgentManifest({ name: "Test Agent" }),
|
||||
);
|
||||
const skillDir = join(companyDir, "skills", "my-skill");
|
||||
mkdirSync(skillDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(skillDir, "SKILL.md"),
|
||||
makeSkillManifest({
|
||||
name: "My Skill",
|
||||
slug: "custom-slug",
|
||||
description: "A test skill",
|
||||
version: "1.0.0",
|
||||
license: "MIT",
|
||||
authors: ["Author One", "Author Two"],
|
||||
tags: ["testing", "example"],
|
||||
instructionBody: "Do the thing",
|
||||
}),
|
||||
);
|
||||
|
||||
await runAgentImport(companyDir);
|
||||
|
||||
const skillContent = readFileSync(
|
||||
join(projectDir, "skills", "imported", "test-co", "my-skill", "SKILL.md"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
expect(skillContent).toContain("description: A test skill");
|
||||
expect(skillContent).toContain("version: 1.0.0");
|
||||
expect(skillContent).toContain("license: MIT");
|
||||
expect(skillContent).toContain("authors:");
|
||||
expect(skillContent).toContain("- Author One");
|
||||
expect(skillContent).toContain("- Author Two");
|
||||
expect(skillContent).toContain("tags:");
|
||||
expect(skillContent).toContain("- testing");
|
||||
expect(skillContent).toContain("- example");
|
||||
});
|
||||
|
||||
it("uses fallback template for skill without instruction body", async () => {
|
||||
const companyDir = join(tmpDir, "company-no-body");
|
||||
mkdirSync(companyDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(companyDir, "COMPANY.md"),
|
||||
"---\nname: Test Co\nslug: test-co\n---",
|
||||
);
|
||||
mkdirSync(join(companyDir, "agents", "test"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(companyDir, "agents", "test", "AGENTS.md"),
|
||||
makeAgentManifest({ name: "Test Agent" }),
|
||||
);
|
||||
const skillDir = join(companyDir, "skills", "bare-skill");
|
||||
mkdirSync(skillDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(skillDir, "SKILL.md"),
|
||||
"---\nname: Bare Skill\n---\n",
|
||||
);
|
||||
|
||||
await runAgentImport(companyDir);
|
||||
|
||||
const skillContent = readFileSync(
|
||||
join(projectDir, "skills", "imported", "test-co", "bare-skill", "SKILL.md"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
expect(skillContent).toContain("# Bare Skill");
|
||||
});
|
||||
|
||||
it("skips existing skill files and reports them", async () => {
|
||||
const companyDir = createCompanyDirectoryWithSkills(
|
||||
join(tmpDir, "company-existing-skill"),
|
||||
[{ name: "Existing Skill", instructionBody: "Original content" }],
|
||||
);
|
||||
|
||||
// Pre-create the skill file
|
||||
const existingSkillDir = join(projectDir, "skills", "imported", "example-company", "existing-skill");
|
||||
mkdirSync(existingSkillDir, { recursive: true });
|
||||
writeFileSync(join(existingSkillDir, "SKILL.md"), "---\nname: Existing Skill\n---\nAlready exists");
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await runAgentImport(companyDir);
|
||||
|
||||
const output = logSpy.mock.calls.flat().join(" ");
|
||||
expect(output).toContain("1 skipped");
|
||||
expect(output).toContain("Existing Skill");
|
||||
|
||||
// Verify file was not overwritten
|
||||
const skillContent = readFileSync(join(existingSkillDir, "SKILL.md"), "utf-8");
|
||||
expect(skillContent).toContain("Already exists");
|
||||
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("does not write skill files in dry-run mode", async () => {
|
||||
const companyDir = createCompanyDirectoryWithSkills(
|
||||
join(tmpDir, "company-dry-run-skills"),
|
||||
[{ name: "Dry Run Skill", instructionBody: "Should not be written" }],
|
||||
);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await runAgentImport(companyDir, { dryRun: true });
|
||||
|
||||
// Verify skill file was NOT created
|
||||
const skillPath = join(projectDir, "skills", "imported", "example-company", "dry-run-skill", "SKILL.md");
|
||||
expect(existsSync(skillPath)).toBe(false);
|
||||
|
||||
// Verify output shows what would be imported
|
||||
const output = logSpy.mock.calls.flat().join(" ");
|
||||
expect(output).toContain("[DRY RUN]");
|
||||
expect(output).toContain("1 imported");
|
||||
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("does not import skills for single AGENTS.md file", async () => {
|
||||
const manifestPath = join(tmpDir, "solo-agent-with-skill.md");
|
||||
writeFileSync(
|
||||
manifestPath,
|
||||
makeAgentManifest({
|
||||
name: "Solo Agent",
|
||||
skills: ["some-skill"],
|
||||
}),
|
||||
);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await runAgentImport(manifestPath);
|
||||
|
||||
const output = logSpy.mock.calls.flat().join(" ");
|
||||
// Should not have a Skills section for single file imports
|
||||
expect(output).not.toContain("Skills:");
|
||||
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("imports skills from tar.gz archive", async () => {
|
||||
const companyDir = createCompanyDirectoryWithSkills(
|
||||
join(tmpDir, "company-archive-skills"),
|
||||
[{ name: "Archived Skill", instructionBody: "From archive" }],
|
||||
);
|
||||
const archivePath = join(tmpDir, "company-with-skills.tar.gz");
|
||||
execSync(`tar czf ${JSON.stringify(archivePath)} -C ${JSON.stringify(companyDir)} .`);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await runAgentImport(archivePath);
|
||||
|
||||
// Verify skill file was created
|
||||
const skillDir = join(projectDir, "skills", "imported", "example-company");
|
||||
expect(existsSync(join(skillDir, "archived-skill", "SKILL.md"))).toBe(true);
|
||||
|
||||
const output = logSpy.mock.calls.flat().join(" ");
|
||||
expect(output).toContain("Skills:");
|
||||
expect(output).toContain("1 imported");
|
||||
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("handles company without slug using fallback directory name", async () => {
|
||||
const companyDir = join(tmpDir, "company-no-slug");
|
||||
mkdirSync(companyDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(companyDir, "COMPANY.md"),
|
||||
"---\nname: Company Without Slug\n---\nNo slug provided",
|
||||
);
|
||||
mkdirSync(join(companyDir, "agents", "test"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(companyDir, "agents", "test", "AGENTS.md"),
|
||||
makeAgentManifest({ name: "Test Agent" }),
|
||||
);
|
||||
const skillDir = join(companyDir, "skills", "no-slug-skill");
|
||||
mkdirSync(skillDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(skillDir, "SKILL.md"),
|
||||
makeSkillManifest({ name: "No Slug Skill" }),
|
||||
);
|
||||
|
||||
await runAgentImport(companyDir);
|
||||
|
||||
// Should use "unknown-company" fallback
|
||||
const skillDir2 = join(projectDir, "skills", "imported", "unknown-company", "no-slug-skill");
|
||||
expect(existsSync(join(skillDir2, "SKILL.md"))).toBe(true);
|
||||
});
|
||||
|
||||
it("uses company slug for directory naming", async () => {
|
||||
const companyDir = join(tmpDir, "company-custom-slug");
|
||||
mkdirSync(companyDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(companyDir, "COMPANY.md"),
|
||||
"---\nname: Custom Name\nslug: my-custom-slug\n---",
|
||||
);
|
||||
mkdirSync(join(companyDir, "agents", "test"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(companyDir, "agents", "test", "AGENTS.md"),
|
||||
makeAgentManifest({ name: "Test Agent" }),
|
||||
);
|
||||
const skillDir = join(companyDir, "skills", "slugged-skill");
|
||||
mkdirSync(skillDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(skillDir, "SKILL.md"),
|
||||
makeSkillManifest({ name: "Slugged Skill" }),
|
||||
);
|
||||
|
||||
await runAgentImport(companyDir);
|
||||
|
||||
// Should use the custom slug
|
||||
const skillDir2 = join(projectDir, "skills", "imported", "my-custom-slug", "slugged-skill");
|
||||
expect(existsSync(join(skillDir2, "SKILL.md"))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @module agent-import
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
AgentStore,
|
||||
@@ -18,11 +18,139 @@ import {
|
||||
AgentCompaniesParseError,
|
||||
} from "@fusion/core";
|
||||
import type { AgentCreateInput } from "@fusion/core";
|
||||
import type { SkillManifest } from "@fusion/core";
|
||||
import { stringify as stringifyYaml } from "yaml";
|
||||
import { resolveProject } from "../project-context.js";
|
||||
|
||||
export interface SkillImportResult {
|
||||
imported: string[];
|
||||
skipped: string[];
|
||||
errors: Array<{ name: string; error: string }>;
|
||||
}
|
||||
|
||||
const UNSUPPORTED_FORMAT_MESSAGE =
|
||||
"Unsupported format. Provide an Agent Companies directory, .tar.gz/.tgz/.zip archive, or AGENTS.md file.";
|
||||
|
||||
/**
|
||||
* Convert a string to a safe path segment (slug).
|
||||
* - Lowercase
|
||||
* - Replace spaces with hyphens
|
||||
* - Remove characters that are not alphanumeric, hyphens, or underscores
|
||||
* - Collapse multiple hyphens/underscores to single
|
||||
* - Trim leading/trailing hyphens/underscores
|
||||
* - Fallback to "unnamed" if empty after sanitization
|
||||
*/
|
||||
function slugifyPathSegment(input: string): string {
|
||||
if (!input || typeof input !== "string") {
|
||||
return "unnamed";
|
||||
}
|
||||
let slug = input
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[^a-z0-9\-_]/g, "")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/_+/g, "_")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.replace(/^_+|_+$/g, "");
|
||||
if (!slug) {
|
||||
return "unnamed";
|
||||
}
|
||||
return slug.slice(0, 64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SKILL.md content from a SkillManifest.
|
||||
* Uses the same format as the dashboard/skills adapter.
|
||||
*/
|
||||
function toSkillMarkdown(skill: SkillManifest): string {
|
||||
const frontmatter: Record<string, unknown> = {
|
||||
name: skill.name,
|
||||
schema: "agentcompanies/v1",
|
||||
kind: "skill",
|
||||
};
|
||||
|
||||
// Copy optional fields when present and valid
|
||||
if (typeof skill.description === "string" && skill.description.length > 0) {
|
||||
frontmatter.description = skill.description;
|
||||
}
|
||||
if (typeof skill.slug === "string" && skill.slug.length > 0) {
|
||||
frontmatter.slug = skill.slug;
|
||||
}
|
||||
if (typeof skill.version === "string" && skill.version.length > 0) {
|
||||
frontmatter.version = skill.version;
|
||||
}
|
||||
if (typeof skill.license === "string" && skill.license.length > 0) {
|
||||
frontmatter.license = skill.license;
|
||||
}
|
||||
if (Array.isArray(skill.authors) && skill.authors.length > 0) {
|
||||
frontmatter.authors = skill.authors.filter((a): a is string => typeof a === "string" && a.length > 0);
|
||||
}
|
||||
if (Array.isArray(skill.tags) && skill.tags.length > 0) {
|
||||
frontmatter.tags = skill.tags.filter((t): t is string => typeof t === "string" && t.length > 0);
|
||||
}
|
||||
|
||||
const body = skill.instructionBody && skill.instructionBody.trim().length > 0
|
||||
? skill.instructionBody
|
||||
: `# ${skill.name}`;
|
||||
|
||||
const yaml = stringifyYaml(frontmatter, { lineWidth: 0 }).trimEnd();
|
||||
return `---\n${yaml}\n---\n${body}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import skills from a package to the project skills directory.
|
||||
* Skills are written to: {projectPath}/skills/imported/{companySlug}/{skillSlug}/SKILL.md
|
||||
*/
|
||||
async function importSkillsToProject(
|
||||
projectPath: string,
|
||||
skills: SkillManifest[],
|
||||
companySlug: string | undefined,
|
||||
dryRun: boolean,
|
||||
): Promise<SkillImportResult> {
|
||||
const result: SkillImportResult = {
|
||||
imported: [],
|
||||
skipped: [],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const companyDir = slugifyPathSegment(companySlug ?? "unknown-company");
|
||||
const baseSkillsDir = resolve(projectPath, "skills", "imported", companyDir);
|
||||
|
||||
for (const skill of skills) {
|
||||
// Skip skills without a name
|
||||
if (!skill.name || typeof skill.name !== "string" || skill.name.trim().length === 0) {
|
||||
result.errors.push({ name: "(unnamed)", error: "Skill is missing required 'name' field" });
|
||||
continue;
|
||||
}
|
||||
|
||||
const skillSlug = slugifyPathSegment(skill.name);
|
||||
const skillDir = resolve(baseSkillsDir, skillSlug);
|
||||
const skillPath = resolve(skillDir, "SKILL.md");
|
||||
|
||||
// Check if skill already exists
|
||||
if (existsSync(skillPath)) {
|
||||
result.skipped.push(skill.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
// In dry-run mode, just report what would be imported
|
||||
result.imported.push(skill.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirSync(skillDir, { recursive: true });
|
||||
writeFileSync(skillPath, toSkillMarkdown(skill), "utf-8");
|
||||
result.imported.push(skill.name);
|
||||
} catch (err) {
|
||||
result.errors.push({ name: skill.name, error: (err as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the project path for agent operations.
|
||||
* Falls back to process.cwd() if no project is specified.
|
||||
@@ -52,6 +180,7 @@ function printSummary(
|
||||
skipped: string[],
|
||||
errors: Array<{ name: string; error: string }>,
|
||||
dryRun: boolean,
|
||||
skillResult?: SkillImportResult,
|
||||
): void {
|
||||
const prefix = dryRun ? "[DRY RUN] " : "";
|
||||
console.log();
|
||||
@@ -74,6 +203,18 @@ function printSummary(
|
||||
console.log(` ✗ ${err.name}: ${err.error}`);
|
||||
}
|
||||
}
|
||||
if (skillResult) {
|
||||
console.log(` ${prefix}Skills: ${skillResult.imported.length} imported, ${skillResult.skipped.length} skipped, ${skillResult.errors.length} errors`);
|
||||
for (const name of skillResult.imported) {
|
||||
console.log(` ✓ ${name}`);
|
||||
}
|
||||
for (const name of skillResult.skipped) {
|
||||
console.log(` ○ ${name}`);
|
||||
}
|
||||
for (const err of skillResult.errors) {
|
||||
console.log(` ✗ ${err.name}: ${err.error}`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
@@ -117,6 +258,7 @@ export async function runAgentImport(
|
||||
};
|
||||
|
||||
let companyName: string | undefined;
|
||||
let companySlug: string | undefined;
|
||||
let agentCount = 0;
|
||||
let teamCount = 0;
|
||||
let importItems: Array<{
|
||||
@@ -137,6 +279,8 @@ export async function runAgentImport(
|
||||
skipped: [],
|
||||
errors: [],
|
||||
};
|
||||
let skills: SkillManifest[] = [];
|
||||
let isPackageImport = false;
|
||||
|
||||
try {
|
||||
const sourceStats = statSync(sourcePath);
|
||||
@@ -144,14 +288,20 @@ export async function runAgentImport(
|
||||
if (sourceStats.isDirectory()) {
|
||||
const pkg = parseCompanyDirectory(sourcePath);
|
||||
companyName = pkg.company?.name;
|
||||
companySlug = pkg.company?.slug;
|
||||
agentCount = pkg.agents.length;
|
||||
teamCount = pkg.teams.length;
|
||||
skills = pkg.skills ?? [];
|
||||
isPackageImport = true;
|
||||
({ items: importItems, result } = prepareAgentCompaniesImport(pkg, conversionOptions));
|
||||
} else if (isArchivePath(sourcePath)) {
|
||||
const pkg = await parseCompanyArchive(sourcePath);
|
||||
companyName = pkg.company?.name;
|
||||
companySlug = pkg.company?.slug;
|
||||
agentCount = pkg.agents.length;
|
||||
teamCount = pkg.teams.length;
|
||||
skills = pkg.skills ?? [];
|
||||
isPackageImport = true;
|
||||
({ items: importItems, result } = prepareAgentCompaniesImport(pkg, conversionOptions));
|
||||
} else if (sourcePath.endsWith(".md")) {
|
||||
const content = readFileSync(sourcePath, "utf-8");
|
||||
@@ -165,6 +315,8 @@ export async function runAgentImport(
|
||||
};
|
||||
agentCount = pkg.agents.length;
|
||||
teamCount = 0;
|
||||
skills = [];
|
||||
isPackageImport = false;
|
||||
({ items: importItems, result } = prepareAgentCompaniesImport(pkg, conversionOptions));
|
||||
} else {
|
||||
throw new Error(UNSUPPORTED_FORMAT_MESSAGE);
|
||||
@@ -191,9 +343,12 @@ export async function runAgentImport(
|
||||
return;
|
||||
}
|
||||
|
||||
// Dry run: just preview
|
||||
// Dry run: just preview (includes skill preview for package imports)
|
||||
if (dryRun) {
|
||||
printSummary(companyName, agentCount, teamCount, result.created, result.skipped, result.errors, true);
|
||||
const skillResult = isPackageImport
|
||||
? await importSkillsToProject(projectPath, skills, companySlug, true)
|
||||
: undefined;
|
||||
printSummary(companyName, agentCount, teamCount, result.created, result.skipped, result.errors, true, skillResult);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -237,5 +392,10 @@ export async function runAgentImport(
|
||||
}
|
||||
}
|
||||
|
||||
printSummary(companyName, agentCount, teamCount, created, result.skipped, errors, false);
|
||||
// Import skills for package imports (directory/archive)
|
||||
const skillResult = isPackageImport && skills.length > 0
|
||||
? await importSkillsToProject(projectPath, skills, companySlug, false)
|
||||
: undefined;
|
||||
|
||||
printSummary(companyName, agentCount, teamCount, created, result.skipped, errors, false, skillResult);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user