Add explicit skill selection to company portability
This commit is contained in:
@@ -85,16 +85,17 @@ function normalizeSelector(input: string): string {
|
||||
}
|
||||
|
||||
function parseInclude(input: string | undefined): CompanyPortabilityInclude {
|
||||
if (!input || !input.trim()) return { company: true, agents: true, projects: false, issues: false };
|
||||
if (!input || !input.trim()) return { company: true, agents: true, projects: false, issues: false, skills: false };
|
||||
const values = input.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
|
||||
const include = {
|
||||
company: values.includes("company"),
|
||||
agents: values.includes("agents"),
|
||||
projects: values.includes("projects"),
|
||||
issues: values.includes("issues"),
|
||||
issues: values.includes("issues") || values.includes("tasks"),
|
||||
skills: values.includes("skills"),
|
||||
};
|
||||
if (!include.company && !include.agents && !include.projects && !include.issues) {
|
||||
throw new Error("Invalid --include value. Use one or more of: company,agents,projects,issues");
|
||||
if (!include.company && !include.agents && !include.projects && !include.issues && !include.skills) {
|
||||
throw new Error("Invalid --include value. Use one or more of: company,agents,projects,issues,tasks,skills");
|
||||
}
|
||||
return include;
|
||||
}
|
||||
@@ -337,7 +338,7 @@ export function registerCompanyCommands(program: Command): void {
|
||||
.description("Export a company into a portable markdown package")
|
||||
.argument("<companyId>", "Company ID")
|
||||
.requiredOption("--out <path>", "Output directory")
|
||||
.option("--include <values>", "Comma-separated include set: company,agents,projects,issues", "company,agents")
|
||||
.option("--include <values>", "Comma-separated include set: company,agents,projects,issues,tasks,skills", "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")
|
||||
@@ -390,7 +391,7 @@ export function registerCompanyCommands(program: Command): void {
|
||||
.command("import")
|
||||
.description("Import a portable markdown company package from local path, URL, or GitHub")
|
||||
.requiredOption("--from <pathOrUrl>", "Source path or URL")
|
||||
.option("--include <values>", "Comma-separated include set: company,agents,projects,issues", "company,agents")
|
||||
.option("--include <values>", "Comma-separated include set: company,agents,projects,issues,tasks,skills", "company,agents")
|
||||
.option("--target <mode>", "Target mode: new | existing")
|
||||
.option("-C, --company-id <id>", "Existing target company ID")
|
||||
.option("--new-company-name <name>", "Name override for --target new")
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface CompanyPortabilityInclude {
|
||||
agents: boolean;
|
||||
projects: boolean;
|
||||
issues: boolean;
|
||||
skills: boolean;
|
||||
}
|
||||
|
||||
export interface CompanyPortabilityEnvInput {
|
||||
|
||||
@@ -6,6 +6,7 @@ export const portabilityIncludeSchema = z
|
||||
agents: z.boolean().optional(),
|
||||
projects: z.boolean().optional(),
|
||||
issues: z.boolean().optional(),
|
||||
skills: z.boolean().optional(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
@@ -119,6 +120,7 @@ export const portabilityManifestSchema = z.object({
|
||||
agents: z.boolean(),
|
||||
projects: z.boolean(),
|
||||
issues: z.boolean(),
|
||||
skills: z.boolean(),
|
||||
}),
|
||||
company: portabilityCompanyManifestEntrySchema.nullable(),
|
||||
agents: z.array(portabilityAgentManifestEntrySchema),
|
||||
|
||||
@@ -97,7 +97,7 @@ describe("company portability routes", () => {
|
||||
});
|
||||
mockCompanyPortabilityService.previewExport.mockResolvedValue({
|
||||
rootPath: "paperclip",
|
||||
manifest: { agents: [], skills: [], projects: [], issues: [], envInputs: [], includes: { company: true, agents: true, projects: true, issues: false }, company: null, schemaVersion: 1, generatedAt: new Date().toISOString(), source: null },
|
||||
manifest: { agents: [], skills: [], projects: [], issues: [], envInputs: [], includes: { company: true, agents: true, projects: true, issues: false, skills: false }, company: null, schemaVersion: 1, generatedAt: new Date().toISOString(), source: null },
|
||||
files: {},
|
||||
fileInventory: [],
|
||||
counts: { files: 0, agents: 0, skills: 0, projects: 0, issues: 0 },
|
||||
|
||||
@@ -666,7 +666,8 @@ describe("company portability", () => {
|
||||
collisionStrategy: "rename",
|
||||
}, "user-1");
|
||||
|
||||
expect(companySkillSvc.importPackageFiles).toHaveBeenCalledWith("company-imported", exported.files, {
|
||||
const textOnlyFiles = Object.fromEntries(Object.entries(exported.files).filter(([, v]) => typeof v === "string"));
|
||||
expect(companySkillSvc.importPackageFiles).toHaveBeenCalledWith("company-imported", textOnlyFiles, {
|
||||
onConflict: "replace",
|
||||
});
|
||||
expect(agentSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({
|
||||
@@ -812,7 +813,8 @@ describe("company portability", () => {
|
||||
expect(accessSvc.listActiveUserMemberships).toHaveBeenCalledWith("company-1");
|
||||
expect(accessSvc.copyActiveUserMemberships).toHaveBeenCalledWith("company-1", "company-imported");
|
||||
expect(accessSvc.ensureMembership).not.toHaveBeenCalledWith("company-imported", "user", expect.anything(), "owner", "active");
|
||||
expect(companySkillSvc.importPackageFiles).toHaveBeenCalledWith("company-imported", exported.files, {
|
||||
const textOnlyFiles = Object.fromEntries(Object.entries(exported.files).filter(([, v]) => typeof v === "string"));
|
||||
expect(companySkillSvc.importPackageFiles).toHaveBeenCalledWith("company-imported", textOnlyFiles, {
|
||||
onConflict: "rename",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,6 +98,7 @@ const DEFAULT_INCLUDE: CompanyPortabilityInclude = {
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
skills: false,
|
||||
};
|
||||
|
||||
const DEFAULT_COLLISION_STRATEGY: CompanyPortabilityCollisionStrategy = "rename";
|
||||
@@ -561,6 +562,7 @@ function normalizeInclude(input?: Partial<CompanyPortabilityInclude>): CompanyPo
|
||||
agents: input?.agents ?? DEFAULT_INCLUDE.agents,
|
||||
projects: input?.projects ?? DEFAULT_INCLUDE.projects,
|
||||
issues: input?.issues ?? DEFAULT_INCLUDE.issues,
|
||||
skills: input?.skills ?? DEFAULT_INCLUDE.skills,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1193,6 +1195,7 @@ function applySelectedFilesToSource(source: ResolvedSource, selectedFiles?: stri
|
||||
agents: filtered.manifest.agents.length > 0,
|
||||
projects: filtered.manifest.projects.length > 0,
|
||||
issues: filtered.manifest.issues.length > 0,
|
||||
skills: filtered.manifest.skills.length > 0,
|
||||
};
|
||||
|
||||
return filtered;
|
||||
@@ -1656,6 +1659,7 @@ function buildManifestFromPackageFiles(
|
||||
agents: true,
|
||||
projects: projectPaths.length > 0,
|
||||
issues: taskPaths.length > 0,
|
||||
skills: skillPaths.length > 0,
|
||||
},
|
||||
company: {
|
||||
path: resolvedCompanyPath,
|
||||
@@ -2051,6 +2055,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
||||
(input.issues && input.issues.length > 0) || (input.projectIssues && input.projectIssues.length > 0)
|
||||
? true
|
||||
: input.include?.issues,
|
||||
skills: input.skills && input.skills.length > 0 ? true : input.include?.skills,
|
||||
});
|
||||
const company = await companies.getById(companyId);
|
||||
if (!company) throw notFound("Company not found");
|
||||
@@ -2063,7 +2068,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
||||
|
||||
const allAgentRows = include.agents ? await agents.list(companyId, { includeTerminated: true }) : [];
|
||||
const liveAgentRows = allAgentRows.filter((agent) => agent.status !== "terminated");
|
||||
const companySkillRows = await companySkills.listFull(companyId);
|
||||
const companySkillRows = include.skills || include.agents ? await companySkills.listFull(companyId) : [];
|
||||
if (include.agents) {
|
||||
const skipped = allAgentRows.length - liveAgentRows.length;
|
||||
if (skipped > 0) {
|
||||
@@ -2464,6 +2469,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
||||
agents: resolved.manifest.agents.length > 0,
|
||||
projects: resolved.manifest.projects.length > 0,
|
||||
issues: resolved.manifest.issues.length > 0,
|
||||
skills: resolved.manifest.skills.length > 0,
|
||||
};
|
||||
resolved.manifest.envInputs = dedupeEnvInputs(envInputs);
|
||||
resolved.warnings.unshift(...warnings);
|
||||
@@ -2497,6 +2503,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
||||
agents: resolved.manifest.agents.length > 0,
|
||||
projects: resolved.manifest.projects.length > 0,
|
||||
issues: resolved.manifest.issues.length > 0,
|
||||
skills: resolved.manifest.skills.length > 0,
|
||||
};
|
||||
resolved.manifest.envInputs = dedupeEnvInputs(envInputs);
|
||||
resolved.warnings.unshift(...warnings);
|
||||
@@ -2559,6 +2566,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
||||
agents: requestedInclude.agents && manifest.agents.length > 0,
|
||||
projects: requestedInclude.projects && manifest.projects.length > 0,
|
||||
issues: requestedInclude.issues && manifest.issues.length > 0,
|
||||
skills: requestedInclude.skills && manifest.skills.length > 0,
|
||||
};
|
||||
const collisionStrategy = input.collisionStrategy ?? DEFAULT_COLLISION_STRATEGY;
|
||||
if (mode === "agent_safe" && collisionStrategy === "replace") {
|
||||
@@ -3019,9 +3027,11 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
||||
existingProjectSlugToId.set(existing.urlKey, existing.id);
|
||||
}
|
||||
|
||||
const importedSkills = await companySkills.importPackageFiles(targetCompany.id, pickTextFiles(plan.source.files), {
|
||||
onConflict: resolveSkillConflictStrategy(mode, plan.collisionStrategy),
|
||||
});
|
||||
const importedSkills = include.skills || include.agents
|
||||
? await companySkills.importPackageFiles(targetCompany.id, pickTextFiles(plan.source.files), {
|
||||
onConflict: resolveSkillConflictStrategy(mode, plan.collisionStrategy),
|
||||
})
|
||||
: [];
|
||||
const desiredSkillRefMap = new Map<string, string>();
|
||||
for (const importedSkill of importedSkills) {
|
||||
desiredSkillRefMap.set(importedSkill.originalKey, importedSkill.skill.key);
|
||||
|
||||
Reference in New Issue
Block a user