diff --git a/docs/development.md b/docs/development.md index 02ec592010..2d8d86aa79 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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 diff --git a/packages/cli/src/cli-surface.test.ts b/packages/cli/src/cli-surface.test.ts index fd779016e0..3243529c23 100644 --- a/packages/cli/src/cli-surface.test.ts +++ b/packages/cli/src/cli-surface.test.ts @@ -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 "); + }); + it("shows project, workspace, and heartbeat commands while hiding worktree compatibility", () => { const cli = createCli(); const help = cli.helpInformation(); diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index c1e46366a7..275d8f6170 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -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), diff --git a/packages/cli/src/commands/agent/index.ts b/packages/cli/src/commands/agent/index.ts index 85d34ef917..e15f2b5a08 100644 --- a/packages/cli/src/commands/agent/index.ts +++ b/packages/cli/src/commands/agent/index.ts @@ -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), diff --git a/packages/cli/src/commands/plugin/scaffold.test.ts b/packages/cli/src/commands/plugin/scaffold.test.ts index 135286175d..79db0e2878 100644 --- a/packages/cli/src/commands/plugin/scaffold.test.ts +++ b/packages/cli/src/commands/plugin/scaffold.test.ts @@ -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( - `/// `, - ); - 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( + `/// `, + ); + 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-")); diff --git a/packages/cli/src/commands/schedule/shared.test.ts b/packages/cli/src/commands/schedule/shared.test.ts index 25f1bb0538..0777134b0e 100644 --- a/packages/cli/src/commands/schedule/shared.test.ts +++ b/packages/cli/src/commands/schedule/shared.test.ts @@ -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: " " }), diff --git a/packages/cli/src/commands/schedule/shared.ts b/packages/cli/src/commands/schedule/shared.ts index f0937dd40b..beae80acc0 100644 --- a/packages/cli/src/commands/schedule/shared.ts +++ b/packages/cli/src/commands/schedule/shared.ts @@ -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: diff --git a/packages/cli/src/commands/terminal/index.ts b/packages/cli/src/commands/terminal/index.ts index 0d0ddfd8b7..ce647f78e6 100644 --- a/packages/cli/src/commands/terminal/index.ts +++ b/packages/cli/src/commands/terminal/index.ts @@ -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 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; } diff --git a/packages/cli/src/output/with-output.ts b/packages/cli/src/output/with-output.ts index 8a0d373156..ad0f631988 100644 --- a/packages/cli/src/output/with-output.ts +++ b/packages/cli/src/output/with-output.ts @@ -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 { @@ -77,11 +78,8 @@ function extractOutputOptions(options: CommandOptions): OutputOptions { export function withOutput( handler: (...args: [...Args, CommandOptions, Command]) => Promise>, ): (...args: [...Args, CommandOptions, Command]) => Promise { - 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( process.stderr.write(errorOutput + "\n"); process.exit(1); } - }; + }); } /** diff --git a/packages/cli/src/utils/client.ts b/packages/cli/src/utils/client.ts index b5140ddda9..bd774ddaff 100644 --- a/packages/cli/src/utils/client.ts +++ b/packages/cli/src/utils/client.ts @@ -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 { + const program = new Command().exitOverride().option("--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 "); + let called = false; + program.command("status").action(() => { + called = true; + }); + + await program.parseAsync(["--host", "ignored:6767", "status"], { from: "user" }); + + expect(called).toBe(true); + }); +}); diff --git a/packages/cli/src/utils/command-options.ts b/packages/cli/src/utils/command-options.ts index 18285fd2f3..b02b4abdb1 100644 --- a/packages/cli/src/utils/command-options.ts +++ b/packages/cli/src/utils/command-options.ts @@ -21,3 +21,14 @@ export function addDaemonHostOption(command: T): T { export function addJsonAndDaemonHostOptions(command: T): T { return addDaemonHostOption(addJsonOption(command)); } + +export function withGlobalOptions( + 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)); + }; +} diff --git a/packages/cli/tests/03-daemon.test.ts b/packages/cli/tests/03-daemon.test.ts index c55c406475..aa84c4caae 100644 --- a/packages/cli/tests/03-daemon.test.ts +++ b/packages/cli/tests/03-daemon.test.ts @@ -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"); diff --git a/packages/cli/tests/04-agent-ls.test.ts b/packages/cli/tests/04-agent-ls.test.ts index 883411ab72..363eba5694 100644 --- a/packages/cli/tests/04-agent-ls.test.ts +++ b/packages/cli/tests/04-agent-ls.test.ts @@ -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 }); diff --git a/packages/cli/tests/31-schedule.test.ts b/packages/cli/tests/31-schedule.test.ts index b3d3c5ada5..64cd6ca64b 100644 --- a/packages/cli/tests/31-schedule.test.ts +++ b/packages/cli/tests/31-schedule.test.ts @@ -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 }, diff --git a/public-docs/cli.md b/public-docs/cli.md index c002654b27..9f8f5f7d8e 100644 --- a/public-docs/cli.md +++ b/public-docs/cli.md @@ -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 `--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 ` to select a different directory, or `--workspace ` 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 ` to select a different directory, or `--workspace ` 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 `:` for a plugin in a monorepo. `paseo plugin logs ` returns the plugin's recent daemon-side stdout and stderr. Add `--json` for -structured entries or `--host ` for another daemon. See the +structured entries, or run `paseo --host plugin logs ` 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 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 diff --git a/public-docs/configuration.md b/public-docs/configuration.md index fe111978e5..a220420ee4 100644 --- a/public-docs/configuration.md +++ b/public-docs/configuration.md @@ -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 diff --git a/public-docs/connectivity.md b/public-docs/connectivity.md index a80d483f5a..425e605fd1 100644 --- a/public-docs/connectivity.md +++ b/public-docs/connectivity.md @@ -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. diff --git a/public-docs/plugins/reference.md b/public-docs/plugins/reference.md index 2a47172be0..bf3417018e 100644 --- a/public-docs/plugins/reference.md +++ b/public-docs/plugins/reference.md @@ -901,7 +901,7 @@ CLI: ```bash paseo plugin logs my-plugin paseo plugin logs my-plugin --json -paseo plugin logs my-plugin --host +paseo --host 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 ` to management commands when the target is not the CLI's default daemon. `remove` +Put `--host ` 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. diff --git a/public-docs/schedules-cli.md b/public-docs/schedules-cli.md index 48ffbd66c9..8f1fd62302 100644 --- a/public-docs/schedules-cli.md +++ b/public-docs/schedules-cli.md @@ -105,4 +105,4 @@ Schedules default to UTC. Pass `--timezone ` 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 schedule create ...` when targeting a remote daemon. Pass `--cwd`; your local working directory may not exist on the remote machine. diff --git a/public-docs/troubleshooting.md b/public-docs/troubleshooting.md index 0ced52caf3..77b2497e03 100644 --- a/public-docs/troubleshooting.md +++ b/public-docs/troubleshooting.md @@ -28,10 +28,10 @@ From a terminal or agent, request the same diagnostic from the affected daemon: ```bash paseo provider diagnostic -paseo provider diagnostic --host --json +paseo --host provider diagnostic --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.