Skip to content

Commit cf882bf

Browse files
committed
release: export buildUnifiedItems function and enhance package extension filtering logic
1 parent 6ea5108 commit cf882bf

2 files changed

Lines changed: 174 additions & 2 deletions

File tree

src/ui/unified.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ async function showInteractiveOnce(
269269
return await handleUnifiedAction(result, items, staged, byId, ctx, pi);
270270
}
271271

272-
function buildUnifiedItems(
272+
export function buildUnifiedItems(
273273
localEntries: Awaited<ReturnType<typeof discoverExtensions>>,
274274
installedPackages: InstalledPackage[],
275275
packageExtensions: PackageExtensionEntry[],
@@ -278,6 +278,23 @@ function buildUnifiedItems(
278278
const items: UnifiedItem[] = [];
279279
const localPaths = new Set<string>();
280280

281+
const packageExtensionGroups = new Map<string, PackageExtensionEntry[]>();
282+
for (const entry of packageExtensions) {
283+
const key = `${entry.packageScope}:${entry.packageSource.toLowerCase()}`;
284+
const group = packageExtensionGroups.get(key) ?? [];
285+
group.push(entry);
286+
packageExtensionGroups.set(key, group);
287+
}
288+
289+
const visiblePackageExtensions = packageExtensions.filter((entry) => {
290+
const key = `${entry.packageScope}:${entry.packageSource.toLowerCase()}`;
291+
const group = packageExtensionGroups.get(key) ?? [];
292+
293+
// Avoid duplicate-looking rows for packages that expose a single enabled extension entrypoint.
294+
// Keep extension rows when there are multiple entrypoints, or when an entry is disabled so it can be re-enabled.
295+
return group.length > 1 || entry.state === "disabled";
296+
});
297+
281298
// Add local extensions
282299
for (const entry of localEntries) {
283300
localPaths.add(entry.activePath?.toLowerCase() ?? "");
@@ -294,7 +311,7 @@ function buildUnifiedItems(
294311
});
295312
}
296313

297-
for (const entry of packageExtensions) {
314+
for (const entry of visiblePackageExtensions) {
298315
items.push({
299316
type: "package-extension",
300317
id: entry.id,

test/unified-items.test.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
4+
import { tmpdir } from "node:os";
5+
import { join } from "node:path";
6+
import { parseInstalledPackagesOutput } from "../src/packages/discovery.js";
7+
import { discoverPackageExtensions } from "../src/packages/extensions.js";
8+
import type { InstalledPackage, PackageExtensionEntry } from "../src/types/index.js";
9+
import { buildUnifiedItems } from "../src/ui/unified.js";
10+
11+
function createPackage(source: string, name: string): InstalledPackage {
12+
return {
13+
source,
14+
name,
15+
scope: "global",
16+
};
17+
}
18+
19+
function createPackageExtension(
20+
id: string,
21+
packageSource: string,
22+
extensionPath: string,
23+
state: "enabled" | "disabled" = "enabled"
24+
): PackageExtensionEntry {
25+
return {
26+
id,
27+
packageSource,
28+
packageName: "pi-extmgr",
29+
packageScope: "global",
30+
extensionPath,
31+
absolutePath: `/tmp/${extensionPath}`,
32+
displayName: `pi-extmgr/${extensionPath}`,
33+
summary: "package extension",
34+
state,
35+
};
36+
}
37+
38+
void test("buildUnifiedItems hides single enabled package-extension row to avoid duplicate-looking entries", () => {
39+
const installedPackages = [createPackage("npm:pi-extmgr", "pi-extmgr")];
40+
const packageExtensions = [
41+
createPackageExtension(
42+
"pkg-ext:global:npm:pi-extmgr:src/index.ts",
43+
"npm:pi-extmgr",
44+
"src/index.ts"
45+
),
46+
];
47+
48+
const items = buildUnifiedItems([], installedPackages, packageExtensions, new Set());
49+
50+
assert.equal(items.length, 1);
51+
assert.equal(items[0]?.type, "package");
52+
assert.equal(items[0]?.source, "npm:pi-extmgr");
53+
});
54+
55+
void test("buildUnifiedItems keeps disabled package-extension row visible for re-enable", () => {
56+
const installedPackages = [createPackage("npm:pi-extmgr", "pi-extmgr")];
57+
const packageExtensions = [
58+
createPackageExtension(
59+
"pkg-ext:global:npm:pi-extmgr:src/index.ts",
60+
"npm:pi-extmgr",
61+
"src/index.ts",
62+
"disabled"
63+
),
64+
];
65+
66+
const items = buildUnifiedItems([], installedPackages, packageExtensions, new Set());
67+
const types = items.map((item) => item.type);
68+
69+
assert.deepEqual(types, ["package", "package-extension"]);
70+
});
71+
72+
void test("buildUnifiedItems keeps multiple package-extension rows visible", () => {
73+
const installedPackages = [createPackage("npm:multi-ext", "multi-ext")];
74+
const packageExtensions = [
75+
{
76+
...createPackageExtension(
77+
"pkg-ext:global:npm:multi-ext:extensions/a.ts",
78+
"npm:multi-ext",
79+
"extensions/a.ts"
80+
),
81+
packageName: "multi-ext",
82+
displayName: "multi-ext/extensions/a.ts",
83+
},
84+
{
85+
...createPackageExtension(
86+
"pkg-ext:global:npm:multi-ext:extensions/b.ts",
87+
"npm:multi-ext",
88+
"extensions/b.ts"
89+
),
90+
packageName: "multi-ext",
91+
displayName: "multi-ext/extensions/b.ts",
92+
},
93+
];
94+
95+
const items = buildUnifiedItems([], installedPackages, packageExtensions, new Set());
96+
97+
assert.equal(items.length, 3);
98+
assert.equal(items[0]?.type, "package");
99+
assert.equal(items[1]?.type, "package-extension");
100+
assert.equal(items[2]?.type, "package-extension");
101+
});
102+
103+
void test("integration: pi list fixture with single-entry npm packages does not render duplicate extension rows", async () => {
104+
const cwd = await mkdtemp(join(tmpdir(), "pi-extmgr-unified-"));
105+
106+
try {
107+
const extmgrRoot = join(cwd, "fixtures", "pi-extmgr");
108+
const shittyPromptRoot = join(cwd, "fixtures", "shitty-prompt");
109+
110+
await mkdir(join(extmgrRoot, "src"), { recursive: true });
111+
await mkdir(join(shittyPromptRoot, "extensions"), { recursive: true });
112+
113+
await writeFile(
114+
join(extmgrRoot, "package.json"),
115+
JSON.stringify({ name: "pi-extmgr", pi: { extensions: ["./src/index.ts"] } }, null, 2),
116+
"utf8"
117+
);
118+
await writeFile(join(extmgrRoot, "src", "index.ts"), "// extmgr\n", "utf8");
119+
120+
await writeFile(
121+
join(shittyPromptRoot, "package.json"),
122+
JSON.stringify(
123+
{ name: "shitty-prompt", pi: { extensions: ["./extensions/index.ts"] } },
124+
null,
125+
2
126+
),
127+
"utf8"
128+
);
129+
await writeFile(join(shittyPromptRoot, "extensions", "index.ts"), "// prompt\n", "utf8");
130+
131+
const listOutput = [
132+
"User packages:",
133+
` npm:pi-extmgr@0.1.12`,
134+
` ${extmgrRoot}`,
135+
` npm:shitty-prompt@0.0.1`,
136+
` ${shittyPromptRoot}`,
137+
"",
138+
].join("\n");
139+
140+
const installed = parseInstalledPackagesOutput(listOutput);
141+
const packageExtensions = await discoverPackageExtensions(installed, cwd);
142+
const items = buildUnifiedItems([], installed, packageExtensions, new Set());
143+
144+
assert.equal(installed.length, 2);
145+
assert.equal(packageExtensions.length, 2);
146+
assert.equal(items.filter((item) => item.type === "package").length, 2);
147+
assert.equal(items.filter((item) => item.type === "package-extension").length, 0);
148+
assert.deepEqual(
149+
items.filter((item) => item.type === "package").map((item) => item.displayName),
150+
["pi-extmgr", "shitty-prompt"]
151+
);
152+
} finally {
153+
await rm(cwd, { recursive: true, force: true });
154+
}
155+
});

0 commit comments

Comments
 (0)