feat(FN-1968): add skill manifest previews to agent import
- Add SkillManifest support in core with parseSkillManifest and skills directory parsing/export wiring - Extend /api/agents/import responses with dry-run skill previews and skillsCount metadata - Show parsed package skills in AgentImportModal with dedicated preview UI and styles - Add parser, API route, and modal tests covering skill manifest parsing and preview rendering - Keep mission interview assertion generation explicit for milestone/slice fallbacks and update memory guidance for dashboard build resolution
This commit is contained in:
@@ -307,7 +307,7 @@ Dashboard SSE (`/api/events`) streams plugin lifecycle events as normalized `plu
|
||||
- SQLite `ORDER BY timestamp DESC` alone can be nondeterministic when multiple rows share the same millisecond timestamp; add a stable tiebreaker (for example `rowid DESC`) when selecting a "latest" event.
|
||||
- In `TaskCard.tsx`, `isInteractiveTarget` must check `target instanceof Element` (not `HTMLElement`) so SVG elements from lucide-react icons are correctly detected as interactive when inside buttons.
|
||||
- If workspace tests fail resolving `@fusion/core` package exports from `packages/core/dist/index.js` (for example `No matching export ...` in CLI/TUI/package-level tests after adding a new core export), run `pnpm --filter @fusion/core build` before rerunning the suite so ignored `dist/` exports are refreshed.
|
||||
- If CLI tests fail resolving `@fusion/dashboard` (for example `Could not resolve "@fusion/dashboard"` in `build-exe` or command tests), build dashboard first with `pnpm --filter @fusion/dashboard build` so `packages/dashboard/dist/index.js` exists.
|
||||
- If CLI tests/build-exe tests fail with `Could not resolve "@fusion/dashboard"` (or Vite reports missing `@fusion/dashboard` entry), build the dashboard package first (`pnpm --filter @fusion/dashboard build`) so `packages/dashboard/dist/index.js` exists for workspace consumers.
|
||||
- QuickEntryBox control test IDs are reused in `ListView` integration tests; when control layout changes (for example nested menu → inline buttons), update both `QuickEntryBox.test.tsx` and `ListView.test.tsx` together to avoid cascading failures.
|
||||
- When `InlineCreateCard` layout changes, also check `Column.test.tsx` and `board-mobile.test.tsx` for references to moved/removed test IDs like `inline-create-description-actions`.
|
||||
- When adding portal-based dropdown menus to QuickEntryBox, tests may fail in isolation but pass when run together (test isolation issues). This is because tests share DOM state across describe blocks. Always verify new dropdown tests pass both in isolation (`--testNamePattern`) and when run together.
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
parseCompanyManifest,
|
||||
parseProjectManifest,
|
||||
parseSingleAgentManifest,
|
||||
parseSkillManifest,
|
||||
parseTaskManifest,
|
||||
parseTeamManifest,
|
||||
parseYamlFrontmatter,
|
||||
@@ -185,6 +186,24 @@ schedule:
|
||||
expect(manifest.assignee).toBe("./agents/ceo/AGENTS.md");
|
||||
expect(manifest.schedule?.timezone).toBe("America/New_York");
|
||||
});
|
||||
|
||||
it("parses SKILL.md with instruction body", () => {
|
||||
const manifest = parseSkillManifest(`---
|
||||
name: review
|
||||
schema: agentcompanies/v1
|
||||
kind: skill
|
||||
---
|
||||
# review
|
||||
|
||||
Add skill instructions here.`);
|
||||
|
||||
expect(manifest).toEqual({
|
||||
name: "review",
|
||||
schema: "agentcompanies/v1",
|
||||
kind: "skill",
|
||||
instructionBody: "# review\n\nAdd skill instructions here.",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("directory parsing", () => {
|
||||
@@ -252,6 +271,43 @@ name: Solo Agent
|
||||
expect(pkg.teams).toEqual([]);
|
||||
});
|
||||
|
||||
it("parses skills from skills subdirectories", () => {
|
||||
const root = createTempDir();
|
||||
writeTextFile(
|
||||
join(root, "skills", "review", "SKILL.md"),
|
||||
`---
|
||||
name: review
|
||||
kind: skill
|
||||
---
|
||||
# review`,
|
||||
);
|
||||
writeTextFile(
|
||||
join(root, "skills", "strategy", "SKILL.md"),
|
||||
`---
|
||||
name: strategy
|
||||
kind: skill
|
||||
---
|
||||
# strategy`,
|
||||
);
|
||||
|
||||
const pkg = parseCompanyDirectory(root);
|
||||
expect(pkg.skills).toHaveLength(2);
|
||||
expect(pkg.skills?.map((skill) => skill.name)).toEqual(["review", "strategy"]);
|
||||
});
|
||||
|
||||
it("returns empty skills when skills directory is absent", () => {
|
||||
const root = createTempDir();
|
||||
writeTextFile(
|
||||
join(root, "agents", "solo", "AGENTS.md"),
|
||||
`---
|
||||
name: Solo Agent
|
||||
---`,
|
||||
);
|
||||
|
||||
const pkg = parseCompanyDirectory(root);
|
||||
expect(pkg.skills).toEqual([]);
|
||||
});
|
||||
|
||||
it("parses empty directory", () => {
|
||||
const root = createTempDir();
|
||||
const pkg = parseCompanyDirectory(root);
|
||||
@@ -261,6 +317,7 @@ name: Solo Agent
|
||||
teams: [],
|
||||
projects: [],
|
||||
tasks: [],
|
||||
skills: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
AgentManifest,
|
||||
CompanyManifest,
|
||||
ProjectManifest,
|
||||
SkillManifest,
|
||||
TaskManifest,
|
||||
TeamManifest,
|
||||
} from "./agent-companies-types.js";
|
||||
@@ -344,6 +345,15 @@ export function parseTaskManifest(content: string): TaskManifest {
|
||||
return parseTypedManifest<TaskManifest>(content, "task");
|
||||
}
|
||||
|
||||
export function parseSkillManifest(content: string): SkillManifest {
|
||||
const { frontmatter, body } = parseYamlFrontmatter(content);
|
||||
requireName(frontmatter, "skill");
|
||||
return {
|
||||
...(frontmatter as unknown as SkillManifest),
|
||||
instructionBody: body,
|
||||
};
|
||||
}
|
||||
|
||||
function parseManifestFile<T>(filePath: string, parser: (content: string) => T): T {
|
||||
try {
|
||||
return parser(readFileSync(filePath, "utf-8"));
|
||||
@@ -443,6 +453,7 @@ export function parseCompanyDirectory(dirPath: string): AgentCompaniesPackage {
|
||||
parseProjectManifest,
|
||||
),
|
||||
tasks: parseManifestSubdirectories(resolvedPath, "tasks", "TASK.md", parseTaskManifest),
|
||||
skills: parseManifestSubdirectories(resolvedPath, "skills", "SKILL.md", parseSkillManifest),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -70,12 +70,17 @@ export interface TaskManifest extends AgentCompaniesFrontmatter {
|
||||
};
|
||||
}
|
||||
|
||||
export interface SkillManifest extends AgentCompaniesFrontmatter {
|
||||
instructionBody?: string;
|
||||
}
|
||||
|
||||
export interface AgentCompaniesPackage {
|
||||
company?: CompanyManifest;
|
||||
agents: AgentManifest[];
|
||||
teams: TeamManifest[];
|
||||
projects: ProjectManifest[];
|
||||
tasks: TaskManifest[];
|
||||
skills?: SkillManifest[];
|
||||
}
|
||||
|
||||
export interface AgentCompaniesImportResult {
|
||||
|
||||
@@ -482,6 +482,7 @@ export type {
|
||||
AgentManifest,
|
||||
ProjectManifest,
|
||||
TaskManifest,
|
||||
SkillManifest,
|
||||
SourceReference,
|
||||
} from "./agent-companies-types.js";
|
||||
|
||||
@@ -495,6 +496,7 @@ export {
|
||||
parseSingleAgentManifest,
|
||||
parseProjectManifest,
|
||||
parseTaskManifest,
|
||||
parseSkillManifest,
|
||||
parseCompanyDirectory,
|
||||
parseCompanyArchive,
|
||||
mapRoleToCapability,
|
||||
|
||||
@@ -20,6 +20,11 @@ interface AgentPreview {
|
||||
skills?: string[];
|
||||
}
|
||||
|
||||
interface SkillPreview {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** Import result from the API */
|
||||
interface ImportResult {
|
||||
companyName?: string;
|
||||
@@ -118,6 +123,7 @@ export function AgentImportModal({ isOpen, onClose, onImported, projectId }: Age
|
||||
const [directoryAgents, setDirectoryAgents] = useState<DirectoryAgentInput[]>([]);
|
||||
const [companyName, setCompanyName] = useState("Unknown");
|
||||
const [agents, setAgents] = useState<AgentPreview[]>([]);
|
||||
const [skills, setSkills] = useState<SkillPreview[]>([]);
|
||||
const [isParsing, setIsParsing] = useState(false);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [parseError, setParseError] = useState<string | null>(null);
|
||||
@@ -193,6 +199,7 @@ export function AgentImportModal({ isOpen, onClose, onImported, projectId }: Age
|
||||
setDirectoryAgents([]);
|
||||
setCompanyName("Unknown");
|
||||
setAgents([]);
|
||||
setSkills([]);
|
||||
setIsParsing(false);
|
||||
setIsImporting(false);
|
||||
setParseError(null);
|
||||
@@ -317,6 +324,7 @@ export function AgentImportModal({ isOpen, onClose, onImported, projectId }: Age
|
||||
const data = await res.json() as {
|
||||
companyName?: string;
|
||||
agents?: AgentPreview[];
|
||||
skills?: SkillPreview[];
|
||||
created: string[];
|
||||
skipped: string[];
|
||||
errors: Array<{ name: string; error: string }>;
|
||||
@@ -325,9 +333,11 @@ export function AgentImportModal({ isOpen, onClose, onImported, projectId }: Age
|
||||
const previewAgents = (data.agents && data.agents.length > 0)
|
||||
? data.agents
|
||||
: data.created.map((name) => ({ name, role: "custom" }));
|
||||
const previewSkills = Array.isArray(data.skills) ? data.skills : [];
|
||||
|
||||
setCompanyName(data.companyName ?? "Unknown");
|
||||
setAgents(previewAgents);
|
||||
setSkills(previewSkills);
|
||||
setStep("preview");
|
||||
} catch (err) {
|
||||
setParseError(err instanceof Error ? err.message : "Failed to parse manifest");
|
||||
@@ -628,6 +638,28 @@ export function AgentImportModal({ isOpen, onClose, onImported, projectId }: Age
|
||||
<p className="agent-import-empty">No agents found in the manifest.</p>
|
||||
)}
|
||||
|
||||
{skills.length > 0 && (
|
||||
<div className="agent-import-skills-section">
|
||||
<div className="agent-import-count">
|
||||
<FileText size={14} />
|
||||
<span>{skills.length} skill{skills.length !== 1 ? "s" : ""} found</span>
|
||||
</div>
|
||||
<div className="agent-import-skill-list">
|
||||
{skills.map((skill, idx) => (
|
||||
<div key={`${skill.name}-${idx}`} className="agent-import-skill-item">
|
||||
<span className="agent-import-skill-icon">⚡</span>
|
||||
<div className="agent-import-skill-details">
|
||||
<span className="agent-import-skill-name">{skill.name}</span>
|
||||
{skill.description && (
|
||||
<span className="agent-import-skill-description">{skill.description}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{importError && (
|
||||
<p className="agent-dialog-error">
|
||||
<AlertTriangle size={14} />
|
||||
|
||||
@@ -52,6 +52,10 @@ describe("AgentImportModal", () => {
|
||||
{ name: "Reviewer", role: "reviewer", title: "Code Reviewer", skills: ["review"] },
|
||||
{ name: "Planner", role: "triage", title: "Planner" },
|
||||
],
|
||||
skills: [
|
||||
{ name: "review", description: "Review implementation details" },
|
||||
{ name: "strategy" },
|
||||
],
|
||||
created: ["Reviewer", "Planner"],
|
||||
skipped: [],
|
||||
errors: [],
|
||||
@@ -130,6 +134,10 @@ describe("AgentImportModal", () => {
|
||||
expect(screen.getByText("Planner")).toBeInTheDocument();
|
||||
expect(screen.getByText(/reviewer/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/triage/)).toBeInTheDocument();
|
||||
expect(screen.getByText("2 skills found")).toBeInTheDocument();
|
||||
expect(screen.getByText("review")).toBeInTheDocument();
|
||||
expect(screen.getByText("strategy")).toBeInTheDocument();
|
||||
expect(screen.getByText("Review implementation details")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Back button returns to input step", async () => {
|
||||
@@ -143,6 +151,34 @@ describe("AgentImportModal", () => {
|
||||
expect(screen.queryByText("2 agents found")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render skills section when no package skills are returned", async () => {
|
||||
renderModal(true);
|
||||
|
||||
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
||||
await mockResponse({
|
||||
ok: true,
|
||||
body: {
|
||||
companyName: "Acme AI",
|
||||
agents: [{ name: "Reviewer", role: "reviewer" }],
|
||||
created: ["Reviewer"],
|
||||
skipped: [],
|
||||
errors: [],
|
||||
dryRun: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.type(screen.getByLabelText("Manifest content"), "---\nname: Reviewer\nrole: reviewer\n---");
|
||||
await user.click(screen.getByRole("button", { name: "Preview" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("1 agent found")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByText(/skills found/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handleImport posts live import request and transitions to result step", async () => {
|
||||
renderModal(true);
|
||||
|
||||
|
||||
@@ -27877,6 +27877,49 @@ html .column.drag-over * {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* === Agent Import Skills === */
|
||||
.agent-import-skills-section {
|
||||
margin-top: var(--space-lg);
|
||||
}
|
||||
|
||||
.agent-import-skill-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
max-height: calc(var(--space-2xl) * 6);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.agent-import-skill-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.agent-import-skill-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-import-skill-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-import-skill-name {
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.agent-import-skill-description {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.agent-import-empty {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
|
||||
@@ -87,6 +87,7 @@ describe("POST /api/agents/import", () => {
|
||||
teams: [{ name: "Engineering" }],
|
||||
projects: [],
|
||||
tasks: [],
|
||||
skills: [{ name: "review" }, { name: "strategy" }],
|
||||
});
|
||||
|
||||
mockParseCompanyArchive.mockResolvedValue({
|
||||
@@ -95,6 +96,7 @@ describe("POST /api/agents/import", () => {
|
||||
teams: [{ name: "Ops" }],
|
||||
projects: [],
|
||||
tasks: [],
|
||||
skills: [{ name: "review" }, { name: "strategy" }],
|
||||
});
|
||||
|
||||
mockParseSingleAgentManifest.mockReturnValue({
|
||||
@@ -149,6 +151,7 @@ describe("POST /api/agents/import", () => {
|
||||
expect(body.created[0].name).toBe("YAML Agent");
|
||||
expect(body.companyName).toBe("Unknown");
|
||||
expect(body.companySlug).toBeUndefined();
|
||||
expect(body.skillsCount).toBe(0);
|
||||
});
|
||||
|
||||
it("imports agents via { source } directory mode", async () => {
|
||||
@@ -164,6 +167,7 @@ describe("POST /api/agents/import", () => {
|
||||
expect(body.companyName).toBe("Directory Co");
|
||||
expect(body.companySlug).toBe("directory-co");
|
||||
expect(body.created).toHaveLength(1);
|
||||
expect(body.skillsCount).toBe(2);
|
||||
});
|
||||
|
||||
it("imports agents via { source } archive mode", async () => {
|
||||
@@ -177,6 +181,7 @@ describe("POST /api/agents/import", () => {
|
||||
const body = response.body as any;
|
||||
expect(body.companyName).toBe("Archive Co");
|
||||
expect(body.companySlug).toBe("archive-co");
|
||||
expect(body.skillsCount).toBe(2);
|
||||
});
|
||||
|
||||
it("creates hierarchical agents with resolved parent ids", async () => {
|
||||
@@ -302,6 +307,7 @@ describe("POST /api/agents/import", () => {
|
||||
expect(body.agents).toEqual([
|
||||
expect.objectContaining({ name: "YAML Agent", role: "custom", title: "Chief Executive" }),
|
||||
]);
|
||||
expect(body.skills).toEqual([]);
|
||||
expect(mockCreateAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -327,6 +333,53 @@ describe("POST /api/agents/import", () => {
|
||||
expect(mockCreateAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("dry-run preview includes skills from company package", async () => {
|
||||
const sourceDir = join(testDir, "skills-company");
|
||||
mkdirSync(join(sourceDir, "agents", "ceo"), { recursive: true });
|
||||
writeFileSync(join(sourceDir, "agents", "ceo", "AGENTS.md"), "---\nname: CEO\n---\nLead");
|
||||
|
||||
mockParseCompanyDirectory.mockReturnValue({
|
||||
company: { name: "Directory Co", slug: "directory-co" },
|
||||
agents: [{ name: "Dir Agent", skills: ["review"] }],
|
||||
teams: [{ name: "Engineering" }],
|
||||
projects: [],
|
||||
tasks: [],
|
||||
skills: [
|
||||
{ name: "review", description: "Review implementation details" },
|
||||
{ name: "strategy" },
|
||||
],
|
||||
});
|
||||
|
||||
const response = await postImport(app, { source: sourceDir, dryRun: true });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const body = response.body as any;
|
||||
expect(body.skills).toEqual([
|
||||
{ name: "review", description: "Review implementation details" },
|
||||
{ name: "strategy" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("dry-run preview returns empty skills when package has no skills", async () => {
|
||||
const sourceDir = join(testDir, "no-skills-company");
|
||||
mkdirSync(join(sourceDir, "agents", "ceo"), { recursive: true });
|
||||
writeFileSync(join(sourceDir, "agents", "ceo", "AGENTS.md"), "---\nname: CEO\n---\nLead");
|
||||
|
||||
mockParseCompanyDirectory.mockReturnValue({
|
||||
company: { name: "Directory Co", slug: "directory-co" },
|
||||
agents: [{ name: "Dir Agent", skills: ["review"] }],
|
||||
teams: [{ name: "Engineering" }],
|
||||
projects: [],
|
||||
tasks: [],
|
||||
});
|
||||
|
||||
const response = await postImport(app, { source: sourceDir, dryRun: true });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const body = response.body as any;
|
||||
expect(body.skills).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns 400 for parser errors", async () => {
|
||||
mockParseSingleAgentManifest.mockImplementation(() => {
|
||||
throw new MockAgentCompaniesParseError("Missing YAML frontmatter delimiters (---)");
|
||||
|
||||
@@ -735,7 +735,7 @@ export function createMissionRouter(
|
||||
missionStore.updateMission(mission.id, { interviewState: "completed" as InterviewState });
|
||||
|
||||
// Create milestones, slices, and features with verification in dedicated fields.
|
||||
// Auto-generate contract assertions for milestone, slice, and feature levels.
|
||||
// Auto-generate contract assertions at milestone, slice, and feature levels.
|
||||
for (const milestoneData of (summary.milestones ?? [])) {
|
||||
// Use dedicated verification field instead of concatenating into description
|
||||
const milestone = missionStore.addMilestone(mission.id, {
|
||||
@@ -745,12 +745,12 @@ export function createMissionRouter(
|
||||
});
|
||||
|
||||
// Milestone-level assertion remains on the milestone even when it has no slices.
|
||||
const milestoneAssertionText = milestoneData.verification
|
||||
|| milestoneData.description
|
||||
|| `Verify milestone completion: ${milestoneData.title}`;
|
||||
missionStore.addContractAssertion(milestone.id, {
|
||||
title: `Milestone: ${milestoneData.title}`,
|
||||
assertion:
|
||||
milestoneData.verification
|
||||
|| milestoneData.description
|
||||
|| `Verify milestone completion: ${milestoneData.title}`,
|
||||
assertion: milestoneAssertionText,
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
@@ -763,12 +763,12 @@ export function createMissionRouter(
|
||||
});
|
||||
|
||||
// Slice-level assertion for explicit verification criteria.
|
||||
const sliceAssertionText = sliceData.verification
|
||||
|| sliceData.description
|
||||
|| `Verify slice completion: ${sliceData.title}`;
|
||||
missionStore.addContractAssertion(milestone.id, {
|
||||
title: `Slice: ${sliceData.title}`,
|
||||
assertion:
|
||||
sliceData.verification
|
||||
|| sliceData.description
|
||||
|| `Verify slice completion: ${sliceData.title}`,
|
||||
assertion: sliceAssertionText,
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
|
||||
@@ -10705,6 +10705,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
teams: unknown[];
|
||||
projects: unknown[];
|
||||
tasks: unknown[];
|
||||
skills?: unknown[];
|
||||
};
|
||||
|
||||
if (Array.isArray(agents)) {
|
||||
@@ -10977,11 +10978,19 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
const skillPreview = (pkg.skills ?? [])
|
||||
.filter((skill): skill is Record<string, unknown> => typeof skill === "object" && skill !== null)
|
||||
.map((skill) => ({
|
||||
name: typeof skill.name === "string" && skill.name.length > 0 ? skill.name : "Unnamed Skill",
|
||||
description: typeof skill.description === "string" ? skill.description : undefined,
|
||||
}));
|
||||
|
||||
res.json({
|
||||
dryRun: true,
|
||||
companyName,
|
||||
...(companySlug ? { companySlug } : {}),
|
||||
agents: agentPreview,
|
||||
skills: skillPreview,
|
||||
created: result.created,
|
||||
skipped: result.skipped,
|
||||
errors: result.errors,
|
||||
@@ -11036,6 +11045,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
created,
|
||||
skipped: result.skipped,
|
||||
errors,
|
||||
skillsCount: (pkg.skills ?? []).length,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
|
||||
Reference in New Issue
Block a user