@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
- Fixed `ctrl+z` suspend and `fg` resume reliability by keeping the process alive until the `SIGCONT` handler restores the TUI, avoiding immediate process exit in environments with no other live event-loop handles ([#2454](https://github.com/badlogic/pi-mono/issues/2454))
|
- Fixed `ctrl+z` suspend and `fg` resume reliability by keeping the process alive until the `SIGCONT` handler restores the TUI, avoiding immediate process exit in environments with no other live event-loop handles ([#2454](https://github.com/badlogic/pi-mono/issues/2454))
|
||||||
- Fixed `createAgentSession({ agentDir })` to derive the default persisted session path from the provided `agentDir`, keeping session storage aligned with settings, auth, models, and resource loading ([#2457](https://github.com/badlogic/pi-mono/issues/2457))
|
- Fixed `createAgentSession({ agentDir })` to derive the default persisted session path from the provided `agentDir`, keeping session storage aligned with settings, auth, models, and resource loading ([#2457](https://github.com/badlogic/pi-mono/issues/2457))
|
||||||
|
- Fixed project-local npm package updates to install npm `latest` instead of reusing stale saved dependency ranges, and added `Did you mean ...?` suggestions when `pi update <source>` omits the configured npm or git source prefix ([#2459](https://github.com/badlogic/pi-mono/issues/2459))
|
||||||
|
|
||||||
## [0.61.0] - 2026-03-20
|
## [0.61.0] - 2026-03-20
|
||||||
|
|
||||||
|
|||||||
@@ -860,17 +860,29 @@ export class DefaultPackageManager implements PackageManager {
|
|||||||
const globalSettings = this.settingsManager.getGlobalSettings();
|
const globalSettings = this.settingsManager.getGlobalSettings();
|
||||||
const projectSettings = this.settingsManager.getProjectSettings();
|
const projectSettings = this.settingsManager.getProjectSettings();
|
||||||
const identity = source ? this.getPackageIdentity(source) : undefined;
|
const identity = source ? this.getPackageIdentity(source) : undefined;
|
||||||
|
let matched = false;
|
||||||
|
|
||||||
for (const pkg of globalSettings.packages ?? []) {
|
for (const pkg of globalSettings.packages ?? []) {
|
||||||
const sourceStr = typeof pkg === "string" ? pkg : pkg.source;
|
const sourceStr = typeof pkg === "string" ? pkg : pkg.source;
|
||||||
if (identity && this.getPackageIdentity(sourceStr, "user") !== identity) continue;
|
if (identity && this.getPackageIdentity(sourceStr, "user") !== identity) continue;
|
||||||
|
matched = true;
|
||||||
await this.updateSourceForScope(sourceStr, "user");
|
await this.updateSourceForScope(sourceStr, "user");
|
||||||
}
|
}
|
||||||
for (const pkg of projectSettings.packages ?? []) {
|
for (const pkg of projectSettings.packages ?? []) {
|
||||||
const sourceStr = typeof pkg === "string" ? pkg : pkg.source;
|
const sourceStr = typeof pkg === "string" ? pkg : pkg.source;
|
||||||
if (identity && this.getPackageIdentity(sourceStr, "project") !== identity) continue;
|
if (identity && this.getPackageIdentity(sourceStr, "project") !== identity) continue;
|
||||||
|
matched = true;
|
||||||
await this.updateSourceForScope(sourceStr, "project");
|
await this.updateSourceForScope(sourceStr, "project");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (source && !matched) {
|
||||||
|
throw new Error(
|
||||||
|
this.buildNoMatchingPackageMessage(source, [
|
||||||
|
...(globalSettings.packages ?? []),
|
||||||
|
...(projectSettings.packages ?? []),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateSourceForScope(source: string, scope: SourceScope): Promise<void> {
|
private async updateSourceForScope(source: string, scope: SourceScope): Promise<void> {
|
||||||
@@ -881,7 +893,14 @@ export class DefaultPackageManager implements PackageManager {
|
|||||||
if (parsed.type === "npm") {
|
if (parsed.type === "npm") {
|
||||||
if (parsed.pinned) return;
|
if (parsed.pinned) return;
|
||||||
await this.withProgress("update", source, `Updating ${source}...`, async () => {
|
await this.withProgress("update", source, `Updating ${source}...`, async () => {
|
||||||
await this.installNpm(parsed, scope, false);
|
await this.installNpm(
|
||||||
|
{
|
||||||
|
...parsed,
|
||||||
|
spec: `${parsed.name}@latest`,
|
||||||
|
},
|
||||||
|
scope,
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1088,6 +1107,39 @@ export class DefaultPackageManager implements PackageManager {
|
|||||||
return `local:${this.resolvePathFromBase(parsed.path, baseDir)}`;
|
return `local:${this.resolvePathFromBase(parsed.path, baseDir)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildNoMatchingPackageMessage(source: string, configuredPackages: PackageSource[]): string {
|
||||||
|
const suggestion = this.findSuggestedConfiguredSource(source, configuredPackages);
|
||||||
|
if (!suggestion) {
|
||||||
|
return `No matching package found for ${source}`;
|
||||||
|
}
|
||||||
|
return `No matching package found for ${source}. Did you mean ${suggestion}?`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private findSuggestedConfiguredSource(source: string, configuredPackages: PackageSource[]): string | undefined {
|
||||||
|
const trimmedSource = source.trim();
|
||||||
|
const suggestions = new Set<string>();
|
||||||
|
|
||||||
|
for (const pkg of configuredPackages) {
|
||||||
|
const sourceStr = this.getPackageSourceString(pkg);
|
||||||
|
const parsed = this.parseSource(sourceStr);
|
||||||
|
if (parsed.type === "npm") {
|
||||||
|
if (trimmedSource === parsed.name || trimmedSource === parsed.spec) {
|
||||||
|
suggestions.add(sourceStr);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (parsed.type === "git") {
|
||||||
|
const shorthand = `${parsed.host}/${parsed.path}`;
|
||||||
|
const shorthandWithRef = parsed.ref ? `${shorthand}@${parsed.ref}` : undefined;
|
||||||
|
if (trimmedSource === shorthand || (shorthandWithRef && trimmedSource === shorthandWithRef)) {
|
||||||
|
suggestions.add(sourceStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions.values().next().value;
|
||||||
|
}
|
||||||
|
|
||||||
private packageSourcesMatch(existing: PackageSource, inputSource: string, scope: SourceScope): boolean {
|
private packageSourcesMatch(existing: PackageSource, inputSource: string, scope: SourceScope): boolean {
|
||||||
const left = this.getSourceMatchKeyForSettings(this.getPackageSourceString(existing), scope);
|
const left = this.getSourceMatchKeyForSettings(this.getPackageSourceString(existing), scope);
|
||||||
const right = this.getSourceMatchKeyForInput(inputSource);
|
const right = this.getSourceMatchKeyForInput(inputSource);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { mkdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
@@ -117,4 +117,28 @@ describe("package commands", () => {
|
|||||||
errorSpy.mockRestore();
|
errorSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("suggests the configured source when update input omits the npm prefix", async () => {
|
||||||
|
const settingsPath = join(agentDir, "settings.json");
|
||||||
|
writeFileSync(settingsPath, JSON.stringify({ packages: ["npm:pi-formatter"] }, null, 2));
|
||||||
|
|
||||||
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(main(["update", "pi-formatter"])).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
||||||
|
const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
||||||
|
expect(stderr).toContain("Did you mean npm:pi-formatter?");
|
||||||
|
expect(stdout).not.toContain("Updated pi-formatter");
|
||||||
|
expect(process.exitCode).toBe(1);
|
||||||
|
|
||||||
|
const settings = JSON.parse(readFileSync(settingsPath, "utf-8")) as { packages?: string[] };
|
||||||
|
expect(settings.packages).toContain("npm:pi-formatter");
|
||||||
|
} finally {
|
||||||
|
errorSpy.mockRestore();
|
||||||
|
logSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1293,6 +1293,36 @@ export default function(api) { api.registerTool({ name: "test", description: "te
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("offline mode and network timeouts", () => {
|
describe("offline mode and network timeouts", () => {
|
||||||
|
it("should update project npm packages using @latest", async () => {
|
||||||
|
settingsManager.setProjectPackages(["npm:example"]);
|
||||||
|
|
||||||
|
const runCommandSpy = vi.spyOn(packageManager as any, "runCommand").mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await packageManager.update("npm:example");
|
||||||
|
|
||||||
|
expect(runCommandSpy).toHaveBeenCalledWith(
|
||||||
|
"npm",
|
||||||
|
["install", "example@latest", "--prefix", join(tempDir, ".pi", "npm")],
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should suggest npm source prefixes for update lookups", async () => {
|
||||||
|
settingsManager.setProjectPackages(["npm:example"]);
|
||||||
|
|
||||||
|
await expect(packageManager.update("example")).rejects.toThrow(
|
||||||
|
"No matching package found for example. Did you mean npm:example?",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should suggest git source prefixes for update lookups", async () => {
|
||||||
|
settingsManager.setProjectPackages(["git:github.com/example/repo"]);
|
||||||
|
|
||||||
|
await expect(packageManager.update("github.com/example/repo")).rejects.toThrow(
|
||||||
|
"No matching package found for github.com/example/repo. Did you mean git:github.com/example/repo?",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("should skip installing missing package sources when offline", async () => {
|
it("should skip installing missing package sources when offline", async () => {
|
||||||
process.env.PI_OFFLINE = "1";
|
process.env.PI_OFFLINE = "1";
|
||||||
settingsManager.setProjectPackages(["npm:missing-package", "git:github.com/example/missing-repo"]);
|
settingsManager.setProjectPackages(["npm:missing-package", "git:github.com/example/missing-repo"]);
|
||||||
|
|||||||
Reference in New Issue
Block a user