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:
Mohamed Boudra
2026-09-02 22:53:55 +02:00
committed by GitHub
parent 7178583612
commit a835136854
21 changed files with 263 additions and 109 deletions
+6 -3
View File
@@ -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
+4
View File
@@ -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();
+8 -2
View File
@@ -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),
+7 -2
View File
@@ -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: " " }),
+2 -2
View File
@@ -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:
+7 -3
View File
@@ -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;
}
+4 -6
View File
@@ -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);
}
};
});
}
/**
+10 -2
View File
@@ -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);
});
});
+11
View File
@@ -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));
};
}
+16
View File
@@ -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");
+27
View File
@@ -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 });
+8
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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
+4 -4
View File
@@ -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.
+4 -4
View File
@@ -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.
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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.