Merge branch 'main' into dev

Resolve 38 conflicts preserving both sides' work:
- Server: DB-based registries (dev) + WorkspaceGitService/DaemonConfigStore (main)
- App: script proxy/voice MCP (dev) + pin/hide tabs/bootstrap boundary (main)
- Shared: WorkspaceScriptPayload (dev) + git/GitHub runtime schemas (main)
- Deleted workspace-registry-bootstrap.ts (dev's DB approach supersedes)
This commit is contained in:
Mohamed Boudra
2026-04-14 03:05:45 +07:00
333 changed files with 13490 additions and 6317 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/cli",
"version": "0.1.52",
"version": "0.1.54",
"description": "Paseo CLI - control your AI coding agents from the command line",
"type": "module",
"files": [
@@ -24,8 +24,8 @@
},
"dependencies": {
"@clack/prompts": "^1.0.0",
"@getpaseo/relay": "0.1.52",
"@getpaseo/server": "0.1.52",
"@getpaseo/relay": "0.1.54",
"@getpaseo/server": "0.1.54",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"mime-types": "^2.1.35",
+24 -30
View File
@@ -62,45 +62,39 @@ export function createCli(): Command {
.option("--no-color", "disable colored output");
// Primary agent commands (top-level)
addJsonAndDaemonHostOptions(
addLsOptions(program.command("ls")),
).action(withOutput(runLsCommand));
addJsonAndDaemonHostOptions(addLsOptions(program.command("ls"))).action(withOutput(runLsCommand));
addJsonAndDaemonHostOptions(
addRunOptions(program.command("run")),
).action(withOutput(runRunCommand));
addJsonAndDaemonHostOptions(addRunOptions(program.command("run"))).action(
withOutput(runRunCommand),
);
addDaemonHostOption(
addAttachOptions(program.command("attach")),
).action(runAttachCommand);
addDaemonHostOption(addAttachOptions(program.command("attach"))).action(runAttachCommand);
addDaemonHostOption(
addLogsOptions(program.command("logs")),
).action(runLogsCommand);
addDaemonHostOption(addLogsOptions(program.command("logs"))).action(runLogsCommand);
addJsonAndDaemonHostOptions(
addStopOptions(program.command("stop")),
).action(withOutput(runStopCommand));
addJsonAndDaemonHostOptions(addStopOptions(program.command("stop"))).action(
withOutput(runStopCommand),
);
addJsonAndDaemonHostOptions(
addDeleteOptions(program.command("delete")),
).action(withOutput(runDeleteCommand));
addJsonAndDaemonHostOptions(addDeleteOptions(program.command("delete"))).action(
withOutput(runDeleteCommand),
);
addJsonAndDaemonHostOptions(
addSendOptions(program.command("send")),
).action(withOutput(runSendCommand));
addJsonAndDaemonHostOptions(addSendOptions(program.command("send"))).action(
withOutput(runSendCommand),
);
addJsonAndDaemonHostOptions(
addInspectOptions(program.command("inspect")),
).action(withOutput(runInspectCommand));
addJsonAndDaemonHostOptions(addInspectOptions(program.command("inspect"))).action(
withOutput(runInspectCommand),
);
addJsonAndDaemonHostOptions(
addWaitOptions(program.command("wait")),
).action(withOutput(runWaitCommand));
addJsonAndDaemonHostOptions(addWaitOptions(program.command("wait"))).action(
withOutput(runWaitCommand),
);
addJsonAndDaemonHostOptions(
addArchiveOptions(program.command("archive")),
).action(withOutput(runArchiveCommand));
addJsonAndDaemonHostOptions(addArchiveOptions(program.command("archive"))).action(
withOutput(runArchiveCommand),
);
// Top-level local daemon shortcuts
program.addCommand(onboardCommand());
+1 -1
View File
@@ -26,7 +26,7 @@ export const archiveSchema: OutputSchema<AgentArchiveResult> = {
export function addArchiveOptions(cmd: Command): Command {
return cmd
.description('Archive an agent (soft-delete)')
.description("Archive an agent (soft-delete)")
.argument("<id>", "Agent ID, prefix, or name")
.option("--force", "Force archive running agent (interrupts active run first)");
}
+29 -30
View File
@@ -10,6 +10,7 @@ import { addSendOptions, runSendCommand } from "./send.js";
import { addInspectOptions, runInspectCommand } from "./inspect.js";
import { addWaitOptions, runWaitCommand } from "./wait.js";
import { addAttachOptions, runAttachCommand } from "./attach.js";
import { addReloadOptions, runReloadCommand } from "./reload.js";
import { runUpdateCommand } from "./update.js";
import { withOutput } from "../../output/index.js";
import {
@@ -22,41 +23,35 @@ export function createAgentCommand(): Command {
const agent = new Command("agent").description("Manage agents (advanced operations)");
// Primary agent commands (same as top-level)
addJsonAndDaemonHostOptions(
addLsOptions(agent.command("ls")),
).action(withOutput(runLsCommand));
addJsonAndDaemonHostOptions(addLsOptions(agent.command("ls"))).action(withOutput(runLsCommand));
addJsonAndDaemonHostOptions(
addRunOptions(agent.command("run")),
).action(withOutput(runRunCommand));
addJsonAndDaemonHostOptions(addRunOptions(agent.command("run"))).action(
withOutput(runRunCommand),
);
addDaemonHostOption(
addAttachOptions(agent.command("attach")),
).action(runAttachCommand);
addDaemonHostOption(addAttachOptions(agent.command("attach"))).action(runAttachCommand);
addDaemonHostOption(
addLogsOptions(agent.command("logs")),
).action(runLogsCommand);
addDaemonHostOption(addLogsOptions(agent.command("logs"))).action(runLogsCommand);
addJsonAndDaemonHostOptions(
addStopOptions(agent.command("stop")),
).action(withOutput(runStopCommand));
addJsonAndDaemonHostOptions(addStopOptions(agent.command("stop"))).action(
withOutput(runStopCommand),
);
addJsonAndDaemonHostOptions(
addDeleteOptions(agent.command("delete")),
).action(withOutput(runDeleteCommand));
addJsonAndDaemonHostOptions(addDeleteOptions(agent.command("delete"))).action(
withOutput(runDeleteCommand),
);
addJsonAndDaemonHostOptions(
addSendOptions(agent.command("send")),
).action(withOutput(runSendCommand));
addJsonAndDaemonHostOptions(addSendOptions(agent.command("send"))).action(
withOutput(runSendCommand),
);
addJsonAndDaemonHostOptions(
addInspectOptions(agent.command("inspect")),
).action(withOutput(runInspectCommand));
addJsonAndDaemonHostOptions(addInspectOptions(agent.command("inspect"))).action(
withOutput(runInspectCommand),
);
addJsonAndDaemonHostOptions(
addWaitOptions(agent.command("wait")),
).action(withOutput(runWaitCommand));
addJsonAndDaemonHostOptions(addWaitOptions(agent.command("wait"))).action(
withOutput(runWaitCommand),
);
// Advanced agent commands (less common operations)
addJsonAndDaemonHostOptions(
@@ -68,9 +63,13 @@ export function createAgentCommand(): Command {
.option("--list", "List available modes for this agent"),
).action(withOutput(runModeCommand));
addJsonAndDaemonHostOptions(
addArchiveOptions(agent.command("archive")),
).action(withOutput(runArchiveCommand));
addJsonAndDaemonHostOptions(addArchiveOptions(agent.command("archive"))).action(
withOutput(runArchiveCommand),
);
addJsonAndDaemonHostOptions(addReloadOptions(agent.command("reload"))).action(
withOutput(runReloadCommand),
);
addJsonAndDaemonHostOptions(
agent
+106
View File
@@ -0,0 +1,106 @@
import { Command } from "commander";
import { connectToDaemon, getDaemonHost, resolveAgentId } from "../../utils/client.js";
import type {
CommandOptions,
SingleResult,
OutputSchema,
CommandError,
} from "../../output/index.js";
export interface AgentReloadResult {
agentId: string;
status: "reloaded";
timelineSize: number;
}
export const reloadSchema: OutputSchema<AgentReloadResult> = {
idField: "agentId",
columns: [
{ header: "AGENT ID", field: "agentId" },
{ header: "STATUS", field: "status" },
{ header: "TIMELINE", field: "timelineSize" },
],
};
export function addReloadOptions(cmd: Command): Command {
return cmd
.description("Reload an agent (restarts the underlying process)")
.argument("<id>", "Agent ID, prefix, or name");
}
export interface AgentReloadOptions extends CommandOptions {
host?: string;
}
export type AgentReloadCommandResult = SingleResult<AgentReloadResult>;
export async function runReloadCommand(
agentIdArg: string,
options: AgentReloadOptions,
_command: Command,
): Promise<AgentReloadCommandResult> {
const host = getDaemonHost({ host: options.host as string | undefined });
if (!agentIdArg || agentIdArg.trim().length === 0) {
const error: CommandError = {
code: "MISSING_AGENT_ID",
message: "Agent ID is required",
details: "Usage: paseo agent reload <id-or-name>",
};
throw error;
}
let client;
try {
client = await connectToDaemon({ host: options.host as string | undefined });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: "Start the daemon with: paseo daemon start",
};
throw error;
}
try {
const agentsPayload = await client.fetchAgents({ filter: { includeArchived: true } });
const agents = agentsPayload.entries.map((entry) => entry.agent);
const agentId = resolveAgentId(agentIdArg, agents);
if (!agentId) {
const error: CommandError = {
code: "AGENT_NOT_FOUND",
message: `Agent not found: ${agentIdArg}`,
details: 'Use "paseo ls" to list available agents',
};
throw error;
}
const result = await client.refreshAgent(agentId);
await client.close();
return {
type: "single",
data: {
agentId: result.agentId,
status: "reloaded",
timelineSize: result.timelineSize ?? 0,
},
schema: reloadSchema,
};
} catch (err) {
await client.close().catch(() => {});
if (err && typeof err === "object" && "code" in err) {
throw err;
}
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: "RELOAD_FAILED",
message: `Failed to reload agent: ${message}`,
};
throw error;
}
}
+7 -4
View File
@@ -1,4 +1,4 @@
import { Command } from "commander";
import { Command, Option } from "commander";
import {
getStructuredAgentResponse,
StructuredAgentResponseError,
@@ -22,7 +22,8 @@ export function addRunOptions(cmd: Command): Command {
.description("Create and start an agent with a task")
.argument("<prompt>", "The task/prompt for the agent")
.option("-d, --detach", "Run in background (detached)")
.option("--name <name>", "Assign a name/title to the agent")
.option("--title <title>", "Assign a title to the agent")
.addOption(new Option("--name <name>", "Hidden alias for --title").hideHelp())
.option(
"--provider <provider>",
"Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)",
@@ -82,6 +83,7 @@ export const agentRunSchema: OutputSchema<AgentRunResult> = {
export interface AgentRunOptions extends CommandOptions {
detach?: boolean;
title?: string;
name?: string;
provider?: string;
model?: string;
@@ -324,6 +326,7 @@ export async function runRunCommand(
}
const resolvedProviderModel = resolveProviderAndModel(options);
const resolvedTitle = options.title ?? options.name;
let client;
try {
@@ -413,7 +416,7 @@ export async function runRunCommand(
structuredAgent = await client.createAgent({
provider: resolvedProviderModel.provider,
cwd,
title: options.name,
title: resolvedTitle,
modeId: options.mode,
model: resolvedProviderModel.model,
thinkingOptionId,
@@ -512,7 +515,7 @@ export async function runRunCommand(
const agent = await client.createAgent({
provider: resolvedProviderModel.provider,
cwd,
title: options.name,
title: resolvedTitle,
modeId: options.mode,
model: resolvedProviderModel.model,
thinkingOptionId,
+3 -3
View File
@@ -20,9 +20,9 @@ export function createChatCommand(): Command {
.option("--purpose <text>", "Room purpose/description"),
).action(withOutput(runCreateCommand));
addJsonAndDaemonHostOptions(
chat.command("ls").description("List chat rooms"),
).action(withOutput(runLsCommand));
addJsonAndDaemonHostOptions(chat.command("ls").description("List chat rooms")).action(
withOutput(runLsCommand),
);
addJsonAndDaemonHostOptions(
chat
+3 -1
View File
@@ -25,7 +25,9 @@ export async function runPostCommand(
body,
replyToMessageId: options.replyTo,
});
const [message] = await attachAgentNamesToMessages(client, [toChatMessageRow(payload.message!)]);
const [message] = await attachAgentNamesToMessages(client, [
toChatMessageRow(payload.message!),
]);
return {
type: "single",
data: message!,
+3 -1
View File
@@ -60,7 +60,9 @@ function renderChatMessageBlock(message: ChatMessageRow): string {
const authorLabel = message.authorName
? `${message.authorName} (${message.author})`
: message.author;
const lines = [`┌─ ${authorLabel} ── ${formatTimestamp(message.createdAt)} ── [msg ${message.id}]`];
const lines = [
`┌─ ${authorLabel} ── ${formatTimestamp(message.createdAt)} ── [msg ${message.id}]`,
];
if (message.replyTo !== "-") {
lines.push(`│ reply-to: msg ${message.replyTo}`);
@@ -34,6 +34,7 @@ export function createDaemonCommand(): Command {
.option("--port <port>", "Port for restarted daemon listen target")
.option("--no-relay", "Disable relay on restarted daemon")
.option("--no-mcp", "Disable Agent MCP on restarted daemon")
.option("--no-inject-mcp", "Disable auto-injecting the Paseo MCP into created agents")
.option(
"--allowed-hosts <hosts>",
'Comma-separated Host allowlist values (example: "localhost,.example.com" or "true")',
@@ -12,6 +12,7 @@ export interface DaemonStartOptions {
foreground?: boolean;
relay?: boolean;
mcp?: boolean;
injectMcp?: boolean;
allowedHosts?: string;
}
@@ -95,6 +96,9 @@ function buildRunnerArgs(options: DaemonStartOptions): string[] {
if (options.mcp === false) {
args.push("--no-mcp");
}
if (options.injectMcp === false) {
args.push("--no-inject-mcp");
}
return args;
}
@@ -125,12 +129,7 @@ function resolveDaemonRunnerEntry(): string {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { name?: string };
if (packageJson.name === "@getpaseo/server") {
const distRunner = path.join(
currentDir,
"dist",
"scripts",
"supervisor-entrypoint.js",
);
const distRunner = path.join(currentDir, "dist", "scripts", "supervisor-entrypoint.js");
if (existsSync(distRunner)) {
return distRunner;
}
@@ -60,6 +60,7 @@ function toStartOptions(options: CommandOptions): DaemonStartOptions {
port: typeof options.port === "string" ? options.port : undefined,
relay: typeof options.relay === "boolean" ? options.relay : undefined,
mcp: typeof options.mcp === "boolean" ? options.mcp : undefined,
injectMcp: typeof options.injectMcp === "boolean" ? options.injectMcp : undefined,
allowedHosts: typeof options.allowedHosts === "string" ? options.allowedHosts : undefined,
};
@@ -32,10 +32,15 @@ function resolveNodePathFromPidUnix(pid: number): NodePathFromPidResult {
}
const resolved = result.stdout.trim();
return resolved ? { nodePath: resolved } : { nodePath: null, error: "ps returned an empty command path" };
return resolved
? { nodePath: resolved }
: { nodePath: null, error: "ps returned an empty command path" };
}
function runProcessProbe(command: string, args: string[]): {
function runProcessProbe(
command: string,
args: string[],
): {
resolved: string | null;
error?: string;
} {
@@ -52,16 +57,25 @@ function runProcessProbe(command: string, args: string[]): {
const details = result.stderr?.trim();
return {
resolved: null,
error: details ? `${command} failed: ${details}` : `${command} exited with code ${result.status ?? 1}`,
error: details
? `${command} failed: ${details}`
: `${command} exited with code ${result.status ?? 1}`,
};
}
const resolved = result.stdout.trim();
return resolved ? { resolved } : { resolved: null, error: `${command} returned no executable path` };
return resolved
? { resolved }
: { resolved: null, error: `${command} returned no executable path` };
}
function resolveNodePathFromPidWindows(pid: number): NodePathFromPidResult {
const probes: Array<{ label: string; command: string; args: string[]; parseValue?: (stdout: string) => string | null }> = [
const probes: Array<{
label: string;
command: string;
args: string[];
parseValue?: (stdout: string) => string | null;
}> = [
{
label: "powershell-cim",
command: "powershell",
@@ -103,7 +117,10 @@ function resolveNodePathFromPidWindows(pid: number): NodePathFromPidResult {
}
}
return { nodePath: null, error: errors.join("; ") || "could not resolve executable path from PID" };
return {
nodePath: null,
error: errors.join("; ") || "could not resolve executable path from PID",
};
}
export function resolveNodePathFromPid(pid: number): NodePathFromPidResult {
@@ -18,6 +18,7 @@ export function startCommand(): Command {
.option("--foreground", "Run in foreground (don't daemonize)")
.option("--no-relay", "Disable relay connection")
.option("--no-mcp", "Disable the Agent MCP HTTP endpoint")
.option("--no-inject-mcp", "Disable auto-injecting the Paseo MCP into created agents")
.option(
"--allowed-hosts <hosts>",
'Comma-separated Host allowlist values (example: "localhost,.example.com" or "true")',
+4 -6
View File
@@ -2,11 +2,7 @@ import type { Command } from "commander";
import { execFile } from "node:child_process";
import { createRequire } from "node:module";
import { promisify } from "node:util";
import {
getOrCreateServerId,
findExecutable,
applyProviderEnv,
} from "@getpaseo/server";
import { getOrCreateServerId, findExecutable, applyProviderEnv } from "@getpaseo/server";
const execFileAsync = promisify(execFile);
import { tryConnectToDaemon } from "../../utils/client.js";
@@ -173,7 +169,9 @@ const PROVIDER_BINARIES: { label: string; binary: string }[] = [
{ label: "OpenCode", binary: "opencode" },
];
async function checkProviderBinary(binary: string): Promise<{ path: string | null; version: string | null }> {
async function checkProviderBinary(
binary: string,
): Promise<{ path: string | null; version: string | null }> {
const binaryPath = await findExecutable(binary);
if (!binaryPath) {
return { path: null, version: null };
+13 -15
View File
@@ -10,25 +10,23 @@ import { addLoopStopOptions, runLoopStopCommand } from "./stop.js";
export function createLoopCommand(): Command {
const loop = new Command("loop").description("Run iterative worker loops");
addJsonAndDaemonHostOptions(
addLoopRunOptions(loop.command("run")),
).action(withOutput(runLoopRunCommand));
addJsonAndDaemonHostOptions(addLoopRunOptions(loop.command("run"))).action(
withOutput(runLoopRunCommand),
);
addJsonAndDaemonHostOptions(
addLoopLsOptions(loop.command("ls")),
).action(withOutput(runLoopLsCommand));
addJsonAndDaemonHostOptions(addLoopLsOptions(loop.command("ls"))).action(
withOutput(runLoopLsCommand),
);
addJsonAndDaemonHostOptions(
addLoopInspectOptions(loop.command("inspect")),
).action(withOutput(runLoopInspectCommand));
addJsonAndDaemonHostOptions(addLoopInspectOptions(loop.command("inspect"))).action(
withOutput(runLoopInspectCommand),
);
addDaemonHostOption(
addLoopLogsOptions(loop.command("logs")),
).action(runLoopLogsCommand);
addDaemonHostOption(addLoopLogsOptions(loop.command("logs"))).action(runLoopLogsCommand);
addJsonAndDaemonHostOptions(
addLoopStopOptions(loop.command("stop")),
).action(withOutput(runLoopStopCommand));
addJsonAndDaemonHostOptions(addLoopStopOptions(loop.command("stop"))).action(
withOutput(runLoopStopCommand),
);
return loop;
}
+9 -8
View File
@@ -1,11 +1,6 @@
import { Command } from "commander";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import type {
CommandOptions,
CommandError,
OutputSchema,
ListResult,
} from "../../output/index.js";
import type { CommandOptions, CommandError, OutputSchema, ListResult } from "../../output/index.js";
import type { LoopDaemonClient, LoopRecord } from "./types.js";
interface InspectRow {
@@ -44,10 +39,16 @@ function toRows(loop: LoopRecord): InspectRow[] {
{ key: "VerifierModel", value: loop.verifierModel ?? "null" },
{ key: "Prompt", value: loop.prompt },
{ key: "VerifyPrompt", value: loop.verifyPrompt ?? "null" },
{ key: "VerifyChecks", value: loop.verifyChecks.length > 0 ? loop.verifyChecks.join(" | ") : "[]" },
{
key: "VerifyChecks",
value: loop.verifyChecks.length > 0 ? loop.verifyChecks.join(" | ") : "[]",
},
{ key: "Archive", value: String(loop.archive) },
{ key: "SleepMs", value: String(loop.sleepMs) },
{ key: "MaxIterations", value: loop.maxIterations === null ? "null" : String(loop.maxIterations) },
{
key: "MaxIterations",
value: loop.maxIterations === null ? "null" : String(loop.maxIterations),
},
{ key: "MaxTimeMs", value: loop.maxTimeMs === null ? "null" : String(loop.maxTimeMs) },
{ key: "CreatedAt", value: loop.createdAt },
{ key: "UpdatedAt", value: loop.updatedAt },
+1 -6
View File
@@ -1,11 +1,6 @@
import { Command } from "commander";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import type {
CommandOptions,
CommandError,
OutputSchema,
ListResult,
} from "../../output/index.js";
import type { CommandOptions, CommandError, OutputSchema, ListResult } from "../../output/index.js";
import type { LoopDaemonClient, LoopListItem } from "./types.js";
interface LoopListRow {
+5 -1
View File
@@ -1,7 +1,11 @@
import type { Command } from "commander";
import type { SingleResult } from "../../output/index.js";
import type { OutputSchema } from "../../output/index.js";
import { connectScheduleClient, toScheduleCommandError, type ScheduleCommandOptions } from "./shared.js";
import {
connectScheduleClient,
toScheduleCommandError,
type ScheduleCommandOptions,
} from "./shared.js";
interface ScheduleDeleteRow {
id: string;
+6 -15
View File
@@ -25,15 +25,12 @@ export function createScheduleCommand(): Command {
.option("--expires-in <duration>", "Time to live for the schedule"),
).action(withOutput(runCreateCommand));
addJsonAndDaemonHostOptions(
schedule.command("ls").description("List schedules"),
).action(withOutput(runLsCommand));
addJsonAndDaemonHostOptions(schedule.command("ls").description("List schedules")).action(
withOutput(runLsCommand),
);
addJsonAndDaemonHostOptions(
schedule
.command("inspect")
.description("Inspect a schedule")
.argument("<id>", "Schedule ID"),
schedule.command("inspect").description("Inspect a schedule").argument("<id>", "Schedule ID"),
).action(withOutput(runInspectCommand));
addJsonAndDaemonHostOptions(
@@ -44,10 +41,7 @@ export function createScheduleCommand(): Command {
).action(withOutput(runLogsCommand));
addJsonAndDaemonHostOptions(
schedule
.command("pause")
.description("Pause a schedule")
.argument("<id>", "Schedule ID"),
schedule.command("pause").description("Pause a schedule").argument("<id>", "Schedule ID"),
).action(withOutput(runPauseCommand));
addJsonAndDaemonHostOptions(
@@ -58,10 +52,7 @@ export function createScheduleCommand(): Command {
).action(withOutput(runResumeCommand));
addJsonAndDaemonHostOptions(
schedule
.command("delete")
.description("Delete a schedule")
.argument("<id>", "Schedule ID"),
schedule.command("delete").description("Delete a schedule").argument("<id>", "Schedule ID"),
).action(withOutput(runDeleteCommand));
return schedule;
@@ -5,7 +5,11 @@ import {
createScheduleInspectSchema,
type ScheduleInspectRow,
} from "./schema.js";
import { connectScheduleClient, toScheduleCommandError, type ScheduleCommandOptions } from "./shared.js";
import {
connectScheduleClient,
toScheduleCommandError,
type ScheduleCommandOptions,
} from "./shared.js";
export async function runInspectCommand(
id: string,
+5 -1
View File
@@ -1,7 +1,11 @@
import type { Command } from "commander";
import type { ListResult } from "../../output/index.js";
import { scheduleLogSchema, toScheduleLogRow, type ScheduleLogRow } from "./schema.js";
import { connectScheduleClient, toScheduleCommandError, type ScheduleCommandOptions } from "./shared.js";
import {
connectScheduleClient,
toScheduleCommandError,
type ScheduleCommandOptions,
} from "./shared.js";
export async function runLogsCommand(
id: string,
+3 -1
View File
@@ -19,7 +19,9 @@ export interface ScheduleInspectRow {
value: string;
}
export function createScheduleInspectSchema(record: ScheduleRecord): OutputSchema<ScheduleInspectRow> {
export function createScheduleInspectSchema(
record: ScheduleRecord,
): OutputSchema<ScheduleInspectRow> {
return {
idField: "key",
columns: [
+1 -5
View File
@@ -33,11 +33,7 @@ export async function connectScheduleClient(
}
}
export function toScheduleCommandError(
code: string,
action: string,
error: unknown,
): CommandError {
export function toScheduleCommandError(code: string, action: string, error: unknown): CommandError {
if (error && typeof error === "object" && "code" in error) {
return error as CommandError;
}
+2 -1
View File
@@ -20,7 +20,8 @@ export async function runLsCommand(
const cwd = options.all ? undefined : (options.cwd ?? process.cwd());
try {
const payload = cwd === undefined ? await client.listTerminals() : await client.listTerminals(cwd);
const payload =
cwd === undefined ? await client.listTerminals() : await client.listTerminals(cwd);
return {
type: "list",
data: payload.terminals.map((terminal) => toTerminalRow(terminal, payload.cwd ?? cwd)),
+3 -14
View File
@@ -10,7 +10,7 @@ export interface ConnectOptions {
}
const DEFAULT_HOST = "localhost:6767";
const DEFAULT_TIMEOUT = 5000;
const DEFAULT_TIMEOUT = 15000;
const PID_FILENAME = "paseo.pid";
type DaemonTarget =
@@ -207,6 +207,7 @@ export async function connectToDaemon(options?: ConnectOptions): Promise<DaemonC
url: target.url,
clientId,
clientType: "cli",
connectTimeoutMs: timeout,
webSocketFactory: (url: string, config?: { headers?: Record<string, string> }) =>
nodeWebSocketFactory(url, {
headers: config?.headers,
@@ -216,23 +217,11 @@ export async function connectToDaemon(options?: ConnectOptions): Promise<DaemonC
} as unknown as ConstructorParameters<typeof DaemonClient>[0]);
const connectPromise = client.connect();
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutHandle = setTimeout(() => {
reject(new Error(`Connection timeout after ${timeout}ms`));
}, timeout);
});
try {
await Promise.race([connectPromise, timeoutPromise]);
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
await connectPromise;
return client;
} catch (err) {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
lastError = err;
await client.close().catch(() => {});
}
+1 -2
View File
@@ -21,7 +21,6 @@ export function isSameOrDescendantPath(basePath: string, candidatePath: string):
}
return (
normalizedCandidate === normalizedBase ||
normalizedCandidate.startsWith(normalizedBase + "/")
normalizedCandidate === normalizedBase || normalizedCandidate.startsWith(normalizedBase + "/")
);
}
+2 -2
View File
@@ -60,7 +60,7 @@ try {
const result = await daemonCommand(["status"]);
assert.strictEqual(result.exitCode, 0, "status should succeed when daemon is stopped");
const output = result.stdout.toLowerCase();
assert(output.includes("status"), "status table should include Status row");
assert(output.includes("local daemon"), "status table should include Local Daemon row");
assert(output.includes("stopped"), "status should report stopped");
console.log("✓ daemon status reports stopped when not running\n");
}
@@ -84,7 +84,7 @@ try {
assert.strictEqual(result.exitCode, 0, "--json status should succeed");
const status = JSON.parse(result.stdout);
assert.strictEqual(typeof status.serverId, "string", "json status should include serverId");
assert.strictEqual(status.status, "stopped", "json status should report stopped");
assert.strictEqual(status.localDaemon, "stopped", "json status should report stopped");
assert.strictEqual(status.home, paseoHome, "json status should reflect the isolated home");
assert.strictEqual(
status.hostname,
+1 -1
View File
@@ -54,7 +54,7 @@ try {
assert.strictEqual(result.exitCode, 0, "run --help should exit 0");
assert(result.stdout.includes("-d"), "help should mention -d flag");
assert(result.stdout.includes("--detach"), "help should mention --detach flag");
assert(result.stdout.includes("--name"), "help should mention --name option");
assert(result.stdout.includes("--title"), "help should mention --title option");
assert(result.stdout.includes("--provider"), "help should mention --provider option");
assert(result.stdout.includes("--mode"), "help should mention --mode option");
assert(result.stdout.includes("--cwd"), "help should mention --cwd option");
+36 -19
View File
@@ -31,6 +31,11 @@ type ProviderModel = {
};
const EXPECTED_CLAUDE_MODELS = [
{
id: "claude-opus-4-6[1m]",
model: "Opus 4.6 1M",
descriptionFragment: "1M context window",
},
{
id: "claude-sonnet-4-6",
model: "Sonnet 4.6",
@@ -53,9 +58,7 @@ let claudeModelsFromJson: ProviderModel[] = [];
const ctx = await createE2ETestContext({ timeout: 120000 });
async function runProviderModelsJson(
provider: string,
): Promise<ProviderModel[]> {
async function runProviderModelsJson(provider: string): Promise<ProviderModel[]> {
const transientNeedles = ["transport closed", "timed out", "timeout", "socket", "econn"];
for (let attempt = 1; attempt <= 3; attempt++) {
@@ -137,7 +140,7 @@ try {
assert.strictEqual(result.exitCode, 0, "should exit 0");
const data = JSON.parse(result.stdout.trim());
assert(Array.isArray(data), "output should be an array");
assert.strictEqual(data.length, 3, "should have 3 providers");
assert.strictEqual(data.length, 5, "should have 5 providers");
assert(
data.some((p: { provider: string }) => p.provider === "claude"),
"should include claude",
@@ -150,6 +153,14 @@ try {
data.some((p: { provider: string }) => p.provider === "opencode"),
"should include opencode",
);
assert(
data.some((p: { provider: string }) => p.provider === "copilot"),
"should include copilot",
);
assert(
data.some((p: { provider: string }) => p.provider === "pi"),
"should include pi",
);
console.log("✓ provider ls --json outputs valid JSON\n");
}
@@ -159,10 +170,12 @@ try {
const result = await ctx.paseo(["provider", "ls", "--quiet"]);
assert.strictEqual(result.exitCode, 0, "should exit 0");
const lines = result.stdout.trim().split("\n");
assert.strictEqual(lines.length, 3, "should have 3 lines");
assert.strictEqual(lines.length, 5, "should have 5 lines");
assert(lines.includes("claude"), "should include claude");
assert(lines.includes("codex"), "should include codex");
assert(lines.includes("opencode"), "should include opencode");
assert(lines.includes("copilot"), "should include copilot");
assert(lines.includes("pi"), "should include pi");
console.log("✓ provider ls --quiet outputs provider names only\n");
}
@@ -178,13 +191,21 @@ try {
{
console.log("Test 6: provider models codex includes concrete codex model IDs");
const data = await runProviderModelsJson("codex");
assert(data.length >= 6, "codex model list should include current codex lineup");
assert(data.length >= 1, "codex model list should not be empty");
const ids = data.map((m) => m.id);
assert.strictEqual(new Set(ids).size, ids.length, "codex model IDs should be unique");
assert(ids.includes("gpt-5.3-codex"), "codex output should include gpt-5.3-codex");
assert(ids.includes("gpt-5.3-codex-spark"), "codex output should include gpt-5.3-codex-spark");
assert(ids.includes("gpt-5.1-codex-max"), "codex output should include gpt-5.1-codex-max");
assert(ids.includes("gpt-5.1-codex-mini"), "codex output should include gpt-5.1-codex-mini");
assert(
ids.every((id) => id.startsWith("gpt-")),
"all codex model IDs should be from the gpt family",
);
assert(
ids.some((id) => id.includes("codex")),
"codex model list should include at least one codex-optimized model",
);
assert(
data.every((m) => m.model && m.id && m.description),
"every codex model should have model, id, and description fields",
);
console.log("✓ provider models codex includes concrete codex model IDs\n");
}
@@ -192,23 +213,19 @@ try {
{
console.log("Test 7: provider models opencode returns namespaced model IDs");
const data = await runProviderModelsJson("opencode");
assert(data.length >= 3, "opencode model list should not be empty");
assert(data.length >= 1, "opencode model list should not be empty");
const ids = data.map((m) => m.id);
assert(
data.every((m) => m.id.includes("/")),
"opencode model IDs should be provider-namespaced",
);
assert(
ids.includes("opencode/gpt-5-nano"),
"opencode output should include opencode/gpt-5-nano",
ids.some((id) => id.startsWith("opencode/")),
"opencode output should include at least one first-party opencode model",
);
assert(
ids.some((id) => id.startsWith("openrouter/openai/")),
"opencode output should include OpenRouter OpenAI models",
);
assert(
ids.includes("openrouter/openai/gpt-5.3-codex"),
"opencode output should include openrouter/openai/gpt-5.3-codex",
data.every((m) => m.model && m.id && m.description !== undefined),
"every opencode model should have model, id, and description fields",
);
console.log("✓ provider models opencode returns namespaced model IDs\n");
}
@@ -69,7 +69,7 @@ function readProcessCommand(pid: number): string | null {
}
type DaemonStatus = {
status: string | null;
localDaemon: string | null;
pid: number | null;
};
@@ -77,19 +77,19 @@ async function readDaemonStatus(paseoHome: string): Promise<DaemonStatus> {
const result =
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon status --home ${paseoHome} --json`.nothrow();
if (result.exitCode !== 0) {
return { status: null, pid: null };
return { localDaemon: null, pid: null };
}
try {
const parsed = JSON.parse(result.stdout) as { status?: unknown; pid?: unknown };
const status = typeof parsed.status === "string" ? parsed.status : null;
const parsed = JSON.parse(result.stdout) as { localDaemon?: unknown; pid?: unknown };
const localDaemon = typeof parsed.localDaemon === "string" ? parsed.localDaemon : null;
const pid =
typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null;
return { status, pid };
return { localDaemon, pid };
} catch {
return { status: null, pid: null };
return { localDaemon: null, pid: null };
}
}
@@ -123,8 +123,8 @@ try {
console.log("Test 1: start supervisor-entrypoint in dev mode with isolated PASEO_HOME");
supervisorProcess = spawn(
"npx",
["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"],
process.execPath,
["--import", "tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"],
{
cwd: cliRoot,
env: {
@@ -149,7 +149,9 @@ try {
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome);
return status.status === "running" && status.pid !== null && isProcessRunning(status.pid);
return (
status.localDaemon === "running" && status.pid !== null && isProcessRunning(status.pid)
);
},
120000,
"daemon did not become running in time",
@@ -157,7 +159,11 @@ try {
const statusBeforeStop = await readDaemonStatus(paseoHome);
const daemonPid = statusBeforeStop.pid;
assert.strictEqual(statusBeforeStop.status, "running", "daemon should be running before stop");
assert.strictEqual(
statusBeforeStop.localDaemon,
"running",
"daemon should be running before stop",
);
assert(daemonPid !== null, "daemon pid should exist once daemon starts");
assert(isProcessRunning(daemonPid), "daemon process should be running");
const pidLockBeforeStop = await readPidLockState(paseoHome);
@@ -165,8 +171,7 @@ try {
const command = readProcessCommand(daemonPid);
assert(command !== null, "pid lock pid should resolve to a running process command");
assert(
command.includes("supervisor-entrypoint.ts") ||
command.includes("supervisor-entrypoint.js"),
command.includes("supervisor-entrypoint.ts") || command.includes("supervisor-entrypoint.js"),
`pid lock pid should be supervisor-entrypoint process, got: ${command}`,
);
console.log(`✓ dev daemon started with daemon pid ${daemonPid}\n`);
@@ -181,7 +186,7 @@ try {
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome);
return status.status === "stopped";
return status.localDaemon === "stopped";
},
15000,
"daemon status did not transition to stopped after stop",
@@ -207,7 +212,7 @@ try {
const statusAfterStop = await readDaemonStatus(paseoHome);
assert.strictEqual(
statusAfterStop.status,
statusAfterStop.localDaemon,
"stopped",
"daemon should remain stopped after stop command",
);
@@ -62,7 +62,7 @@ function signalProcessGroup(pid: number, signal: NodeJS.Signals): boolean {
}
type DaemonStatus = {
status: string | null;
localDaemon: string | null;
pid: number | null;
};
@@ -70,19 +70,19 @@ async function readDaemonStatus(paseoHome: string): Promise<DaemonStatus> {
const result =
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon status --home ${paseoHome} --json`.nothrow();
if (result.exitCode !== 0) {
return { status: null, pid: null };
return { localDaemon: null, pid: null };
}
try {
const parsed = JSON.parse(result.stdout) as { status?: unknown; pid?: unknown };
const status = typeof parsed.status === "string" ? parsed.status : null;
const parsed = JSON.parse(result.stdout) as { localDaemon?: unknown; pid?: unknown };
const localDaemon = typeof parsed.localDaemon === "string" ? parsed.localDaemon : null;
const pid =
typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null;
return { status, pid };
return { localDaemon, pid };
} catch {
return { status: null, pid: null };
return { localDaemon: null, pid: null };
}
}
@@ -134,8 +134,8 @@ try {
console.log("Test 1: start supervisor-entrypoint in dev mode with isolated PASEO_HOME");
supervisorProcess = spawn(
"npx",
["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"],
process.execPath,
["--import", "tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"],
{
cwd: cliRoot,
env: {
@@ -161,7 +161,9 @@ try {
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome);
return status.status === "running" && status.pid !== null && isProcessRunning(status.pid);
return (
status.localDaemon === "running" && status.pid !== null && isProcessRunning(status.pid)
);
},
120000,
"daemon did not become running in time",
@@ -187,7 +189,7 @@ try {
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome);
return status.status === "stopped";
return status.localDaemon === "stopped";
},
15000,
"daemon status did not transition to stopped after SIGINT",
@@ -70,7 +70,7 @@ function readWorkerPid(supervisorPid: number): number | null {
}
type DaemonStatus = {
status: string | null;
localDaemon: string | null;
pid: number | null;
};
@@ -78,19 +78,19 @@ async function readDaemonStatus(paseoHome: string): Promise<DaemonStatus> {
const result =
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon status --home ${paseoHome} --json`.nothrow();
if (result.exitCode !== 0) {
return { status: null, pid: null };
return { localDaemon: null, pid: null };
}
try {
const parsed = JSON.parse(result.stdout) as { status?: unknown; pid?: unknown };
const status = typeof parsed.status === "string" ? parsed.status : null;
const parsed = JSON.parse(result.stdout) as { localDaemon?: unknown; pid?: unknown };
const localDaemon = typeof parsed.localDaemon === "string" ? parsed.localDaemon : null;
const pid =
typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null;
return { status, pid };
return { localDaemon, pid };
} catch {
return { status: null, pid: null };
return { localDaemon: null, pid: null };
}
}
@@ -125,8 +125,8 @@ try {
console.log("Test 1: start supervisor-entrypoint in dev mode with isolated PASEO_HOME");
supervisorProcess = spawn(
"npx",
["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"],
process.execPath,
["--import", "tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"],
{
cwd: cliRoot,
env: {
@@ -151,7 +151,9 @@ try {
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome);
return status.status === "running" && status.pid !== null && isProcessRunning(status.pid);
return (
status.localDaemon === "running" && status.pid !== null && isProcessRunning(status.pid)
);
},
120000,
"daemon did not become running in time",
@@ -160,7 +162,7 @@ try {
const statusBeforeRestart = await readDaemonStatus(paseoHome);
const supervisorPid = statusBeforeRestart.pid;
assert.strictEqual(
statusBeforeRestart.status,
statusBeforeRestart.localDaemon,
"running",
"daemon should be running before restart",
);
@@ -211,7 +213,7 @@ try {
const statusAfterRestart = await readDaemonStatus(paseoHome);
assert.strictEqual(
statusAfterRestart.status,
statusAfterRestart.localDaemon,
"running",
"daemon should stay running after restart",
);
@@ -10,12 +10,9 @@ import { spawn, type ChildProcess } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { $ } from "zx";
import { tryConnectToDaemon } from "../src/utils/client.ts";
import { getAvailablePort } from "./helpers/network.ts";
$.verbose = false;
const pollIntervalMs = 100;
const testEnv = {
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? "0",
@@ -40,31 +37,6 @@ function isProcessRunning(pid: number): boolean {
}
}
type DaemonStatus = {
status: string | null;
pid: number | null;
};
async function readDaemonStatus(paseoHome: string): Promise<DaemonStatus> {
const result =
await $`PASEO_HOME=${paseoHome} PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnv.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnv.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnv.PASEO_VOICE_MODE_ENABLED} npx paseo daemon status --home ${paseoHome} --json`.nothrow();
if (result.exitCode !== 0) {
return { status: null, pid: null };
}
try {
const parsed = JSON.parse(result.stdout) as { status?: unknown; pid?: unknown };
const status = typeof parsed.status === "string" ? parsed.status : null;
const pid =
typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: null;
return { status, pid };
} catch {
return { status: null, pid: null };
}
}
async function waitFor(
check: () => Promise<boolean> | boolean,
timeoutMs: number,
@@ -100,6 +72,21 @@ function waitForProcessExit(processRef: ChildProcess, timeoutMs: number): Promis
});
}
async function canConnectToDaemon(host: string, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const client = await tryConnectToDaemon({ host, timeout: 500 }).catch(() => null);
if (client) {
await client.close().catch(() => undefined);
return true;
}
await sleep(pollIntervalMs);
}
return false;
}
async function readPidLockPid(paseoHome: string): Promise<number | null> {
const pidPath = join(paseoHome, "paseo.pid");
try {
@@ -122,48 +109,40 @@ const cliRoot = join(import.meta.dirname, "..");
const host = `127.0.0.1:${port}`;
let daemonProcess: ChildProcess | null = null;
let recentDaemonLogs = "";
try {
console.log("Test 1: start unsupervised daemon worker directly");
daemonProcess = spawn(
process.execPath,
[...process.execArgv, "--import", "tsx", "../server/src/server/index.ts"],
{
cwd: cliRoot,
env: {
...process.env,
...testEnv,
PASEO_HOME: paseoHome,
PASEO_LISTEN: host,
PASEO_RELAY_ENABLED: "false",
CI: "true",
},
stdio: ["ignore", "pipe", "pipe"],
daemonProcess = spawn(process.execPath, ["--import", "tsx", "../server/src/server/index.ts"], {
cwd: cliRoot,
env: {
...process.env,
...testEnv,
PASEO_HOME: paseoHome,
PASEO_LISTEN: host,
PASEO_RELAY_ENABLED: "false",
CI: "true",
},
);
stdio: ["ignore", "pipe", "pipe"],
});
daemonProcess.stdout?.on("data", (chunk) => {
recentDaemonLogs = (recentDaemonLogs + chunk.toString()).slice(-8000);
});
daemonProcess.stderr?.on("data", (chunk) => {
recentDaemonLogs = (recentDaemonLogs + chunk.toString()).slice(-8000);
});
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome);
return status.status === "running" && status.pid !== null && isProcessRunning(status.pid);
},
async () =>
Boolean(daemonProcess?.pid && isProcessRunning(daemonProcess.pid)) &&
(await canConnectToDaemon(host, 1000)),
120000,
"daemon did not become running in time",
);
const statusBeforeRestart = await readDaemonStatus(paseoHome);
assert.strictEqual(
statusBeforeRestart.status,
"running",
"daemon should be running before restart",
);
assert(daemonProcess.pid, "unsupervised daemon process pid should exist");
assert.strictEqual(
statusBeforeRestart.pid,
daemonProcess.pid,
"status pid should match daemon process pid",
);
const lockPid = await readPidLockPid(paseoHome);
assert.strictEqual(lockPid, daemonProcess.pid, "unsupervised worker should own pid lock");
console.log(`✓ unsupervised daemon started with pid ${daemonProcess.pid}\n`);
@@ -186,15 +165,16 @@ try {
const exit = await exitPromise;
assert.strictEqual(exit.signal, null, `daemon should exit cleanly, got signal=${exit.signal}`);
assert.strictEqual(exit.code, 0, `daemon should exit with status 0, got code=${exit.code}`);
assert.strictEqual(
exit.code,
0,
`daemon should exit with status 0, got code=${exit.code}\nRecent daemon logs:\n${recentDaemonLogs}`,
);
await waitFor(
async () => {
const status = await readDaemonStatus(paseoHome);
return status.status === "stopped";
},
async () => (await readPidLockPid(paseoHome)) === null,
15000,
"daemon status did not transition to stopped after unsupervised restart request",
"pid lock was not released after unsupervised restart request",
);
console.log("✓ unsupervised restart exited cleanly with code 0\n");
+1 -6
View File
@@ -27,12 +27,7 @@ try {
{
console.log("Test 2: chat post/read/wait work");
const posted = await ctx.paseo([
"chat",
"post",
"coord-room",
"first message for @agent-1",
], {
const posted = await ctx.paseo(["chat", "post", "coord-room", "first message for @agent-1"], {
env: { PASEO_AGENT_ID: "00000000-0000-4000-8000-000000000111" },
});
assert.strictEqual(posted.exitCode, 0, posted.stderr);
+18 -3
View File
@@ -28,7 +28,10 @@ try {
assert.strictEqual(listed.exitCode, 0, listed.stderr);
const listedJson = JSON.parse(listed.stdout);
assert(Array.isArray(listedJson), listed.stdout);
assert(listedJson.some((item: { id: string }) => item.id === createdJson.id), listed.stdout);
assert(
listedJson.some((item: { id: string }) => item.id === createdJson.id),
listed.stdout,
);
const inspected = await ctx.paseo(["schedule", "inspect", createdJson.id, "--json"]);
assert.strictEqual(inspected.exitCode, 0, inspected.stderr);
@@ -53,7 +56,16 @@ try {
{
console.log("Test 2: loop run/ls/inspect/logs/stop work");
const run = await ctx.paseo(
["loop", "run", "Return any response", "--name", "smoke-loop", "--verify-check", "true", "--json"],
[
"loop",
"run",
"Return any response",
"--name",
"smoke-loop",
"--verify-check",
"true",
"--json",
],
{ timeout: 30000 },
);
assert.strictEqual(run.exitCode, 0, run.stderr);
@@ -64,7 +76,10 @@ try {
assert.strictEqual(listed.exitCode, 0, listed.stderr);
const listedJson = JSON.parse(listed.stdout);
assert(Array.isArray(listedJson), listed.stdout);
assert(listedJson.some((item: { id: string }) => item.id === runJson.id), listed.stdout);
assert(
listedJson.some((item: { id: string }) => item.id === runJson.id),
listed.stdout,
);
let status = "running";
for (let attempt = 0; attempt < 40; attempt += 1) {
+2 -6
View File
@@ -4,16 +4,12 @@ import assert from "node:assert/strict";
import { mkdir, mkdtemp } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
classifyInvocation,
isExistingDirectory,
isPathLikeArg,
} from "../src/classify.ts";
import { classifyInvocation, isExistingDirectory, isPathLikeArg } from "../src/classify.ts";
import { openDesktopWithProject } from "../src/commands/open.ts";
console.log("📋 Phase 32: Open Project CLI Tests\n");
console.log(" Testing path-like detection exports...");
console.log(" Testing path-like detection exports...");
assert.equal(isPathLikeArg("."), true);
assert.equal(isPathLikeArg("./app"), true);
assert.equal(isPathLikeArg("/tmp/app"), true);
@@ -27,13 +27,14 @@ async function cleanup(): Promise<void> {
}
async function test_invalid_opencode_model_does_not_report_completed_while_still_running() {
const result = await ctx.paseo(
["run", "--provider", "opencode/adklasldkdas", "hello"],
{ timeout: 45_000 },
);
const result = await ctx.paseo(["run", "--provider", "opencode/adklasldkdas", "hello"], {
timeout: 45_000,
});
const output = `${result.stdout}\n${result.stderr}`;
const agentId = output.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i)?.[0];
const agentId = output.match(
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i,
)?.[0];
if (result.exitCode !== 0) {
assert(
@@ -46,7 +47,11 @@ async function test_invalid_opencode_model_does_not_report_completed_while_still
assert(agentId, `expected run output to include an agent id\nstdout:\n${result.stdout}`);
const inspect = await ctx.paseo(["inspect", agentId], { timeout: 15_000 });
assert.strictEqual(inspect.exitCode, 0, `inspect failed\nstdout:\n${inspect.stdout}\nstderr:\n${inspect.stderr}`);
assert.strictEqual(
inspect.exitCode,
0,
`inspect failed\nstdout:\n${inspect.stdout}\nstderr:\n${inspect.stderr}`,
);
const runReportedCompleted = result.stdout.includes("completed");
const inspectStillRunning = inspect.stdout.includes("Status running");
+4 -2
View File
@@ -411,8 +411,10 @@ export async function createE2ETestContext(options?: { timeout?: number }): Prom
> {
const ctx = await startTestDaemon({ timeout: options?.timeout });
const paseo = (args: string[], opts?: { timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv }) =>
runPaseoCli(ctx, args, opts);
const paseo = (
args: string[],
opts?: { timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv },
) => runPaseoCli(ctx, args, opts);
return {
...ctx,