mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 09:01:14 +00:00
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:
parent
dd838d0fe0
commit
ef1fc3103e
8 changed files with 434 additions and 63 deletions
|
|
@ -5,6 +5,7 @@
|
|||
### Breaking Changes
|
||||
|
||||
- Header values in `models.json` now resolve environment variables (if a header value matches an env var name, the env var value is used). This may change behavior if a literal header value accidentally matches an env var name. ([#909](https://github.com/badlogic/pi-mono/issues/909))
|
||||
- External packages (npm/git) are now configured via `packages` array in settings.json instead of `extensions`. Existing npm:/git: entries in `extensions` are auto-migrated. ([#645](https://github.com/badlogic/pi-mono/issues/645))
|
||||
- Resource loading now uses `ResourceLoader` only and settings.json uses arrays for extensions, skills, prompts, and themes ([#645](https://github.com/badlogic/pi-mono/issues/645))
|
||||
- Removed `discoverAuthStorage` and `discoverModels` from the SDK. `AuthStorage` and `ModelRegistry` now default to `~/.pi/agent` paths unless you pass an `agentDir` ([#645](https://github.com/badlogic/pi-mono/issues/645))
|
||||
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
- Header values in `models.json` now support environment variables and shell commands, matching `apiKey` resolution ([#909](https://github.com/badlogic/pi-mono/issues/909))
|
||||
- `markdown.codeBlockIndent` setting to customize code block indentation in rendered output
|
||||
- Extension package management with `pi install`, `pi remove`, `pi update`, and `pi list` commands ([#645](https://github.com/badlogic/pi-mono/issues/645))
|
||||
- Package filtering: selectively load resources from packages using object form in `packages` array ([#645](https://github.com/badlogic/pi-mono/issues/645))
|
||||
- `/reload` command to reload extensions, skills, prompts, and themes ([#645](https://github.com/badlogic/pi-mono/issues/645))
|
||||
- CLI flags for `--skill`, `--prompt-template`, `--theme`, `--no-prompt-templates`, and `--no-themes` ([#645](https://github.com/badlogic/pi-mono/issues/645))
|
||||
- Show provider alongside the model in the footer if multiple providers are available
|
||||
|
|
|
|||
|
|
@ -846,7 +846,11 @@ Global `~/.pi/agent/settings.json` stores persistent preferences:
|
|||
| `doubleEscapeAction` | Action for double-escape with empty editor: `tree` or `fork` | `tree` |
|
||||
| `editorPaddingX` | Horizontal padding for input editor (0-3) | `0` |
|
||||
| `markdown.codeBlockIndent` | Prefix for each rendered code block line | `" "` |
|
||||
| `extensions` | Extension sources or file paths (npm:, git:, local) | `[]` |
|
||||
| `packages` | External package sources (npm:, git:) with optional filtering | `[]` |
|
||||
| `extensions` | Local extension file paths or directories | `[]` |
|
||||
| `skills` | Local skill file paths or directories | `[]` |
|
||||
| `prompts` | Local prompt template file paths or directories | `[]` |
|
||||
| `themes` | Local theme file paths or directories | `[]` |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -983,19 +987,43 @@ Extensions are TypeScript modules that extend pi's behavior.
|
|||
**Locations:**
|
||||
- Global: `~/.pi/agent/extensions/*.ts` or `~/.pi/agent/extensions/*/index.ts`
|
||||
- Project: `.pi/extensions/*.ts` or `.pi/extensions/*/index.ts`
|
||||
- Settings: `extensions` array supports file paths and `npm:` or `git:` sources
|
||||
- Settings: `extensions` array for local paths, `packages` array for npm/git sources
|
||||
- CLI: `--extension <path>` or `-e <path>` (temporary for this run)
|
||||
|
||||
Install and remove extension sources with the CLI:
|
||||
**Install packages:**
|
||||
|
||||
```bash
|
||||
pi install npm:@foo/bar@1.0.0
|
||||
pi install git:github.com/user/repo@v1
|
||||
pi install https://github.com/user/repo # raw URLs work too
|
||||
pi remove npm:@foo/bar
|
||||
pi list # show installed packages
|
||||
pi update # update all non-pinned packages
|
||||
```
|
||||
|
||||
Use `-l` to install into project settings (`.pi/settings.json`).
|
||||
|
||||
**Package filtering:** By default, packages load all resources (extensions, skills, prompts, themes). To selectively load only certain resources, use the object form in settings.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"packages": [
|
||||
"npm:simple-pkg",
|
||||
{
|
||||
"source": "npm:shitty-extensions",
|
||||
"extensions": ["extensions/oracle.ts", "extensions/memory-mode.ts"],
|
||||
"skills": ["skills/a-nach-b"],
|
||||
"themes": [],
|
||||
"prompts": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- Omit a key to load all of that type
|
||||
- Use empty array `[]` to load none of that type
|
||||
- Paths are relative to package root
|
||||
|
||||
**Dependencies:** Extensions can have their own dependencies. Place a `package.json` next to the extension (or in a parent directory), run `npm install`, and imports are resolved via [jiti](https://github.com/unjs/jiti). See [examples/extensions/with-deps/](examples/extensions/with-deps/).
|
||||
|
||||
#### Custom Tools
|
||||
|
|
|
|||
|
|
@ -115,22 +115,49 @@ Additional paths via `settings.json`:
|
|||
|
||||
```json
|
||||
{
|
||||
"extensions": [
|
||||
"packages": [
|
||||
"npm:@foo/bar@1.0.0",
|
||||
"git:github.com/user/repo@v1",
|
||||
"/path/to/extension.ts",
|
||||
"/path/to/extension/dir"
|
||||
"git:github.com/user/repo@v1"
|
||||
],
|
||||
"extensions": [
|
||||
"/path/to/local/extension.ts",
|
||||
"/path/to/local/extension/dir"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Use `pi install` and `pi remove` to manage extension sources in settings:
|
||||
Use `pi install`, `pi remove`, `pi list`, and `pi update` to manage packages:
|
||||
|
||||
```bash
|
||||
pi install npm:@foo/bar@1.0.0
|
||||
pi install git:github.com/user/repo@v1
|
||||
pi install https://github.com/user/repo # raw URLs work too
|
||||
pi remove npm:@foo/bar
|
||||
pi list # show installed packages
|
||||
pi update # update all non-pinned packages
|
||||
```
|
||||
|
||||
**Package filtering:** By default, packages load all resources (extensions, skills, prompts, themes). To selectively load only certain resources:
|
||||
|
||||
```json
|
||||
{
|
||||
"packages": [
|
||||
"npm:simple-pkg",
|
||||
{
|
||||
"source": "npm:shitty-extensions",
|
||||
"extensions": ["extensions/oracle.ts", "extensions/memory-mode.ts"],
|
||||
"skills": ["skills/a-nach-b"],
|
||||
"themes": [],
|
||||
"prompts": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- Omit a key to load all of that type from the package
|
||||
- Use empty array `[]` to load none of that type
|
||||
- Paths are relative to package root
|
||||
|
||||
**Discovery rules:**
|
||||
|
||||
1. **Direct files:** `extensions/*.ts` or `*.js` → loaded directly
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync }
|
|||
import { homedir, tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { CONFIG_DIR_NAME } from "../config.js";
|
||||
import type { SettingsManager } from "./settings-manager.js";
|
||||
import type { PackageSource, SettingsManager } from "./settings-manager.js";
|
||||
|
||||
export interface ResolvedPaths {
|
||||
extensions: string[];
|
||||
|
|
@ -81,6 +81,13 @@ interface ResourceAccumulator {
|
|||
themes: Set<string>;
|
||||
}
|
||||
|
||||
interface PackageFilter {
|
||||
extensions?: string[];
|
||||
skills?: string[];
|
||||
prompts?: string[];
|
||||
themes?: string[];
|
||||
}
|
||||
|
||||
export class DefaultPackageManager implements PackageManager {
|
||||
private cwd: string;
|
||||
private agentDir: string;
|
||||
|
|
@ -108,46 +115,66 @@ export class DefaultPackageManager implements PackageManager {
|
|||
const globalSettings = this.settingsManager.getGlobalSettings();
|
||||
const projectSettings = this.settingsManager.getProjectSettings();
|
||||
|
||||
const extensionSources: Array<{ source: string; scope: SourceScope }> = [];
|
||||
for (const source of globalSettings.extensions ?? []) {
|
||||
extensionSources.push({ source, scope: "global" });
|
||||
// Resolve packages (npm/git sources)
|
||||
const packageSources: Array<{ pkg: PackageSource; scope: SourceScope }> = [];
|
||||
for (const pkg of globalSettings.packages ?? []) {
|
||||
packageSources.push({ pkg, scope: "global" });
|
||||
}
|
||||
for (const source of projectSettings.extensions ?? []) {
|
||||
extensionSources.push({ source, scope: "project" });
|
||||
for (const pkg of projectSettings.packages ?? []) {
|
||||
packageSources.push({ pkg, scope: "project" });
|
||||
}
|
||||
await this.resolvePackageSources(packageSources, accumulator, onMissing);
|
||||
|
||||
// Resolve local extensions
|
||||
for (const ext of globalSettings.extensions ?? []) {
|
||||
this.resolveLocalPath(ext, accumulator.extensions);
|
||||
}
|
||||
for (const ext of projectSettings.extensions ?? []) {
|
||||
this.resolveLocalPath(ext, accumulator.extensions);
|
||||
}
|
||||
|
||||
await this.resolveExtensionSourcesInternal(extensionSources, accumulator, onMissing);
|
||||
|
||||
// Resolve local skills
|
||||
for (const skill of globalSettings.skills ?? []) {
|
||||
this.addPath(accumulator.skills, this.resolvePath(skill));
|
||||
}
|
||||
for (const skill of projectSettings.skills ?? []) {
|
||||
this.addPath(accumulator.skills, this.resolvePath(skill));
|
||||
}
|
||||
for (const skill of globalSettings.skills ?? []) {
|
||||
this.addPath(accumulator.skills, this.resolvePath(skill));
|
||||
|
||||
// Resolve local prompts
|
||||
for (const prompt of globalSettings.prompts ?? []) {
|
||||
this.addPath(accumulator.prompts, this.resolvePath(prompt));
|
||||
}
|
||||
for (const prompt of projectSettings.prompts ?? []) {
|
||||
this.addPath(accumulator.prompts, this.resolvePath(prompt));
|
||||
}
|
||||
for (const prompt of globalSettings.prompts ?? []) {
|
||||
this.addPath(accumulator.prompts, this.resolvePath(prompt));
|
||||
|
||||
// Resolve local themes
|
||||
for (const theme of globalSettings.themes ?? []) {
|
||||
this.addPath(accumulator.themes, this.resolvePath(theme));
|
||||
}
|
||||
for (const theme of projectSettings.themes ?? []) {
|
||||
this.addPath(accumulator.themes, this.resolvePath(theme));
|
||||
}
|
||||
for (const theme of globalSettings.themes ?? []) {
|
||||
this.addPath(accumulator.themes, this.resolvePath(theme));
|
||||
}
|
||||
|
||||
return this.toResolvedPaths(accumulator);
|
||||
}
|
||||
|
||||
private resolveLocalPath(path: string, target: Set<string>): void {
|
||||
const resolved = this.resolvePath(path);
|
||||
if (existsSync(resolved)) {
|
||||
this.addPath(target, resolved);
|
||||
}
|
||||
}
|
||||
|
||||
async resolveExtensionSources(
|
||||
sources: string[],
|
||||
options?: { local?: boolean; temporary?: boolean },
|
||||
): Promise<ResolvedPaths> {
|
||||
const accumulator = this.createAccumulator();
|
||||
const scope: SourceScope = options?.temporary ? "temporary" : options?.local ? "project" : "global";
|
||||
const extensionSources = sources.map((source) => ({ source, scope }));
|
||||
await this.resolveExtensionSourcesInternal(extensionSources, accumulator);
|
||||
const packageSources = sources.map((source) => ({ pkg: source as PackageSource, scope }));
|
||||
await this.resolvePackageSources(packageSources, accumulator);
|
||||
return this.toResolvedPaths(accumulator);
|
||||
}
|
||||
|
||||
|
|
@ -244,13 +271,16 @@ export class DefaultPackageManager implements PackageManager {
|
|||
}
|
||||
}
|
||||
|
||||
private async resolveExtensionSourcesInternal(
|
||||
sources: Array<{ source: string; scope: SourceScope }>,
|
||||
private async resolvePackageSources(
|
||||
sources: Array<{ pkg: PackageSource; scope: SourceScope }>,
|
||||
accumulator: ResourceAccumulator,
|
||||
onMissing?: (source: string) => Promise<MissingSourceAction>,
|
||||
): Promise<void> {
|
||||
for (const { source, scope } of sources) {
|
||||
const parsed = this.parseSource(source);
|
||||
for (const { pkg, scope } of sources) {
|
||||
const sourceStr = typeof pkg === "string" ? pkg : pkg.source;
|
||||
const filter = typeof pkg === "object" ? pkg : undefined;
|
||||
const parsed = this.parseSource(sourceStr);
|
||||
|
||||
if (parsed.type === "local") {
|
||||
this.resolveLocalExtensionSource(parsed, accumulator);
|
||||
continue;
|
||||
|
|
@ -261,9 +291,9 @@ export class DefaultPackageManager implements PackageManager {
|
|||
await this.installParsedSource(parsed, scope);
|
||||
return true;
|
||||
}
|
||||
const action = await onMissing(source);
|
||||
const action = await onMissing(sourceStr);
|
||||
if (action === "skip") return false;
|
||||
if (action === "error") throw new Error(`Missing source: ${source}`);
|
||||
if (action === "error") throw new Error(`Missing source: ${sourceStr}`);
|
||||
await this.installParsedSource(parsed, scope);
|
||||
return true;
|
||||
};
|
||||
|
|
@ -274,7 +304,7 @@ export class DefaultPackageManager implements PackageManager {
|
|||
const installed = await installMissing();
|
||||
if (!installed) continue;
|
||||
}
|
||||
this.collectPackageResources(installedPath, accumulator);
|
||||
this.collectPackageResources(installedPath, accumulator, filter);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -284,7 +314,7 @@ export class DefaultPackageManager implements PackageManager {
|
|||
const installed = await installMissing();
|
||||
if (!installed) continue;
|
||||
}
|
||||
this.collectPackageResources(installedPath, accumulator);
|
||||
this.collectPackageResources(installedPath, accumulator, filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -517,7 +547,62 @@ export class DefaultPackageManager implements PackageManager {
|
|||
return resolve(this.cwd, trimmed);
|
||||
}
|
||||
|
||||
private collectPackageResources(packageRoot: string, accumulator: ResourceAccumulator): boolean {
|
||||
private collectPackageResources(
|
||||
packageRoot: string,
|
||||
accumulator: ResourceAccumulator,
|
||||
filter?: PackageFilter,
|
||||
): boolean {
|
||||
// If filter is provided, use it to selectively load resources
|
||||
if (filter) {
|
||||
// Empty array means "load none", undefined means "load all"
|
||||
if (filter.extensions !== undefined) {
|
||||
for (const entry of filter.extensions) {
|
||||
const resolved = resolve(packageRoot, entry);
|
||||
if (existsSync(resolved)) {
|
||||
this.addPath(accumulator.extensions, resolved);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.collectDefaultExtensions(packageRoot, accumulator);
|
||||
}
|
||||
|
||||
if (filter.skills !== undefined) {
|
||||
for (const entry of filter.skills) {
|
||||
const resolved = resolve(packageRoot, entry);
|
||||
if (existsSync(resolved)) {
|
||||
this.addPath(accumulator.skills, resolved);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.collectDefaultSkills(packageRoot, accumulator);
|
||||
}
|
||||
|
||||
if (filter.prompts !== undefined) {
|
||||
for (const entry of filter.prompts) {
|
||||
const resolved = resolve(packageRoot, entry);
|
||||
if (existsSync(resolved)) {
|
||||
this.addPath(accumulator.prompts, resolved);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.collectDefaultPrompts(packageRoot, accumulator);
|
||||
}
|
||||
|
||||
if (filter.themes !== undefined) {
|
||||
for (const entry of filter.themes) {
|
||||
const resolved = resolve(packageRoot, entry);
|
||||
if (existsSync(resolved)) {
|
||||
this.addPath(accumulator.themes, resolved);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.collectDefaultThemes(packageRoot, accumulator);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// No filter: load everything based on manifest or directory structure
|
||||
const manifest = this.readPiManifest(packageRoot);
|
||||
if (manifest) {
|
||||
this.addManifestEntries(manifest.extensions, packageRoot, accumulator.extensions);
|
||||
|
|
@ -553,6 +638,54 @@ export class DefaultPackageManager implements PackageManager {
|
|||
return true;
|
||||
}
|
||||
|
||||
private collectDefaultExtensions(packageRoot: string, accumulator: ResourceAccumulator): void {
|
||||
const manifest = this.readPiManifest(packageRoot);
|
||||
if (manifest?.extensions) {
|
||||
this.addManifestEntries(manifest.extensions, packageRoot, accumulator.extensions);
|
||||
return;
|
||||
}
|
||||
const extensionsDir = join(packageRoot, "extensions");
|
||||
if (existsSync(extensionsDir)) {
|
||||
this.addPath(accumulator.extensions, extensionsDir);
|
||||
}
|
||||
}
|
||||
|
||||
private collectDefaultSkills(packageRoot: string, accumulator: ResourceAccumulator): void {
|
||||
const manifest = this.readPiManifest(packageRoot);
|
||||
if (manifest?.skills) {
|
||||
this.addManifestEntries(manifest.skills, packageRoot, accumulator.skills);
|
||||
return;
|
||||
}
|
||||
const skillsDir = join(packageRoot, "skills");
|
||||
if (existsSync(skillsDir)) {
|
||||
this.addPath(accumulator.skills, skillsDir);
|
||||
}
|
||||
}
|
||||
|
||||
private collectDefaultPrompts(packageRoot: string, accumulator: ResourceAccumulator): void {
|
||||
const manifest = this.readPiManifest(packageRoot);
|
||||
if (manifest?.prompts) {
|
||||
this.addManifestEntries(manifest.prompts, packageRoot, accumulator.prompts);
|
||||
return;
|
||||
}
|
||||
const promptsDir = join(packageRoot, "prompts");
|
||||
if (existsSync(promptsDir)) {
|
||||
this.addPath(accumulator.prompts, promptsDir);
|
||||
}
|
||||
}
|
||||
|
||||
private collectDefaultThemes(packageRoot: string, accumulator: ResourceAccumulator): void {
|
||||
const manifest = this.readPiManifest(packageRoot);
|
||||
if (manifest?.themes) {
|
||||
this.addManifestEntries(manifest.themes, packageRoot, accumulator.themes);
|
||||
return;
|
||||
}
|
||||
const themesDir = join(packageRoot, "themes");
|
||||
if (existsSync(themesDir)) {
|
||||
this.addPath(accumulator.themes, themesDir);
|
||||
}
|
||||
}
|
||||
|
||||
private readPiManifest(packageRoot: string): PiManifest | null {
|
||||
const packageJsonPath = join(packageRoot, "package.json");
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
|
|
|
|||
|
|
@ -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 ?? [])];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ export {
|
|||
export {
|
||||
type CompactionSettings,
|
||||
type ImageSettings,
|
||||
type PackageSource,
|
||||
type RetrySettings,
|
||||
SettingsManager,
|
||||
} from "./core/settings-manager.js";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,6 +106,112 @@ describe("SettingsManager", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("packages migration", () => {
|
||||
it("should migrate npm: sources from extensions to packages", () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify({
|
||||
extensions: ["npm:pi-doom", "/local/ext.ts", "npm:shitty-extensions"],
|
||||
}),
|
||||
);
|
||||
|
||||
const manager = SettingsManager.create(projectDir, agentDir);
|
||||
|
||||
expect(manager.getPackages()).toEqual(["npm:pi-doom", "npm:shitty-extensions"]);
|
||||
expect(manager.getExtensionPaths()).toEqual(["/local/ext.ts"]);
|
||||
});
|
||||
|
||||
it("should migrate git: sources from extensions to packages", () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify({
|
||||
extensions: ["git:github.com/user/repo", "/local/ext.ts"],
|
||||
}),
|
||||
);
|
||||
|
||||
const manager = SettingsManager.create(projectDir, agentDir);
|
||||
|
||||
expect(manager.getPackages()).toEqual(["git:github.com/user/repo"]);
|
||||
expect(manager.getExtensionPaths()).toEqual(["/local/ext.ts"]);
|
||||
});
|
||||
|
||||
it("should migrate raw github URLs from extensions to packages", () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify({
|
||||
extensions: ["https://github.com/user/repo", "/local/ext.ts"],
|
||||
}),
|
||||
);
|
||||
|
||||
const manager = SettingsManager.create(projectDir, agentDir);
|
||||
|
||||
expect(manager.getPackages()).toEqual(["https://github.com/user/repo"]);
|
||||
expect(manager.getExtensionPaths()).toEqual(["/local/ext.ts"]);
|
||||
});
|
||||
|
||||
it("should keep local-only extensions in extensions array", () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify({
|
||||
extensions: ["/local/ext.ts", "./relative/ext.ts"],
|
||||
}),
|
||||
);
|
||||
|
||||
const manager = SettingsManager.create(projectDir, agentDir);
|
||||
|
||||
expect(manager.getPackages()).toEqual([]);
|
||||
expect(manager.getExtensionPaths()).toEqual(["/local/ext.ts", "./relative/ext.ts"]);
|
||||
});
|
||||
|
||||
it("should preserve existing packages when migrating", () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify({
|
||||
packages: ["npm:existing-pkg"],
|
||||
extensions: ["npm:new-pkg", "/local/ext.ts"],
|
||||
}),
|
||||
);
|
||||
|
||||
const manager = SettingsManager.create(projectDir, agentDir);
|
||||
|
||||
expect(manager.getPackages()).toEqual(["npm:existing-pkg", "npm:new-pkg"]);
|
||||
expect(manager.getExtensionPaths()).toEqual(["/local/ext.ts"]);
|
||||
});
|
||||
|
||||
it("should handle packages with filtering objects", () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify({
|
||||
packages: [
|
||||
"npm:simple-pkg",
|
||||
{
|
||||
source: "npm:shitty-extensions",
|
||||
extensions: ["extensions/oracle.ts"],
|
||||
skills: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const manager = SettingsManager.create(projectDir, agentDir);
|
||||
|
||||
const packages = manager.getPackages();
|
||||
expect(packages).toHaveLength(2);
|
||||
expect(packages[0]).toBe("npm:simple-pkg");
|
||||
expect(packages[1]).toEqual({
|
||||
source: "npm:shitty-extensions",
|
||||
extensions: ["extensions/oracle.ts"],
|
||||
skills: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("shellCommandPrefix", () => {
|
||||
it("should load shellCommandPrefix from settings", () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue