mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
Addresses #5958. Adds `changelogPath` to `PiManifest`. Safe package-relative changelog resolution, with `CHANGELOG.md` fallback, and carries it through `PathMetadata`/`SourceInfo`. Added extension changelog handling like Pi’s own changelog. If the package exposes `pi.changelogPath` or has `CHANGELOG.md`, Pi stores the last seen package version in settings. On each fresh interactive launch, we compare the stored version to the installed package version. Then parses the changelog entries in between, shows an Extension Updates section, then marks the current version as seen. The first time an extension is seen, its version is recorded without showing a changelog, same as Pi's own on a fresh install. `collapseChangelog` condenses this to one line per package. Any update that bumps the extension's version shows this on the next launch, e.g.: ```bash pi update --extensions pi update --all pi update <extension-source> pi update --extension <extension-source> pi update --self --extensions pi update pi --extensions pi update self --extensions ``` <img width="798" height="626" alt="Screenshot 2026-08-28 at 16 34 15" src="https://github.com/user-attachments/assets/c882f01c-94b9-4857-b9b7-243d88c0d437" />
139 lines
4.2 KiB
TypeScript
139 lines
4.2 KiB
TypeScript
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
import { InMemorySettingsStorage, SettingsManager } from "../src/core/settings-manager.ts";
|
|
import { getSourcePackageKey, type SourceInfo } from "../src/core/source-info.ts";
|
|
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
|
|
|
|
type ExtensionChangelogUpdate = { name: string; version: string; markdown: string };
|
|
|
|
type ExtensionChangelogContext = {
|
|
session: {
|
|
state: { messages: unknown[] };
|
|
resourceLoader: { getExtensions(): { extensions: Array<{ sourceInfo: SourceInfo }> } };
|
|
};
|
|
settingsManager: SettingsManager;
|
|
readPackageJson(packageRoot: string): { name?: string; version?: string } | undefined;
|
|
};
|
|
|
|
const prototype = InteractiveMode.prototype as unknown as {
|
|
getExtensionChangelogUpdates(this: ExtensionChangelogContext): ExtensionChangelogUpdate[];
|
|
readPackageJson(packageRoot: string): { name?: string; version?: string } | undefined;
|
|
};
|
|
|
|
const CHANGELOG = `# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
- Not released yet
|
|
|
|
## [1.2.0]
|
|
|
|
- Added feature B
|
|
|
|
## [1.1.0]
|
|
|
|
- Added feature A
|
|
|
|
## [1.0.0]
|
|
|
|
- Initial release
|
|
`;
|
|
|
|
describe("InteractiveMode extension changelog", () => {
|
|
let tempDir: string;
|
|
let packageRoot: string;
|
|
let sourceInfo: SourceInfo;
|
|
let settingsManager: SettingsManager;
|
|
let messages: unknown[];
|
|
|
|
function writePackage(version: string): void {
|
|
writeFileSync(join(packageRoot, "package.json"), JSON.stringify({ name: "pi-demo-extension", version }));
|
|
}
|
|
|
|
function createContext(): ExtensionChangelogContext {
|
|
return {
|
|
session: {
|
|
state: { messages },
|
|
resourceLoader: {
|
|
getExtensions: () => ({
|
|
// Two extensions from the same package must only produce one changelog section.
|
|
extensions: [{ sourceInfo }, { sourceInfo: { ...sourceInfo, path: join(packageRoot, "b.ts") } }],
|
|
}),
|
|
},
|
|
},
|
|
settingsManager,
|
|
readPackageJson: prototype.readPackageJson,
|
|
};
|
|
}
|
|
|
|
function display(): ExtensionChangelogUpdate[] {
|
|
return prototype.getExtensionChangelogUpdates.call(createContext());
|
|
}
|
|
|
|
beforeEach(() => {
|
|
tempDir = mkdtempSync(join(tmpdir(), "pi-extension-changelog-"));
|
|
packageRoot = join(tempDir, "pkg");
|
|
mkdirSync(packageRoot);
|
|
writeFileSync(join(packageRoot, "CHANGELOG.md"), CHANGELOG);
|
|
sourceInfo = {
|
|
path: join(packageRoot, "a.ts"),
|
|
source: "npm:pi-demo-extension",
|
|
scope: "user",
|
|
origin: "package",
|
|
baseDir: packageRoot,
|
|
changelogPath: join(packageRoot, "CHANGELOG.md"),
|
|
};
|
|
settingsManager = SettingsManager.fromStorage(new InMemorySettingsStorage());
|
|
messages = [];
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test("records the version on first launch without showing the changelog", () => {
|
|
writePackage("1.0.0");
|
|
|
|
expect(display()).toEqual([]);
|
|
expect(settingsManager.getExtensionChangelogVersion(getSourcePackageKey(sourceInfo))).toBe("1.0.0");
|
|
});
|
|
|
|
test("shows entries newer than the last seen version, headed by package name", () => {
|
|
writePackage("1.0.0");
|
|
display();
|
|
writePackage("1.2.0");
|
|
|
|
expect(display()).toEqual([
|
|
{
|
|
name: "pi-demo-extension",
|
|
version: "1.2.0",
|
|
markdown:
|
|
"## pi-demo-extension [1.2.0]\n\n- Added feature B\n\n## pi-demo-extension [1.1.0]\n\n- Added feature A",
|
|
},
|
|
]);
|
|
expect(settingsManager.getExtensionChangelogVersion(getSourcePackageKey(sourceInfo))).toBe("1.2.0");
|
|
expect(display()).toEqual([]);
|
|
});
|
|
|
|
test("skips resumed sessions", () => {
|
|
writePackage("1.0.0");
|
|
display();
|
|
writePackage("1.2.0");
|
|
messages.push({ role: "user", content: "hi" });
|
|
|
|
expect(display()).toEqual([]);
|
|
expect(settingsManager.getExtensionChangelogVersion(getSourcePackageKey(sourceInfo))).toBe("1.0.0");
|
|
});
|
|
|
|
test("ignores non-package extensions and packages without a changelog", () => {
|
|
writePackage("1.0.0");
|
|
sourceInfo = { ...sourceInfo, origin: "top-level" };
|
|
expect(display()).toEqual([]);
|
|
|
|
sourceInfo = { ...sourceInfo, origin: "package", changelogPath: undefined };
|
|
expect(display()).toEqual([]);
|
|
expect(settingsManager.getExtensionChangelogVersion(getSourcePackageKey(sourceInfo))).toBeUndefined();
|
|
});
|
|
});
|