mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 10:02:23 +00:00
fix(coding-agent): refresh temporary git extension caches on cache hits
This commit is contained in:
parent
92fdb53c10
commit
310da43042
3 changed files with 101 additions and 2 deletions
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed temporary git package caches (`-e <git-url>`) to refresh on cache hits for unpinned sources, including detached/no-upstream checkouts
|
||||||
|
|
||||||
## [0.52.7] - 2026-02-06
|
## [0.52.7] - 2026-02-06
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
|
||||||
|
|
@ -870,6 +870,8 @@ export class DefaultPackageManager implements PackageManager {
|
||||||
if (!existsSync(installedPath)) {
|
if (!existsSync(installedPath)) {
|
||||||
const installed = await installMissing();
|
const installed = await installMissing();
|
||||||
if (!installed) continue;
|
if (!installed) continue;
|
||||||
|
} else if (scope === "temporary" && !parsed.pinned) {
|
||||||
|
await this.refreshTemporaryGitSource(parsed, sourceStr);
|
||||||
}
|
}
|
||||||
metadata.baseDir = installedPath;
|
metadata.baseDir = installedPath;
|
||||||
this.collectPackageResources(installedPath, accumulator, filter, metadata);
|
this.collectPackageResources(installedPath, accumulator, filter, metadata);
|
||||||
|
|
@ -1153,8 +1155,13 @@ export class DefaultPackageManager implements PackageManager {
|
||||||
// Fetch latest from remote (handles force-push by getting new history)
|
// Fetch latest from remote (handles force-push by getting new history)
|
||||||
await this.runCommand("git", ["fetch", "--prune", "origin"], { cwd: targetDir });
|
await this.runCommand("git", ["fetch", "--prune", "origin"], { cwd: targetDir });
|
||||||
|
|
||||||
// Reset to upstream tracking branch (handles force-push gracefully)
|
// Reset to tracking branch. Fall back to origin/HEAD when no upstream is configured.
|
||||||
await this.runCommand("git", ["reset", "--hard", "@{upstream}"], { cwd: targetDir });
|
try {
|
||||||
|
await this.runCommand("git", ["reset", "--hard", "@{upstream}"], { cwd: targetDir });
|
||||||
|
} catch {
|
||||||
|
await this.runCommand("git", ["remote", "set-head", "origin", "-a"], { cwd: targetDir }).catch(() => {});
|
||||||
|
await this.runCommand("git", ["reset", "--hard", "origin/HEAD"], { cwd: targetDir });
|
||||||
|
}
|
||||||
|
|
||||||
// Clean untracked files (extensions should be pristine)
|
// Clean untracked files (extensions should be pristine)
|
||||||
await this.runCommand("git", ["clean", "-fdx"], { cwd: targetDir });
|
await this.runCommand("git", ["clean", "-fdx"], { cwd: targetDir });
|
||||||
|
|
@ -1165,6 +1172,16 @@ export class DefaultPackageManager implements PackageManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async refreshTemporaryGitSource(source: GitSource, sourceStr: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.withProgress("pull", sourceStr, `Refreshing ${sourceStr}...`, async () => {
|
||||||
|
await this.updateGit(source, "temporary");
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Keep cached temporary checkout if refresh fails.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async removeGit(source: GitSource, scope: SourceScope): Promise<void> {
|
private async removeGit(source: GitSource, scope: SourceScope): Promise<void> {
|
||||||
const targetDir = this.getGitInstallPath(source, scope);
|
const targetDir = this.getGitInstallPath(source, scope);
|
||||||
if (!existsSync(targetDir)) return;
|
if (!existsSync(targetDir)) return;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readFileSync, 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";
|
||||||
|
|
@ -132,6 +133,20 @@ describe("DefaultPackageManager git update", () => {
|
||||||
expect(getCurrentCommit(installedDir)).toBe(latestCommit);
|
expect(getCurrentCommit(installedDir)).toBe(latestCommit);
|
||||||
expect(getFileContent(installedDir, "extension.ts")).toBe("// v4");
|
expect(getFileContent(installedDir, "extension.ts")).toBe("// v4");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should update even when local checkout has no upstream", async () => {
|
||||||
|
setupRemoteAndInstall();
|
||||||
|
createCommit(remoteDir, "extension.ts", "// v2", "Second commit");
|
||||||
|
const latestCommit = createCommit(remoteDir, "extension.ts", "// v3", "Third commit");
|
||||||
|
|
||||||
|
const detachedCommit = getCurrentCommit(installedDir);
|
||||||
|
git(["checkout", detachedCommit], installedDir);
|
||||||
|
|
||||||
|
await packageManager.update();
|
||||||
|
|
||||||
|
expect(getCurrentCommit(installedDir)).toBe(latestCommit);
|
||||||
|
expect(getFileContent(installedDir, "extension.ts")).toBe("// v3");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("force-push scenarios", () => {
|
describe("force-push scenarios", () => {
|
||||||
|
|
@ -233,6 +248,69 @@ describe("DefaultPackageManager git update", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("temporary git sources", () => {
|
||||||
|
it("should refresh cached temporary git sources when resolving", async () => {
|
||||||
|
const gitHost = "github.com";
|
||||||
|
const gitPath = "test/extension";
|
||||||
|
const hash = createHash("sha256").update(`git-${gitHost}-${gitPath}`).digest("hex").slice(0, 8);
|
||||||
|
const cachedDir = join(tmpdir(), "pi-extensions", `git-${gitHost}`, hash, gitPath);
|
||||||
|
const extensionFile = join(cachedDir, "pi-extensions", "session-breakdown.ts");
|
||||||
|
|
||||||
|
rmSync(cachedDir, { recursive: true, force: true });
|
||||||
|
mkdirSync(join(cachedDir, "pi-extensions"), { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(cachedDir, "package.json"),
|
||||||
|
JSON.stringify({ pi: { extensions: ["./pi-extensions"] } }, null, 2),
|
||||||
|
);
|
||||||
|
writeFileSync(extensionFile, "// stale");
|
||||||
|
|
||||||
|
const executedCommands: string[] = [];
|
||||||
|
const managerWithInternals = packageManager as unknown as {
|
||||||
|
runCommand: (command: string, args: string[], options?: { cwd?: string }) => Promise<void>;
|
||||||
|
};
|
||||||
|
managerWithInternals.runCommand = async (command, args) => {
|
||||||
|
executedCommands.push(`${command} ${args.join(" ")}`);
|
||||||
|
if (command === "git" && args[0] === "reset") {
|
||||||
|
writeFileSync(extensionFile, "// fresh");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await packageManager.resolveExtensionSources([gitSource], { temporary: true });
|
||||||
|
|
||||||
|
expect(executedCommands).toContain("git fetch --prune origin");
|
||||||
|
expect(getFileContent(cachedDir, "pi-extensions/session-breakdown.ts")).toBe("// fresh");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not refresh pinned temporary git sources", async () => {
|
||||||
|
const gitHost = "github.com";
|
||||||
|
const gitPath = "test/extension";
|
||||||
|
const hash = createHash("sha256").update(`git-${gitHost}-${gitPath}`).digest("hex").slice(0, 8);
|
||||||
|
const cachedDir = join(tmpdir(), "pi-extensions", `git-${gitHost}`, hash, gitPath);
|
||||||
|
const extensionFile = join(cachedDir, "pi-extensions", "session-breakdown.ts");
|
||||||
|
|
||||||
|
rmSync(cachedDir, { recursive: true, force: true });
|
||||||
|
mkdirSync(join(cachedDir, "pi-extensions"), { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(cachedDir, "package.json"),
|
||||||
|
JSON.stringify({ pi: { extensions: ["./pi-extensions"] } }, null, 2),
|
||||||
|
);
|
||||||
|
writeFileSync(extensionFile, "// pinned");
|
||||||
|
|
||||||
|
const executedCommands: string[] = [];
|
||||||
|
const managerWithInternals = packageManager as unknown as {
|
||||||
|
runCommand: (command: string, args: string[], options?: { cwd?: string }) => Promise<void>;
|
||||||
|
};
|
||||||
|
managerWithInternals.runCommand = async (command, args) => {
|
||||||
|
executedCommands.push(`${command} ${args.join(" ")}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
await packageManager.resolveExtensionSources([`${gitSource}@main`], { temporary: true });
|
||||||
|
|
||||||
|
expect(executedCommands).toEqual([]);
|
||||||
|
expect(getFileContent(cachedDir, "pi-extensions/session-breakdown.ts")).toBe("// pinned");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("scope-aware update", () => {
|
describe("scope-aware update", () => {
|
||||||
it("should not install locally when source is only registered globally", async () => {
|
it("should not install locally when source is only registered globally", async () => {
|
||||||
setupRemoteAndInstall();
|
setupRemoteAndInstall();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue