mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 01:57:31 +01:00
fix(teams): return saved resource links after catalog installation (#18923)
## Thinking Path > - Paperclip manages AI agents and their work. > - Catalog installation creates agents, projects, and routines in one company. > - The install result includes saved IDs but no links to those resources. > - A chat agent can describe a partial installation without giving the operator a way to inspect it. > - This change returns canonical links for saved resources and documents them for API discovery. > - The operator can use the returned links to inspect the actual installation. ## Linked Issues or Issue Description **What happened?** A catalog install can create the intended resources and report skill warnings, while its response leaves callers to construct board URLs from IDs. A native chat attempt described the resources and warned about an incomplete skill, but omitted resource links in both its initial and repeat replies. **Expected behavior** The installation response should provide canonical company-scoped links for its saved agents, projects, and routines. It must preserve warnings and omit links for rows without saved IDs. **Steps to reproduce** Install a catalog team through the API. Inspect its saved resource rows and response. Before this change, the response has IDs and names but no resource links. **Paperclip version or commit** Parent `e7549ab613` from master. Related #16495 changes comment authorization metadata in the same OpenAPI file; it does not address team installation responses. No duplicate team-link PR was found. ## What Changed - Add typed `resourceLinks` entries with resource kind, saved ID, name, and canonical company-scoped href. - Read the company prefix with one bounded primary-key query before import. Encode URL segments and use saved IDs, including IDs returned for reused resources. - Preserve the portability import result, skill preparations, warnings, permissions, and collision behavior. - Document the response and add service, native API wire, OpenAPI, and real database regression coverage. ## Verification - Parent service regression: 1 failed, 28 passed. The new failure was the missing resource-link response. - Owning service/native wire, route authorization, and OpenAPI suites: 72 passed. - Real PostgreSQL catalog-install suite: 4 passed. Returned links match persisted agent IDs and the company prefix. - Shared and server TypeScript checks passed. Fresh shared/SDK builds and the owning Runner TypeScript prerequisite passed. - UI typecheck, Storybook catalog check, 30 catalog/map controls, and token gates passed. - Actual CI found an omitted required `resourceLinks` field in the existing Storybook install fixture; that fixture is now synchronized. The earlier response-status review correction is also retained: install schema and wire control use actual HTTP 201. Final head `0bca72cf1f7641fd842a04c5a75cae4bcc274f05` passed all 48 normal CI jobs, including `ci / verify`: [run 41773413940](https://github.com/paperclipai/paperclip/actions/runs/41773413940). Greptile is 5/5 with zero unresolved review threads. The optional Paperclip review remains queued and its comment says work is paused. The Superagent security scan is neutral; neither supplies completed assessment credit. - `git diff --check` passed. - Setup failures were retained: incorrect test working directory, sandbox listener denial, missing built SDK/Runner declarations, and native library aliases not hydrated from the package directory. Corrected setup used the owning build and shipped hydration script. No assertions or deadlines were relaxed. - Repository-wide tests/build were not run locally; the full normal CI suite passed. A new native chat attempt has not run for this change. The earlier chat attempt remains failed for missing links. Local response coverage does not prove that a model will include the links in its next reply. ## Risks - This is an additive JSON response field. Manually constructed TypeScript install results must include the new typed field. - The query adds one company-prefix lookup per installation. It selects one scalar by company primary key and runs before mutation. - Supplying links reduces URL construction work but does not guarantee model use. Native qualification is still required. ## Model Used OpenAI `gpt-6.1-sol`, with reasoning, repository inspection, code editing, and tool execution. The host did not expose the model context-window limit. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All required normal CI gates are green; optional incomplete/neutral assessments are disclosed above - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
d12de135a7
commit
5b50c6b32c
7 files changed
+99
-24
No files matched your search
@@ -182,6 +182,7 @@ export interface CatalogTeamImportPreviewResult {
|
||||
}
|
||||
|
||||
export interface CatalogTeamInstallResult {
|
||||
resourceLinks: { kind: "agent" | "project" | "routine"; id: string; name: string; href: string }[];
|
||||
team: CatalogTeam;
|
||||
portabilityImport: CompanyPortabilityImportResult;
|
||||
skillPreparations: CatalogTeamSkillPreparation[];
|
||||
|
||||
@@ -1437,3 +1437,12 @@ it("documents explicit owner removals in the timeline response event enum", () =
|
||||
"created", "commented", "approved", "delegated", "assigned", "unassigned",
|
||||
]);
|
||||
});
|
||||
|
||||
it("documents canonical saved-resource links in catalog team install responses", () => {
|
||||
const operation = buildOpenApiSpec().paths["/api/companies/{companyId}/teams/catalog/{catalogId}/install"].post;
|
||||
expect(operation.description).toContain("resourceLinks");
|
||||
const schema = operation.responses[201].content["application/json"].schema;
|
||||
expect(schema.required).toContain("resourceLinks");
|
||||
expect(schema.properties.resourceLinks.items.properties.kind.enum).toEqual(["agent", "project", "routine"]);
|
||||
expect(schema.properties.resourceLinks.items.required).toEqual(["kind", "id", "name", "href"]);
|
||||
});
|
||||
@@ -81,13 +81,18 @@ describeEmbeddedPostgres("teams catalog install with no caller adapter overrides
|
||||
const companyId = await seedEmptyCompany();
|
||||
const svc = teamsCatalogService(db);
|
||||
|
||||
await svc.installCatalogTeam(companyId, "core-exec-team", {
|
||||
const installed = await svc.installCatalogTeam(companyId, "core-exec-team", {
|
||||
collisionStrategy: "rename",
|
||||
include: { projects: false, issues: false },
|
||||
});
|
||||
|
||||
const byName = await listAdapterTypesByName(companyId);
|
||||
expect(byName.size).toBe(3);
|
||||
const company = await db.query.companies.findFirst({ where: eq(companies.id, companyId) });
|
||||
const saved = await db.select({ id: agents.id, name: agents.name }).from(agents).where(eq(agents.companyId, companyId));
|
||||
expect([...installed.resourceLinks].sort((a, b) => a.id.localeCompare(b.id))).toEqual(saved.map(agent => ({ kind: "agent", id: agent.id, name: agent.name,
|
||||
href: `/${company!.issuePrefix}/agents/${agent.id}` })).sort((a, b) => a.id.localeCompare(b.id)));
|
||||
|
||||
|
||||
const adapterTypes = Array.from(byName.values()).map((row) => row.adapterType);
|
||||
expect(adapterTypes).toEqual(["paperclip_runner", "paperclip_runner", "paperclip_runner"]);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { parseFrontmatterMarkdown } from "@paperclipai/shared/frontmatter";
|
||||
import { executeRunnerApi } from "../services/native-runtime/runner-api-client.js";
|
||||
import type { CatalogTeam } from "@paperclipai/shared";
|
||||
|
||||
const companyPrefix = vi.hoisted(() => vi.fn());
|
||||
const testDb = { select: () => ({ from: () => ({ where: () => ({ limit: companyPrefix }) }) }) };
|
||||
|
||||
const mockAgentService = vi.hoisted(() => ({
|
||||
getById: vi.fn(),
|
||||
list: vi.fn(),
|
||||
@@ -64,6 +68,7 @@ function agentWithCatalogTeam(originHash: string | null, extra: Record<string, u
|
||||
describe("teamsCatalogService", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
companyPrefix.mockResolvedValue([{ issuePrefix: "ACME" }]);
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
id: "manager-1",
|
||||
companyId: "company-1",
|
||||
@@ -102,7 +107,7 @@ describe("teamsCatalogService", () => {
|
||||
});
|
||||
|
||||
it("builds an inline portability source with catalog skill keys and target-manager reparenting", async () => {
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
const prepared = await svc.prepareCatalogTeamSource("company-1", "core-exec-team", {
|
||||
targetManagerAgentId: "manager-1",
|
||||
@@ -119,7 +124,7 @@ describe("teamsCatalogService", () => {
|
||||
it.each(["core-exec-team", "product-engineering", "product-design", "content-machine"])(
|
||||
"preserves %s role files and canonical skill references through catalog import",
|
||||
async (teamRef) => {
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
const prepared = await svc.prepareCatalogTeamSource("company-1", teamRef);
|
||||
expect(prepared.errors).toEqual([]);
|
||||
for (const slug of prepared.team.agentSlugs) {
|
||||
@@ -153,7 +158,7 @@ describe("teamsCatalogService", () => {
|
||||
mockAgentService.list.mockResolvedValue([
|
||||
{ id: "manager-1", companyId: "company-1", name: "CEO" },
|
||||
]);
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
const prepared = await svc.prepareCatalogTeamSource("company-1", "core-exec-team", {
|
||||
targetManagerSlug: "ceo",
|
||||
@@ -165,7 +170,7 @@ describe("teamsCatalogService", () => {
|
||||
});
|
||||
|
||||
it("preserves package-declared Paperclip sidecar permissions while adding generated catalog provenance", async () => {
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
const prepared = await svc.prepareCatalogTeamSource("company-1", "product-engineering");
|
||||
|
||||
@@ -176,7 +181,7 @@ describe("teamsCatalogService", () => {
|
||||
});
|
||||
|
||||
it("preserves package sidecar permissions when generated target-manager metadata is merged onto the same root agent", async () => {
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
const prepared = await svc.prepareCatalogTeamSource("company-1", "product-engineering", {
|
||||
targetManagerAgentId: "manager-1",
|
||||
@@ -191,7 +196,7 @@ describe("teamsCatalogService", () => {
|
||||
|
||||
it("rejects missing target-manager slugs instead of emitting unresolved reparent metadata", async () => {
|
||||
mockAgentService.list.mockResolvedValue([]);
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
await expect(
|
||||
svc.prepareCatalogTeamSource("company-1", "core-exec-team", {
|
||||
@@ -201,7 +206,7 @@ describe("teamsCatalogService", () => {
|
||||
});
|
||||
|
||||
it("previews through company portability in agent-safe mode", async () => {
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
const preview = await svc.previewCatalogTeamImport("company-1", "content-machine");
|
||||
|
||||
@@ -223,7 +228,7 @@ describe("teamsCatalogService", () => {
|
||||
});
|
||||
|
||||
it("forces catalog previews to exclude company metadata even when requested", async () => {
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
await svc.previewCatalogTeamImport("company-1", "content-machine", {
|
||||
include: { company: true, agents: false },
|
||||
@@ -254,7 +259,7 @@ describe("teamsCatalogService", () => {
|
||||
warnings: [],
|
||||
errors: ["Safe import does not allow process adapter type."],
|
||||
});
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
await expect(svc.installCatalogTeam("company-1", "core-exec-team")).rejects.toMatchObject({ status: 422 });
|
||||
|
||||
@@ -264,7 +269,7 @@ describe("teamsCatalogService", () => {
|
||||
|
||||
it("does not install catalog skills when bundle import fails", async () => {
|
||||
mockCompanyPortabilityService.importBundle.mockRejectedValueOnce(new Error("import failed"));
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
await expect(svc.installCatalogTeam("company-1", "core-exec-team")).rejects.toThrow("import failed");
|
||||
|
||||
@@ -272,9 +277,39 @@ describe("teamsCatalogService", () => {
|
||||
expect(mockCompanySkillService.importFromSource).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects a missing company before importing resources", async () => {
|
||||
companyPrefix.mockResolvedValueOnce([]);
|
||||
await expect(teamsCatalogService(testDb as any).installCatalogTeam("company-1", "core-exec-team")).rejects.toMatchObject({ status: 404 });
|
||||
expect(mockCompanyPortabilityService.importBundle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns scoped saved-resource links while preserving warnings and skipped-resource semantics", async () => {
|
||||
mockCompanyPortabilityService.importBundle.mockResolvedValueOnce({
|
||||
company: { id: "company-1", name: "Paperclip", action: "unchanged" },
|
||||
agents: [{ id: "saved-agent", name: "CTO 2", slug: "cto", action: "created" }, { id: null, name: "Skipped", slug: "skipped", action: "skipped" }],
|
||||
projects: [{ id: "saved-project", name: "Engineering", slug: "engineering", action: "updated" }],
|
||||
routines: [{ id: "saved-routine", title: "Weekly sync", slug: "weekly", action: "created" }],
|
||||
envInputs: [], warnings: ["required skill unavailable"],
|
||||
});
|
||||
const result = await teamsCatalogService(testDb as any).installCatalogTeam("company-1", "core-exec-team");
|
||||
expect(result.resourceLinks).toEqual([
|
||||
{ kind: "agent", id: "saved-agent", name: "CTO 2", href: "/ACME/agents/saved-agent" },
|
||||
{ kind: "project", id: "saved-project", name: "Engineering", href: "/ACME/projects/saved-project" },
|
||||
{ kind: "routine", id: "saved-routine", name: "Weekly sync", href: "/ACME/routines/saved-routine" },
|
||||
]);
|
||||
const wire = await executeRunnerApi({ operationId: "POST /api/companies/{companyId}/teams/catalog/{catalogId}/install",
|
||||
pathParams: { catalogId: "core-exec-team" }, body: {} },
|
||||
{ companyId: "company-1", issueId: "issue-1", issueIdentifier: "ACME-1", runId: "run-1", workMode: "standard" },
|
||||
{ apiUrl: "http://fixture.invalid", token: "private-test-token", fetch: async () => Response.json(result, { status: 201 }),
|
||||
readFile: async () => { throw new Error("not used"); }, saveResponse: async () => { throw new Error("unexpected artifact"); } });
|
||||
expect(wire).toMatchObject({ ok: true, status: 201, data: { resourceLinks: result.resourceLinks, warnings: result.warnings } });
|
||||
expect(result.warnings).toContain("required skill unavailable");
|
||||
expect(result.portabilityImport.agents[1].id).toBeNull();
|
||||
});
|
||||
|
||||
it("surfaces post-import catalog skill install failures as warnings", async () => {
|
||||
mockCompanySkillService.installFromCatalog.mockRejectedValueOnce(new Error("catalog unavailable"));
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
const result = await svc.installCatalogTeam("company-1", "core-exec-team");
|
||||
|
||||
@@ -287,7 +322,7 @@ describe("teamsCatalogService", () => {
|
||||
});
|
||||
|
||||
it("injects safe claude_local adapter defaults for every bundled agent when no overrides are supplied", async () => {
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
await svc.installCatalogTeam("company-1", "core-exec-team");
|
||||
|
||||
@@ -303,7 +338,7 @@ describe("teamsCatalogService", () => {
|
||||
const previousDefault = process.env.PAPERCLIP_TEAMS_CATALOG_DEFAULT_ADAPTER_TYPE;
|
||||
process.env.PAPERCLIP_TEAMS_CATALOG_DEFAULT_ADAPTER_TYPE = "opencode_local";
|
||||
try {
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
await svc.installCatalogTeam("company-1", "core-exec-team");
|
||||
|
||||
@@ -323,7 +358,7 @@ describe("teamsCatalogService", () => {
|
||||
});
|
||||
|
||||
it("supplies safe adapter defaults for product-design and product-engineering installs", async () => {
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
await svc.installCatalogTeam("company-1", "product-design");
|
||||
const [designInput] = mockCompanyPortabilityService.importBundle.mock.calls.at(-1)!;
|
||||
@@ -341,7 +376,7 @@ describe("teamsCatalogService", () => {
|
||||
});
|
||||
|
||||
it("never sends a forbidden process adapter type from the default catalog path", async () => {
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
await svc.installCatalogTeam("company-1", "core-exec-team");
|
||||
|
||||
@@ -353,7 +388,7 @@ describe("teamsCatalogService", () => {
|
||||
});
|
||||
|
||||
it("preserves an explicit caller adapter override for the affected slug", async () => {
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
const callerOverrides = {
|
||||
cto: { adapterType: "opencode_local", adapterConfig: { model: "anthropic/claude-opus-4" } },
|
||||
@@ -375,7 +410,7 @@ describe("teamsCatalogService", () => {
|
||||
});
|
||||
|
||||
it("omits the default-adapter warning when every agent has an explicit override", async () => {
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
const result = await svc.installCatalogTeam("company-1", "core-exec-team", {
|
||||
adapterOverrides: {
|
||||
@@ -391,7 +426,7 @@ describe("teamsCatalogService", () => {
|
||||
});
|
||||
|
||||
it("passes install secretValues through to company portability import", async () => {
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
await svc.installCatalogTeam("company-1", "core-exec-team", {
|
||||
secretValues: { "agent:ceo:OPENAI_API_KEY": "sk-imported" },
|
||||
@@ -430,7 +465,7 @@ describe("teamsCatalogService", () => {
|
||||
agentWithCatalogTeam("sha256:stale-hash"),
|
||||
{ id: "no-provenance", companyId: "company-1", metadata: null },
|
||||
]);
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
const installed = await svc.listInstalledCatalogTeams("company-1");
|
||||
|
||||
@@ -449,7 +484,7 @@ describe("teamsCatalogService", () => {
|
||||
|
||||
it("marks a team up to date when the installed originHash matches the catalog hash", async () => {
|
||||
mockAgentService.list.mockResolvedValue([agentWithCatalogTeam(CORE_EXEC_TEAM_HASH)]);
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
const installed = await svc.listInstalledCatalogTeams("company-1");
|
||||
|
||||
@@ -465,7 +500,7 @@ describe("teamsCatalogService", () => {
|
||||
metadata: { paperclip: { catalogTeam: { catalogId: "paperclipai:bundled:gone:removed", originHash: "sha256:x" } } },
|
||||
},
|
||||
]);
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
const installed = await svc.listInstalledCatalogTeams("company-1");
|
||||
|
||||
@@ -476,7 +511,7 @@ describe("teamsCatalogService", () => {
|
||||
|
||||
it("returns an empty list when no agents carry catalog-team provenance", async () => {
|
||||
mockAgentService.list.mockResolvedValue([{ id: "a", companyId: "company-1", metadata: {} }]);
|
||||
const svc = teamsCatalogService({} as any);
|
||||
const svc = teamsCatalogService(testDb as any);
|
||||
|
||||
expect(await svc.listInstalledCatalogTeams("company-1")).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -3130,6 +3130,12 @@ for (const route of [
|
||||
path: route[1],
|
||||
tags: ["teams"],
|
||||
summary: route[2],
|
||||
...(route[1].endsWith("/install") ? {
|
||||
description: "Imports team resources; HTTP success does not prove every required skill is ready. Read warnings for post-import failures and skillPreparations for required sources; a preparation plan is not proof of installation. Report unresolved requirements as partial completion and preserve existing resources. Use the actual returned portabilityImport agents and projects for company-scoped resource links. Before repeating an uncertain install, inspect installed teams and saved resource identities rather than creating another copy. resourceLinks provides canonical company-scoped hrefs for actual saved agents, projects and routines, including reused resources; share these links with the installation outcome.",
|
||||
responses: { 201: r.ok(z.object({ resourceLinks: z.array(z.object({
|
||||
kind: z.enum(["agent", "project", "routine"]), id: z.string(), name: z.string(), href: z.string(),
|
||||
})) }).passthrough()), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
||||
} : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { companies, type Db } from "@paperclipai/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type {
|
||||
CatalogManifest,
|
||||
CatalogTeam,
|
||||
CatalogTeamInstallResult as SharedCatalogTeamInstallResult,
|
||||
CatalogTeamFileKind,
|
||||
CatalogTeamKind,
|
||||
CatalogTeamSkillRequirement,
|
||||
@@ -105,6 +107,7 @@ export interface CatalogTeamImportPreviewResult {
|
||||
}
|
||||
|
||||
export interface CatalogTeamInstallResult {
|
||||
resourceLinks: SharedCatalogTeamInstallResult["resourceLinks"];
|
||||
team: CatalogTeam;
|
||||
portabilityImport: CompanyPortabilityImportResult;
|
||||
skillPreparations: CatalogTeamSkillPreparation[];
|
||||
@@ -904,6 +907,8 @@ export function teamsCatalogService(db: Db) {
|
||||
catalogRef: string,
|
||||
options: CatalogTeamImportOptions = {},
|
||||
): Promise<CatalogTeamInstallResult> {
|
||||
const [company] = await db.select({ issuePrefix: companies.issuePrefix }).from(companies).where(eq(companies.id, companyId)).limit(1);
|
||||
if (!company) throw notFound("Company not found");
|
||||
const prepared = await prepareCatalogTeamSource(companyId, catalogRef, options);
|
||||
if (prepared.errors.length > 0) {
|
||||
throw unprocessable(`Catalog team source preparation failed: ${prepared.errors.join("; ")}`);
|
||||
@@ -957,6 +962,14 @@ export function teamsCatalogService(db: Db) {
|
||||
return {
|
||||
team: prepared.team,
|
||||
portabilityImport: result,
|
||||
resourceLinks: [
|
||||
...result.agents.flatMap(agent => agent.id ? [{ kind: "agent" as const, id: agent.id, name: agent.name,
|
||||
href: `/${encodeURIComponent(company.issuePrefix)}/agents/${encodeURIComponent(agent.id)}` }] : []),
|
||||
...result.projects.flatMap(project => project.id ? [{ kind: "project" as const, id: project.id, name: project.name,
|
||||
href: `/${encodeURIComponent(company.issuePrefix)}/projects/${encodeURIComponent(project.id)}` }] : []),
|
||||
...(result.routines ?? []).flatMap(routine => routine.id ? [{ kind: "routine" as const, id: routine.id, name: routine.title,
|
||||
href: `/${encodeURIComponent(company.issuePrefix)}/routines/${encodeURIComponent(routine.id)}` }] : []),
|
||||
],
|
||||
skillPreparations: prepared.skillPreparations,
|
||||
warnings: result.warnings,
|
||||
};
|
||||
|
||||
@@ -360,6 +360,12 @@ export const InstallSuccess: Story = {
|
||||
team={baseTeam}
|
||||
result={{
|
||||
team: baseTeam,
|
||||
resourceLinks: [
|
||||
{ kind: "agent", id: "a1", name: "CEO", href: "/STORY/agents/a1" },
|
||||
{ kind: "agent", id: "a2", name: "CTO", href: "/STORY/agents/a2" },
|
||||
{ kind: "agent", id: "a3", name: "CMO (from Core Exec Team)", href: "/STORY/agents/a3" },
|
||||
{ kind: "project", id: "p1", name: "Launch", href: "/STORY/projects/p1" },
|
||||
],
|
||||
portabilityImport: {
|
||||
company: { id: "company-storybook", name: "Paperclip", action: "unchanged" },
|
||||
agents: [
|
||||
|
||||
Reference in new issue
Block a user