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

@ -38,6 +38,21 @@ export interface MarkdownSettings {
codeBlockIndent?: string; // default: " "
}
/**
* Package source for npm/git packages.
* - String form: load all resources from the package
* - Object form: filter which resources to load
*/
export type PackageSource =
| string
| {
source: string;
extensions?: string[];
skills?: string[];
prompts?: string[];
themes?: string[];
};
export interface Settings {
lastChangelogVersion?: string;
defaultProvider?: string;
@ -54,10 +69,11 @@ export interface Settings {
quietStartup?: boolean;
shellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., "shopt -s expand_aliases" for alias support)
collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)
extensions?: string[]; // Array of extension file paths or directories
skills?: string[]; // Array of skill file paths or directories
prompts?: string[]; // Array of prompt template paths or directories
themes?: string[]; // Array of theme file paths or directories
packages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering)
extensions?: string[]; // Array of local extension file paths or directories
skills?: string[]; // Array of local skill file paths or directories
prompts?: string[]; // Array of local prompt template paths or directories
themes?: string[]; // Array of local theme file paths or directories
enableSkillCommands?: boolean; // default: true - register skills as /skill:name commands
terminal?: TerminalSettings;
images?: ImageSettings;
@ -156,6 +172,7 @@ export class SettingsManager {
delete settings.queueMode;
}
// Migrate old skills object format to new array format
if (
"skills" in settings &&
typeof settings.skills === "object" &&
@ -176,9 +193,39 @@ export class SettingsManager {
}
}
// Migrate npm:/git: sources from extensions array to packages array
if (Array.isArray(settings.extensions)) {
const localExtensions: string[] = [];
const packageSources: string[] = [];
for (const ext of settings.extensions) {
if (typeof ext !== "string") continue;
if (ext.startsWith("npm:") || ext.startsWith("git:") || SettingsManager.looksLikeGitUrl(ext)) {
packageSources.push(ext);
} else {
localExtensions.push(ext);
}
}
if (packageSources.length > 0) {
const existingPackages = Array.isArray(settings.packages) ? settings.packages : [];
settings.packages = [...existingPackages, ...packageSources];
settings.extensions = localExtensions.length > 0 ? localExtensions : undefined;
if (settings.extensions === undefined) {
delete settings.extensions;
}
}
}
return settings as Settings;
}
private static looksLikeGitUrl(source: string): boolean {
const gitHosts = ["github.com", "gitlab.com", "bitbucket.org", "codeberg.org"];
const normalized = source.replace(/^https?:\/\//, "");
return gitHosts.some((host) => normalized.startsWith(`${host}/`));
}
private loadProjectSettings(): Settings {
if (!this.projectSettingsPath || !existsSync(this.projectSettingsPath)) {
return {};
@ -416,6 +463,22 @@ export class SettingsManager {
this.save();
}
getPackages(): PackageSource[] {
return [...(this.settings.packages ?? [])];
}
setPackages(packages: PackageSource[]): void {
this.globalSettings.packages = packages;
this.save();
}
setProjectPackages(packages: PackageSource[]): void {
const projectSettings = this.loadProjectSettings();
projectSettings.packages = packages;
this.saveProjectSettings(projectSettings);
this.settings = deepMergeSettings(this.globalSettings, projectSettings);
}
getExtensionPaths(): string[] {
return [...(this.settings.extensions ?? [])];
}