Add --skills flag to company export CLI and fix unsupported URL import path
- Add first-class --skills <list> option to `paperclipai company export`, passing through to the existing service support for skill selection - Remove broken `type: "url"` source branch from import command — the shared schema and server only accept `inline | github`, so non-GitHub HTTP URLs now error clearly instead of failing at validation - Export isHttpUrl/isGithubUrl helpers for testability - Add server tests for skills-filtered export (selected + fallback) - Add CLI tests for URL detection helpers Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
31
cli/src/__tests__/company-import-url.test.ts
Normal file
31
cli/src/__tests__/company-import-url.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isHttpUrl, isGithubUrl } from "../commands/client/company.js";
|
||||
|
||||
describe("isHttpUrl", () => {
|
||||
it("matches http URLs", () => {
|
||||
expect(isHttpUrl("http://example.com/foo")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches https URLs", () => {
|
||||
expect(isHttpUrl("https://example.com/foo")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects local paths", () => {
|
||||
expect(isHttpUrl("/tmp/my-company")).toBe(false);
|
||||
expect(isHttpUrl("./relative")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isGithubUrl", () => {
|
||||
it("matches GitHub URLs", () => {
|
||||
expect(isGithubUrl("https://github.com/org/repo")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects non-GitHub HTTP URLs", () => {
|
||||
expect(isGithubUrl("https://example.com/foo")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects local paths", () => {
|
||||
expect(isGithubUrl("/tmp/my-company")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -34,6 +34,7 @@ interface CompanyDeleteOptions extends BaseClientOptions {
|
||||
interface CompanyExportOptions extends BaseClientOptions {
|
||||
out?: string;
|
||||
include?: string;
|
||||
skills?: string;
|
||||
projects?: string;
|
||||
issues?: string;
|
||||
projectIssues?: string;
|
||||
@@ -112,11 +113,11 @@ function parseCsvValues(input: string | undefined): string[] {
|
||||
return Array.from(new Set(input.split(",").map((part) => part.trim()).filter(Boolean)));
|
||||
}
|
||||
|
||||
function isHttpUrl(input: string): boolean {
|
||||
export function isHttpUrl(input: string): boolean {
|
||||
return /^https?:\/\//i.test(input.trim());
|
||||
}
|
||||
|
||||
function isGithubUrl(input: string): boolean {
|
||||
export function isGithubUrl(input: string): boolean {
|
||||
return /^https?:\/\/github\.com\//i.test(input.trim());
|
||||
}
|
||||
|
||||
@@ -337,6 +338,7 @@ export function registerCompanyCommands(program: Command): void {
|
||||
.argument("<companyId>", "Company ID")
|
||||
.requiredOption("--out <path>", "Output directory")
|
||||
.option("--include <values>", "Comma-separated include set: company,agents,projects,issues", "company,agents")
|
||||
.option("--skills <values>", "Comma-separated skill slugs/keys to export")
|
||||
.option("--projects <values>", "Comma-separated project shortnames/ids to export")
|
||||
.option("--issues <values>", "Comma-separated issue identifiers/ids to export")
|
||||
.option("--project-issues <values>", "Comma-separated project shortnames/ids whose issues should be exported")
|
||||
@@ -349,6 +351,7 @@ export function registerCompanyCommands(program: Command): void {
|
||||
`/api/companies/${companyId}/export`,
|
||||
{
|
||||
include,
|
||||
skills: parseCsvValues(opts.skills),
|
||||
projects: parseCsvValues(opts.projects),
|
||||
issues: parseCsvValues(opts.issues),
|
||||
projectIssues: parseCsvValues(opts.projectIssues),
|
||||
@@ -433,13 +436,16 @@ export function registerCompanyCommands(program: Command): void {
|
||||
|
||||
let sourcePayload:
|
||||
| { type: "inline"; rootPath?: string | null; files: Record<string, CompanyPortabilityFileEntry> }
|
||||
| { type: "url"; url: string }
|
||||
| { type: "github"; url: string };
|
||||
|
||||
if (isHttpUrl(from)) {
|
||||
sourcePayload = isGithubUrl(from)
|
||||
? { type: "github", url: from }
|
||||
: { type: "url", url: from };
|
||||
if (!isGithubUrl(from)) {
|
||||
throw new Error(
|
||||
"Only GitHub URLs and local paths are supported for import. " +
|
||||
"Generic HTTP URLs are not supported. Use a GitHub URL (https://github.com/...) or a local directory path.",
|
||||
);
|
||||
}
|
||||
sourcePayload = { type: "github", url: from };
|
||||
} else {
|
||||
const inline = await resolveInlineSourceFromPath(from);
|
||||
sourcePayload = {
|
||||
|
||||
@@ -356,6 +356,42 @@ describe("company portability", () => {
|
||||
expect(asTextFile(exported.files["skills/paperclipai/paperclip/paperclip/references/api.md"])).toContain("# API");
|
||||
});
|
||||
|
||||
it("exports only selected skills when skills filter is provided", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
skills: ["company-playbook"],
|
||||
});
|
||||
|
||||
expect(exported.files["skills/company/PAP/company-playbook/SKILL.md"]).toBeDefined();
|
||||
expect(asTextFile(exported.files["skills/company/PAP/company-playbook/SKILL.md"])).toContain("# Company Playbook");
|
||||
expect(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("warns and exports all skills when skills filter matches nothing", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
skills: ["nonexistent-skill"],
|
||||
});
|
||||
|
||||
expect(exported.warnings).toContainEqual(expect.stringContaining("nonexistent-skill"));
|
||||
expect(exported.files["skills/company/PAP/company-playbook/SKILL.md"]).toBeDefined();
|
||||
expect(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"]).toBeDefined();
|
||||
});
|
||||
|
||||
it("exports the company logo into images/ and references it from .paperclip.yaml", async () => {
|
||||
const storage = {
|
||||
getObject: vi.fn().mockResolvedValue({
|
||||
|
||||
Reference in New Issue
Block a user