feat(coding-agent): add packages array with filtering support

- Add PackageSource type for npm/git sources with optional filtering
- Migrate npm:/git: sources from extensions to packages array
- Add getPackages(), setPackages(), setProjectPackages() methods
- Update package-manager to resolve from packages array
- Support selective loading: extensions, skills, prompts, themes per package
- Update pi list to show packages
- Add migration tests for settings

closes #645
This commit is contained in:
Mario Zechner 2026-01-23 19:51:23 +01:00
parent dd838d0fe0
commit ef1fc3103e
8 changed files with 434 additions and 63 deletions

View file

@ -23,7 +23,7 @@ import { DefaultPackageManager } from "./core/package-manager.js";
import { DefaultResourceLoader } from "./core/resource-loader.js";
import { type CreateAgentSessionOptions, createAgentSession } from "./core/sdk.js";
import { SessionManager } from "./core/session-manager.js";
import { SettingsManager } from "./core/settings-manager.js";
import { type PackageSource, SettingsManager } from "./core/settings-manager.js";
import { printTimings, time } from "./core/timings.js";
import { allTools } from "./core/tools/index.js";
import { runMigrations, showDeprecationWarnings } from "./migrations.js";
@ -104,27 +104,36 @@ function sourcesMatch(a: string, b: string): boolean {
return left.type === right.type && left.key === right.key;
}
function updateExtensionSources(
function getPackageSourceString(pkg: PackageSource): string {
return typeof pkg === "string" ? pkg : pkg.source;
}
function packageSourcesMatch(a: PackageSource, b: string): boolean {
const aSource = getPackageSourceString(a);
return sourcesMatch(aSource, b);
}
function updatePackageSources(
settingsManager: SettingsManager,
source: string,
local: boolean,
action: "add" | "remove",
): void {
const currentSettings = local ? settingsManager.getProjectSettings() : settingsManager.getGlobalSettings();
const currentSources = currentSettings.extensions ?? [];
const currentPackages = currentSettings.packages ?? [];
let nextSources: string[];
let nextPackages: PackageSource[];
if (action === "add") {
const exists = currentSources.some((existing) => sourcesMatch(existing, source));
nextSources = exists ? currentSources : [...currentSources, source];
const exists = currentPackages.some((existing) => packageSourcesMatch(existing, source));
nextPackages = exists ? currentPackages : [...currentPackages, source];
} else {
nextSources = currentSources.filter((existing) => !sourcesMatch(existing, source));
nextPackages = currentPackages.filter((existing) => !packageSourcesMatch(existing, source));
}
if (local) {
settingsManager.setProjectExtensionPaths(nextSources);
settingsManager.setProjectPackages(nextPackages);
} else {
settingsManager.setExtensionPaths(nextSources);
settingsManager.setPackages(nextPackages);
}
}
@ -154,7 +163,7 @@ async function handlePackageCommand(args: string[]): Promise<boolean> {
process.exit(1);
}
await packageManager.install(options.source, { local: options.local });
updateExtensionSources(settingsManager, options.source, options.local, "add");
updatePackageSources(settingsManager, options.source, options.local, "add");
console.log(chalk.green(`Installed ${options.source}`));
return true;
}
@ -165,7 +174,7 @@ async function handlePackageCommand(args: string[]): Promise<boolean> {
process.exit(1);
}
await packageManager.remove(options.source, { local: options.local });
updateExtensionSources(settingsManager, options.source, options.local, "remove");
updatePackageSources(settingsManager, options.source, options.local, "remove");
console.log(chalk.green(`Removed ${options.source}`));
return true;
}
@ -173,26 +182,28 @@ async function handlePackageCommand(args: string[]): Promise<boolean> {
if (options.command === "list") {
const globalSettings = settingsManager.getGlobalSettings();
const projectSettings = settingsManager.getProjectSettings();
const globalExtensions = globalSettings.extensions ?? [];
const projectExtensions = projectSettings.extensions ?? [];
const globalPackages = globalSettings.packages ?? [];
const projectPackages = projectSettings.packages ?? [];
if (globalExtensions.length === 0 && projectExtensions.length === 0) {
console.log(chalk.dim("No extensions installed."));
if (globalPackages.length === 0 && projectPackages.length === 0) {
console.log(chalk.dim("No packages installed."));
return true;
}
if (globalExtensions.length > 0) {
console.log(chalk.bold("Global extensions:"));
for (const ext of globalExtensions) {
console.log(` ${ext}`);
if (globalPackages.length > 0) {
console.log(chalk.bold("Global packages:"));
for (const pkg of globalPackages) {
const display = typeof pkg === "string" ? pkg : `${pkg.source} (filtered)`;
console.log(` ${display}`);
}
}
if (projectExtensions.length > 0) {
if (globalExtensions.length > 0) console.log();
console.log(chalk.bold("Project extensions:"));
for (const ext of projectExtensions) {
console.log(` ${ext}`);
if (projectPackages.length > 0) {
if (globalPackages.length > 0) console.log();
console.log(chalk.bold("Project packages:"));
for (const pkg of projectPackages) {
const display = typeof pkg === "string" ? pkg : `${pkg.source} (filtered)`;
console.log(` ${display}`);
}
}
@ -203,7 +214,7 @@ async function handlePackageCommand(args: string[]): Promise<boolean> {
if (options.source) {
console.log(chalk.green(`Updated ${options.source}`));
} else {
console.log(chalk.green("Updated extensions"));
console.log(chalk.green("Updated packages"));
}
return true;
}