refactor: move agent-specific logic from CLI into agent plugins
detectActivity, introspect, and getLaunchCommand now live in the agent plugins (claude-code, codex, aider) instead of being hardcoded in the CLI commands. The Agent interface's detectActivity takes a plain string of terminal output instead of a Session object, making plugins trivially unit-testable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
56e3f0b345
commit
ac424c1117
|
|
@ -1,8 +1,9 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
const { mockTmux, mockExec } = vi.hoisted(() => ({
|
||||
const { mockTmux, mockExec, mockDetectActivity } = vi.hoisted(() => ({
|
||||
mockTmux: vi.fn(),
|
||||
mockExec: vi.fn(),
|
||||
mockDetectActivity: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../src/lib/shell.js", () => ({
|
||||
|
|
@ -13,6 +14,29 @@ vi.mock("../../src/lib/shell.js", () => ({
|
|||
gh: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../src/lib/plugins.js", () => ({
|
||||
getAgent: () => ({
|
||||
name: "claude-code",
|
||||
processName: "claude",
|
||||
detectActivity: mockDetectActivity,
|
||||
}),
|
||||
getAgentByName: () => ({
|
||||
name: "claude-code",
|
||||
processName: "claude",
|
||||
detectActivity: mockDetectActivity,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../src/lib/session-utils.js", () => ({
|
||||
findProjectForSession: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("@agent-orchestrator/core", () => ({
|
||||
loadConfig: () => {
|
||||
throw new Error("no config");
|
||||
},
|
||||
}));
|
||||
|
||||
import { Command } from "commander";
|
||||
import { registerSend } from "../../src/commands/send.js";
|
||||
|
||||
|
|
@ -33,6 +57,7 @@ beforeEach(() => {
|
|||
});
|
||||
mockTmux.mockReset();
|
||||
mockExec.mockReset();
|
||||
mockDetectActivity.mockReset();
|
||||
mockExec.mockResolvedValue({ stdout: "", stderr: "" });
|
||||
});
|
||||
|
||||
|
|
@ -57,36 +82,23 @@ describe("send command", () => {
|
|||
});
|
||||
|
||||
describe("busy detection", () => {
|
||||
it("detects idle session (prompt character)", async () => {
|
||||
it("detects idle session via agent plugin", async () => {
|
||||
// has-session succeeds
|
||||
mockTmux.mockImplementation(async (...args: string[]) => {
|
||||
if (args[0] === "has-session") return "";
|
||||
if (args[0] === "capture-pane") {
|
||||
// Check which -S value to determine context
|
||||
const sIdx = args.indexOf("-S");
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-5") {
|
||||
return "some output\n❯ ";
|
||||
}
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-5") return "some output\n❯ ";
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-10") return "esc to interrupt\nThinking";
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
// isProcessing should detect processing after send
|
||||
mockTmux.mockImplementation(async (...args: string[]) => {
|
||||
if (args[0] === "has-session") return "";
|
||||
if (args[0] === "capture-pane") {
|
||||
const sIdx = args.indexOf("-S");
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-5") {
|
||||
return "some output\n❯ ";
|
||||
}
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-10") {
|
||||
return "Thinking about your request\nesc to interrupt";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
// Agent detects idle for wait-for-idle, then active for verification
|
||||
mockDetectActivity
|
||||
.mockReturnValueOnce("idle") // wait-for-idle check
|
||||
.mockReturnValueOnce("active"); // verification check
|
||||
|
||||
await program.parseAsync(["node", "test", "send", "my-session", "hello", "world"]);
|
||||
|
||||
|
|
@ -105,34 +117,19 @@ describe("send command", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("detects busy session (esc to interrupt)", async () => {
|
||||
let callCount = 0;
|
||||
it("detects busy session and waits via agent plugin", async () => {
|
||||
mockTmux.mockImplementation(async (...args: string[]) => {
|
||||
if (args[0] === "has-session") return "";
|
||||
if (args[0] === "capture-pane") {
|
||||
callCount++;
|
||||
const sIdx = args.indexOf("-S");
|
||||
// First few calls: session is busy
|
||||
if (callCount <= 2) {
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-5") {
|
||||
return "Working on something...";
|
||||
}
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-3") {
|
||||
return "esc to interrupt";
|
||||
}
|
||||
}
|
||||
// Then idle
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-5") {
|
||||
return "Done\n❯ ";
|
||||
}
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-10") {
|
||||
return "Thinking\nesc to interrupt";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
if (args[0] === "capture-pane") return "some output";
|
||||
return "";
|
||||
});
|
||||
|
||||
// First call: active (busy), second call: idle, third call: active (verification)
|
||||
mockDetectActivity
|
||||
.mockReturnValueOnce("active") // busy
|
||||
.mockReturnValueOnce("idle") // now idle
|
||||
.mockReturnValueOnce("active"); // verification: processing
|
||||
|
||||
await program.parseAsync(["node", "test", "send", "my-session", "fix", "the", "bug"]);
|
||||
|
||||
// Should have eventually sent the message
|
||||
|
|
@ -148,16 +145,13 @@ describe("send command", () => {
|
|||
it("skips busy detection with --no-wait", async () => {
|
||||
mockTmux.mockImplementation(async (...args: string[]) => {
|
||||
if (args[0] === "has-session") return "";
|
||||
if (args[0] === "capture-pane") {
|
||||
const sIdx = args.indexOf("-S");
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-10") {
|
||||
return "Thinking\nesc to interrupt";
|
||||
}
|
||||
return "busy\nesc to interrupt";
|
||||
}
|
||||
if (args[0] === "capture-pane") return "Thinking\nesc to interrupt";
|
||||
return "";
|
||||
});
|
||||
|
||||
// Agent detects active for verification
|
||||
mockDetectActivity.mockReturnValue("active");
|
||||
|
||||
await program.parseAsync(["node", "test", "send", "--no-wait", "my-session", "urgent"]);
|
||||
|
||||
// Should have sent the message without waiting
|
||||
|
|
@ -169,17 +163,17 @@ describe("send command", () => {
|
|||
if (args[0] === "has-session") return "";
|
||||
if (args[0] === "capture-pane") {
|
||||
const sIdx = args.indexOf("-S");
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-5") {
|
||||
return "Output\n❯ \nPress up to edit queued messages";
|
||||
}
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-10") {
|
||||
return "";
|
||||
}
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-5") return "Output\n❯ ";
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-10")
|
||||
return "Output\nPress up to edit queued messages";
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
// Agent detects idle for wait-for-idle, then idle for verification (not processing)
|
||||
mockDetectActivity.mockReturnValue("idle");
|
||||
|
||||
await program.parseAsync(["node", "test", "send", "my-session", "hello"]);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Message queued"));
|
||||
|
|
@ -190,15 +184,14 @@ describe("send command", () => {
|
|||
it("uses load-buffer for long messages", async () => {
|
||||
mockTmux.mockImplementation(async (...args: string[]) => {
|
||||
if (args[0] === "has-session") return "";
|
||||
if (args[0] === "capture-pane") {
|
||||
const sIdx = args.indexOf("-S");
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-5") return "❯ ";
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-10") return "esc to interrupt";
|
||||
return "";
|
||||
}
|
||||
if (args[0] === "capture-pane") return "❯ ";
|
||||
return "";
|
||||
});
|
||||
|
||||
mockDetectActivity
|
||||
.mockReturnValueOnce("idle") // wait-for-idle
|
||||
.mockReturnValueOnce("active"); // verification
|
||||
|
||||
const longMsg = "x".repeat(250);
|
||||
await program.parseAsync(["node", "test", "send", "my-session", longMsg]);
|
||||
|
||||
|
|
@ -210,15 +203,14 @@ describe("send command", () => {
|
|||
it("uses send-keys for short messages", async () => {
|
||||
mockTmux.mockImplementation(async (...args: string[]) => {
|
||||
if (args[0] === "has-session") return "";
|
||||
if (args[0] === "capture-pane") {
|
||||
const sIdx = args.indexOf("-S");
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-5") return "❯ ";
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-10") return "esc to interrupt";
|
||||
return "";
|
||||
}
|
||||
if (args[0] === "capture-pane") return "❯ ";
|
||||
return "";
|
||||
});
|
||||
|
||||
mockDetectActivity
|
||||
.mockReturnValueOnce("idle") // wait-for-idle
|
||||
.mockReturnValueOnce("active"); // verification
|
||||
|
||||
await program.parseAsync(["node", "test", "send", "my-session", "short", "msg"]);
|
||||
|
||||
expect(mockExec).toHaveBeenCalledWith("tmux", ["send-keys", "-t", "my-session", "-l", "short msg"]);
|
||||
|
|
@ -227,15 +219,14 @@ describe("send command", () => {
|
|||
it("clears partial input before sending", async () => {
|
||||
mockTmux.mockImplementation(async (...args: string[]) => {
|
||||
if (args[0] === "has-session") return "";
|
||||
if (args[0] === "capture-pane") {
|
||||
const sIdx = args.indexOf("-S");
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-5") return "❯ ";
|
||||
if (sIdx >= 0 && args[sIdx + 1] === "-10") return "esc to interrupt";
|
||||
return "";
|
||||
}
|
||||
if (args[0] === "capture-pane") return "❯ ";
|
||||
return "";
|
||||
});
|
||||
|
||||
mockDetectActivity
|
||||
.mockReturnValueOnce("idle") // wait-for-idle
|
||||
.mockReturnValueOnce("active"); // verification
|
||||
|
||||
await program.parseAsync(["node", "test", "send", "my-session", "hello"]);
|
||||
|
||||
// C-u should be called to clear input
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ import { mkdtempSync, mkdirSync, writeFileSync, existsSync, readFileSync, rmSync
|
|||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const { mockTmux, mockExec, mockGit, mockConfigRef } = vi.hoisted(() => ({
|
||||
const { mockTmux, mockExec, mockGit, mockConfigRef, mockGetAgent } = vi.hoisted(() => ({
|
||||
mockTmux: vi.fn(),
|
||||
mockExec: vi.fn(),
|
||||
mockGit: vi.fn(),
|
||||
mockConfigRef: { current: null as Record<string, unknown> | null },
|
||||
mockGetAgent: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../src/lib/shell.js", () => ({
|
||||
|
|
@ -43,6 +44,11 @@ vi.mock("@agent-orchestrator/core", () => ({
|
|||
loadConfig: () => mockConfigRef.current,
|
||||
}));
|
||||
|
||||
vi.mock("../../src/lib/plugins.js", () => ({
|
||||
getAgent: mockGetAgent,
|
||||
getAgentByName: mockGetAgent,
|
||||
}));
|
||||
|
||||
let tmpDir: string;
|
||||
|
||||
import { Command } from "commander";
|
||||
|
|
@ -94,7 +100,15 @@ beforeEach(() => {
|
|||
mockTmux.mockReset();
|
||||
mockExec.mockReset();
|
||||
mockGit.mockReset();
|
||||
mockGetAgent.mockReset();
|
||||
mockExec.mockResolvedValue({ stdout: "", stderr: "" });
|
||||
mockGetAgent.mockReturnValue({
|
||||
name: "claude-code",
|
||||
processName: "claude",
|
||||
getLaunchCommand: () => "unset CLAUDECODE && claude",
|
||||
getEnvironment: () => ({}),
|
||||
detectActivity: () => "idle",
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const { mockTmux, mockGit, mockConfigRef } = vi.hoisted(() => ({
|
||||
const { mockTmux, mockGit, mockConfigRef, mockIntrospect } = vi.hoisted(() => ({
|
||||
mockTmux: vi.fn(),
|
||||
mockGit: vi.fn(),
|
||||
mockConfigRef: { current: null as Record<string, unknown> | null },
|
||||
mockIntrospect: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../src/lib/shell.js", () => ({
|
||||
|
|
@ -32,6 +33,21 @@ vi.mock("@agent-orchestrator/core", () => ({
|
|||
loadConfig: () => mockConfigRef.current,
|
||||
}));
|
||||
|
||||
vi.mock("../../src/lib/plugins.js", () => ({
|
||||
getAgent: () => ({
|
||||
name: "claude-code",
|
||||
processName: "claude",
|
||||
detectActivity: () => "idle",
|
||||
introspect: mockIntrospect,
|
||||
}),
|
||||
getAgentByName: () => ({
|
||||
name: "claude-code",
|
||||
processName: "claude",
|
||||
detectActivity: () => "idle",
|
||||
introspect: mockIntrospect,
|
||||
}),
|
||||
}));
|
||||
|
||||
let tmpDir: string;
|
||||
|
||||
import { Command } from "commander";
|
||||
|
|
@ -76,6 +92,8 @@ beforeEach(() => {
|
|||
});
|
||||
mockTmux.mockReset();
|
||||
mockGit.mockReset();
|
||||
mockIntrospect.mockReset();
|
||||
mockIntrospect.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@agent-orchestrator/core": "workspace:*",
|
||||
"@agent-orchestrator/plugin-agent-claude-code": "workspace:*",
|
||||
"@agent-orchestrator/plugin-agent-codex": "workspace:*",
|
||||
"@agent-orchestrator/plugin-agent-aider": "workspace:*",
|
||||
"chalk": "^5.4.0",
|
||||
"commander": "^13.0.0",
|
||||
"ora": "^8.1.0",
|
||||
|
|
|
|||
|
|
@ -3,50 +3,52 @@ import { tmpdir } from "node:os";
|
|||
import { join } from "node:path";
|
||||
import chalk from "chalk";
|
||||
import type { Command } from "commander";
|
||||
import { type Agent, loadConfig } from "@agent-orchestrator/core";
|
||||
import { exec, tmux } from "../lib/shell.js";
|
||||
import { getAgent, getAgentByName } from "../lib/plugins.js";
|
||||
import { findProjectForSession } from "../lib/session-utils.js";
|
||||
|
||||
async function sessionExists(session: string): Promise<boolean> {
|
||||
const result = await tmux("has-session", "-t", session);
|
||||
return result !== null;
|
||||
}
|
||||
|
||||
async function isBusy(session: string): Promise<boolean> {
|
||||
const output = await tmux("capture-pane", "-t", session, "-p", "-S", "-5");
|
||||
if (!output) return false;
|
||||
|
||||
const lines = output.split("\n").filter(Boolean);
|
||||
const lastLine = lines[lines.length - 1] || "";
|
||||
|
||||
// Idle indicators
|
||||
if (/[❯$⏵]|bypass permissions/.test(lastLine)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Active indicators
|
||||
const recentOutput = await tmux("capture-pane", "-t", session, "-p", "-S", "-3");
|
||||
if (recentOutput && recentOutput.includes("esc to interrupt")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
async function captureOutput(session: string, lines: number): Promise<string> {
|
||||
const output = await tmux("capture-pane", "-t", session, "-p", "-S", String(-lines));
|
||||
return output || "";
|
||||
}
|
||||
|
||||
async function isProcessing(session: string): Promise<boolean> {
|
||||
const output = await tmux("capture-pane", "-t", session, "-p", "-S", "-10");
|
||||
if (!output) return false;
|
||||
return /Thinking|Running|esc to interrupt|⏺/.test(output);
|
||||
function isBusy(agent: Agent, terminalOutput: string): boolean {
|
||||
const state = agent.detectActivity(terminalOutput);
|
||||
return state === "active";
|
||||
}
|
||||
|
||||
async function hasQueuedMessage(session: string): Promise<boolean> {
|
||||
const output = await tmux("capture-pane", "-t", session, "-p", "-S", "-5");
|
||||
if (!output) return false;
|
||||
return output.includes("Press up to edit queued messages");
|
||||
function isProcessing(agent: Agent, terminalOutput: string): boolean {
|
||||
const state = agent.detectActivity(terminalOutput);
|
||||
return state === "active";
|
||||
}
|
||||
|
||||
function hasQueuedMessage(terminalOutput: string): boolean {
|
||||
return terminalOutput.includes("Press up to edit queued messages");
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function resolveAgent(sessionName: string): Agent {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const projectId = findProjectForSession(config, sessionName);
|
||||
if (projectId) {
|
||||
return getAgent(config, projectId);
|
||||
}
|
||||
} catch {
|
||||
// No config or project — fall back to default
|
||||
}
|
||||
return getAgentByName("claude-code");
|
||||
}
|
||||
|
||||
export function registerSend(program: Command): void {
|
||||
program
|
||||
.command("send")
|
||||
|
|
@ -67,6 +69,8 @@ export function registerSend(program: Command): void {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
const agent = resolveAgent(session);
|
||||
|
||||
const parsedTimeout = parseInt(opts.timeout || "600", 10);
|
||||
const timeoutMs = (isNaN(parsedTimeout) || parsedTimeout <= 0 ? 600 : parsedTimeout) * 1000;
|
||||
|
||||
|
|
@ -74,7 +78,7 @@ export function registerSend(program: Command): void {
|
|||
if (opts.wait !== false) {
|
||||
const start = Date.now();
|
||||
let warned = false;
|
||||
while (await isBusy(session)) {
|
||||
while (isBusy(agent, await captureOutput(session, 5))) {
|
||||
if (!warned) {
|
||||
console.log(chalk.dim(`Waiting for ${session} to become idle...`));
|
||||
warned = true;
|
||||
|
|
@ -136,11 +140,12 @@ export function registerSend(program: Command): void {
|
|||
// Verify delivery with retries
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
await sleep(2000);
|
||||
if (await isProcessing(session)) {
|
||||
const output = await captureOutput(session, 10);
|
||||
if (isProcessing(agent, output)) {
|
||||
console.log(chalk.green("Message sent and processing"));
|
||||
return;
|
||||
}
|
||||
if (await hasQueuedMessage(session)) {
|
||||
if (hasQueuedMessage(output)) {
|
||||
console.log(chalk.green("Message queued (session finishing previous task)"));
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { loadConfig, type OrchestratorConfig } from "@agent-orchestrator/core";
|
|||
import { tmux, git, gh, getTmuxSessions, getTmuxActivity } from "../lib/shell.js";
|
||||
import { getSessionDir, readMetadata, archiveMetadata } from "../lib/metadata.js";
|
||||
import { formatAge } from "../lib/format.js";
|
||||
import { findProjectForSession } from "../lib/session-utils.js";
|
||||
|
||||
async function killSession(
|
||||
config: OrchestratorConfig,
|
||||
|
|
@ -39,16 +40,6 @@ async function killSession(
|
|||
console.log(chalk.green(` Archived metadata`));
|
||||
}
|
||||
|
||||
function findProjectForSession(config: OrchestratorConfig, sessionName: string): string | null {
|
||||
for (const [id, project] of Object.entries(config.projects)) {
|
||||
const prefix = project.sessionPrefix || id;
|
||||
if (sessionName.startsWith(`${prefix}-`)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function registerSession(program: Command): void {
|
||||
const session = program.command("session").description("Session management (ls, kill, cleanup)");
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { loadConfig, type OrchestratorConfig, type ProjectConfig } from "@agent-
|
|||
import { exec, git, getTmuxSessions } from "../lib/shell.js";
|
||||
import { getSessionDir, writeMetadata, findSessionForIssue } from "../lib/metadata.js";
|
||||
import { banner } from "../lib/format.js";
|
||||
import { getAgent } from "../lib/plugins.js";
|
||||
|
||||
function escapeRegex(str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
|
@ -134,20 +135,14 @@ async function spawnSession(
|
|||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
// Start agent
|
||||
const agentName = project.agent || config.defaults.agent;
|
||||
let launchCmd: string;
|
||||
if (agentName === "claude-code") {
|
||||
const perms =
|
||||
project.agentConfig?.permissions === "skip" ? " --dangerously-skip-permissions" : "";
|
||||
launchCmd = `unset CLAUDECODE && claude${perms}`;
|
||||
} else if (agentName === "codex") {
|
||||
launchCmd = "codex";
|
||||
} else if (agentName === "aider") {
|
||||
launchCmd = "aider";
|
||||
} else {
|
||||
launchCmd = agentName;
|
||||
}
|
||||
// Start agent via plugin
|
||||
const agent = getAgent(config, projectId);
|
||||
const launchCmd = agent.getLaunchCommand({
|
||||
sessionId: sessionName,
|
||||
projectConfig: project,
|
||||
issueId,
|
||||
permissions: project.agentConfig?.permissions,
|
||||
});
|
||||
|
||||
await exec("tmux", ["send-keys", "-t", sessionName, launchCmd, "Enter"]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import { readFileSync, readdirSync, existsSync, statSync, openSync, readSync, closeSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import chalk from "chalk";
|
||||
import type { Command } from "commander";
|
||||
import { loadConfig, type OrchestratorConfig } from "@agent-orchestrator/core";
|
||||
import { exec, git, getTmuxSessions, getTmuxActivity } from "../lib/shell.js";
|
||||
import {
|
||||
type Agent,
|
||||
type OrchestratorConfig,
|
||||
type Session,
|
||||
type RuntimeHandle,
|
||||
loadConfig,
|
||||
} from "@agent-orchestrator/core";
|
||||
import { git, getTmuxSessions, getTmuxActivity } from "../lib/shell.js";
|
||||
import { getSessionDir, readMetadata } from "../lib/metadata.js";
|
||||
import { banner, header, formatAge, statusColor } from "../lib/format.js";
|
||||
import { getAgent, getAgentByName } from "../lib/plugins.js";
|
||||
|
||||
interface SessionInfo {
|
||||
name: string;
|
||||
|
|
@ -20,95 +25,37 @@ interface SessionInfo {
|
|||
}
|
||||
|
||||
/**
|
||||
* Extracts Claude's auto-generated summary from its internal session data.
|
||||
* Maps: tmux session → TTY → Claude PID → CWD → .claude/projects/ → JSONL summary
|
||||
* Build a minimal Session object for agent.introspect().
|
||||
* Only runtimeHandle and workspacePath are needed by the introspection logic.
|
||||
*/
|
||||
async function getClaudeSessionInfo(
|
||||
sessionName: string,
|
||||
): Promise<{ summary: string | null; sessionId: string | null }> {
|
||||
try {
|
||||
// Get the TTY for this tmux session's pane
|
||||
const ttyOutput = await exec("tmux", [
|
||||
"display-message",
|
||||
"-t",
|
||||
sessionName,
|
||||
"-p",
|
||||
"#{pane_tty}",
|
||||
]);
|
||||
const tty = ttyOutput.stdout.trim();
|
||||
if (!tty) return { summary: null, sessionId: null };
|
||||
|
||||
// Find Claude PID running on that TTY (column-based match to avoid pts/1 matching pts/10)
|
||||
const psOutput = await exec("ps", ["-eo", "pid,tty,comm"]);
|
||||
const ttyShort = tty.replace("/dev/", "");
|
||||
const pidLine = psOutput.stdout.split("\n").find((line) => {
|
||||
const cols = line.trim().split(/\s+/);
|
||||
return cols.length >= 3 && cols[1] === ttyShort && cols[2].includes("claude");
|
||||
});
|
||||
const pid = pidLine?.trim().split(/\s+/)[0];
|
||||
if (!pid) return { summary: null, sessionId: null };
|
||||
|
||||
// Get Claude's working directory
|
||||
const cwdOutput = await exec("lsof", ["-p", pid, "-d", "cwd", "-Fn"]);
|
||||
const cwdMatch = cwdOutput.stdout.match(/n(.+)/);
|
||||
const cwd = cwdMatch?.[1];
|
||||
if (!cwd) return { summary: null, sessionId: null };
|
||||
|
||||
// Encode path for Claude's project directory naming
|
||||
const encodedPath = cwd.replace(/\//g, "-").replace(/^-/, "");
|
||||
const claudeProjectDir = join(process.env.HOME || "~", ".claude", "projects", encodedPath);
|
||||
|
||||
if (!existsSync(claudeProjectDir)) return { summary: null, sessionId: null };
|
||||
|
||||
// Find the most recent session file
|
||||
const files = readdirSync(claudeProjectDir)
|
||||
.filter((f) => f.endsWith(".jsonl"))
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
if (files.length === 0) return { summary: null, sessionId: null };
|
||||
|
||||
const sessionFile = join(claudeProjectDir, files[0]);
|
||||
const sessionId = files[0].replace(".jsonl", "").slice(0, 8);
|
||||
|
||||
// Read only the tail of the file (last ~8KB) to avoid loading large JSONL files
|
||||
const fileSize = statSync(sessionFile).size;
|
||||
const tailSize = Math.min(fileSize, 8192);
|
||||
let tail: string;
|
||||
if (tailSize === fileSize) {
|
||||
tail = readFileSync(sessionFile, "utf-8");
|
||||
} else {
|
||||
const buf = Buffer.alloc(tailSize);
|
||||
const fd = openSync(sessionFile, "r");
|
||||
try {
|
||||
readSync(fd, buf, 0, tailSize, fileSize - tailSize);
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
tail = buf.toString("utf-8");
|
||||
}
|
||||
const lines = tail.trim().split("\n").slice(-20);
|
||||
let summary: string | null = null;
|
||||
|
||||
for (const line of lines.reverse()) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
if (entry.type === "summary" || entry.summary) {
|
||||
summary = entry.summary || entry.message;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip non-JSON lines
|
||||
}
|
||||
}
|
||||
|
||||
return { summary, sessionId };
|
||||
} catch {
|
||||
return { summary: null, sessionId: null };
|
||||
}
|
||||
function buildSessionForIntrospect(sessionName: string, workspacePath?: string): Session {
|
||||
const handle: RuntimeHandle = {
|
||||
id: sessionName,
|
||||
runtimeName: "tmux",
|
||||
data: {},
|
||||
};
|
||||
return {
|
||||
id: sessionName,
|
||||
projectId: "",
|
||||
status: "working",
|
||||
activity: "idle",
|
||||
branch: null,
|
||||
issueId: null,
|
||||
pr: null,
|
||||
workspacePath: workspacePath || null,
|
||||
runtimeHandle: handle,
|
||||
agentInfo: null,
|
||||
createdAt: new Date(),
|
||||
lastActivityAt: new Date(),
|
||||
metadata: {},
|
||||
};
|
||||
}
|
||||
|
||||
async function gatherSessionInfo(sessionName: string, sessionDir: string): Promise<SessionInfo> {
|
||||
async function gatherSessionInfo(
|
||||
sessionName: string,
|
||||
sessionDir: string,
|
||||
agent: Agent,
|
||||
): Promise<SessionInfo> {
|
||||
const metaFile = `${sessionDir}/${sessionName}`;
|
||||
const meta = readMetadata(metaFile);
|
||||
|
||||
|
|
@ -130,15 +77,22 @@ async function gatherSessionInfo(sessionName: string, sessionDir: string): Promi
|
|||
const activityTs = await getTmuxActivity(sessionName);
|
||||
const lastActivity = activityTs ? formatAge(activityTs) : "-";
|
||||
|
||||
// Get Claude's auto-generated summary
|
||||
const claudeInfo = await getClaudeSessionInfo(sessionName);
|
||||
// Get agent's auto-generated summary via introspection
|
||||
let claudeSummary: string | null = null;
|
||||
try {
|
||||
const session = buildSessionForIntrospect(sessionName, worktree);
|
||||
const introspection = await agent.introspect(session);
|
||||
claudeSummary = introspection?.summary ?? null;
|
||||
} catch {
|
||||
// Introspection failed — not critical
|
||||
}
|
||||
|
||||
return {
|
||||
name: sessionName,
|
||||
branch,
|
||||
status,
|
||||
summary,
|
||||
claudeSummary: claudeInfo.summary,
|
||||
claudeSummary,
|
||||
pr,
|
||||
issue,
|
||||
lastActivity,
|
||||
|
|
@ -178,7 +132,6 @@ export function registerStatus(program: Command): void {
|
|||
} catch {
|
||||
console.log(chalk.yellow("No config found. Run `ao init` first."));
|
||||
console.log(chalk.dim("Falling back to session discovery...\n"));
|
||||
// Fall back to finding sessions without config
|
||||
await showFallbackStatus();
|
||||
return;
|
||||
}
|
||||
|
|
@ -206,6 +159,9 @@ export function registerStatus(program: Command): void {
|
|||
const sessionDir = getSessionDir(config.dataDir, projectId);
|
||||
const projectSessions = allTmux.filter((s) => s.startsWith(`${prefix}-`));
|
||||
|
||||
// Resolve agent for this project
|
||||
const agent = getAgent(config, projectId);
|
||||
|
||||
if (!opts.json) {
|
||||
console.log(header(projectConfig.name || projectId));
|
||||
}
|
||||
|
|
@ -221,7 +177,7 @@ export function registerStatus(program: Command): void {
|
|||
totalSessions += projectSessions.length;
|
||||
|
||||
for (const session of projectSessions.sort()) {
|
||||
const info = await gatherSessionInfo(session, sessionDir);
|
||||
const info = await gatherSessionInfo(session, sessionDir, agent);
|
||||
if (opts.json) {
|
||||
jsonOutput.push(info);
|
||||
} else {
|
||||
|
|
@ -257,10 +213,24 @@ async function showFallbackStatus(): Promise<void> {
|
|||
chalk.dim(` ${allTmux.length} tmux session${allTmux.length !== 1 ? "s" : ""} found\n`),
|
||||
);
|
||||
|
||||
// Use claude-code as default agent for fallback introspection
|
||||
const agent = getAgentByName("claude-code");
|
||||
|
||||
for (const session of allTmux.sort()) {
|
||||
const activityTs = await getTmuxActivity(session);
|
||||
const lastActivity = activityTs ? formatAge(activityTs) : "-";
|
||||
console.log(` ${chalk.green(session)} ${chalk.dim(`(${lastActivity})`)}`);
|
||||
|
||||
// Try introspection even without config
|
||||
try {
|
||||
const sessionObj = buildSessionForIntrospect(session);
|
||||
const introspection = await agent.introspect(sessionObj);
|
||||
if (introspection?.summary) {
|
||||
console.log(` ${chalk.dim("Claude:")} ${introspection.summary.slice(0, 65)}`);
|
||||
}
|
||||
} catch {
|
||||
// Not critical
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import type { Agent, OrchestratorConfig } from "@agent-orchestrator/core";
|
||||
import claudeCodePlugin from "@agent-orchestrator/plugin-agent-claude-code";
|
||||
import codexPlugin from "@agent-orchestrator/plugin-agent-codex";
|
||||
import aiderPlugin from "@agent-orchestrator/plugin-agent-aider";
|
||||
|
||||
const agentPlugins: Record<string, { create(): Agent }> = {
|
||||
"claude-code": claudeCodePlugin,
|
||||
codex: codexPlugin,
|
||||
aider: aiderPlugin,
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the Agent plugin for a project (or fall back to the config default).
|
||||
* Direct import — no dynamic loading needed since the CLI depends on all agent plugins.
|
||||
*/
|
||||
export function getAgent(config: OrchestratorConfig, projectId?: string): Agent {
|
||||
const agentName =
|
||||
(projectId ? config.projects[projectId]?.agent : undefined) || config.defaults.agent;
|
||||
const plugin = agentPlugins[agentName];
|
||||
if (!plugin) {
|
||||
throw new Error(`Unknown agent plugin: ${agentName}`);
|
||||
}
|
||||
return plugin.create();
|
||||
}
|
||||
|
||||
/** Get an agent by name directly (for fallback/no-config scenarios). */
|
||||
export function getAgentByName(name: string): Agent {
|
||||
const plugin = agentPlugins[name];
|
||||
if (!plugin) {
|
||||
throw new Error(`Unknown agent plugin: ${name}`);
|
||||
}
|
||||
return plugin.create();
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import type { OrchestratorConfig } from "@agent-orchestrator/core";
|
||||
|
||||
/** Find which project a session belongs to by matching its name against session prefixes. */
|
||||
export function findProjectForSession(
|
||||
config: OrchestratorConfig,
|
||||
sessionName: string,
|
||||
): string | null {
|
||||
for (const [id, project] of Object.entries(config.projects)) {
|
||||
const prefix = project.sessionPrefix || id;
|
||||
if (sessionName.startsWith(`${prefix}-`)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -67,7 +67,7 @@ beforeEach(() => {
|
|||
create: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
sendMessage: vi.fn().mockResolvedValue(undefined),
|
||||
getOutput: vi.fn(),
|
||||
getOutput: vi.fn().mockResolvedValue(""),
|
||||
isAlive: vi.fn().mockResolvedValue(true),
|
||||
};
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ beforeEach(() => {
|
|||
processName: "mock",
|
||||
getLaunchCommand: vi.fn(),
|
||||
getEnvironment: vi.fn(),
|
||||
detectActivity: vi.fn().mockResolvedValue("active" as ActivityState),
|
||||
detectActivity: vi.fn().mockReturnValue("active" as ActivityState),
|
||||
isProcessRunning: vi.fn(),
|
||||
isProcessing: vi.fn().mockResolvedValue(false),
|
||||
getSessionInfo: vi.fn().mockResolvedValue(null),
|
||||
|
|
@ -208,7 +208,7 @@ describe("check (single session)", () => {
|
|||
});
|
||||
|
||||
it("detects stuck state from agent", async () => {
|
||||
vi.mocked(mockAgent.detectActivity).mockResolvedValue("blocked");
|
||||
vi.mocked(mockAgent.detectActivity).mockReturnValue("blocked");
|
||||
|
||||
const session = makeSession({ status: "working" });
|
||||
vi.mocked(mockSessionManager.get).mockResolvedValue(session);
|
||||
|
|
@ -232,7 +232,7 @@ describe("check (single session)", () => {
|
|||
});
|
||||
|
||||
it("detects needs_input from agent", async () => {
|
||||
vi.mocked(mockAgent.detectActivity).mockResolvedValue("waiting_input");
|
||||
vi.mocked(mockAgent.detectActivity).mockReturnValue("waiting_input");
|
||||
|
||||
const session = makeSession({ status: "working" });
|
||||
vi.mocked(mockSessionManager.get).mockResolvedValue(session);
|
||||
|
|
@ -256,7 +256,9 @@ describe("check (single session)", () => {
|
|||
});
|
||||
|
||||
it("preserves stuck state when detectActivity throws", async () => {
|
||||
vi.mocked(mockAgent.detectActivity).mockRejectedValue(new Error("probe failed"));
|
||||
vi.mocked(mockAgent.detectActivity).mockImplementation(() => {
|
||||
throw new Error("probe failed");
|
||||
});
|
||||
|
||||
const session = makeSession({ status: "stuck" });
|
||||
vi.mocked(mockSessionManager.get).mockResolvedValue(session);
|
||||
|
|
@ -281,7 +283,9 @@ describe("check (single session)", () => {
|
|||
});
|
||||
|
||||
it("preserves needs_input state when detectActivity throws", async () => {
|
||||
vi.mocked(mockAgent.detectActivity).mockRejectedValue(new Error("probe failed"));
|
||||
vi.mocked(mockAgent.detectActivity).mockImplementation(() => {
|
||||
throw new Error("probe failed");
|
||||
});
|
||||
|
||||
const session = makeSession({ status: "needs_input" });
|
||||
vi.mocked(mockSessionManager.get).mockResolvedValue(session);
|
||||
|
|
|
|||
|
|
@ -190,10 +190,14 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan
|
|||
}
|
||||
}
|
||||
|
||||
// 2. Check agent activity
|
||||
if (agent) {
|
||||
// 2. Check agent activity via terminal output
|
||||
if (agent && session.runtimeHandle) {
|
||||
try {
|
||||
const activity = await agent.detectActivity(session);
|
||||
const runtime = registry.get<Runtime>("runtime", project.runtime ?? config.defaults.runtime);
|
||||
const terminalOutput = runtime
|
||||
? await runtime.getOutput(session.runtimeHandle, 10).catch(() => "")
|
||||
: "";
|
||||
const activity = agent.detectActivity(terminalOutput);
|
||||
if (activity === "exited") return "killed";
|
||||
if (activity === "blocked") return "stuck";
|
||||
if (activity === "waiting_input") return "needs_input";
|
||||
|
|
|
|||
|
|
@ -182,8 +182,8 @@ export interface Agent {
|
|||
/** Get environment variables for the agent process */
|
||||
getEnvironment(config: AgentLaunchConfig): Record<string, string>;
|
||||
|
||||
/** Detect what the agent is currently doing */
|
||||
detectActivity(session: Session): Promise<ActivityState>;
|
||||
/** Detect what the agent is currently doing from terminal output */
|
||||
detectActivity(terminalOutput: string): ActivityState;
|
||||
|
||||
/** Check if agent process is running (given runtime handle) */
|
||||
isProcessRunning(handle: RuntimeHandle): Promise<boolean>;
|
||||
|
|
|
|||
179
pnpm-lock.yaml
179
pnpm-lock.yaml
|
|
@ -32,6 +32,15 @@ importers:
|
|||
'@agent-orchestrator/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@agent-orchestrator/plugin-agent-aider':
|
||||
specifier: workspace:*
|
||||
version: link:../plugins/agent-aider
|
||||
'@agent-orchestrator/plugin-agent-claude-code':
|
||||
specifier: workspace:*
|
||||
version: link:../plugins/agent-claude-code
|
||||
'@agent-orchestrator/plugin-agent-codex':
|
||||
specifier: workspace:*
|
||||
version: link:../plugins/agent-codex
|
||||
chalk:
|
||||
specifier: ^5.4.0
|
||||
version: 5.6.2
|
||||
|
|
@ -73,6 +82,9 @@ importers:
|
|||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages/plugins/agent-aider:
|
||||
dependencies:
|
||||
|
|
@ -833,6 +845,9 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
|
|
@ -924,6 +939,9 @@ packages:
|
|||
'@vitest/expect@3.2.4':
|
||||
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
|
||||
|
||||
'@vitest/expect@4.0.18':
|
||||
resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==}
|
||||
|
||||
'@vitest/mocker@3.2.4':
|
||||
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
|
||||
peerDependencies:
|
||||
|
|
@ -935,21 +953,47 @@ packages:
|
|||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/mocker@4.0.18':
|
||||
resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^6.0.0 || ^7.0.0-0
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
|
||||
|
||||
'@vitest/pretty-format@4.0.18':
|
||||
resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==}
|
||||
|
||||
'@vitest/runner@3.2.4':
|
||||
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
|
||||
|
||||
'@vitest/runner@4.0.18':
|
||||
resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==}
|
||||
|
||||
'@vitest/snapshot@3.2.4':
|
||||
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
|
||||
|
||||
'@vitest/snapshot@4.0.18':
|
||||
resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==}
|
||||
|
||||
'@vitest/spy@3.2.4':
|
||||
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
|
||||
|
||||
'@vitest/spy@4.0.18':
|
||||
resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==}
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
|
||||
|
||||
'@vitest/utils@4.0.18':
|
||||
resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==}
|
||||
|
||||
acorn-jsx@5.3.2:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
|
|
@ -996,6 +1040,10 @@ packages:
|
|||
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
chai@6.2.2:
|
||||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
chalk@5.6.2:
|
||||
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
|
@ -1285,6 +1333,9 @@ packages:
|
|||
sass:
|
||||
optional: true
|
||||
|
||||
obug@2.1.1:
|
||||
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
|
||||
|
||||
onetime@7.0.0:
|
||||
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -1443,6 +1494,10 @@ packages:
|
|||
tinyexec@0.3.2:
|
||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||
|
||||
tinyexec@1.0.2:
|
||||
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
|
@ -1455,6 +1510,10 @@ packages:
|
|||
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tinyrainbow@3.0.3:
|
||||
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tinyspy@4.0.4:
|
||||
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
|
@ -1568,6 +1627,40 @@ packages:
|
|||
jsdom:
|
||||
optional: true
|
||||
|
||||
vitest@4.0.18:
|
||||
resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==}
|
||||
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@opentelemetry/api': ^1.9.0
|
||||
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||
'@vitest/browser-playwright': 4.0.18
|
||||
'@vitest/browser-preview': 4.0.18
|
||||
'@vitest/browser-webdriverio': 4.0.18
|
||||
'@vitest/ui': 4.0.18
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser-playwright':
|
||||
optional: true
|
||||
'@vitest/browser-preview':
|
||||
optional: true
|
||||
'@vitest/browser-webdriverio':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
|
@ -1926,6 +2019,8 @@ snapshots:
|
|||
'@rollup/rollup-win32-x64-msvc@4.57.1':
|
||||
optional: true
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
|
@ -2054,6 +2149,15 @@ snapshots:
|
|||
chai: 5.3.3
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/expect@4.0.18':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/spy': 4.0.18
|
||||
'@vitest/utils': 4.0.18
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
|
|
@ -2062,32 +2166,62 @@ snapshots:
|
|||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
'@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.18
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/pretty-format@4.0.18':
|
||||
dependencies:
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@vitest/runner@3.2.4':
|
||||
dependencies:
|
||||
'@vitest/utils': 3.2.4
|
||||
pathe: 2.0.3
|
||||
strip-literal: 3.1.0
|
||||
|
||||
'@vitest/runner@4.0.18':
|
||||
dependencies:
|
||||
'@vitest/utils': 4.0.18
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/snapshot@3.2.4':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/snapshot@4.0.18':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.0.18
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/spy@3.2.4':
|
||||
dependencies:
|
||||
tinyspy: 4.0.4
|
||||
|
||||
'@vitest/spy@4.0.18': {}
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
loupe: 3.2.1
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/utils@4.0.18':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.0.18
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.15.0):
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
|
|
@ -2131,6 +2265,8 @@ snapshots:
|
|||
loupe: 3.2.1
|
||||
pathval: 2.0.1
|
||||
|
||||
chai@6.2.2: {}
|
||||
|
||||
chalk@5.6.2: {}
|
||||
|
||||
check-error@2.1.3: {}
|
||||
|
|
@ -2409,6 +2545,8 @@ snapshots:
|
|||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
obug@2.1.1: {}
|
||||
|
||||
onetime@7.0.0:
|
||||
dependencies:
|
||||
mimic-function: 5.0.1
|
||||
|
|
@ -2596,6 +2734,8 @@ snapshots:
|
|||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
tinyexec@1.0.2: {}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
|
|
@ -2605,6 +2745,8 @@ snapshots:
|
|||
|
||||
tinyrainbow@2.0.0: {}
|
||||
|
||||
tinyrainbow@3.0.3: {}
|
||||
|
||||
tinyspy@4.0.4: {}
|
||||
|
||||
ts-api-utils@2.4.0(typescript@5.9.3):
|
||||
|
|
@ -2719,6 +2861,43 @@ snapshots:
|
|||
- tsx
|
||||
- yaml
|
||||
|
||||
vitest@4.0.18(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.0.18
|
||||
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@vitest/pretty-format': 4.0.18
|
||||
'@vitest/runner': 4.0.18
|
||||
'@vitest/snapshot': 4.0.18
|
||||
'@vitest/spy': 4.0.18
|
||||
'@vitest/utils': 4.0.18
|
||||
es-module-lexer: 1.7.0
|
||||
expect-type: 1.3.0
|
||||
magic-string: 0.30.21
|
||||
obug: 2.1.1
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.3
|
||||
std-env: 3.10.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 1.0.2
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.0.3
|
||||
vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 25.2.3
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
|
|
|||
Loading…
Reference in New Issue