chore: remove changeset negation rules from .gitignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 (1–3s, 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user