chore: remove changeset negation rules from .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-30 08:36:32 -07:00
parent d9490bcab4
commit 442238196e
13 changed files with 570 additions and 240 deletions

View File

@@ -9,8 +9,8 @@ import { getModels } from "@mariozechner/pi-ai";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { streamViaCli } from "./src/provider.js";
import {
validateCliPresence,
validateCliAuth,
validateCliPresenceAsync,
validateCliAuthAsync,
killAllProcesses,
} from "./src/process-manager.js";
import { createHash } from "node:crypto";
@@ -26,6 +26,31 @@ process.on("exit", killAllProcesses);
const PROVIDER_ID = "pi-claude-cli";
/**
* Run CLI presence + auth probes at most once per process, asynchronously.
*
* The factory below is invoked on every `createFnAgent` call (the dashboard
* does this per chat message). Doing the probes synchronously with execSync
* froze the entire Node event loop for a few seconds while `claude` cold-
* started. Memoizing as a Promise + spawning the probes async means the
* factory returns immediately and other requests keep flowing; the result
* is logged once on first run and reused thereafter.
*/
let cliValidationPromise: Promise<void> | undefined;
function runCliValidationOnce(): Promise<void> {
if (cliValidationPromise) return cliValidationPromise;
cliValidationPromise = (async () => {
const presence = await validateCliPresenceAsync();
if (!presence.ok) {
console.warn(`[pi-claude-cli] ${presence.error.message}`);
return;
}
await validateCliAuthAsync();
})();
return cliValidationPromise;
}
let cachedMcpConfig: { hash: string; configPath: string } | undefined;
const DEBUG_MCP = process.env.PI_CLAUDE_CLI_DEBUG === "1";
@@ -116,9 +141,10 @@ function ensureMcpConfig(
export default function (pi: ExtensionAPI) {
try {
// Startup validation
validateCliPresence(); // throws if CLI not on PATH
validateCliAuth(); // warns if not authenticated
// Startup validation: kick off async, memoized presence + auth probes
// without blocking the factory. Failures surface via warnings; the actual
// `claude` subprocess in streamViaCli still reports hard errors on send.
void runCliValidationOnce();
const catalogModels = getModels("anthropic").map((model) => ({
id: model.id,

View File

@@ -168,10 +168,10 @@ describe("provider registration (default export)", () => {
});
});
describe("streamViaCli", () => {
describe("streamViaCli", { timeout: 90_000 }, () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
vi.spyOn(console, "warn").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
delete process.env.PI_CLAUDE_CLI_DEBUG;
@@ -183,7 +183,7 @@ describe("streamViaCli", () => {
delete process.env.PI_CLAUDE_CLI_DEBUG;
});
it("returns an AssistantMessageEventStream", () => {
it("returns an AssistantMessageEventStream", async () => {
const model = mockModels[0] as any;
const context = {
messages: [{ role: "user", content: "Hello" }],
@@ -194,6 +194,16 @@ describe("streamViaCli", () => {
expect(result).toBeDefined();
expect(result.push).toBeDefined();
expect(result.end).toBeDefined();
// Ensure the spawned process/readline lifecycle completes so fake timers
// don't leave the test hanging on the inactivity timeout.
await vi.advanceTimersByTimeAsync(0);
const proc = (spawn as any).mock.results[0].value;
proc.stdout.write(
`${JSON.stringify({ type: "result", subtype: "success", result: "ok" })}\n`,
);
proc.stdout.end();
await vi.advanceTimersByTimeAsync(0);
});
it("logs PID and spawn args when debug mode is enabled", async () => {

View File

@@ -241,3 +241,68 @@ export function validateCliAuth(): boolean {
return false;
}
}
/**
* Run a one-shot `claude <args>` and resolve to the exit code.
*
* Why: the sync execSync variants block the Node event loop for the duration
* of a Claude CLI cold start (13s, occasionally longer). When pi-claude-cli's
* factory is invoked from a per-request createFnAgent path (Fusion dashboard
* does this on every chat send), those sync probes freeze every other request.
* This async variant uses spawn so the loop keeps turning while the subprocess
* starts up.
*/
function runClaudeProbe(args: string[], timeoutMs = 5000): Promise<number> {
return new Promise((resolve) => {
const proc = spawn("claude", args, { stdio: "ignore" });
const timer = setTimeout(() => {
try {
proc.kill("SIGKILL");
} catch {
// already dead
}
resolve(124);
}, timeoutMs);
proc.once("error", () => {
clearTimeout(timer);
resolve(127);
});
proc.once("exit", (code) => {
clearTimeout(timer);
resolve(code ?? 1);
});
});
}
/**
* Async, non-blocking variant of validateCliPresence.
* Resolves with `{ok: true}` on success, `{ok: false, error}` on failure —
* never rejects, so callers can fire-and-forget without unhandled rejections.
*/
export async function validateCliPresenceAsync(): Promise<
{ ok: true } | { ok: false; error: Error }
> {
const code = await runClaudeProbe(["--version"]);
if (code === 0) return { ok: true };
return {
ok: false,
error: new Error(
"Claude Code CLI not found. Install it: npm install -g @anthropic-ai/claude-code\n" +
"Then authenticate: claude auth login",
),
};
}
/**
* Async, non-blocking variant of validateCliAuth.
* Returns true if authenticated. Logs a warning (does not throw) otherwise.
*/
export async function validateCliAuthAsync(): Promise<boolean> {
const code = await runClaudeProbe(["auth", "status"]);
if (code === 0) return true;
console.warn(
"[pi-claude-cli] Claude CLI is not authenticated. " +
"Run 'claude auth login' to authenticate.",
);
return false;
}