mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
Let CLI host selection work globally (#4238)
* feat(cli): accept host as a global option Keep the command-local position for compatibility while letting users place one daemon target before the command and reuse it while changing verbs. Local daemon lifecycle commands continue to operate locally. * fix(cli): preserve project host resolution boundary Project creation already handled PASEO_HOST directly. Avoid routing that established behavior through a newly exported client helper so its isolated command tests keep their existing dependency boundary. * test(cli): allow scaffold typecheck under CI load * test(cli): provide remote cwd in schedule e2e * docs(cli): teach only global host syntax
This commit is contained in:
+6
-3
@@ -556,13 +556,16 @@ npm run cli -- daemon status # Check daemon status
|
||||
npm run cli -- clone owner/repo --dir ~/workspace # Clone GitHub repo and register project
|
||||
```
|
||||
|
||||
Use `--host` to point the CLI at a different daemon:
|
||||
Use the global `--host` option to point the CLI at a different daemon:
|
||||
|
||||
```bash
|
||||
npm run cli -- ls -a --host localhost:7777
|
||||
npm run cli -- ls -a --host ssh://user@host
|
||||
npm run cli -- --host localhost:7777 ls -a
|
||||
npm run cli -- --host ssh://user@host ls -a
|
||||
```
|
||||
|
||||
Set `PASEO_HOST` to use the same target across invocations. An explicit
|
||||
`--host` overrides the environment variable.
|
||||
|
||||
In an SSH URI, the URL port is the SSH server port. The remote daemon defaults to `127.0.0.1:6767`; use `?daemonPort=7777` to override it. The transport runs non-interactively through the local OpenSSH client and never installs, starts, or configures the remote daemon. User-facing setup and troubleshooting live in [public-docs/connectivity.md](../public-docs/connectivity.md#ssh).
|
||||
|
||||
Desktop integrations can focus an existing agent without creating one or
|
||||
|
||||
@@ -2,6 +2,10 @@ import { describe, expect, it } from "vitest";
|
||||
import { createCli } from "./cli.js";
|
||||
|
||||
describe("canonical CLI surface", () => {
|
||||
it("offers daemon host selection as a global option", () => {
|
||||
expect(createCli().helpInformation()).toContain("--host <host>");
|
||||
});
|
||||
|
||||
it("shows project, workspace, and heartbeat commands while hiding worktree compatibility", () => {
|
||||
const cli = createCli();
|
||||
const help = cli.helpInformation();
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
addDaemonHostOption,
|
||||
addJsonAndDaemonHostOptions,
|
||||
addJsonOption,
|
||||
withGlobalOptions,
|
||||
} from "./utils/command-options.js";
|
||||
import { resolveCliVersion } from "./version.js";
|
||||
|
||||
@@ -60,6 +61,7 @@ export function createCli(): Command {
|
||||
.option("-q, --quiet", "minimal output (IDs only)")
|
||||
.option("--no-headers", "omit table headers")
|
||||
.option("--no-color", "disable colored output");
|
||||
addDaemonHostOption(program);
|
||||
|
||||
// Primary agent commands (top-level)
|
||||
addJsonAndDaemonHostOptions(addLsOptions(program.command("ls"))).action(withOutput(runLsCommand));
|
||||
@@ -86,9 +88,13 @@ export function createCli(): Command {
|
||||
)
|
||||
.action(withOutput(runCloneCommand));
|
||||
|
||||
addDaemonHostOption(addAttachOptions(program.command("attach"))).action(runAttachCommand);
|
||||
addDaemonHostOption(addAttachOptions(program.command("attach"))).action(
|
||||
withGlobalOptions(runAttachCommand),
|
||||
);
|
||||
|
||||
addDaemonHostOption(addLogsOptions(program.command("logs"))).action(runLogsCommand);
|
||||
addDaemonHostOption(addLogsOptions(program.command("logs"))).action(
|
||||
withGlobalOptions(runLogsCommand),
|
||||
);
|
||||
|
||||
addJsonAndDaemonHostOptions(addStopOptions(program.command("stop"))).action(
|
||||
withOutput(runStopCommand),
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
addDaemonHostOption,
|
||||
addJsonAndDaemonHostOptions,
|
||||
collectMultiple,
|
||||
withGlobalOptions,
|
||||
} from "../../utils/command-options.js";
|
||||
|
||||
export function createAgentCommand(): Command {
|
||||
@@ -36,9 +37,13 @@ export function createAgentCommand(): Command {
|
||||
withOutput(runImportCommand),
|
||||
);
|
||||
|
||||
addDaemonHostOption(addAttachOptions(agent.command("attach"))).action(runAttachCommand);
|
||||
addDaemonHostOption(addAttachOptions(agent.command("attach"))).action(
|
||||
withGlobalOptions(runAttachCommand),
|
||||
);
|
||||
|
||||
addDaemonHostOption(addLogsOptions(agent.command("logs"))).action(runLogsCommand);
|
||||
addDaemonHostOption(addLogsOptions(agent.command("logs"))).action(
|
||||
withGlobalOptions(runLogsCommand),
|
||||
);
|
||||
|
||||
addJsonAndDaemonHostOptions(addOpenOptions(agent.command("open"))).action(
|
||||
withOutput(runOpenCommand),
|
||||
|
||||
@@ -28,75 +28,81 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("plugin scaffold", () => {
|
||||
it("creates a standalone split-runtime project that typechecks", async () => {
|
||||
const parent = await mkdtemp(path.join(process.cwd(), ".plugin-scaffold-"));
|
||||
directories.push(parent);
|
||||
const directory = path.join(parent, "hello-plugin");
|
||||
it(
|
||||
"creates a standalone split-runtime project that typechecks",
|
||||
{ timeout: 15_000 },
|
||||
async () => {
|
||||
const parent = await mkdtemp(path.join(process.cwd(), ".plugin-scaffold-"));
|
||||
directories.push(parent);
|
||||
const directory = path.join(parent, "hello-plugin");
|
||||
|
||||
await scaffoldPluginDirectory(directory);
|
||||
await scaffoldPluginDirectory(directory);
|
||||
|
||||
const configPath = path.join(directory, "tsconfig.json");
|
||||
const config = JSON.parse(await readFile(configPath, "utf8")) as {
|
||||
compilerOptions: { lib: string[]; types: string[] };
|
||||
};
|
||||
expect(config.compilerOptions.lib).toEqual(["ES2023"]);
|
||||
expect(config.compilerOptions.types).toEqual(["react"]);
|
||||
await expect(typecheckPlugin(directory)).resolves.toBeUndefined();
|
||||
expect(JSON.parse(await readFile(path.join(directory, "paseo-plugin.json"), "utf8"))).toEqual({
|
||||
id: "hello-plugin",
|
||||
});
|
||||
const cliPackageJson = JSON.parse(
|
||||
await readFile(new URL("../../../package.json", import.meta.url), "utf8"),
|
||||
) as { version: string };
|
||||
expect(JSON.parse(await readFile(path.join(directory, "package.json"), "utf8"))).toEqual({
|
||||
name: "hello-plugin",
|
||||
private: true,
|
||||
version: "0.0.0",
|
||||
scripts: { typecheck: "tsc --noEmit" },
|
||||
devDependencies: {
|
||||
"@getpaseo/plugin": cliPackageJson.version,
|
||||
"@tanstack/react-query": "^5.90.11",
|
||||
"@types/react": "~19.2.0",
|
||||
react: "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
typescript: "^5.9.3",
|
||||
zod: "^4.4.3",
|
||||
},
|
||||
});
|
||||
expect(await readdir(directory)).not.toContain("paseo-plugin.d.ts");
|
||||
await expect(readFile(path.join(directory, "index.client.tsx"), "utf8")).resolves.toContain(
|
||||
'from "./client/greeting"',
|
||||
);
|
||||
await expect(readFile(path.join(directory, "index.server.ts"), "utf8")).resolves.toContain(
|
||||
'from "./server/greeting"',
|
||||
);
|
||||
await expect(readFile(path.join(directory, "client/greeting.tsx"), "utf8")).resolves.toContain(
|
||||
"useRpc(greetingRpc)",
|
||||
);
|
||||
await expect(readFile(path.join(directory, "client/web.ts"), "utf8")).resolves.not.toContain(
|
||||
`/// <reference lib="dom" />`,
|
||||
);
|
||||
await expect(readFile(path.join(directory, "client/web.ts"), "utf8")).resolves.toContain(
|
||||
`declare const window: { open(url: string, target: string, features: string): unknown };`,
|
||||
);
|
||||
await expect(readFile(path.join(directory, "client/web.ts"), "utf8")).resolves.toContain(
|
||||
`Platform.OS === "web"`,
|
||||
);
|
||||
await expect(readFile(path.join(directory, "client/greeting.tsx"), "utf8")).resolves.toContain(
|
||||
`openExternal("https://paseo.sh")`,
|
||||
);
|
||||
await expect(readFile(path.join(directory, "server/greeting.ts"), "utf8")).resolves.toContain(
|
||||
'"Hello, " + name + "!"',
|
||||
);
|
||||
await expect(readFile(path.join(directory, "shared/greeting.ts"), "utf8")).resolves.toContain(
|
||||
'name: "greeting.create"',
|
||||
);
|
||||
const configPath = path.join(directory, "tsconfig.json");
|
||||
const config = JSON.parse(await readFile(configPath, "utf8")) as {
|
||||
compilerOptions: { lib: string[]; types: string[] };
|
||||
};
|
||||
expect(config.compilerOptions.lib).toEqual(["ES2023"]);
|
||||
expect(config.compilerOptions.types).toEqual(["react"]);
|
||||
await expect(typecheckPlugin(directory)).resolves.toBeUndefined();
|
||||
expect(JSON.parse(await readFile(path.join(directory, "paseo-plugin.json"), "utf8"))).toEqual(
|
||||
{
|
||||
id: "hello-plugin",
|
||||
},
|
||||
);
|
||||
const cliPackageJson = JSON.parse(
|
||||
await readFile(new URL("../../../package.json", import.meta.url), "utf8"),
|
||||
) as { version: string };
|
||||
expect(JSON.parse(await readFile(path.join(directory, "package.json"), "utf8"))).toEqual({
|
||||
name: "hello-plugin",
|
||||
private: true,
|
||||
version: "0.0.0",
|
||||
scripts: { typecheck: "tsc --noEmit" },
|
||||
devDependencies: {
|
||||
"@getpaseo/plugin": cliPackageJson.version,
|
||||
"@tanstack/react-query": "^5.90.11",
|
||||
"@types/react": "~19.2.0",
|
||||
react: "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
typescript: "^5.9.3",
|
||||
zod: "^4.4.3",
|
||||
},
|
||||
});
|
||||
expect(await readdir(directory)).not.toContain("paseo-plugin.d.ts");
|
||||
await expect(readFile(path.join(directory, "index.client.tsx"), "utf8")).resolves.toContain(
|
||||
'from "./client/greeting"',
|
||||
);
|
||||
await expect(readFile(path.join(directory, "index.server.ts"), "utf8")).resolves.toContain(
|
||||
'from "./server/greeting"',
|
||||
);
|
||||
await expect(
|
||||
readFile(path.join(directory, "client/greeting.tsx"), "utf8"),
|
||||
).resolves.toContain("useRpc(greetingRpc)");
|
||||
await expect(readFile(path.join(directory, "client/web.ts"), "utf8")).resolves.not.toContain(
|
||||
`/// <reference lib="dom" />`,
|
||||
);
|
||||
await expect(readFile(path.join(directory, "client/web.ts"), "utf8")).resolves.toContain(
|
||||
`declare const window: { open(url: string, target: string, features: string): unknown };`,
|
||||
);
|
||||
await expect(readFile(path.join(directory, "client/web.ts"), "utf8")).resolves.toContain(
|
||||
`Platform.OS === "web"`,
|
||||
);
|
||||
await expect(
|
||||
readFile(path.join(directory, "client/greeting.tsx"), "utf8"),
|
||||
).resolves.toContain(`openExternal("https://paseo.sh")`);
|
||||
await expect(readFile(path.join(directory, "server/greeting.ts"), "utf8")).resolves.toContain(
|
||||
'"Hello, " + name + "!"',
|
||||
);
|
||||
await expect(readFile(path.join(directory, "shared/greeting.ts"), "utf8")).resolves.toContain(
|
||||
'name: "greeting.create"',
|
||||
);
|
||||
|
||||
await writeFile(path.join(directory, "client/greeting.tsx"), '\ndocument.title = "x";\n', {
|
||||
flag: "a",
|
||||
});
|
||||
await expect(typecheckPlugin(directory)).rejects.toThrow("Cannot find name 'document'.");
|
||||
});
|
||||
await writeFile(path.join(directory, "client/greeting.tsx"), '\ndocument.title = "x";\n', {
|
||||
flag: "a",
|
||||
});
|
||||
await expect(typecheckPlugin(directory)).rejects.toThrow("Cannot find name 'document'.");
|
||||
},
|
||||
);
|
||||
|
||||
it("typechecks client and server Paseo API access", async () => {
|
||||
const parent = await mkdtemp(path.join(process.cwd(), ".plugin-scaffold-"));
|
||||
|
||||
@@ -25,6 +25,7 @@ describe("parseScheduleCreateInput cwd/host validation", () => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
test("no host, no cwd → defaults to process.cwd()", () => {
|
||||
@@ -64,6 +65,17 @@ describe("parseScheduleCreateInput cwd/host validation", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("PASEO_HOST without cwd → throws MISSING_CWD", () => {
|
||||
vi.stubEnv("PASEO_HOST", "dev:6767");
|
||||
|
||||
expect(() => parseScheduleCreateInput(baseOptions)).toThrow(
|
||||
expect.objectContaining({
|
||||
code: "MISSING_CWD",
|
||||
message: expect.stringContaining("--cwd is required"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("host with whitespace-only cwd → throws MISSING_CWD", () => {
|
||||
expect(() =>
|
||||
parseScheduleCreateInput({ ...baseOptions, host: "dev:6767", cwd: " " }),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||
import { connectToDaemon, getDaemonHost, getExplicitDaemonHost } from "../../utils/client.js";
|
||||
import type { CommandError, CommandOptions } from "../../output/index.js";
|
||||
import type {
|
||||
CreateScheduleInput,
|
||||
@@ -168,7 +168,7 @@ export function parseScheduleCreateInput(options: {
|
||||
}
|
||||
|
||||
const cwdInput = options.cwd?.trim();
|
||||
if (options.host !== undefined && !cwdInput) {
|
||||
if (getExplicitDaemonHost(options.host) !== undefined && !cwdInput) {
|
||||
throw {
|
||||
code: "MISSING_CWD",
|
||||
message:
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Command } from "commander";
|
||||
import { withOutput } from "../../output/index.js";
|
||||
import { addDaemonHostOption, addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
||||
import {
|
||||
addDaemonHostOption,
|
||||
addJsonAndDaemonHostOptions,
|
||||
withGlobalOptions,
|
||||
} from "../../utils/command-options.js";
|
||||
import { runCaptureCommand } from "./capture.js";
|
||||
import { runCreateCommand } from "./create.js";
|
||||
import { runKillCommand } from "./kill.js";
|
||||
@@ -43,7 +47,7 @@ export function createTerminalCommand(): Command {
|
||||
.option("-S, --scrollback", "Capture from the beginning of scrollback")
|
||||
.option("--ansi", "Preserve ANSI escape codes")
|
||||
.option("--json", "Output in JSON format"),
|
||||
).action(runCaptureCommand);
|
||||
).action(withGlobalOptions(runCaptureCommand));
|
||||
|
||||
addDaemonHostOption(
|
||||
terminal
|
||||
@@ -53,7 +57,7 @@ export function createTerminalCommand(): Command {
|
||||
.argument("<keys...>", "Keys to send")
|
||||
.option("-l, --literal", "Send raw keys without interpreting special tokens")
|
||||
.option("--json", "Output in JSON format"),
|
||||
).action(runSendKeysCommand);
|
||||
).action(withGlobalOptions(runSendKeysCommand));
|
||||
|
||||
return terminal;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type { Command } from "commander";
|
||||
import type { AnyCommandResult, CommandError, OutputOptions } from "./types.js";
|
||||
import { render, renderError, toCommandError, defaultOutputOptions } from "./render.js";
|
||||
import { withGlobalOptions } from "../utils/command-options.js";
|
||||
|
||||
/** Options that include output settings from global options */
|
||||
export interface CommandOptions extends Partial<OutputOptions> {
|
||||
@@ -77,11 +78,8 @@ function extractOutputOptions(options: CommandOptions): OutputOptions {
|
||||
export function withOutput<T, Args extends unknown[]>(
|
||||
handler: (...args: [...Args, CommandOptions, Command]) => Promise<AnyCommandResult<T>>,
|
||||
): (...args: [...Args, CommandOptions, Command]) => Promise<void> {
|
||||
return async (...args) => {
|
||||
// Last two args are options and command
|
||||
const command = args[args.length - 1] as Command;
|
||||
// Use optsWithGlobals() to get both local and global options
|
||||
const options = command.optsWithGlobals() as CommandOptions;
|
||||
return withGlobalOptions(async (...args) => {
|
||||
const options = args[args.length - 2] as CommandOptions;
|
||||
const outputOptions = extractOutputOptions(options);
|
||||
|
||||
try {
|
||||
@@ -97,7 +95,7 @@ export function withOutput<T, Args extends unknown[]>(
|
||||
process.stderr.write(errorOutput + "\n");
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -189,7 +189,7 @@ export function resolveDefaultDaemonHosts(env: NodeJS.ProcessEnv = process.env):
|
||||
}
|
||||
|
||||
function resolveDaemonHostCandidates(options?: ConnectOptions): string[] {
|
||||
const explicitHost = options?.host ?? process.env.PASEO_HOST;
|
||||
const explicitHost = getExplicitDaemonHost(options?.host);
|
||||
if (explicitHost) {
|
||||
return [explicitHost];
|
||||
}
|
||||
@@ -197,6 +197,14 @@ function resolveDaemonHostCandidates(options?: ConnectOptions): string[] {
|
||||
return resolveDefaultDaemonHosts();
|
||||
}
|
||||
|
||||
export function getExplicitDaemonHost(
|
||||
host: string | undefined,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string | undefined {
|
||||
const explicitHost = host ?? env.PASEO_HOST;
|
||||
return explicitHost?.trim() ? explicitHost : undefined;
|
||||
}
|
||||
|
||||
function stripIpcPrefix(trimmed: string): string {
|
||||
if (trimmed.startsWith("unix://")) return trimmed.slice("unix://".length).trim();
|
||||
if (trimmed.startsWith("pipe://")) return trimmed.slice("pipe://".length).trim();
|
||||
@@ -360,7 +368,7 @@ export async function connectToDaemon(options?: ConnectOptions): Promise<DaemonC
|
||||
const clientId = await getOrCreateCliClientId();
|
||||
const nodeWebSocketFactory = createNodeWebSocketFactory();
|
||||
|
||||
const explicitHost = options?.host ?? process.env.PASEO_HOST;
|
||||
const explicitHost = getExplicitDaemonHost(options?.host);
|
||||
if (explicitHost?.trim().startsWith("ssh://")) {
|
||||
const target = parseSshTransportUri(explicitHost.trim());
|
||||
const tunnel = await createSshTunnel(target);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Command } from "commander";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { addDaemonHostOption, withGlobalOptions } from "./command-options.js";
|
||||
|
||||
async function parseHost(argv: string[]): Promise<string | undefined> {
|
||||
const program = new Command().exitOverride().option("--host <host>");
|
||||
let receivedHost: string | undefined;
|
||||
|
||||
addDaemonHostOption(program.command("ls")).action(
|
||||
withGlobalOptions((options: { host?: string }) => {
|
||||
receivedHost = options.host;
|
||||
}),
|
||||
);
|
||||
|
||||
await program.parseAsync(argv, { from: "user" });
|
||||
return receivedHost;
|
||||
}
|
||||
|
||||
describe("global command options", () => {
|
||||
it("passes a host declared before the command to its handler", async () => {
|
||||
await expect(parseHost(["--host", "global:6767", "ls"])).resolves.toBe("global:6767");
|
||||
});
|
||||
|
||||
it("preserves the command-local host position", async () => {
|
||||
await expect(parseHost(["ls", "--host", "local:6767"])).resolves.toBe("local:6767");
|
||||
});
|
||||
|
||||
it("uses the last explicit host", async () => {
|
||||
await expect(parseHost(["--host", "first:6767", "ls", "--host", "last:6767"])).resolves.toBe(
|
||||
"last:6767",
|
||||
);
|
||||
});
|
||||
|
||||
it("lets local-only commands ignore a global host", async () => {
|
||||
const program = new Command().exitOverride().option("--host <host>");
|
||||
let called = false;
|
||||
program.command("status").action(() => {
|
||||
called = true;
|
||||
});
|
||||
|
||||
await program.parseAsync(["--host", "ignored:6767", "status"], { from: "user" });
|
||||
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -21,3 +21,14 @@ export function addDaemonHostOption<T extends Command>(command: T): T {
|
||||
export function addJsonAndDaemonHostOptions<T extends Command>(command: T): T {
|
||||
return addDaemonHostOption(addJsonOption(command));
|
||||
}
|
||||
|
||||
export function withGlobalOptions<Args extends unknown[], Result>(
|
||||
handler: (...args: Args) => Result,
|
||||
): (...args: Args) => Result {
|
||||
return (...args) => {
|
||||
const command = args.at(-1) as Command;
|
||||
const mergedArgs = [...args];
|
||||
mergedArgs[mergedArgs.length - 2] = command.optsWithGlobals();
|
||||
return handler(...(mergedArgs as Args));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -175,6 +175,22 @@ try {
|
||||
console.log("✓ daemon status --json outputs valid JSON\n");
|
||||
}
|
||||
|
||||
// Global hosts do not change local daemon lifecycle commands.
|
||||
{
|
||||
console.log("Test 5b: daemon status ignores a global --host");
|
||||
const result = await runLocalPaseo(["--host", "localhost:1", "daemon", "status", "--json"], {
|
||||
PASEO_HOME: paseoHome,
|
||||
});
|
||||
assert.strictEqual(result.exitCode, 0, `daemon status should stay local: ${result.stderr}`);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
assert.strictEqual(
|
||||
parsed.localDaemon,
|
||||
"stopped",
|
||||
"daemon should report the local stopped state",
|
||||
);
|
||||
console.log("✓ daemon status ignores a global --host\n");
|
||||
}
|
||||
|
||||
// Test 6: daemon stop handles daemon not running gracefully
|
||||
{
|
||||
console.log("Test 6: daemon stop handles daemon not running");
|
||||
|
||||
@@ -170,6 +170,33 @@ try {
|
||||
assert(output.includes("unknown option"), "should report unknown option for --ui");
|
||||
console.log("✓ paseo ls --ui is rejected\n");
|
||||
}
|
||||
|
||||
// Test 10: global --host reaches the command handler
|
||||
{
|
||||
console.log("Test 10: global --host targets the requested daemon");
|
||||
const host = `localhost:${port}`;
|
||||
const result = await runLocalPaseo(["--host", host, "ls"], {
|
||||
PASEO_HOST: "localhost:1",
|
||||
PASEO_HOME: paseoHome,
|
||||
});
|
||||
const output = result.stdout + result.stderr;
|
||||
assert.notStrictEqual(result.exitCode, 0, "should fail when the selected daemon is absent");
|
||||
assert(output.includes(host), "connection error should name the global host");
|
||||
console.log("✓ global --host targets the requested daemon\n");
|
||||
}
|
||||
|
||||
// Test 11: the last explicit --host wins
|
||||
{
|
||||
console.log("Test 11: the last explicit --host wins");
|
||||
const firstHost = `localhost:${port}`;
|
||||
const lastHost = `localhost:${port + 1}`;
|
||||
const result = await runLocalPaseo(["--host", firstHost, "ls", "--host", lastHost]);
|
||||
const output = result.stdout + result.stderr;
|
||||
assert.notStrictEqual(result.exitCode, 0, "should fail when the selected daemon is absent");
|
||||
assert(output.includes(lastHost), "connection error should name the last explicit host");
|
||||
assert(!output.includes(firstHost), "the earlier host should be fully overridden");
|
||||
console.log("✓ the last explicit --host wins\n");
|
||||
}
|
||||
} finally {
|
||||
// Clean up temp directory
|
||||
await rm(paseoHome, { recursive: true, force: true });
|
||||
|
||||
@@ -22,6 +22,8 @@ try {
|
||||
"review-prs",
|
||||
"--provider",
|
||||
"claude",
|
||||
"--cwd",
|
||||
ctx.workDir,
|
||||
"--json",
|
||||
],
|
||||
{ timeout: 30000 },
|
||||
@@ -78,6 +80,8 @@ try {
|
||||
"codex/gpt-5.4",
|
||||
"--thinking",
|
||||
"high",
|
||||
"--cwd",
|
||||
ctx.workDir,
|
||||
"--json",
|
||||
],
|
||||
{ timeout: 30000 },
|
||||
@@ -111,6 +115,8 @@ try {
|
||||
"self",
|
||||
"--provider",
|
||||
"codex/gpt-5.4",
|
||||
"--cwd",
|
||||
ctx.workDir,
|
||||
],
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
@@ -134,6 +140,8 @@ try {
|
||||
"0 0 1 1 *",
|
||||
"--target",
|
||||
"00000000-0000-4000-8000-000000000001",
|
||||
"--cwd",
|
||||
ctx.workDir,
|
||||
"--json",
|
||||
],
|
||||
{ timeout: 30000 },
|
||||
|
||||
+12
-12
@@ -30,10 +30,10 @@ Ask the daemon to inspect the provider environment it actually uses:
|
||||
```bash
|
||||
paseo provider diagnostic claude
|
||||
paseo provider diagnostic codex --json
|
||||
paseo provider diagnostic opencode --host devbox:6767
|
||||
paseo --host devbox:6767 provider diagnostic opencode
|
||||
```
|
||||
|
||||
The diagnostic includes the configured command, daemon `PATH` and shell, matching binaries, resolved path, version, model count, and provider status. Use `--host` for a remote daemon. This is the same diagnostic shown under **Settings → your host → Providers → provider → Diagnostic**.
|
||||
The diagnostic includes the configured command, daemon `PATH` and shell, matching binaries, resolved path, version, model count, and provider status. Use the global `--host` option for a remote daemon. This is the same diagnostic shown under **Settings → your host → Providers → provider → Diagnostic**.
|
||||
|
||||
## Running agents
|
||||
|
||||
@@ -79,10 +79,10 @@ paseo project delete <project-id>
|
||||
|
||||
`--reset` restores the name derived from the project directory. Deleting a project archives its active workspaces and removes the project from Paseo. It does not delete the project directory.
|
||||
|
||||
For a local daemon, `paseo project create [path]` defaults to the current directory and resolves relative paths on the CLI machine. When you use `--host` or `PASEO_HOST`, provide a path that the target daemon can access:
|
||||
For a local daemon, `paseo project create [path]` defaults to the current directory and resolves relative paths on the CLI machine. When you use the global `--host` option or `PASEO_HOST`, provide a path that the target daemon can access:
|
||||
|
||||
```bash
|
||||
paseo project create /srv/repos/api --host devbox:6767
|
||||
paseo --host devbox:6767 project create /srv/repos/api
|
||||
```
|
||||
|
||||
The remote daemon interprets that path on its own machine. See [Workspaces](/docs/workspaces) for how projects group working directories and sessions.
|
||||
@@ -138,13 +138,13 @@ paseo script start web
|
||||
paseo script stop web
|
||||
```
|
||||
|
||||
By default, Paseo selects the workspace whose directory is the current directory. Pass `--cwd <path>` to select a different directory, or `--workspace <workspace-id>` when a directory has multiple workspaces. These commands also accept `--host` and the standard output options such as `--json`.
|
||||
By default, Paseo selects the workspace whose directory is the current directory. Pass `--cwd <path>` to select a different directory, or `--workspace <workspace-id>` when a directory has multiple workspaces. Use the global `--host` option to target another daemon. These commands also accept standard output options such as `--json`.
|
||||
|
||||
The output includes each script's lifecycle and supervised terminal ID. Services also include their assigned port, proxy URL, and health. See [Git worktrees](/docs/worktrees#scripts-and-services) for `paseo.json` configuration.
|
||||
|
||||
## Plugins
|
||||
|
||||
> **Trust every plugin you add.** `paseo plugin add` and `paseo plugin install` mean “I trust this codebase.” Plugin server code and Git preparation commands run unsandboxed with the daemon user's access on the daemon host; client contributions run inside Paseo. Dependencies and future updates are part of that decision. With `--host`, commands run on the remote daemon host.
|
||||
> **Trust every plugin you add.** `paseo plugin add` and `paseo plugin install` mean “I trust this codebase.” Plugin server code and Git preparation commands run unsandboxed with the daemon user's access on the daemon host; client contributions run inside Paseo. Dependencies and future updates are part of that decision. With the global `--host` option, commands run on the remote daemon host.
|
||||
|
||||
Create and manage trusted plugins on a daemon:
|
||||
|
||||
@@ -167,7 +167,7 @@ paseo plugin remove my-plugin
|
||||
|
||||
GitHub shorthand checks an existing host directory first. Append `:<directory>` for a plugin in a
|
||||
monorepo. `paseo plugin logs <id>` returns the plugin's recent daemon-side stdout and stderr. Add `--json` for
|
||||
structured entries or `--host <target>` for another daemon. See the
|
||||
structured entries, or run `paseo --host <target> plugin logs <id>` for another daemon. See the
|
||||
[Plugin reference](/docs/plugins/reference) for installation, trust, lifecycle, and log-retention
|
||||
behavior.
|
||||
|
||||
@@ -264,7 +264,7 @@ paseo daemon reload # Reload config.json
|
||||
paseo daemon stop # Stop the daemon
|
||||
```
|
||||
|
||||
Reload validates the whole file, applies runtime-safe changes, and reports `appliedPaths`, `restartRequiredPaths`, and `overrideControlledPaths`. Human output prints `paseo daemon restart` only when a changed setting needs it. Use `--json` or `--format yaml` for the structured result, and `--host` to reload a remote daemon's own configuration file. An older host that does not support reload returns an update-host error.
|
||||
Reload validates the whole file, applies runtime-safe changes, and reports `appliedPaths`, `restartRequiredPaths`, and `overrideControlledPaths`. Human output prints `paseo daemon restart` only when a changed setting needs it. Use `--json` or `--format yaml` for the structured result. Run `paseo --host <target> reload` to reload a remote daemon's own configuration file. An older host that does not support reload returns an update-host error.
|
||||
|
||||
Use `PASEO_HOME` to run multiple isolated daemon instances.
|
||||
|
||||
@@ -302,7 +302,7 @@ See [Daemons in Hub](/docs/hub/daemons), [Hub configuration](/docs/hub/configura
|
||||
|
||||
## Connecting to a remote daemon
|
||||
|
||||
`--host` accepts either a local target (`host:port`, a unix socket, or a Windows pipe) or a pairing offer URL, the same `https://app.paseo.sh/#offer=...` link the mobile app uses for QR pairing. With an offer URL the CLI connects through the Paseo relay with end-to-end encryption, so you can drive a daemon on another machine without exposing it to the network.
|
||||
The global `--host` option accepts either a local target (`host:port`, a unix socket, or a Windows pipe) or a pairing offer URL, the same `https://app.paseo.sh/#offer=...` link the mobile app uses for QR pairing. With an offer URL the CLI connects through the Paseo relay with end-to-end encryption, so you can drive a daemon on another machine without exposing it to the network.
|
||||
|
||||
Get an offer URL from the daemon you want to control:
|
||||
|
||||
@@ -317,11 +317,11 @@ Relay is off for new installations. In non-interactive or JSON mode, a disabled
|
||||
Use it from anywhere:
|
||||
|
||||
```bash
|
||||
paseo ls --host 'https://app.paseo.sh/#offer=eyJ2IjoyLC...'
|
||||
paseo run --host "$OFFER_URL" "fix the failing tests"
|
||||
paseo --host 'https://app.paseo.sh/#offer=eyJ2IjoyLC...' ls
|
||||
paseo --host "$OFFER_URL" run "fix the failing tests"
|
||||
```
|
||||
|
||||
You can also set it once via `PASEO_HOST` instead of passing `--host` on every command.
|
||||
You can also set it once via `PASEO_HOST` instead of passing `--host` on every command. An explicit flag overrides the environment variable.
|
||||
|
||||
## Multi-agent workflows
|
||||
|
||||
|
||||
@@ -230,6 +230,7 @@ Set the persisted value in `config.json`:
|
||||
## Common env vars
|
||||
|
||||
- `PASEO_HOME`, set Paseo home directory
|
||||
- `PASEO_HOST`, set the daemon target for CLI commands
|
||||
- `PASEO_PASSWORD`, on the daemon, the password to require (plaintext, hashed at startup); on the CLI, the password used to connect when the host URI doesn't include one
|
||||
- `PASEO_LISTEN`, override `daemon.listen`
|
||||
- `PASEO_RELAY_ENABLED`, enable or disable the outbound relay for this daemon launch
|
||||
|
||||
@@ -28,22 +28,22 @@ Before connecting:
|
||||
The CLI accepts an SSH URI as its host:
|
||||
|
||||
```bash
|
||||
paseo ls -a --host ssh://user@host
|
||||
paseo --host ssh://user@host ls -a
|
||||
```
|
||||
|
||||
The daemon is expected at `127.0.0.1:6767` on the remote host. The port in the SSH URL is the SSH server port:
|
||||
|
||||
```bash
|
||||
paseo ls -a --host ssh://user@host:2222
|
||||
paseo --host ssh://user@host:2222 ls -a
|
||||
```
|
||||
|
||||
Set a different remote daemon port with `daemonPort`:
|
||||
|
||||
```bash
|
||||
paseo ls -a --host 'ssh://user@host?daemonPort=7777'
|
||||
paseo --host 'ssh://user@host?daemonPort=7777' ls -a
|
||||
```
|
||||
|
||||
`--host` belongs after the command. `paseo daemon status` checks only the local daemon; use `paseo ls --host ...` to verify a remote connection. `paseo run --host ...` also requires `--cwd` with a path that exists on the remote host.
|
||||
Put `--host` before the command. `paseo daemon status` checks only the local daemon; use `paseo --host ssh://user@host ls` to verify a remote connection. `paseo --host ssh://user@host run --cwd /path/on/remote ...` requires a working directory that exists on the remote host.
|
||||
|
||||
In Paseo Desktop, open **Settings → Add host → Remote SSH** and enter the same `ssh://` destination.
|
||||
|
||||
|
||||
@@ -901,7 +901,7 @@ CLI:
|
||||
```bash
|
||||
paseo plugin logs my-plugin
|
||||
paseo plugin logs my-plugin --json
|
||||
paseo plugin logs my-plugin --host <url>
|
||||
paseo --host <url> plugin logs my-plugin
|
||||
```
|
||||
|
||||
The command returns a snapshot rather than following live output. Refresh the settings view or run
|
||||
@@ -1025,11 +1025,11 @@ paseo plugin enable my-plugin
|
||||
paseo plugin remove my-plugin
|
||||
```
|
||||
|
||||
Pass `--host <url>` to management commands when the target is not the CLI's default daemon. `remove`
|
||||
Put `--host <url>` before a management command when the target is not the CLI's default daemon. `remove`
|
||||
never deletes a directory source; it deletes the managed checkout for a Git source. The install-time
|
||||
`--id` is the runtime ID and allows the same directory or repository to be installed more than once.
|
||||
|
||||
> **Trust every plugin you add.** `paseo plugin add` and `paseo plugin install` mean “I trust this codebase.” Server code and Git preparation commands run unsandboxed with the daemon user's access on the daemon host; client contributions run inside Paseo. Dependencies and future updates are part of that decision. With `--host`, commands run on the remote daemon host.
|
||||
> **Trust every plugin you add.** `paseo plugin add` and `paseo plugin install` mean “I trust this codebase.” Server code and Git preparation commands run unsandboxed with the daemon user's access on the daemon host; client contributions run inside Paseo. Dependencies and future updates are part of that decision. With the global `--host` option, commands run on the remote daemon host.
|
||||
|
||||
An existing directory wins over `owner/repository` GitHub shorthand. Append `:relative/path` when
|
||||
the plugin lives below the repository root. Omit `--ref` to track the default branch. Explicit
|
||||
@@ -1054,7 +1054,7 @@ from the staged plugin directory after resolving the exact commit and manifest.
|
||||
package manager or commands from lockfiles. Install and update both run `build` before validation,
|
||||
compilation, activation, or replacement. A failing command reports its output, discards the
|
||||
candidate, and leaves the installed/running version intact. The daemon log records each command and
|
||||
output; with `--host`, execution is on that daemon host.
|
||||
output; with the global `--host` option, execution is on that daemon host.
|
||||
|
||||
Run `npm run typecheck` before install or reload. Never edit the daemon config directly.
|
||||
|
||||
|
||||
@@ -105,4 +105,4 @@ Schedules default to UTC. Pass `--timezone <IANA>` to interpret cron fields in a
|
||||
|
||||
Schedules wait for the next matching cron time by default. Pass `--run-now` to start one immediate run on creation.
|
||||
|
||||
When targeting a remote daemon with `--host`, pass `--cwd`; your local working directory may not exist on the remote machine.
|
||||
Start the command with `paseo --host <target> schedule create ...` when targeting a remote daemon. Pass `--cwd`; your local working directory may not exist on the remote machine.
|
||||
|
||||
@@ -28,10 +28,10 @@ From a terminal or agent, request the same diagnostic from the affected daemon:
|
||||
|
||||
```bash
|
||||
paseo provider diagnostic <provider>
|
||||
paseo provider diagnostic <provider> --host <host:port> --json
|
||||
paseo --host <host:port> provider diagnostic <provider> --json
|
||||
```
|
||||
|
||||
Use `--host` when the affected daemon is not the CLI's default local daemon.
|
||||
Use the global `--host` option when the affected daemon is not the CLI's default local daemon.
|
||||
|
||||
`not found` together with a **Daemon PATH** that's missing your binary's directory is the common case: that directory is on your terminal's `PATH` but not on Paseo's.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user