Address remaining Greptile portability feedback
This commit is contained in:
parent
581a654748
commit
79652da520
5 changed files with 131 additions and 9 deletions
|
|
@ -623,6 +623,63 @@ describe("company portability", () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats no-separator auth and api key env names as secrets during export", async () => {
|
||||||
|
const portability = companyPortabilityService({} as any);
|
||||||
|
|
||||||
|
agentSvc.list.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "agent-1",
|
||||||
|
name: "ClaudeCoder",
|
||||||
|
status: "idle",
|
||||||
|
role: "engineer",
|
||||||
|
title: "Software Engineer",
|
||||||
|
icon: "code",
|
||||||
|
reportsTo: null,
|
||||||
|
capabilities: "Writes code",
|
||||||
|
adapterType: "claude_local",
|
||||||
|
adapterConfig: {
|
||||||
|
promptTemplate: "You are ClaudeCoder.",
|
||||||
|
env: {
|
||||||
|
APIKEY: {
|
||||||
|
type: "plain",
|
||||||
|
value: "sk-plain-api",
|
||||||
|
},
|
||||||
|
GITHUBAUTH: {
|
||||||
|
type: "plain",
|
||||||
|
value: "gh-auth-token",
|
||||||
|
},
|
||||||
|
PRIVATEKEY: {
|
||||||
|
type: "plain",
|
||||||
|
value: "private-key-value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtimeConfig: {},
|
||||||
|
budgetMonthlyCents: 0,
|
||||||
|
permissions: {},
|
||||||
|
metadata: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const exported = await portability.exportBundle("company-1", {
|
||||||
|
include: {
|
||||||
|
company: true,
|
||||||
|
agents: true,
|
||||||
|
projects: false,
|
||||||
|
issues: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const extension = asTextFile(exported.files[".paperclip.yaml"]);
|
||||||
|
expect(extension).toContain("APIKEY:");
|
||||||
|
expect(extension).toContain("GITHUBAUTH:");
|
||||||
|
expect(extension).toContain("PRIVATEKEY:");
|
||||||
|
expect(extension).not.toContain("sk-plain-api");
|
||||||
|
expect(extension).not.toContain("gh-auth-token");
|
||||||
|
expect(extension).not.toContain("private-key-value");
|
||||||
|
expect(extension).toContain('kind: "secret"');
|
||||||
|
});
|
||||||
|
|
||||||
it("imports packaged skills and restores desired skill refs on agents", async () => {
|
it("imports packaged skills and restores desired skill refs on agents", async () => {
|
||||||
const portability = companyPortabilityService({} as any);
|
const portability = companyPortabilityService({} as any);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,45 @@ describe("project workspace skill discovery", () => {
|
||||||
expect(imported.fileInventory.map((entry) => entry.kind)).toContain("script");
|
expect(imported.fileInventory.map((entry) => entry.kind)).toContain("script");
|
||||||
expect(imported.metadata?.sourceKind).toBe("project_scan");
|
expect(imported.metadata?.sourceKind).toBe("project_scan");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("parses inline object array items in skill frontmatter metadata", async () => {
|
||||||
|
const workspace = await makeTempDir("paperclip-inline-skill-yaml-");
|
||||||
|
await fs.mkdir(workspace, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(workspace, "SKILL.md"),
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"name: Inline Metadata Skill",
|
||||||
|
"metadata:",
|
||||||
|
" sources:",
|
||||||
|
" - kind: github-dir",
|
||||||
|
" repo: paperclipai/paperclip",
|
||||||
|
" path: skills/paperclip",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"# Inline Metadata Skill",
|
||||||
|
"",
|
||||||
|
].join("\n"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const imported = await readLocalSkillImportFromDirectory(
|
||||||
|
"33333333-3333-4333-8333-333333333333",
|
||||||
|
workspace,
|
||||||
|
{ inventoryMode: "full" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(imported.metadata).toMatchObject({
|
||||||
|
sourceKind: "local_path",
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
kind: "github-dir",
|
||||||
|
repo: "paperclipai/paperclip",
|
||||||
|
path: "skills/paperclip",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("missing local skill reconciliation", () => {
|
describe("missing local skill reconciliation", () => {
|
||||||
|
|
|
||||||
|
|
@ -351,10 +351,12 @@ function isSensitiveEnvKey(key: string) {
|
||||||
normalized === "token" ||
|
normalized === "token" ||
|
||||||
normalized.endsWith("_token") ||
|
normalized.endsWith("_token") ||
|
||||||
normalized.endsWith("-token") ||
|
normalized.endsWith("-token") ||
|
||||||
|
normalized.includes("apikey") ||
|
||||||
normalized.includes("api_key") ||
|
normalized.includes("api_key") ||
|
||||||
normalized.includes("api-key") ||
|
normalized.includes("api-key") ||
|
||||||
normalized.includes("access_token") ||
|
normalized.includes("access_token") ||
|
||||||
normalized.includes("access-token") ||
|
normalized.includes("access-token") ||
|
||||||
|
normalized.includes("auth") ||
|
||||||
normalized.includes("auth_token") ||
|
normalized.includes("auth_token") ||
|
||||||
normalized.includes("auth-token") ||
|
normalized.includes("auth-token") ||
|
||||||
normalized.includes("authorization") ||
|
normalized.includes("authorization") ||
|
||||||
|
|
@ -364,6 +366,7 @@ function isSensitiveEnvKey(key: string) {
|
||||||
normalized.includes("password") ||
|
normalized.includes("password") ||
|
||||||
normalized.includes("credential") ||
|
normalized.includes("credential") ||
|
||||||
normalized.includes("jwt") ||
|
normalized.includes("jwt") ||
|
||||||
|
normalized.includes("privatekey") ||
|
||||||
normalized.includes("private_key") ||
|
normalized.includes("private_key") ||
|
||||||
normalized.includes("private-key") ||
|
normalized.includes("private-key") ||
|
||||||
normalized.includes("cookie") ||
|
normalized.includes("cookie") ||
|
||||||
|
|
|
||||||
|
|
@ -377,6 +377,28 @@ function parseYamlBlock(
|
||||||
index = nested.nextIndex;
|
index = nested.nextIndex;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const inlineObjectSeparator = remainder.indexOf(":");
|
||||||
|
if (
|
||||||
|
inlineObjectSeparator > 0 &&
|
||||||
|
!remainder.startsWith("\"") &&
|
||||||
|
!remainder.startsWith("{") &&
|
||||||
|
!remainder.startsWith("[")
|
||||||
|
) {
|
||||||
|
const key = remainder.slice(0, inlineObjectSeparator).trim();
|
||||||
|
const rawValue = remainder.slice(inlineObjectSeparator + 1).trim();
|
||||||
|
const nextObject: Record<string, unknown> = {
|
||||||
|
[key]: parseYamlScalar(rawValue),
|
||||||
|
};
|
||||||
|
if (index < lines.length && lines[index]!.indent > indentLevel) {
|
||||||
|
const nested = parseYamlBlock(lines, index, indentLevel + 2);
|
||||||
|
if (isPlainRecord(nested.value)) {
|
||||||
|
Object.assign(nextObject, nested.value);
|
||||||
|
}
|
||||||
|
index = nested.nextIndex;
|
||||||
|
}
|
||||||
|
values.push(nextObject);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
values.push(parseYamlScalar(remainder));
|
values.push(parseYamlScalar(remainder));
|
||||||
}
|
}
|
||||||
return { value: values, nextIndex: index };
|
return { value: values, nextIndex: index };
|
||||||
|
|
@ -804,12 +826,11 @@ export async function readLocalSkillImportFromDirectory(
|
||||||
const markdown = await fs.readFile(skillFilePath, "utf8");
|
const markdown = await fs.readFile(skillFilePath, "utf8");
|
||||||
const parsed = parseFrontmatterMarkdown(markdown);
|
const parsed = parseFrontmatterMarkdown(markdown);
|
||||||
const slug = deriveImportedSkillSlug(parsed.frontmatter, path.basename(resolvedSkillDir));
|
const slug = deriveImportedSkillSlug(parsed.frontmatter, path.basename(resolvedSkillDir));
|
||||||
const skillKey = readCanonicalSkillKey(
|
const parsedMetadata = isPlainRecord(parsed.frontmatter.metadata) ? parsed.frontmatter.metadata : null;
|
||||||
parsed.frontmatter,
|
const skillKey = readCanonicalSkillKey(parsed.frontmatter, parsedMetadata);
|
||||||
isPlainRecord(parsed.frontmatter.metadata) ? parsed.frontmatter.metadata : null,
|
|
||||||
);
|
|
||||||
const metadata = {
|
const metadata = {
|
||||||
...(skillKey ? { skillKey } : {}),
|
...(skillKey ? { skillKey } : {}),
|
||||||
|
...(parsedMetadata ?? {}),
|
||||||
sourceKind: "local_path",
|
sourceKind: "local_path",
|
||||||
...(options?.metadata ?? {}),
|
...(options?.metadata ?? {}),
|
||||||
};
|
};
|
||||||
|
|
@ -877,12 +898,11 @@ async function readLocalSkillImports(companyId: string, sourcePath: string): Pro
|
||||||
const markdown = await fs.readFile(resolvedPath, "utf8");
|
const markdown = await fs.readFile(resolvedPath, "utf8");
|
||||||
const parsed = parseFrontmatterMarkdown(markdown);
|
const parsed = parseFrontmatterMarkdown(markdown);
|
||||||
const slug = deriveImportedSkillSlug(parsed.frontmatter, path.basename(path.dirname(resolvedPath)));
|
const slug = deriveImportedSkillSlug(parsed.frontmatter, path.basename(path.dirname(resolvedPath)));
|
||||||
const skillKey = readCanonicalSkillKey(
|
const parsedMetadata = isPlainRecord(parsed.frontmatter.metadata) ? parsed.frontmatter.metadata : null;
|
||||||
parsed.frontmatter,
|
const skillKey = readCanonicalSkillKey(parsed.frontmatter, parsedMetadata);
|
||||||
isPlainRecord(parsed.frontmatter.metadata) ? parsed.frontmatter.metadata : null,
|
|
||||||
);
|
|
||||||
const metadata = {
|
const metadata = {
|
||||||
...(skillKey ? { skillKey } : {}),
|
...(skillKey ? { skillKey } : {}),
|
||||||
|
...(parsedMetadata ?? {}),
|
||||||
sourceKind: "local_path",
|
sourceKind: "local_path",
|
||||||
};
|
};
|
||||||
const inventory: CompanySkillFileInventoryEntry[] = [
|
const inventory: CompanySkillFileInventoryEntry[] = [
|
||||||
|
|
|
||||||
|
|
@ -655,6 +655,9 @@ export function CompanyImport() {
|
||||||
return ceo?.adapterType ?? "claude_local";
|
return ceo?.adapterType ?? "claude_local";
|
||||||
}, [companyAgents]);
|
}, [companyAgents]);
|
||||||
|
|
||||||
|
const localZipHelpText =
|
||||||
|
"Upload a .zip exported directly from Paperclip. Re-zipped archives created by Finder, Explorer, or other zip tools may not import correctly.";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBreadcrumbs([
|
setBreadcrumbs([
|
||||||
{ label: "Org Chart", href: "/org" },
|
{ label: "Org Chart", href: "/org" },
|
||||||
|
|
@ -1093,7 +1096,7 @@ export function CompanyImport() {
|
||||||
</div>
|
</div>
|
||||||
{!localPackage && (
|
{!localPackage && (
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
<p className="mt-2 text-xs text-muted-foreground">
|
||||||
Upload a `.zip` exported from Paperclip that contains COMPANY.md and the related package files.
|
{localZipHelpText}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue