Merge remote-tracking branch 'origin/main' into pr-4756-squash-cleanup
# Conflicts: # scripts/build-binaries.sh
This commit is contained in:
@@ -3,7 +3,7 @@ import type { AddressInfo } from "node:net";
|
||||
import { Type } from "typebox";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts";
|
||||
import { getModel } from "../src/models.ts";
|
||||
import { getModel, getModels } from "../src/models.ts";
|
||||
import { streamAnthropic } from "../src/providers/anthropic.ts";
|
||||
import type { Context, Model, Tool } from "../src/types.ts";
|
||||
|
||||
@@ -38,12 +38,14 @@ describe("Fireworks models", () => {
|
||||
});
|
||||
|
||||
it("registers the Fire Pass turbo router model", () => {
|
||||
const model = getModel("fireworks", "accounts/fireworks/routers/kimi-k2p5-turbo");
|
||||
const model = getModels("fireworks").find(
|
||||
(candidate) => candidate.id.startsWith("accounts/fireworks/routers/") && candidate.id.endsWith("-turbo"),
|
||||
);
|
||||
|
||||
expect(model).toBeDefined();
|
||||
expect(model.api).toBe("anthropic-messages");
|
||||
expect(model.baseUrl).toBe("https://api.fireworks.ai/inference");
|
||||
expect(model.input).toEqual(["text", "image"]);
|
||||
expect(model?.api).toBe("anthropic-messages");
|
||||
expect(model?.baseUrl).toBe("https://api.fireworks.ai/inference");
|
||||
expect(model?.input).toEqual(["text", "image"]);
|
||||
});
|
||||
|
||||
it("resolves FIREWORKS_API_KEY from the environment", () => {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `pi update` to reconcile git-pinned packages to their configured ref ([#4869](https://github.com/earendil-works/pi/issues/4869)).
|
||||
- Fixed OpenCode Zen/Go requests to send per-session OpenCode routing headers ([#4847](https://github.com/earendil-works/pi/issues/4847)).
|
||||
- Fixed Amazon Bedrock provider loading under strict package managers by inheriting the declared `@smithy/node-http-handler` dependency from `@earendil-works/pi-ai` ([#4842](https://github.com/earendil-works/pi/issues/4842)).
|
||||
- Fixed exported session HTML to escape quote characters in attribute values ([#4832](https://github.com/earendil-works/pi/issues/4832)).
|
||||
- Fixed GitHub Copilot device-code login to keep opening the verification URL in browser-capable environments while ignoring browser launch failures for headless use.
|
||||
@@ -20,6 +22,8 @@
|
||||
- Published a 0.74.2 rescue release that tells Node 20 users to upgrade Node before updating to newer Pi versions ([#4876](https://github.com/earendil-works/pi/issues/4876)).
|
||||
- Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)).
|
||||
- Fixed bash tool truncation line counts to ignore the trailing newline as an extra output line ([#4818](https://github.com/earendil-works/pi/issues/4818)).
|
||||
- Fixed footer home-directory abbreviation to avoid shortening sibling paths that only share the same prefix ([#4878](https://github.com/earendil-works/pi/issues/4878)).
|
||||
- Fixed macOS Bun release binaries to resolve the native clipboard sidecar so Ctrl+V image paste can load `@mariozechner/clipboard` ([#4307](https://github.com/earendil-works/pi/issues/4307)).
|
||||
|
||||
## [0.75.4] - 2026-05-20
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ pi install ./relative/path/to/package
|
||||
|
||||
pi remove npm:@foo/bar
|
||||
pi list # show installed packages from settings
|
||||
pi update # update pi and all non-pinned packages
|
||||
pi update --extensions # update all non-pinned packages only
|
||||
pi update # update pi, update packages, and reconcile pinned git refs
|
||||
pi update --extensions # update packages and reconcile pinned git refs only
|
||||
pi update --self # update pi only
|
||||
pi update --self --force # reinstall pi even if current
|
||||
pi update npm:@foo/bar # update one package
|
||||
@@ -85,9 +85,10 @@ ssh://git@github.com/user/repo@v1
|
||||
- HTTPS and SSH URLs are both supported.
|
||||
- SSH URLs use your configured SSH keys automatically (respects `~/.ssh/config`).
|
||||
- For non-interactive runs (for example CI), you can set `GIT_TERMINAL_PROMPT=0` to disable credential prompts and set `GIT_SSH_COMMAND` (for example `ssh -o BatchMode=yes -o ConnectTimeout=5`) to fail fast.
|
||||
- Refs are pinned tags or commits and skip package updates (`pi update`, `pi update --extensions`). Use `pi install git:host/user/repo@new-ref` to move an existing package to a new pinned ref.
|
||||
- Refs are pinned tags or commits. `pi update` and `pi update --extensions` do not move them to newer refs, but they do reconcile an existing clone to the configured ref.
|
||||
- Use `pi install git:host/user/repo@new-ref` to update settings and move an existing package to a new pinned ref.
|
||||
- Cloned to `~/.pi/agent/git/<host>/<path>` (global) or `.pi/git/<host>/<path>` (project).
|
||||
- Runs `npm install` after clone, pull, or pinned ref change if `package.json` exists.
|
||||
- When reconciliation changes the checkout, pi resets and cleans the clone, then runs `npm install` if `package.json` exists.
|
||||
|
||||
**SSH examples:**
|
||||
```bash
|
||||
|
||||
@@ -129,8 +129,8 @@ pi [options] [@files...] [messages...]
|
||||
pi install <source> [-l] # Install package, -l for project-local
|
||||
pi remove <source> [-l] # Remove package
|
||||
pi uninstall <source> [-l] # Alias for remove
|
||||
pi update [source|self|pi] # Update pi and packages; skips pinned packages
|
||||
pi update --extensions # Update packages only
|
||||
pi update [source|self|pi] # Update pi and packages; reconcile pinned git refs
|
||||
pi update --extensions # Update packages only; reconcile pinned git refs
|
||||
pi update --self # Update pi only
|
||||
pi update --extension <src> # Update one package
|
||||
pi list # List installed packages
|
||||
|
||||
@@ -1047,14 +1047,15 @@ export class DefaultPackageManager implements PackageManager {
|
||||
|
||||
for (const entry of sources) {
|
||||
const parsed = this.parseSource(entry.source);
|
||||
if (parsed.type === "local" || parsed.pinned) {
|
||||
continue;
|
||||
}
|
||||
// Pinned npm versions are fixed. Pinned git refs are configured checkout targets,
|
||||
// so include them to reconcile an existing clone when the configured ref changes.
|
||||
if (parsed.type === "npm") {
|
||||
npmCandidates.push({ ...entry, parsed });
|
||||
continue;
|
||||
if (!parsed.pinned) {
|
||||
npmCandidates.push({ ...entry, parsed });
|
||||
}
|
||||
} else if (parsed.type === "git") {
|
||||
gitCandidates.push({ ...entry, parsed });
|
||||
}
|
||||
gitCandidates.push({ ...entry, parsed });
|
||||
}
|
||||
|
||||
const npmCheckTasks = npmCandidates.map((entry) => async () => ({
|
||||
@@ -1766,6 +1767,11 @@ export class DefaultPackageManager implements PackageManager {
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.ref) {
|
||||
await this.ensureGitRef(targetDir, ["fetch", "origin", source.ref], "FETCH_HEAD");
|
||||
return;
|
||||
}
|
||||
|
||||
const target = await this.getLocalGitUpdateTarget(targetDir);
|
||||
await this.ensureGitRef(targetDir, target.fetchArgs, target.ref);
|
||||
}
|
||||
@@ -1778,7 +1784,8 @@ export class DefaultPackageManager implements PackageManager {
|
||||
cwd: targetDir,
|
||||
timeoutMs: NETWORK_TIMEOUT_MS,
|
||||
});
|
||||
const targetHead = await this.runCommandCapture("git", ["rev-parse", ref], {
|
||||
const commitRef = `${ref}^{commit}`;
|
||||
const targetHead = await this.runCommandCapture("git", ["rev-parse", commitRef], {
|
||||
cwd: targetDir,
|
||||
timeoutMs: NETWORK_TIMEOUT_MS,
|
||||
});
|
||||
@@ -1786,7 +1793,7 @@ export class DefaultPackageManager implements PackageManager {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runCommand("git", ["reset", "--hard", ref], { cwd: targetDir });
|
||||
await this.runCommand("git", ["reset", "--hard", commitRef], { cwd: targetDir });
|
||||
|
||||
// Clean untracked files (extensions should be pristine)
|
||||
await this.runCommand("git", ["clean", "-fdx"], { cwd: targetDir });
|
||||
|
||||
@@ -129,7 +129,15 @@ function getDefaultAgentDir(): string {
|
||||
function getAttributionHeaders(
|
||||
model: Model<any>,
|
||||
settingsManager: SettingsManager,
|
||||
sessionId?: string,
|
||||
): Record<string, string> | undefined {
|
||||
if (
|
||||
sessionId &&
|
||||
(model.provider === "opencode" || model.provider === "opencode-go" || model.baseUrl.includes("opencode.ai"))
|
||||
) {
|
||||
return { "x-opencode-session": sessionId, "x-opencode-client": "pi" };
|
||||
}
|
||||
|
||||
if (!isInstallTelemetryEnabled(settingsManager)) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -332,7 +340,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
throw new Error(auth.error);
|
||||
}
|
||||
const providerRetrySettings = settingsManager.getProviderRetrySettings();
|
||||
const attributionHeaders = getAttributionHeaders(model, settingsManager);
|
||||
const attributionHeaders = getAttributionHeaders(model, settingsManager, options?.sessionId);
|
||||
return streamSimple(model, context, {
|
||||
...options,
|
||||
apiKey: auth.apiKey,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isAbsolute, relative, resolve, sep } from "node:path";
|
||||
import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
||||
import type { AgentSession } from "../../../core/agent-session.ts";
|
||||
import type { ReadonlyFooterDataProvider } from "../../../core/footer-data-provider.ts";
|
||||
@@ -26,6 +27,20 @@ function formatTokens(count: number): string {
|
||||
return `${Math.round(count / 1000000)}M`;
|
||||
}
|
||||
|
||||
export function formatCwdForFooter(cwd: string, home: string | undefined): string {
|
||||
if (!home) return cwd;
|
||||
|
||||
const resolvedCwd = resolve(cwd);
|
||||
const resolvedHome = resolve(home);
|
||||
const relativeToHome = relative(resolvedHome, resolvedCwd);
|
||||
const isInsideHome =
|
||||
relativeToHome === "" ||
|
||||
(relativeToHome !== ".." && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));
|
||||
|
||||
if (!isInsideHome) return cwd;
|
||||
return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Footer component that shows pwd, token stats, and context usage.
|
||||
* Computes token/context stats from session, gets git branch and extension statuses from provider.
|
||||
@@ -92,11 +107,7 @@ export class FooterComponent implements Component {
|
||||
const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
|
||||
|
||||
// Replace home directory with ~
|
||||
let pwd = this.session.sessionManager.getCwd();
|
||||
const home = process.env.HOME || process.env.USERPROFILE;
|
||||
if (home && pwd.startsWith(home)) {
|
||||
pwd = `~${pwd.slice(home.length)}`;
|
||||
}
|
||||
let pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);
|
||||
|
||||
// Add git branch if available
|
||||
const branch = this.footerData.getGitBranch();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { createRequire } from "module";
|
||||
import { dirname, join } from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
export type ClipboardModule = {
|
||||
setText: (text: string) => Promise<void>;
|
||||
@@ -6,17 +8,25 @@ export type ClipboardModule = {
|
||||
getImageBinary: () => Promise<Array<number>>;
|
||||
};
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let clipboard: ClipboardModule | null = null;
|
||||
type ClipboardRequire = (id: string) => unknown;
|
||||
|
||||
const moduleRequire = createRequire(import.meta.url);
|
||||
const executableDirRequire = createRequire(pathToFileURL(join(dirname(process.execPath), "package.json")).href);
|
||||
const hasDisplay = process.platform !== "linux" || Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
||||
|
||||
if (!process.env.TERMUX_VERSION && hasDisplay) {
|
||||
try {
|
||||
clipboard = require("@mariozechner/clipboard") as ClipboardModule;
|
||||
} catch {
|
||||
clipboard = null;
|
||||
export function loadClipboardNative(
|
||||
requires: readonly ClipboardRequire[] = [moduleRequire, executableDirRequire],
|
||||
): ClipboardModule | null {
|
||||
for (const requireClipboard of requires) {
|
||||
try {
|
||||
return requireClipboard("@mariozechner/clipboard") as ClipboardModule;
|
||||
} catch {
|
||||
// Try the next resolution root.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const clipboard = !process.env.TERMUX_VERSION && hasDisplay ? loadClipboardNative() : null;
|
||||
|
||||
export { clipboard };
|
||||
|
||||
31
packages/coding-agent/test/clipboard-native.test.ts
Normal file
31
packages/coding-agent/test/clipboard-native.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { type ClipboardModule, loadClipboardNative } from "../src/utils/clipboard-native.ts";
|
||||
|
||||
type ClipboardRequire = (id: string) => unknown;
|
||||
|
||||
const fakeClipboard: ClipboardModule = {
|
||||
setText: async () => {},
|
||||
hasImage: () => true,
|
||||
getImageBinary: async () => [1, 2, 3],
|
||||
};
|
||||
|
||||
describe("loadClipboardNative", () => {
|
||||
test("falls back to the next require root", () => {
|
||||
const primary = vi.fn<ClipboardRequire>(() => {
|
||||
throw new Error("missing from bundled root");
|
||||
});
|
||||
const fallback = vi.fn<ClipboardRequire>(() => fakeClipboard);
|
||||
|
||||
expect(loadClipboardNative([primary, fallback])).toBe(fakeClipboard);
|
||||
expect(primary).toHaveBeenCalledWith("@mariozechner/clipboard");
|
||||
expect(fallback).toHaveBeenCalledWith("@mariozechner/clipboard");
|
||||
});
|
||||
|
||||
test("returns null when no require root can load clipboard", () => {
|
||||
const missing = vi.fn<ClipboardRequire>(() => {
|
||||
throw new Error("missing");
|
||||
});
|
||||
|
||||
expect(loadClipboardNative([missing])).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { visibleWidth } from "@earendil-works/pi-tui";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import type { AgentSession } from "../src/core/agent-session.ts";
|
||||
import type { ReadonlyFooterDataProvider } from "../src/core/footer-data-provider.ts";
|
||||
import { FooterComponent } from "../src/modes/interactive/components/footer.ts";
|
||||
import { FooterComponent, formatCwdForFooter } from "../src/modes/interactive/components/footer.ts";
|
||||
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
|
||||
|
||||
type AssistantUsage = {
|
||||
@@ -73,6 +73,17 @@ function createFooterData(providerCount: number): ReadonlyFooterDataProvider {
|
||||
return provider;
|
||||
}
|
||||
|
||||
describe("formatCwdForFooter", () => {
|
||||
it("does not abbreviate sibling paths that share the home prefix", () => {
|
||||
expect(formatCwdForFooter("/home/user2", "/home/user")).toBe("/home/user2");
|
||||
});
|
||||
|
||||
it("abbreviates the home directory and descendants", () => {
|
||||
expect(formatCwdForFooter("/home/user", "/home/user")).toBe("~");
|
||||
expect(formatCwdForFooter("/home/user/project", "/home/user")).toBe("~/project");
|
||||
});
|
||||
});
|
||||
|
||||
describe("FooterComponent width handling", () => {
|
||||
beforeAll(() => {
|
||||
initTheme(undefined, false);
|
||||
|
||||
@@ -282,7 +282,7 @@ describe("DefaultPackageManager git update", () => {
|
||||
});
|
||||
|
||||
describe("pinned sources", () => {
|
||||
it("should not update pinned git sources (with @ref)", async () => {
|
||||
it("should not move pinned git sources past their configured ref", async () => {
|
||||
// Create remote repo first to get the initial commit
|
||||
mkdirSync(remoteDir, { recursive: true });
|
||||
initGitRepo(remoteDir);
|
||||
@@ -301,13 +301,77 @@ describe("DefaultPackageManager git update", () => {
|
||||
// Add new commit to remote
|
||||
createCommit(remoteDir, "extension.ts", "// v2", "Second commit");
|
||||
|
||||
// Update should be skipped for pinned sources
|
||||
await packageManager.update();
|
||||
|
||||
// Should still be on initial commit
|
||||
expect(getCurrentCommit(installedDir)).toBe(initialCommit);
|
||||
expect(getFileContent(installedDir, "extension.ts")).toBe("// v1");
|
||||
});
|
||||
|
||||
it("should checkout the configured pinned git ref during full and targeted updates", async () => {
|
||||
mkdirSync(remoteDir, { recursive: true });
|
||||
initGitRepo(remoteDir);
|
||||
const v1Commit = createCommit(remoteDir, "extension.ts", "// v1", "Initial commit");
|
||||
git(["tag", "v1"], remoteDir);
|
||||
const v2Commit = createCommit(remoteDir, "extension.ts", "// v2", "Second commit");
|
||||
git(["tag", "v2"], remoteDir);
|
||||
|
||||
mkdirSync(join(agentDir, "git", "github.com", "test"), { recursive: true });
|
||||
git(["clone", remoteDir, installedDir], tempDir);
|
||||
git(["checkout", "v1"], installedDir);
|
||||
expect(getCurrentCommit(installedDir)).toBe(v1Commit);
|
||||
|
||||
const pinnedSource = `${gitSource}@v2`;
|
||||
settingsManager.setPackages([pinnedSource]);
|
||||
|
||||
await packageManager.update();
|
||||
|
||||
expect(getCurrentCommit(installedDir)).toBe(v2Commit);
|
||||
expect(getFileContent(installedDir, "extension.ts")).toBe("// v2");
|
||||
|
||||
git(["checkout", "v1"], installedDir);
|
||||
|
||||
await packageManager.update(pinnedSource);
|
||||
|
||||
expect(getCurrentCommit(installedDir)).toBe(v2Commit);
|
||||
expect(getFileContent(installedDir, "extension.ts")).toBe("// v2");
|
||||
});
|
||||
|
||||
it("should not reset an annotated tag checkout that already matches the configured ref", async () => {
|
||||
mkdirSync(remoteDir, { recursive: true });
|
||||
initGitRepo(remoteDir);
|
||||
const taggedCommit = createCommit(remoteDir, "extension.ts", "// v1", "Initial commit");
|
||||
git(["tag", "-a", "v1", "-m", "v1"], remoteDir);
|
||||
|
||||
mkdirSync(join(agentDir, "git", "github.com", "test"), { recursive: true });
|
||||
git(["clone", remoteDir, installedDir], tempDir);
|
||||
git(["checkout", "v1"], installedDir);
|
||||
expect(getCurrentCommit(installedDir)).toBe(taggedCommit);
|
||||
|
||||
settingsManager.setPackages([`${gitSource}@v1`]);
|
||||
|
||||
const executedCommands: string[] = [];
|
||||
const managerWithInternals = packageManager as unknown as {
|
||||
runCommand: (command: string, args: string[], options?: { cwd?: string }) => Promise<void>;
|
||||
};
|
||||
managerWithInternals.runCommand = async (command, args, options) => {
|
||||
executedCommands.push(`${command} ${args.join(" ")}`);
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: options?.cwd,
|
||||
encoding: "utf-8",
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`Command failed: ${command} ${args.join(" ")}\n${result.stderr}`);
|
||||
}
|
||||
};
|
||||
|
||||
await packageManager.update();
|
||||
|
||||
expect(executedCommands).toContain("git fetch origin v1");
|
||||
expect(executedCommands.some((command) => command.startsWith("git reset --hard"))).toBe(false);
|
||||
expect(executedCommands).not.toContain("git clean -fdx");
|
||||
expect(getCurrentCommit(installedDir)).toBe(taggedCommit);
|
||||
});
|
||||
});
|
||||
|
||||
describe("temporary git sources", () => {
|
||||
|
||||
@@ -748,7 +748,7 @@ Content`,
|
||||
if (args[0] === "rev-parse" && args[1] === "HEAD") {
|
||||
return "old-head";
|
||||
}
|
||||
if (args[0] === "rev-parse" && args[1] === "FETCH_HEAD") {
|
||||
if (args[0] === "rev-parse" && args[1] === "FETCH_HEAD^{commit}") {
|
||||
return "new-head";
|
||||
}
|
||||
throw new Error(`Unexpected runCommandCapture args: ${args.join(" ")}`);
|
||||
@@ -758,7 +758,9 @@ Content`,
|
||||
await packageManager.install(source);
|
||||
|
||||
expect(runCommandSpy).toHaveBeenCalledWith("git", ["fetch", "origin", "v2"], { cwd: targetDir });
|
||||
expect(runCommandSpy).toHaveBeenCalledWith("git", ["reset", "--hard", "FETCH_HEAD"], { cwd: targetDir });
|
||||
expect(runCommandSpy).toHaveBeenCalledWith("git", ["reset", "--hard", "FETCH_HEAD^{commit}"], {
|
||||
cwd: targetDir,
|
||||
});
|
||||
expect(runCommandSpy).toHaveBeenCalledWith("git", ["clean", "-fdx"], { cwd: targetDir });
|
||||
expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev"], { cwd: targetDir });
|
||||
});
|
||||
@@ -779,7 +781,7 @@ Content`,
|
||||
if (args[0] === "rev-parse" && args[1] === "HEAD") {
|
||||
return "old-head";
|
||||
}
|
||||
if (args[0] === "rev-parse" && args[1] === "origin/HEAD") {
|
||||
if (args[0] === "rev-parse" && args[1] === "origin/HEAD^{commit}") {
|
||||
return "new-head";
|
||||
}
|
||||
throw new Error(`Unexpected runCommandCapture args: ${args.join(" ")}`);
|
||||
@@ -789,7 +791,9 @@ Content`,
|
||||
await packageManager.install(source);
|
||||
|
||||
expect(runCommandSpy).toHaveBeenCalledWith("git", fetchArgs, { cwd: targetDir });
|
||||
expect(runCommandSpy).toHaveBeenCalledWith("git", ["reset", "--hard", "origin/HEAD"], { cwd: targetDir });
|
||||
expect(runCommandSpy).toHaveBeenCalledWith("git", ["reset", "--hard", "origin/HEAD^{commit}"], {
|
||||
cwd: targetDir,
|
||||
});
|
||||
expect(runCommandSpy).toHaveBeenCalledWith("git", ["clean", "-fdx"], { cwd: targetDir });
|
||||
});
|
||||
|
||||
@@ -832,7 +836,7 @@ Content`,
|
||||
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}") {
|
||||
return "origin/main";
|
||||
}
|
||||
if (args[0] === "rev-parse" && args[1] === "@{upstream}") {
|
||||
if (args[0] === "rev-parse" && (args[1] === "@{upstream}" || args[1] === "@{upstream}^{commit}")) {
|
||||
return "remote-head";
|
||||
}
|
||||
if (args[0] === "rev-parse" && args[1] === "HEAD") {
|
||||
@@ -868,7 +872,7 @@ Content`,
|
||||
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}") {
|
||||
return "origin/main";
|
||||
}
|
||||
if (args[0] === "rev-parse" && args[1] === "@{upstream}") {
|
||||
if (args[0] === "rev-parse" && (args[1] === "@{upstream}" || args[1] === "@{upstream}^{commit}")) {
|
||||
return "remote-head";
|
||||
}
|
||||
if (args[0] === "rev-parse" && args[1] === "HEAD") {
|
||||
@@ -2057,7 +2061,7 @@ export default function(api) { api.registerTool({ name: "test", description: "te
|
||||
expect(packageManager.getInstalledPath("npm:legacy-pkg", "user")).toBe(managedPath);
|
||||
});
|
||||
|
||||
it("should batch npm updates per scope and run git updates in parallel while skipping pinned and current packages", async () => {
|
||||
it("should batch npm updates per scope and run git updates in parallel while skipping pinned npm and current packages", async () => {
|
||||
const userOldPath = join(agentDir, "npm", "node_modules", "user-old");
|
||||
const userCurrentPath = join(agentDir, "npm", "node_modules", "user-current");
|
||||
const userUnknownPath = join(agentDir, "npm", "node_modules", "user-unknown");
|
||||
@@ -2159,7 +2163,7 @@ export default function(api) { api.registerTool({ name: "test", description: "te
|
||||
["install", "project-old@latest", "project-missing@latest", "--prefix", join(tempDir, ".pi", "npm")],
|
||||
undefined,
|
||||
);
|
||||
expect(updateGitSpy).toHaveBeenCalledTimes(3);
|
||||
expect(updateGitSpy).toHaveBeenCalledTimes(4);
|
||||
expect(maxConcurrentNpmUpdates).toBeGreaterThan(1);
|
||||
expect(maxConcurrentGitUpdates).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
@@ -86,6 +86,7 @@ describe("createAgentSession OpenRouter attribution headers", () => {
|
||||
telemetryEnabled?: boolean;
|
||||
providerHeaders?: Record<string, string>;
|
||||
requestHeaders?: Record<string, string>;
|
||||
sessionId?: string;
|
||||
} = {},
|
||||
): Promise<Record<string, string> | undefined> {
|
||||
const settingsManager = SettingsManager.create(cwd, agentDir);
|
||||
@@ -112,6 +113,11 @@ describe("createAgentSession OpenRouter attribution headers", () => {
|
||||
registeredProviders.push(model.provider);
|
||||
}
|
||||
|
||||
const sessionManager = SessionManager.inMemory(cwd);
|
||||
if (options.sessionId) {
|
||||
sessionManager.newSession({ id: options.sessionId });
|
||||
}
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
cwd,
|
||||
agentDir,
|
||||
@@ -119,14 +125,17 @@ describe("createAgentSession OpenRouter attribution headers", () => {
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
settingsManager,
|
||||
sessionManager: SessionManager.inMemory(cwd),
|
||||
sessionManager,
|
||||
});
|
||||
|
||||
try {
|
||||
await session.agent.streamFn(
|
||||
model,
|
||||
{ messages: [] },
|
||||
options.requestHeaders ? { headers: options.requestHeaders } : undefined,
|
||||
{
|
||||
sessionId: session.sessionId,
|
||||
...(options.requestHeaders ? { headers: options.requestHeaders } : {}),
|
||||
},
|
||||
);
|
||||
return capturedOptions?.headers;
|
||||
} finally {
|
||||
@@ -178,4 +187,26 @@ describe("createAgentSession OpenRouter attribution headers", () => {
|
||||
expect(headers?.["X-OpenRouter-Title"]).toBe("request-title");
|
||||
expect(headers?.["X-OpenRouter-Categories"]).toBe("provider-category");
|
||||
});
|
||||
|
||||
it("adds OpenCode session headers", async () => {
|
||||
const headers = await captureHeaders(createModel("opencode", "https://opencode.ai/zen/v1"), {
|
||||
sessionId: "opencode-session",
|
||||
});
|
||||
|
||||
expect(headers?.["x-opencode-session"]).toBe("opencode-session");
|
||||
expect(headers?.["x-opencode-client"]).toBe("pi");
|
||||
});
|
||||
|
||||
it("lets configured OpenCode headers override the defaults", async () => {
|
||||
const headers = await captureHeaders(createModel("opencode", "https://opencode.ai/zen/v1"), {
|
||||
sessionId: "opencode-session",
|
||||
providerHeaders: {
|
||||
"x-opencode-session": "configured-session",
|
||||
"x-opencode-client": "configured-client",
|
||||
},
|
||||
});
|
||||
|
||||
expect(headers?.["x-opencode-session"]).toBe("configured-session");
|
||||
expect(headers?.["x-opencode-client"]).toBe("configured-client");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
# Mirrors .github/workflows/build-binaries.yml
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build-binaries.sh [--skip-deps] [--platform <platform>]
|
||||
# ./scripts/build-binaries.sh [--skip-install] [--skip-deps] [--skip-build] [--platform <platform>] [--out <dir>]
|
||||
#
|
||||
# Options:
|
||||
# --skip-install Skip npm ci
|
||||
# --skip-deps Skip installing cross-platform dependencies
|
||||
# --skip-build Skip npm run build
|
||||
# --platform <name> Build only for specified platform (darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64, windows-arm64)
|
||||
# --out <dir> Output directory (default: packages/coding-agent/binaries)
|
||||
#
|
||||
# Output:
|
||||
# packages/coding-agent/binaries/
|
||||
@@ -23,19 +26,34 @@ set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
SKIP_INSTALL=false
|
||||
SKIP_DEPS=false
|
||||
SKIP_BUILD=false
|
||||
PLATFORM=""
|
||||
OUTPUT_DIR=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--skip-install)
|
||||
SKIP_INSTALL=true
|
||||
shift
|
||||
;;
|
||||
--skip-deps)
|
||||
SKIP_DEPS=true
|
||||
shift
|
||||
;;
|
||||
--skip-build)
|
||||
SKIP_BUILD=true
|
||||
shift
|
||||
;;
|
||||
--platform)
|
||||
PLATFORM="$2"
|
||||
shift 2
|
||||
;;
|
||||
--out)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
@@ -56,8 +74,19 @@ if [[ -n "$PLATFORM" ]]; then
|
||||
esac
|
||||
fi
|
||||
|
||||
echo "==> Installing dependencies..."
|
||||
npm ci --ignore-scripts
|
||||
if [[ -z "$OUTPUT_DIR" ]]; then
|
||||
OUTPUT_DIR="packages/coding-agent/binaries"
|
||||
fi
|
||||
if [[ "$OUTPUT_DIR" != /* ]]; then
|
||||
OUTPUT_DIR="$(pwd)/$OUTPUT_DIR"
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_INSTALL" == "false" ]]; then
|
||||
echo "==> Installing dependencies..."
|
||||
npm ci --ignore-scripts
|
||||
else
|
||||
echo "==> Skipping npm ci (--skip-install)"
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_DEPS" == "false" ]]; then
|
||||
echo "==> Installing cross-platform native bindings..."
|
||||
@@ -66,35 +95,29 @@ if [[ "$SKIP_DEPS" == "false" ]]; then
|
||||
# Use --force to bypass platform checks (os/cpu restrictions in package.json)
|
||||
# Install all in one command to avoid npm removing packages from previous installs
|
||||
npm install --no-save --package-lock=false --force --ignore-scripts \
|
||||
@mariozechner/clipboard-darwin-arm64@0.3.2 \
|
||||
@mariozechner/clipboard-darwin-x64@0.3.2 \
|
||||
@mariozechner/clipboard-linux-x64-gnu@0.3.2 \
|
||||
@mariozechner/clipboard-linux-arm64-gnu@0.3.2 \
|
||||
@mariozechner/clipboard-win32-x64-msvc@0.3.2 \
|
||||
@mariozechner/clipboard-win32-arm64-msvc@0.3.2 \
|
||||
@img/sharp-darwin-arm64@0.34.5 \
|
||||
@img/sharp-darwin-x64@0.34.5 \
|
||||
@img/sharp-linux-x64@0.34.5 \
|
||||
@img/sharp-linux-arm64@0.34.5 \
|
||||
@img/sharp-win32-x64@0.34.5 \
|
||||
@img/sharp-win32-arm64@0.34.5 \
|
||||
@img/sharp-libvips-darwin-arm64@1.2.4 \
|
||||
@img/sharp-libvips-darwin-x64@1.2.4 \
|
||||
@img/sharp-libvips-linux-x64@1.2.4 \
|
||||
@img/sharp-libvips-linux-arm64@1.2.4
|
||||
@mariozechner/clipboard-darwin-arm64@0.3.6 \
|
||||
@mariozechner/clipboard-darwin-x64@0.3.6 \
|
||||
@mariozechner/clipboard-linux-x64-gnu@0.3.6 \
|
||||
@mariozechner/clipboard-linux-arm64-gnu@0.3.6 \
|
||||
@mariozechner/clipboard-win32-x64-msvc@0.3.6 \
|
||||
@mariozechner/clipboard-win32-arm64-msvc@0.3.6
|
||||
else
|
||||
echo "==> Skipping cross-platform native bindings (--skip-deps)"
|
||||
fi
|
||||
|
||||
echo "==> Building all packages..."
|
||||
npm run build
|
||||
if [[ "$SKIP_BUILD" == "false" ]]; then
|
||||
echo "==> Building all packages..."
|
||||
npm run build
|
||||
else
|
||||
echo "==> Skipping package build (--skip-build)"
|
||||
fi
|
||||
|
||||
echo "==> Building binaries..."
|
||||
cd packages/coding-agent
|
||||
|
||||
# Clean previous builds
|
||||
rm -rf binaries
|
||||
mkdir -p binaries/{darwin-arm64,darwin-x64,linux-x64,linux-arm64,windows-x64,windows-arm64}
|
||||
rm -rf "$OUTPUT_DIR"
|
||||
mkdir -p "$OUTPUT_DIR"/{darwin-arm64,darwin-x64,linux-x64,linux-arm64,windows-x64,windows-arm64}
|
||||
|
||||
# Determine which platforms to build
|
||||
if [[ -n "$PLATFORM" ]]; then
|
||||
@@ -109,9 +132,9 @@ for platform in "${PLATFORMS[@]}"; do
|
||||
# explicit build entrypoints. The runtime can still use new URL(...), but the
|
||||
# worker must be present in the compiled executable.
|
||||
if [[ "$platform" == windows-* ]]; then
|
||||
bun build --compile --target=bun-$platform ./dist/bun/cli.js ./dist/utils/image-resize-worker.js --outfile binaries/$platform/pi.exe
|
||||
bun build --compile --target=bun-$platform ./dist/bun/cli.js ./dist/utils/image-resize-worker.js --outfile "$OUTPUT_DIR/$platform/pi.exe"
|
||||
else
|
||||
bun build --compile --target=bun-$platform ./dist/bun/cli.js ./dist/utils/image-resize-worker.js --outfile binaries/$platform/pi
|
||||
bun build --compile --target=bun-$platform ./dist/bun/cli.js ./dist/utils/image-resize-worker.js --outfile "$OUTPUT_DIR/$platform/pi"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -119,17 +142,41 @@ echo "==> Creating release archives..."
|
||||
|
||||
# Copy shared files to each platform directory
|
||||
for platform in "${PLATFORMS[@]}"; do
|
||||
cp package.json binaries/$platform/
|
||||
cp README.md binaries/$platform/
|
||||
cp CHANGELOG.md binaries/$platform/
|
||||
cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm binaries/$platform/
|
||||
mkdir -p binaries/$platform/theme
|
||||
cp dist/modes/interactive/theme/*.json binaries/$platform/theme/
|
||||
mkdir -p binaries/$platform/assets
|
||||
cp dist/modes/interactive/assets/* binaries/$platform/assets/
|
||||
cp -r dist/core/export-html binaries/$platform/
|
||||
cp -r docs binaries/$platform/
|
||||
cp -r examples binaries/$platform/
|
||||
cp package.json "$OUTPUT_DIR/$platform/"
|
||||
cp README.md "$OUTPUT_DIR/$platform/"
|
||||
cp CHANGELOG.md "$OUTPUT_DIR/$platform/"
|
||||
cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm "$OUTPUT_DIR/$platform/"
|
||||
mkdir -p "$OUTPUT_DIR/$platform/theme"
|
||||
cp dist/modes/interactive/theme/*.json "$OUTPUT_DIR/$platform/theme/"
|
||||
mkdir -p "$OUTPUT_DIR/$platform/assets"
|
||||
cp dist/modes/interactive/assets/* "$OUTPUT_DIR/$platform/assets/"
|
||||
cp -r dist/core/export-html "$OUTPUT_DIR/$platform/"
|
||||
cp -r docs "$OUTPUT_DIR/$platform/"
|
||||
cp -r examples "$OUTPUT_DIR/$platform/"
|
||||
|
||||
case "$platform" in
|
||||
darwin-arm64)
|
||||
clipboard_native_package="clipboard-darwin-arm64"
|
||||
;;
|
||||
darwin-x64)
|
||||
clipboard_native_package="clipboard-darwin-x64"
|
||||
;;
|
||||
linux-x64)
|
||||
clipboard_native_package="clipboard-linux-x64-gnu"
|
||||
;;
|
||||
linux-arm64)
|
||||
clipboard_native_package="clipboard-linux-arm64-gnu"
|
||||
;;
|
||||
windows-x64)
|
||||
clipboard_native_package="clipboard-win32-x64-msvc"
|
||||
;;
|
||||
windows-arm64)
|
||||
clipboard_native_package="clipboard-win32-arm64-msvc"
|
||||
;;
|
||||
esac
|
||||
mkdir -p "$OUTPUT_DIR/$platform/node_modules/@mariozechner"
|
||||
cp -r ../../node_modules/@mariozechner/clipboard "$OUTPUT_DIR/$platform/node_modules/@mariozechner/"
|
||||
cp -r ../../node_modules/@mariozechner/$clipboard_native_package "$OUTPUT_DIR/$platform/node_modules/@mariozechner/"
|
||||
|
||||
# Copy Windows VT input native helper next to compiled Windows binaries.
|
||||
if [[ "$platform" == windows-* ]]; then
|
||||
@@ -138,47 +185,47 @@ for platform in "${PLATFORMS[@]}"; do
|
||||
else
|
||||
win32_arch_dir="win32-x64"
|
||||
fi
|
||||
mkdir -p binaries/$platform/native/win32/prebuilds/$win32_arch_dir
|
||||
cp ../tui/native/win32/prebuilds/$win32_arch_dir/win32-console-mode.node binaries/$platform/native/win32/prebuilds/$win32_arch_dir/
|
||||
mkdir -p "$OUTPUT_DIR/$platform/native/win32/prebuilds/$win32_arch_dir"
|
||||
cp ../tui/native/win32/prebuilds/$win32_arch_dir/win32-console-mode.node "$OUTPUT_DIR/$platform/native/win32/prebuilds/$win32_arch_dir/"
|
||||
fi
|
||||
done
|
||||
|
||||
# Create archives
|
||||
cd binaries
|
||||
cd "$OUTPUT_DIR"
|
||||
|
||||
for platform in "${PLATFORMS[@]}"; do
|
||||
if [[ "$platform" == windows-* ]]; then
|
||||
# Windows (zip)
|
||||
echo "Creating pi-$platform.zip..."
|
||||
(cd $platform && zip -r ../pi-$platform.zip .)
|
||||
(cd "$platform" && zip -r ../pi-$platform.zip .)
|
||||
else
|
||||
# Unix platforms (tar.gz) - use wrapper directory for mise compatibility
|
||||
echo "Creating pi-$platform.tar.gz..."
|
||||
mv $platform pi && tar -czf pi-$platform.tar.gz pi && mv pi $platform
|
||||
mv "$platform" pi && tar -czf pi-$platform.tar.gz pi && mv pi "$platform"
|
||||
fi
|
||||
done
|
||||
|
||||
# Extract archives for easy local testing
|
||||
echo "==> Extracting archives for testing..."
|
||||
for platform in "${PLATFORMS[@]}"; do
|
||||
rm -rf $platform
|
||||
rm -rf "$platform"
|
||||
if [[ "$platform" == windows-* ]]; then
|
||||
mkdir -p $platform && (cd $platform && unzip -q ../pi-$platform.zip)
|
||||
mkdir -p "$platform" && (cd "$platform" && unzip -q ../pi-$platform.zip)
|
||||
else
|
||||
tar -xzf pi-$platform.tar.gz && mv pi $platform
|
||||
tar -xzf pi-$platform.tar.gz && mv pi "$platform"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "==> Build complete!"
|
||||
echo "Archives available in packages/coding-agent/binaries/"
|
||||
echo "Archives available in $OUTPUT_DIR/"
|
||||
ls -lh *.tar.gz *.zip 2>/dev/null || true
|
||||
echo ""
|
||||
echo "Extracted directories for testing:"
|
||||
for platform in "${PLATFORMS[@]}"; do
|
||||
if [[ "$platform" == windows-* ]]; then
|
||||
echo " binaries/$platform/pi.exe"
|
||||
echo " $OUTPUT_DIR/$platform/pi.exe"
|
||||
else
|
||||
echo " binaries/$platform/pi"
|
||||
echo " $OUTPUT_DIR/$platform/pi"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
||||
import { isAbsolute, join, relative, resolve } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const packages = [
|
||||
@@ -131,48 +131,25 @@ function currentBinaryPlatform() {
|
||||
throw new Error(`Unsupported binary platform: ${process.platform} ${process.arch}`);
|
||||
}
|
||||
|
||||
function copyBinaryAssets(targetDirectory) {
|
||||
const codingAgentDirectory = join(repoRoot, "packages", "coding-agent");
|
||||
const distDirectory = join(codingAgentDirectory, "dist");
|
||||
cpSync(join(codingAgentDirectory, "package.json"), join(targetDirectory, "package.json"));
|
||||
cpSync(join(codingAgentDirectory, "README.md"), join(targetDirectory, "README.md"));
|
||||
cpSync(join(codingAgentDirectory, "CHANGELOG.md"), join(targetDirectory, "CHANGELOG.md"));
|
||||
cpSync(join(repoRoot, "node_modules", "@silvia-odwyer", "photon-node", "photon_rs_bg.wasm"), join(targetDirectory, "photon_rs_bg.wasm"));
|
||||
cpSync(join(distDirectory, "modes", "interactive", "theme"), join(targetDirectory, "theme"), { recursive: true });
|
||||
cpSync(join(distDirectory, "modes", "interactive", "assets"), join(targetDirectory, "assets"), { recursive: true });
|
||||
cpSync(join(distDirectory, "core", "export-html"), join(targetDirectory, "export-html"), { recursive: true });
|
||||
cpSync(join(codingAgentDirectory, "docs"), join(targetDirectory, "docs"), { recursive: true });
|
||||
cpSync(join(codingAgentDirectory, "examples"), join(targetDirectory, "examples"), { recursive: true });
|
||||
}
|
||||
|
||||
function copyWindowsConsoleModeHelper(targetDirectory, platform) {
|
||||
if (!platform.startsWith("windows-")) return;
|
||||
const win32Arch = platform === "windows-arm64" ? "win32-arm64" : "win32-x64";
|
||||
const relativeNativePath = join("native", "win32", "prebuilds", win32Arch);
|
||||
mkdirSync(join(targetDirectory, relativeNativePath), { recursive: true });
|
||||
cpSync(
|
||||
join(repoRoot, "packages", "tui", "native", "win32", "prebuilds", win32Arch, "win32-console-mode.node"),
|
||||
join(targetDirectory, relativeNativePath, "win32-console-mode.node"),
|
||||
);
|
||||
}
|
||||
|
||||
function buildBunBinaryRelease(targetDirectory, archiveDirectory) {
|
||||
if (!commandExists("bun")) {
|
||||
throw new Error("Bun is required for the local binary release build.");
|
||||
}
|
||||
const platform = currentBinaryPlatform();
|
||||
mkdirSync(targetDirectory, { recursive: true });
|
||||
const executableName = platform.startsWith("windows-") ? "pi.exe" : "pi";
|
||||
const executablePath = join(targetDirectory, executableName);
|
||||
const target = `bun-${platform}`;
|
||||
run("bun", ["build", "--compile", `--target=${target}`, join(repoRoot, "packages", "coding-agent", "dist", "bun", "cli.js"), "--outfile", executablePath]);
|
||||
copyBinaryAssets(targetDirectory);
|
||||
copyWindowsConsoleModeHelper(targetDirectory, platform);
|
||||
if (platform.startsWith("windows-")) {
|
||||
run("powershell", ["-NoProfile", "-Command", `Compress-Archive -Path '${join(targetDirectory, "*").replaceAll("'", "''")}' -DestinationPath '${join(archiveDirectory, `pi-${platform}.zip`).replaceAll("'", "''")}' -Force`]);
|
||||
} else {
|
||||
run("tar", ["-czf", join(archiveDirectory, `pi-${platform}.tar.gz`), "-C", targetDirectory, "."]);
|
||||
}
|
||||
const binaryBuildDirectory = join(archiveDirectory, "binary-build");
|
||||
run("./scripts/build-binaries.sh", [
|
||||
"--skip-install",
|
||||
"--skip-deps",
|
||||
"--skip-build",
|
||||
"--platform",
|
||||
platform,
|
||||
"--out",
|
||||
binaryBuildDirectory,
|
||||
]);
|
||||
rmSync(targetDirectory, { force: true, recursive: true });
|
||||
cpSync(join(binaryBuildDirectory, platform), targetDirectory, { recursive: true });
|
||||
const archiveName = platform.startsWith("windows-") ? `pi-${platform}.zip` : `pi-${platform}.tar.gz`;
|
||||
cpSync(join(binaryBuildDirectory, archiveName), join(archiveDirectory, archiveName));
|
||||
return platform;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user