Files
paseo/packages/cli/src/commands/agent/stop.ts
T
Mohamed Boudra 0110302b6f fix(daemon): keep lifecycle operations on the selected instance (#4575)
* fix(daemon): keep lifecycle operations on the selected instance

Restart reconstructed launch inputs and address fallback could redirect a home-selected shutdown to another daemon. Let supervisors publish bound endpoints, retain their worker restart contract, and share captured local lifecycle ownership between CLI and Desktop.

* test(daemon): verify bounded desktop readiness and cancellable artifacts

* test(cli): verify reload target hints and durable offline pairing

* test(daemon): align lifecycle fixtures with ownership and targeting

* fix(daemon): preserve reachability when status details time out

Provider discovery can outlast a status request after authentication succeeds. Keep local connection facts separate from unavailable details, while explicit remote queries retain their error outcome.

Migrate CLI and packaged Desktop fixtures to persistent managed configuration and explicit targets. Isolate generic Git observation from forge discovery and reuse disposable CLI homes on Windows.

* test(daemon): observe restart through the worker status RPC

The bounded CLI status observation may omit worker details while an authenticated connection remains healthy. Exercise the app-style restart through its full RPC and retain the replacement observation, with a real slow provider probe covering the distinction.
2026-09-11 08:33:22 +02:00

131 lines
4.0 KiB
TypeScript

import { Command } from "commander";
import { connectToDaemon } from "../../utils/client.js";
import { isSameOrDescendantPath } from "../../utils/paths.js";
import type {
CommandOptions,
SingleResult,
OutputSchema,
CommandError,
} from "../../output/index.js";
/** Result type for agent stop command */
export interface StopResult {
stoppedCount: number;
agentIds: string[];
}
/** Schema for stop command output */
export const stopSchema: OutputSchema<StopResult> = {
// For quiet mode, output the stopped agent IDs (one per line)
idField: (item) => item.agentIds.join("\n"),
columns: [{ header: "INTERRUPTED", field: "stoppedCount" }],
};
export function addStopOptions(cmd: Command): Command {
return cmd
.description("Interrupt an agent if it is running (no-op for idle agents)")
.argument("[id]", "Agent ID (or prefix) - optional if --all or --cwd specified")
.option("--all", "Stop all agents")
.option("--cwd <path>", "Stop all agents in directory");
}
export interface AgentStopOptions extends CommandOptions {
all?: boolean;
cwd?: string;
}
export type AgentStopResult = SingleResult<StopResult>;
export async function runStopCommand(
id: string | undefined,
options: AgentStopOptions,
_command: Command,
): Promise<AgentStopResult> {
// Validate arguments - need either an id, --all, or --cwd
if (!id && !options.all && !options.cwd) {
const error: CommandError = {
code: "MISSING_ARGUMENT",
message: "Agent ID required unless --all or --cwd is specified",
details: "Usage: paseo agent stop <id> | --all | --cwd <path>",
};
throw error;
}
const client = await connectToDaemon({ target: options.daemonTarget });
try {
const fetchPayload = await client.fetchAgents({ filter: { includeArchived: true } });
let agents = fetchPayload.entries.map((entry) => entry.agent);
const stoppedIds: string[] = [];
if (options.all) {
// Stop all agents (not archived)
agents = agents.filter((a) => !a.archivedAt);
} else if (options.cwd) {
// Stop agents in directory
agents = agents.filter((a) => {
if (a.archivedAt) return false;
return isSameOrDescendantPath(options.cwd!, a.cwd);
});
} else if (id) {
// Stop specific agent
const fetchResult = await client.fetchAgent({ agentId: id });
if (!fetchResult) {
const error: CommandError = {
code: "AGENT_NOT_FOUND",
message: `No agent found matching: ${id}`,
details: "Use `paseo ls` to list available agents",
};
throw error;
}
agents = [fetchResult.agent];
}
// Interrupt each running agent. Idle agents are a no-op.
const stopResults = await Promise.all(
agents.map(async (agent) => {
if (agent.status !== "running") return { ok: true as const, id: agent.id, stopped: false };
try {
await client.cancelAgent(agent.id);
return { ok: true as const, id: agent.id, stopped: true };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { ok: false as const, id: agent.id, message };
}
}),
);
for (const result of stopResults) {
if (!result.ok) {
console.error(`Warning: Failed to stop agent ${result.id.slice(0, 7)}: ${result.message}`);
continue;
}
if (result.stopped) {
stoppedIds.push(result.id);
}
}
await client.close();
return {
type: "single",
data: {
stoppedCount: stoppedIds.length,
agentIds: stoppedIds,
},
schema: stopSchema,
};
} catch (err) {
await client.close().catch(() => {});
// Re-throw if it's already a CommandError
if (err && typeof err === "object" && "code" in err) {
throw err;
}
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: "STOP_AGENT_FAILED",
message: `Failed to stop agent(s): ${message}`,
};
throw error;
}
}