Manage projects from the CLI (#3460)
* feat(cli): manage projects Expose the existing project lifecycle through one CLI command group so projects can be managed without opening the app. * fix(cli): preserve remote project paths Project paths belong to the daemon when a target is explicit. Require that path instead of resolving the CLI machine's cwd, and keep local convenience resolution for the default connection. * docs(cli): document project management * docs(cli): remove repeated project reference
This commit is contained in:
@@ -134,6 +134,7 @@ Commander.js CLI with Docker-style commands. Common agent operations are also ex
|
|||||||
- `paseo script ls/start/stop`
|
- `paseo script ls/start/stop`
|
||||||
- `paseo schedule create/ls/inspect/update/pause/resume/run-once/logs/delete`
|
- `paseo schedule create/ls/inspect/update/pause/resume/run-once/logs/delete`
|
||||||
- `paseo heartbeat create/update/delete`
|
- `paseo heartbeat create/update/delete`
|
||||||
|
- `paseo project create/ls/rename/delete`
|
||||||
- `paseo workspace create/ls/rename/archive`
|
- `paseo workspace create/ls/rename/archive`
|
||||||
- `paseo permit allow/deny/ls`
|
- `paseo permit allow/deny/ls`
|
||||||
- `paseo provider ls/models`
|
- `paseo provider ls/models`
|
||||||
|
|||||||
+1
-1
@@ -510,7 +510,7 @@ install.
|
|||||||
|
|
||||||
Use `npm run cli` to run the in-repo CLI from source (`npx tsx packages/cli/src/index.ts`). The script wraps the CLI with `scripts/dev-home.sh`, so it automatically uses this checkout's `.dev/paseo-home` and dev daemon endpoint unless you pass an explicit override. The globally installed `paseo` binary on macOS is a symlink into the installed Paseo desktop app, not this checkout — use it to drive the desktop's built-in daemon, but use `npm run cli` when you want to talk to the CLI you are editing.
|
Use `npm run cli` to run the in-repo CLI from source (`npx tsx packages/cli/src/index.ts`). The script wraps the CLI with `scripts/dev-home.sh`, so it automatically uses this checkout's `.dev/paseo-home` and dev daemon endpoint unless you pass an explicit override. The globally installed `paseo` binary on macOS is a symlink into the installed Paseo desktop app, not this checkout — use it to drive the desktop's built-in daemon, but use `npm run cli` when you want to talk to the CLI you are editing.
|
||||||
|
|
||||||
Canonical automation uses `paseo workspace create/ls/rename/archive`, `paseo heartbeat create/update/delete`, and the full `paseo schedule` group. MCP heartbeat automation is intentionally smaller: create and delete only. Detach remains an explicit user lifecycle action rather than an agent tool. `paseo run --new-workspace local|worktree` composes workspace creation with agent creation. The old `paseo worktree` and `paseo run --worktree` forms are hidden compatibility aliases.
|
Canonical automation uses `paseo project create/ls/rename/delete`, `paseo workspace create/ls/rename/archive`, `paseo heartbeat create/update/delete`, and the full `paseo schedule` group. MCP heartbeat automation is intentionally smaller: create and delete only. Detach remains an explicit user lifecycle action rather than an agent tool. `paseo run --new-workspace local|worktree` composes workspace creation with agent creation. The old `paseo worktree` and `paseo run --worktree` forms are hidden compatibility aliases.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run cli -- ls -a -g # List all agents globally
|
npm run cli -- ls -a -g # List all agents globally
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { createCli } from "./cli.js";
|
import { createCli } from "./cli.js";
|
||||||
|
|
||||||
describe("canonical CLI surface", () => {
|
describe("canonical CLI surface", () => {
|
||||||
it("shows workspace and heartbeat commands while hiding worktree compatibility", () => {
|
it("shows project, workspace, and heartbeat commands while hiding worktree compatibility", () => {
|
||||||
const cli = createCli();
|
const cli = createCli();
|
||||||
const help = cli.helpInformation();
|
const help = cli.helpInformation();
|
||||||
|
expect(help).toContain("project");
|
||||||
expect(help).toContain("workspace");
|
expect(help).toContain("workspace");
|
||||||
expect(help).toContain("heartbeat");
|
expect(help).toContain("heartbeat");
|
||||||
expect(help).not.toContain("worktree");
|
expect(help).not.toContain("worktree");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createDaemonCommand } from "./commands/daemon/index.js";
|
|||||||
import { createPermitCommand } from "./commands/permit/index.js";
|
import { createPermitCommand } from "./commands/permit/index.js";
|
||||||
import { createProviderCommand } from "./commands/provider/index.js";
|
import { createProviderCommand } from "./commands/provider/index.js";
|
||||||
import { createPluginCommand } from "./commands/plugin/index.js";
|
import { createPluginCommand } from "./commands/plugin/index.js";
|
||||||
|
import { createProjectCommand } from "./commands/project/index.js";
|
||||||
import { createScheduleCommand } from "./commands/schedule/index.js";
|
import { createScheduleCommand } from "./commands/schedule/index.js";
|
||||||
import { createSpeechCommand } from "./commands/speech/index.js";
|
import { createSpeechCommand } from "./commands/speech/index.js";
|
||||||
import { createScriptCommand } from "./commands/script/index.js";
|
import { createScriptCommand } from "./commands/script/index.js";
|
||||||
@@ -194,6 +195,7 @@ export function createCli(): Command {
|
|||||||
program.addCommand(createSpeechCommand());
|
program.addCommand(createSpeechCommand());
|
||||||
|
|
||||||
// Workspace commands
|
// Workspace commands
|
||||||
|
program.addCommand(createProjectCommand());
|
||||||
program.addCommand(createWorkspaceCommand());
|
program.addCommand(createWorkspaceCommand());
|
||||||
// COMPAT(worktreeCli): legacy command alias added before workspace was the product unit.
|
// COMPAT(worktreeCli): legacy command alias added before workspace was the product unit.
|
||||||
// Added in v0.2.0; remove after 2027-01-17.
|
// Added in v0.2.0; remove after 2027-01-17.
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import type { Command } from "commander";
|
||||||
|
import type { CommandError, CommandOptions, SingleResult } from "../../output/index.js";
|
||||||
|
import { buildDaemonConnectionCommandError, connectToDaemon } from "../../utils/client.js";
|
||||||
|
import { projectSchema, toProjectRow, type ProjectRow } from "./shared.js";
|
||||||
|
|
||||||
|
export function resolveProjectPath(input: {
|
||||||
|
pathArg?: string;
|
||||||
|
cwd: string;
|
||||||
|
daemonTarget?: string;
|
||||||
|
}): string {
|
||||||
|
if (input.daemonTarget) {
|
||||||
|
if (input.pathArg === undefined) {
|
||||||
|
throw {
|
||||||
|
code: "MISSING_PATH",
|
||||||
|
message: "Project path is required when targeting a daemon explicitly",
|
||||||
|
details: "Usage: paseo project create <path> --host <host>",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
return input.pathArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.resolve(input.cwd, input.pathArg ?? ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCreateCommand(
|
||||||
|
pathArg: string | undefined,
|
||||||
|
options: CommandOptions,
|
||||||
|
_command: Command,
|
||||||
|
): Promise<SingleResult<ProjectRow>> {
|
||||||
|
const projectPath = resolveProjectPath({
|
||||||
|
pathArg,
|
||||||
|
cwd: process.cwd(),
|
||||||
|
daemonTarget: options.host ?? process.env.PASEO_HOST,
|
||||||
|
});
|
||||||
|
const client = await connectToDaemon({ host: options.host }).catch((error: unknown) => {
|
||||||
|
throw buildDaemonConnectionCommandError({ host: options.host, error });
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await client.addProject(projectPath);
|
||||||
|
if (!payload.project) {
|
||||||
|
throw new Error(payload.error ?? "Project creation failed");
|
||||||
|
}
|
||||||
|
return { type: "single", data: toProjectRow(payload.project), schema: projectSchema };
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
throw { code: "PROJECT_CREATE_FAILED", message } satisfies CommandError;
|
||||||
|
} finally {
|
||||||
|
await client.close().catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { Command } from "commander";
|
||||||
|
import type { CommandError, CommandOptions, SingleResult } from "../../output/index.js";
|
||||||
|
import { buildDaemonConnectionCommandError, connectToDaemon } from "../../utils/client.js";
|
||||||
|
|
||||||
|
interface ProjectDeleteResult {
|
||||||
|
projectId: string;
|
||||||
|
removedWorkspaceIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectDeleteSchema = {
|
||||||
|
idField: "projectId" as const,
|
||||||
|
columns: [
|
||||||
|
{ header: "PROJECT ID", field: "projectId" as const, width: 20 },
|
||||||
|
{
|
||||||
|
header: "REMOVED WORKSPACES",
|
||||||
|
field: (result: ProjectDeleteResult) => result.removedWorkspaceIds.length,
|
||||||
|
width: 18,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function runDeleteCommand(
|
||||||
|
projectId: string,
|
||||||
|
options: CommandOptions,
|
||||||
|
_command: Command,
|
||||||
|
): Promise<SingleResult<ProjectDeleteResult>> {
|
||||||
|
const client = await connectToDaemon({ host: options.host }).catch((error: unknown) => {
|
||||||
|
throw buildDaemonConnectionCommandError({ host: options.host, error });
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await client.removeProject(projectId);
|
||||||
|
return {
|
||||||
|
type: "single",
|
||||||
|
data: { projectId, removedWorkspaceIds: result.removedWorkspaceIds },
|
||||||
|
schema: projectDeleteSchema,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
throw { code: "PROJECT_DELETE_FAILED", message } satisfies CommandError;
|
||||||
|
} finally {
|
||||||
|
await client.close().catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
import { withOutput } from "../../output/index.js";
|
||||||
|
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
||||||
|
import { runCreateCommand } from "./create.js";
|
||||||
|
import { runDeleteCommand } from "./delete.js";
|
||||||
|
import { runLsCommand } from "./ls.js";
|
||||||
|
import { runRenameCommand } from "./rename.js";
|
||||||
|
|
||||||
|
export function createProjectCommand(): Command {
|
||||||
|
const project = new Command("project").description("Manage projects");
|
||||||
|
|
||||||
|
addJsonAndDaemonHostOptions(
|
||||||
|
project
|
||||||
|
.command("create")
|
||||||
|
.description("Register a project directory")
|
||||||
|
.argument("[path]", "Project directory (default: current directory)"),
|
||||||
|
).action(withOutput(runCreateCommand));
|
||||||
|
|
||||||
|
addJsonAndDaemonHostOptions(project.command("ls").description("List projects")).action(
|
||||||
|
withOutput(runLsCommand),
|
||||||
|
);
|
||||||
|
|
||||||
|
addJsonAndDaemonHostOptions(
|
||||||
|
project
|
||||||
|
.command("rename")
|
||||||
|
.description("Set a project's user-visible name")
|
||||||
|
.argument("<project-id>", "Project id")
|
||||||
|
.argument("[name]", "New project name")
|
||||||
|
.option("--reset", "Clear the custom name and use the directory name")
|
||||||
|
.allowExcessArguments(false),
|
||||||
|
).action(withOutput(runRenameCommand));
|
||||||
|
|
||||||
|
addJsonAndDaemonHostOptions(
|
||||||
|
project
|
||||||
|
.command("delete")
|
||||||
|
.description("Delete a project and its workspaces")
|
||||||
|
.argument("<project-id>", "Project id"),
|
||||||
|
).action(withOutput(runDeleteCommand));
|
||||||
|
|
||||||
|
return project;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { Command } from "commander";
|
||||||
|
import type { CommandOptions, ListResult } from "../../output/index.js";
|
||||||
|
import { buildDaemonConnectionCommandError, connectToDaemon } from "../../utils/client.js";
|
||||||
|
import { projectSchema, toProjectRow, type ProjectRow } from "./shared.js";
|
||||||
|
|
||||||
|
export async function runLsCommand(
|
||||||
|
options: CommandOptions,
|
||||||
|
_command: Command,
|
||||||
|
): Promise<ListResult<ProjectRow>> {
|
||||||
|
const client = await connectToDaemon({ host: options.host }).catch((error: unknown) => {
|
||||||
|
throw buildDaemonConnectionCommandError({ host: options.host, error });
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await client.listProjects();
|
||||||
|
return {
|
||||||
|
type: "list",
|
||||||
|
data: payload.projects.map(toProjectRow),
|
||||||
|
schema: projectSchema,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await client.close().catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { resolveProjectPath, runCreateCommand } from "./create.js";
|
||||||
|
import { runDeleteCommand } from "./delete.js";
|
||||||
|
import { createProjectCommand } from "./index.js";
|
||||||
|
import { runLsCommand } from "./ls.js";
|
||||||
|
import { resolveProjectName, runRenameCommand } from "./rename.js";
|
||||||
|
|
||||||
|
const project = {
|
||||||
|
projectId: "project-1",
|
||||||
|
projectDisplayName: "Paseo",
|
||||||
|
projectCustomName: null,
|
||||||
|
projectRootPath: "/tmp/paseo",
|
||||||
|
projectKind: "git" as const,
|
||||||
|
};
|
||||||
|
const addProject = vi.fn(async () => ({ project, error: null }));
|
||||||
|
const listProjects = vi.fn(async () => ({ projects: [project] }));
|
||||||
|
const renameProject = vi.fn(async (_projectId: string, name: string | null) => ({
|
||||||
|
customName: name,
|
||||||
|
}));
|
||||||
|
const removeProject = vi.fn(async () => ({ removedWorkspaceIds: ["workspace-1"] }));
|
||||||
|
const close = vi.fn(async () => undefined);
|
||||||
|
|
||||||
|
vi.mock("../../utils/client.js", () => ({
|
||||||
|
buildDaemonConnectionCommandError: vi.fn(({ error }: { error: unknown }) => error),
|
||||||
|
connectToDaemon: vi.fn(async () => ({
|
||||||
|
addProject,
|
||||||
|
listProjects,
|
||||||
|
renameProject,
|
||||||
|
removeProject,
|
||||||
|
close,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("project commands", () => {
|
||||||
|
it("exposes the complete project lifecycle", () => {
|
||||||
|
expect(createProjectCommand().commands.map((command) => command.name())).toEqual([
|
||||||
|
"create",
|
||||||
|
"ls",
|
||||||
|
"rename",
|
||||||
|
"delete",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a project from an absolute directory path", async () => {
|
||||||
|
const result = await runCreateCommand("relative/project", {}, {} as never);
|
||||||
|
|
||||||
|
expect(addProject).toHaveBeenCalledWith(path.resolve("relative/project"));
|
||||||
|
expect(result.data).toEqual({
|
||||||
|
projectId: "project-1",
|
||||||
|
name: "Paseo",
|
||||||
|
kind: "git",
|
||||||
|
path: "/tmp/paseo",
|
||||||
|
});
|
||||||
|
expect(close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves paths locally and leaves daemon-owned paths unchanged", () => {
|
||||||
|
expect(resolveProjectPath({ cwd: "/home/user", pathArg: "repo" })).toBe(
|
||||||
|
path.resolve("/home/user/repo"),
|
||||||
|
);
|
||||||
|
expect(resolveProjectPath({ cwd: "/home/user" })).toBe(path.resolve("/home/user"));
|
||||||
|
expect(
|
||||||
|
resolveProjectPath({ cwd: "/home/user", pathArg: "/srv/repo", daemonTarget: "host:6767" }),
|
||||||
|
).toBe("/srv/repo");
|
||||||
|
expect(
|
||||||
|
resolveProjectPath({ cwd: "/home/user", pathArg: "~/repo", daemonTarget: "host:6767" }),
|
||||||
|
).toBe("~/repo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires a daemon-owned path for an explicit target", () => {
|
||||||
|
expect(() => resolveProjectPath({ cwd: "/home/user", daemonTarget: "host:6767" })).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lists projects", async () => {
|
||||||
|
const result = await runLsCommand({}, {} as never);
|
||||||
|
|
||||||
|
expect(listProjects).toHaveBeenCalled();
|
||||||
|
expect(result.data).toHaveLength(1);
|
||||||
|
expect(result.data[0]?.projectId).toBe("project-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renames and resets a project", async () => {
|
||||||
|
expect(resolveProjectName({ name: " New name " })).toBe("New name");
|
||||||
|
expect(resolveProjectName({ reset: true })).toBeNull();
|
||||||
|
|
||||||
|
const result = await runRenameCommand("project-1", " New name ", {}, {} as never);
|
||||||
|
expect(renameProject).toHaveBeenCalledWith("project-1", "New name");
|
||||||
|
expect(result.data).toEqual({ projectId: "project-1", name: "New name" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects an empty project name", () => {
|
||||||
|
expect(() => resolveProjectName({ name: " " })).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes a project", async () => {
|
||||||
|
const result = await runDeleteCommand("project-1", {}, {} as never);
|
||||||
|
|
||||||
|
expect(removeProject).toHaveBeenCalledWith("project-1");
|
||||||
|
expect(result.data).toEqual({
|
||||||
|
projectId: "project-1",
|
||||||
|
removedWorkspaceIds: ["workspace-1"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import type { Command } from "commander";
|
||||||
|
import type { CommandError, CommandOptions, SingleResult } from "../../output/index.js";
|
||||||
|
import { buildDaemonConnectionCommandError, connectToDaemon } from "../../utils/client.js";
|
||||||
|
|
||||||
|
interface ProjectRenameResult {
|
||||||
|
projectId: string;
|
||||||
|
name: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectRenameSchema = {
|
||||||
|
idField: "projectId" as const,
|
||||||
|
columns: [
|
||||||
|
{ header: "PROJECT ID", field: "projectId" as const, width: 20 },
|
||||||
|
{ header: "NAME", field: "name" as const, width: 30 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveProjectName(input: { name?: string; reset?: boolean }): string | null {
|
||||||
|
if (input.reset) {
|
||||||
|
if (input.name !== undefined) {
|
||||||
|
throw {
|
||||||
|
code: "INVALID_OPTIONS",
|
||||||
|
message: "--reset cannot be combined with a name",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = input.name?.trim() ?? "";
|
||||||
|
if (name.length === 0) {
|
||||||
|
throw {
|
||||||
|
code: "MISSING_NAME",
|
||||||
|
message: "Project name cannot be empty",
|
||||||
|
details: "Usage: paseo project rename <project-id> <name> | --reset",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runRenameCommand(
|
||||||
|
projectId: string,
|
||||||
|
nameArg: string | undefined,
|
||||||
|
options: CommandOptions & { reset?: boolean },
|
||||||
|
_command: Command,
|
||||||
|
): Promise<SingleResult<ProjectRenameResult>> {
|
||||||
|
const name = resolveProjectName({ name: nameArg, reset: options.reset });
|
||||||
|
const client = await connectToDaemon({ host: options.host }).catch((error: unknown) => {
|
||||||
|
throw buildDaemonConnectionCommandError({ host: options.host, error });
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const applied = await client.renameProject(projectId, name);
|
||||||
|
return {
|
||||||
|
type: "single",
|
||||||
|
data: { projectId, name: applied.customName },
|
||||||
|
schema: projectRenameSchema,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
throw { code: "PROJECT_RENAME_FAILED", message } satisfies CommandError;
|
||||||
|
} finally {
|
||||||
|
await client.close().catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { WorkspaceProjectDescriptorPayload } from "@getpaseo/protocol/messages";
|
||||||
|
import type { OutputSchema } from "../../output/index.js";
|
||||||
|
|
||||||
|
export interface ProjectRow {
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
kind: "git" | "non_git" | "directory";
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const projectSchema: OutputSchema<ProjectRow> = {
|
||||||
|
idField: "projectId",
|
||||||
|
columns: [
|
||||||
|
{ header: "PROJECT ID", field: "projectId", width: 20 },
|
||||||
|
{ header: "NAME", field: "name", width: 24 },
|
||||||
|
{ header: "KIND", field: "kind", width: 10 },
|
||||||
|
{ header: "PATH", field: "path", width: 42 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toProjectRow(project: WorkspaceProjectDescriptorPayload): ProjectRow {
|
||||||
|
return {
|
||||||
|
projectId: project.projectId,
|
||||||
|
name: project.projectDisplayName,
|
||||||
|
kind: project.projectKind,
|
||||||
|
path: project.projectRootPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -54,13 +54,13 @@ describe("runCli", () => {
|
|||||||
|
|
||||||
it("classifies existing unknown directories as open-project invocations", () => {
|
it("classifies existing unknown directories as open-project invocations", () => {
|
||||||
const root = mkdtempSync(path.join(tmpdir(), "paseo-cli-run-"));
|
const root = mkdtempSync(path.join(tmpdir(), "paseo-cli-run-"));
|
||||||
const project = path.join(root, "project");
|
const project = path.join(root, "repository");
|
||||||
mkdirSync(project);
|
mkdirSync(project);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(
|
expect(
|
||||||
createCliParseArgv({
|
createCliParseArgv({
|
||||||
argv: ["project"],
|
argv: ["repository"],
|
||||||
cwd: root,
|
cwd: root,
|
||||||
nodeArgv: ["node", "paseo"],
|
nodeArgv: ["node", "paseo"],
|
||||||
}),
|
}),
|
||||||
|
|||||||
+29
-1
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: CLI
|
title: CLI
|
||||||
description: "Paseo CLI reference: manage agents, workspaces, plugins, scripts, schedules, daemons, and permissions from your terminal."
|
description: "Paseo CLI reference: manage projects, workspaces, agents, plugins, scripts, schedules, daemons, and permissions from your terminal."
|
||||||
nav: CLI
|
nav: CLI
|
||||||
order: 3
|
order: 3
|
||||||
category: Getting started
|
category: Getting started
|
||||||
@@ -59,6 +59,34 @@ Use `--output-schema` to return only matching JSON output. You can pass a schema
|
|||||||
|
|
||||||
By default, `paseo run` waits for completion. Use `--background` to return immediately while the agent keeps running.
|
By default, `paseo run` waits for completion. Use `--background` to return immediately while the agent keeps running.
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
|
||||||
|
Register the current directory as a project, then list the projects known to the daemon:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/dev/my-app
|
||||||
|
paseo project create
|
||||||
|
paseo project ls
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the project ID from `paseo project ls` to rename, reset, or delete a project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
paseo project rename <project-id> "My app"
|
||||||
|
paseo project rename <project-id> --reset
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
paseo project create /srv/repos/api --host devbox:6767
|
||||||
|
```
|
||||||
|
|
||||||
|
The remote daemon interprets that path on its own machine. See [Workspaces](/docs/workspaces) for how projects group working directories and sessions.
|
||||||
|
|
||||||
## Workspaces
|
## Workspaces
|
||||||
|
|
||||||
Create a workspace independently when you want to prepare its files before starting an agent:
|
Create a workspace independently when you want to prepare its files before starting an agent:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ When the desktop app finds installed Paseo skills, it keeps the bundled skills u
|
|||||||
|
|
||||||
## `/paseo`, Paseo Reference
|
## `/paseo`, Paseo Reference
|
||||||
|
|
||||||
The foundational skill. Paseo reference for managing agents and workspaces. Load it when an agent needs to create agents, send them prompts, or manage workspace isolation.
|
The foundational skill. Paseo reference for managing projects, workspaces, and agents. Load it when an agent needs to register a project, create agents, send them prompts, or manage workspace isolation.
|
||||||
|
|
||||||
Not typically invoked directly by users, it's a reference that other skills depend on.
|
Not typically invoked directly by users, it's a reference that other skills depend on.
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ my-app
|
|||||||
|
|
||||||
Each workspace is a separate place to work. You can keep one for your main checkout, create another for a feature, or open a GitHub PR as another workspace.
|
Each workspace is a separate place to work. You can keep one for your main checkout, create another for a feature, or open a GitHub PR as another workspace.
|
||||||
|
|
||||||
|
Use the [CLI project commands](/docs/cli#projects) to register, list, rename, or delete projects.
|
||||||
|
|
||||||
## Workspaces contain sessions
|
## Workspaces contain sessions
|
||||||
|
|
||||||
Agents run inside a workspace as sessions. A workspace can have one agent session, several agent sessions, terminals, browsers, and diffs open at the same time.
|
Agents run inside a workspace as sessions. A workspace can have one agent session, several agent sessions, terminals, browsers, and diffs open at the same time.
|
||||||
|
|||||||
+15
-1
@@ -1,10 +1,24 @@
|
|||||||
---
|
---
|
||||||
name: paseo
|
name: paseo
|
||||||
description: Paseo reference for managing workspaces, workspace scripts, agents, schedules, and heartbeats.
|
description: Paseo reference for managing projects, workspaces, workspace scripts, agents, schedules, and heartbeats.
|
||||||
---
|
---
|
||||||
|
|
||||||
Paseo is a remote daemon that manages coding agents, terminals. Control it through MCP tools or the CLI.
|
Paseo is a remote daemon that manages coding agents, terminals. Control it through MCP tools or the CLI.
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
|
||||||
|
Manage the daemon's project registry through the CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
paseo project create [path]
|
||||||
|
paseo project ls
|
||||||
|
paseo project rename <project-id> <name>
|
||||||
|
paseo project rename <project-id> --reset
|
||||||
|
paseo project delete <project-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
For a local daemon, `project create` defaults to the current directory and resolves relative paths on the CLI machine. With `--host` or `PASEO_HOST`, always provide a path; the target daemon interprets it on its own machine. Deleting a project archives its active workspaces and removes the project from Paseo without deleting the project directory.
|
||||||
|
|
||||||
## Workspaces
|
## Workspaces
|
||||||
|
|
||||||
**`create_workspace`** — create a workspace independently of any agent. Required: `isolation` (`local` or `worktree`). Worktree isolation supports `mode: "branch-off" | "checkout-branch" | "checkout-pr"`: use `branchName`/`baseBranch` for a new branch, `branch` for an existing branch, or `prNumber` plus optional `forge`/`projectPath` for a change request. `worktreeSlug` controls the managed path. Returns the workspace descriptor centered on `workspaceId`.
|
**`create_workspace`** — create a workspace independently of any agent. Required: `isolation` (`local` or `worktree`). Worktree isolation supports `mode: "branch-off" | "checkout-branch" | "checkout-pr"`: use `branchName`/`baseBranch` for a new branch, `branch` for an existing branch, or `prNumber` plus optional `forge`/`projectPath` for a change request. `worktreeSlug` controls the managed path. Returns the workspace descriptor centered on `workspaceId`.
|
||||||
|
|||||||
Reference in New Issue
Block a user