mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 04:25:52 +01:00
PAP-10287 Reparent catalog tasks and show installed status
This commit is contained in:
1 parent
372caafae2
commit
159d6699ef
13 files changed
+598
-33
No files matched your search
@@ -56,6 +56,19 @@ function catalogTeam(overrides: Record<string, unknown> = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function installedCatalogTeam(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
catalogId: "paperclipai:bundled:software-development:product-engineering",
|
||||
catalogKey: "paperclipai/bundled/software-development/product-engineering",
|
||||
present: true,
|
||||
currentContentHash: "sha256:catalog-team",
|
||||
installedOriginHashes: ["sha256:catalog-team"],
|
||||
agentCount: 3,
|
||||
outOfDate: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("teams CLI commands", () => {
|
||||
let fetchMock: ReturnType<typeof vi.fn>;
|
||||
let logSpy: ReturnType<typeof vi.spyOn>;
|
||||
@@ -130,6 +143,103 @@ describe("teams CLI commands", () => {
|
||||
expect(JSON.parse(String(logSpy.mock.calls[0]?.[0]))).toEqual(rows);
|
||||
});
|
||||
|
||||
it("lists catalog teams with installed status for a company", async () => {
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(jsonResponse([
|
||||
catalogTeam(),
|
||||
catalogTeam({
|
||||
id: "paperclipai:optional:content:content-machine",
|
||||
key: "paperclipai/optional/content/content-machine",
|
||||
kind: "optional",
|
||||
category: "content",
|
||||
slug: "content-machine",
|
||||
name: "Content Machine",
|
||||
contentHash: "sha256:content-current",
|
||||
}),
|
||||
]))
|
||||
.mockResolvedValueOnce(jsonResponse([
|
||||
installedCatalogTeam({
|
||||
currentContentHash: "sha256:catalog-team",
|
||||
installedOriginHashes: ["sha256:older"],
|
||||
outOfDate: true,
|
||||
}),
|
||||
]));
|
||||
|
||||
await runCommand([
|
||||
"teams",
|
||||
"list",
|
||||
"--kind",
|
||||
"bundled",
|
||||
"--company-id",
|
||||
"company-1",
|
||||
"--api-base",
|
||||
"http://paperclip.test",
|
||||
"--api-key",
|
||||
"token",
|
||||
]);
|
||||
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"http://paperclip.test/api/teams/catalog?kind=bundled",
|
||||
expect.objectContaining({ method: "GET" }),
|
||||
);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"http://paperclip.test/api/companies/company-1/teams/catalog/installed",
|
||||
expect.objectContaining({ method: "GET" }),
|
||||
);
|
||||
const rendered = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||
expect(rendered).toContain("installedStatus");
|
||||
expect(rendered).toContain("out_of_date");
|
||||
expect(rendered).toContain("not_installed");
|
||||
});
|
||||
|
||||
it("lists installed status as JSON including removed catalog teams", async () => {
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(jsonResponse([catalogTeam()]))
|
||||
.mockResolvedValueOnce(jsonResponse([
|
||||
installedCatalogTeam(),
|
||||
installedCatalogTeam({
|
||||
catalogId: "paperclipai:removed:team",
|
||||
catalogKey: "paperclipai/removed/team",
|
||||
present: false,
|
||||
currentContentHash: null,
|
||||
installedOriginHashes: ["sha256:removed"],
|
||||
agentCount: 2,
|
||||
}),
|
||||
]));
|
||||
|
||||
await runCommand([
|
||||
"teams",
|
||||
"list",
|
||||
"--company-id",
|
||||
"company-1",
|
||||
"--api-base",
|
||||
"http://paperclip.test",
|
||||
"--api-key",
|
||||
"token",
|
||||
"--json",
|
||||
]);
|
||||
|
||||
const rows = JSON.parse(String(logSpy.mock.calls[0]?.[0]));
|
||||
expect(rows).toMatchObject([
|
||||
{
|
||||
catalogId: "paperclipai:bundled:software-development:product-engineering",
|
||||
catalogKey: "paperclipai/bundled/software-development/product-engineering",
|
||||
installedStatus: "installed",
|
||||
installedAgentCount: 3,
|
||||
outOfDate: false,
|
||||
},
|
||||
{
|
||||
catalogId: "paperclipai:removed:team",
|
||||
catalogKey: "paperclipai/removed/team",
|
||||
installedStatus: "installed_missing",
|
||||
installedAgentCount: 2,
|
||||
present: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("inspects catalog team detail by query ref so keys with slashes work", async () => {
|
||||
const detail = catalogTeam();
|
||||
fetchMock.mockResolvedValueOnce(jsonResponse(detail));
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
CatalogTeamInstallOptions,
|
||||
CatalogTeamImportOptions,
|
||||
CatalogTeamSourcePolicy,
|
||||
InstalledCatalogTeam,
|
||||
} from "@paperclipai/shared";
|
||||
import {
|
||||
addCommonClientOptions,
|
||||
@@ -23,6 +24,8 @@ interface TeamBrowseOptions extends BaseClientOptions {
|
||||
query?: string;
|
||||
}
|
||||
|
||||
interface TeamListOptions extends TeamBrowseOptions {}
|
||||
|
||||
interface TeamPreviewOptions extends BaseClientOptions {
|
||||
companyId?: string;
|
||||
targetManagerAgentId?: string;
|
||||
@@ -63,6 +66,29 @@ export function registerTeamCommands(program: Command): void {
|
||||
}),
|
||||
);
|
||||
|
||||
addCommonClientOptions(
|
||||
teams
|
||||
.command("list")
|
||||
.description("List app-shipped catalog teams with installed status for a company")
|
||||
.option("--kind <kind>", "Catalog kind filter (bundled or optional)")
|
||||
.option("--category <slug>", "Catalog category filter")
|
||||
.option("--query <text>", "Search catalog text")
|
||||
.action(async (opts: TeamListOptions) => {
|
||||
try {
|
||||
const ctx = resolveCommandContext(opts, { requireCompany: true });
|
||||
const rows = await listCatalogTeamStatusRows(ctx, opts);
|
||||
if (ctx.json) {
|
||||
printOutput(rows, { json: true });
|
||||
return;
|
||||
}
|
||||
printCatalogTeamStatusRows(rows);
|
||||
} catch (err) {
|
||||
handleCommandError(err);
|
||||
}
|
||||
}),
|
||||
{ includeCompany: true },
|
||||
);
|
||||
|
||||
addCommonClientOptions(
|
||||
teams
|
||||
.command("search")
|
||||
@@ -198,6 +224,58 @@ async function listCatalogTeams(
|
||||
return (await ctx.api.get<CatalogTeam[]>(`/api/teams/catalog${query ? `?${query}` : ""}`)) ?? [];
|
||||
}
|
||||
|
||||
type CatalogTeamInstalledStatus = "not_installed" | "installed" | "out_of_date" | "installed_missing";
|
||||
|
||||
interface CatalogTeamStatusRow {
|
||||
catalogId: string;
|
||||
catalogKey: string | null;
|
||||
kind: CatalogTeam["kind"] | null;
|
||||
category: string | null;
|
||||
slug: string | null;
|
||||
name: string;
|
||||
installedStatus: CatalogTeamInstalledStatus;
|
||||
installedAgentCount: number;
|
||||
catalogAgentCount: number | null;
|
||||
projectCount: number | null;
|
||||
trustLevel: CatalogTeam["trustLevel"] | null;
|
||||
present: boolean;
|
||||
outOfDate: boolean;
|
||||
currentContentHash: string | null;
|
||||
installedOriginHashes: string[];
|
||||
}
|
||||
|
||||
async function listCatalogTeamStatusRows(
|
||||
ctx: ResolvedClientContext,
|
||||
opts: TeamListOptions,
|
||||
): Promise<CatalogTeamStatusRow[]> {
|
||||
if (!ctx.companyId) {
|
||||
throw new Error("Company ID is required.");
|
||||
}
|
||||
|
||||
const [teams, installed] = await Promise.all([
|
||||
listCatalogTeams(ctx, opts),
|
||||
ctx.api.get<InstalledCatalogTeam[]>(
|
||||
`/api/companies/${encodeURIComponent(ctx.companyId)}/teams/catalog/installed`,
|
||||
),
|
||||
]);
|
||||
|
||||
const installedByCatalogId = new Map((installed ?? []).map((row) => [row.catalogId, row]));
|
||||
const rows = teams.map((team) => {
|
||||
const installedTeam = installedByCatalogId.get(team.id);
|
||||
if (installedTeam) {
|
||||
installedByCatalogId.delete(team.id);
|
||||
}
|
||||
return buildCatalogTeamStatusRow(team, installedTeam ?? null);
|
||||
});
|
||||
|
||||
for (const installedTeam of installedByCatalogId.values()) {
|
||||
if (installedTeam.present) continue;
|
||||
rows.push(buildMissingInstalledTeamStatusRow(installedTeam));
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getCatalogTeam(ctx: ResolvedClientContext, catalogRef: string): Promise<CatalogTeam> {
|
||||
const ref = catalogRef.trim();
|
||||
if (!ref) {
|
||||
@@ -310,6 +388,70 @@ function printCatalogTeamRows(rows: CatalogTeam[]): void {
|
||||
})));
|
||||
}
|
||||
|
||||
function buildCatalogTeamStatusRow(
|
||||
team: CatalogTeam,
|
||||
installed: InstalledCatalogTeam | null,
|
||||
): CatalogTeamStatusRow {
|
||||
return {
|
||||
catalogId: team.id,
|
||||
catalogKey: team.key,
|
||||
kind: team.kind,
|
||||
category: team.category,
|
||||
slug: team.slug,
|
||||
name: team.name,
|
||||
installedStatus: installed ? (installed.outOfDate ? "out_of_date" : "installed") : "not_installed",
|
||||
installedAgentCount: installed?.agentCount ?? 0,
|
||||
catalogAgentCount: team.counts.agents,
|
||||
projectCount: team.counts.projects,
|
||||
trustLevel: team.trustLevel,
|
||||
present: true,
|
||||
outOfDate: installed?.outOfDate ?? false,
|
||||
currentContentHash: team.contentHash,
|
||||
installedOriginHashes: installed?.installedOriginHashes ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function buildMissingInstalledTeamStatusRow(installed: InstalledCatalogTeam): CatalogTeamStatusRow {
|
||||
const name = installed.catalogKey ?? installed.catalogId;
|
||||
return {
|
||||
catalogId: installed.catalogId,
|
||||
catalogKey: installed.catalogKey,
|
||||
kind: null,
|
||||
category: null,
|
||||
slug: null,
|
||||
name,
|
||||
installedStatus: "installed_missing",
|
||||
installedAgentCount: installed.agentCount,
|
||||
catalogAgentCount: null,
|
||||
projectCount: null,
|
||||
trustLevel: null,
|
||||
present: false,
|
||||
outOfDate: false,
|
||||
currentContentHash: installed.currentContentHash,
|
||||
installedOriginHashes: installed.installedOriginHashes,
|
||||
};
|
||||
}
|
||||
|
||||
function printCatalogTeamStatusRows(rows: CatalogTeamStatusRow[]): void {
|
||||
if (rows.length === 0) {
|
||||
printOutput([], { json: false });
|
||||
return;
|
||||
}
|
||||
printTable(rows.map((row) => ({
|
||||
id: row.catalogId,
|
||||
key: row.catalogKey,
|
||||
kind: row.kind,
|
||||
category: row.category,
|
||||
slug: row.slug,
|
||||
name: row.name,
|
||||
installedStatus: row.installedStatus,
|
||||
installedAgents: row.installedAgentCount,
|
||||
catalogAgents: row.catalogAgentCount,
|
||||
projects: row.projectCount,
|
||||
trust: row.trustLevel,
|
||||
})));
|
||||
}
|
||||
|
||||
function printCatalogTeamDetail(team: CatalogTeam): void {
|
||||
console.log(
|
||||
formatInlineRecord({
|
||||
|
||||
Reference in new issue
Block a user