feat(cli): manage workspace scripts (#1992)
* feat(cli): manage workspace scripts * docs: document workspace script management --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Mohamed Boudra
parent
21597bdc1b
commit
21404fbdec
@@ -7,6 +7,7 @@ import { createPermitCommand } from "./commands/permit/index.js";
|
||||
import { createProviderCommand } from "./commands/provider/index.js";
|
||||
import { createScheduleCommand } from "./commands/schedule/index.js";
|
||||
import { createSpeechCommand } from "./commands/speech/index.js";
|
||||
import { createScriptCommand } from "./commands/script/index.js";
|
||||
import { createTerminalCommand } from "./commands/terminal/index.js";
|
||||
import { createWorktreeCommand } from "./commands/worktree/index.js";
|
||||
import { createWorkspaceCommand } from "./commands/workspace/index.js";
|
||||
@@ -171,6 +172,9 @@ export function createCli(): Command {
|
||||
// Terminal commands
|
||||
program.addCommand(createTerminalCommand());
|
||||
|
||||
// Workspace script commands
|
||||
program.addCommand(createScriptCommand());
|
||||
|
||||
// Loop commands
|
||||
program.addCommand(createLoopCommand());
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Command } from "commander";
|
||||
import { withOutput } from "../../output/index.js";
|
||||
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
||||
import { runLsCommand } from "./ls.js";
|
||||
import { runStartCommand } from "./start.js";
|
||||
import { runStopCommand } from "./stop.js";
|
||||
|
||||
function addWorkspaceSelectionOptions(command: Command): Command {
|
||||
return command
|
||||
.option("--cwd <path>", "Workspace directory (default: current directory)")
|
||||
.option(
|
||||
"--workspace <workspace-id>",
|
||||
"Workspace ID (required when a directory has multiple workspaces)",
|
||||
);
|
||||
}
|
||||
|
||||
export function createScriptCommand(): Command {
|
||||
const script = new Command("script").description("Manage configured workspace scripts");
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
addWorkspaceSelectionOptions(
|
||||
script.command("ls").description("List configured workspace scripts"),
|
||||
),
|
||||
).action(withOutput(runLsCommand));
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
addWorkspaceSelectionOptions(
|
||||
script.command("start").description("Start a configured workspace script").argument("<name>"),
|
||||
),
|
||||
).action(withOutput(runStartCommand));
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
addWorkspaceSelectionOptions(
|
||||
script.command("stop").description("Stop a running workspace script").argument("<name>"),
|
||||
),
|
||||
).action(withOutput(runStopCommand));
|
||||
|
||||
return script;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Command } from "commander";
|
||||
import type { ListResult } from "../../output/index.js";
|
||||
import {
|
||||
connectWorkspaceScriptClient,
|
||||
resolveWorkspaceScriptWorkspaceId,
|
||||
toWorkspaceScriptCommandError,
|
||||
type WorkspaceScriptCommandOptions,
|
||||
} from "./shared.js";
|
||||
import { workspaceScriptSchema, type WorkspaceScriptRow } from "./schema.js";
|
||||
|
||||
export async function runLsCommand(
|
||||
options: WorkspaceScriptCommandOptions,
|
||||
_command: Command,
|
||||
): Promise<ListResult<WorkspaceScriptRow>> {
|
||||
const client = await connectWorkspaceScriptClient(options.host);
|
||||
try {
|
||||
const workspaceId = await resolveWorkspaceScriptWorkspaceId(client, options);
|
||||
const payload = await client.listWorkspaceScripts(workspaceId);
|
||||
if (payload.error) {
|
||||
throw new Error(payload.error);
|
||||
}
|
||||
return { type: "list", data: payload.scripts ?? [], schema: workspaceScriptSchema };
|
||||
} catch (error) {
|
||||
throw toWorkspaceScriptCommandError(
|
||||
"WORKSPACE_SCRIPT_LIST_FAILED",
|
||||
"list workspace scripts",
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { WorkspaceScriptPayload } from "@getpaseo/protocol/messages";
|
||||
import type { OutputSchema } from "../../output/index.js";
|
||||
|
||||
export type WorkspaceScriptRow = WorkspaceScriptPayload;
|
||||
|
||||
export const workspaceScriptSchema: OutputSchema<WorkspaceScriptRow> = {
|
||||
idField: "scriptName",
|
||||
columns: [
|
||||
{ header: "NAME", field: "scriptName", width: 20 },
|
||||
{ header: "TYPE", field: "type", width: 9 },
|
||||
{ header: "LIFECYCLE", field: "lifecycle", width: 10 },
|
||||
{ header: "HEALTH", field: (script) => script.health ?? "-", width: 10 },
|
||||
{ header: "PORT", field: (script) => script.port ?? "-", width: 7 },
|
||||
{ header: "PROXY URL", field: (script) => script.proxyUrl ?? "-", width: 42 },
|
||||
{ header: "TERMINAL", field: (script) => script.terminalId ?? "-", width: 12 },
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import { resolve } from "node:path";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||
import type { CommandError, CommandOptions } from "../../output/index.js";
|
||||
|
||||
export interface WorkspaceScriptCommandOptions extends CommandOptions {
|
||||
host?: string;
|
||||
cwd?: string;
|
||||
workspace?: string;
|
||||
}
|
||||
|
||||
export async function connectWorkspaceScriptClient(host?: string): Promise<DaemonClient> {
|
||||
const daemonHost = getDaemonHost({ host });
|
||||
try {
|
||||
const client = await connectToDaemon({ host });
|
||||
// COMPAT(workspaceScriptManagement): added in v0.1.105, remove gate after 2027-01-10.
|
||||
if (!client.getLastServerInfoMessage()?.features?.workspaceScriptManagement) {
|
||||
await client.close().catch(() => {});
|
||||
throw {
|
||||
code: "DAEMON_UPDATE_REQUIRED",
|
||||
message: "Update the host to use workspace script management.",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
return client;
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "code" in error && "message" in error) {
|
||||
throw error;
|
||||
}
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw {
|
||||
code: "DAEMON_NOT_RUNNING",
|
||||
message: `Cannot connect to daemon at ${daemonHost}: ${message}`,
|
||||
details: "Start the daemon with: paseo daemon start",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveWorkspaceScriptWorkspaceId(
|
||||
client: DaemonClient,
|
||||
options: WorkspaceScriptCommandOptions,
|
||||
): Promise<string> {
|
||||
if (options.workspace) {
|
||||
return options.workspace;
|
||||
}
|
||||
|
||||
const cwd = resolve(options.cwd ?? process.cwd());
|
||||
const payload = await client.fetchWorkspaces({ page: { limit: 200 } });
|
||||
const matches = payload.entries.filter(
|
||||
(workspace) => resolve(workspace.workspaceDirectory) === cwd,
|
||||
);
|
||||
if (matches.length === 1) {
|
||||
return matches[0]!.id;
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
throw {
|
||||
code: "WORKSPACE_AMBIGUOUS",
|
||||
message: `Multiple workspaces use ${cwd}`,
|
||||
details: "Pass --workspace <workspace-id> to select one.",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
throw {
|
||||
code: "WORKSPACE_NOT_FOUND",
|
||||
message: `No Paseo workspace found for ${cwd}`,
|
||||
details: "Open the directory in Paseo first, or pass --workspace <workspace-id>.",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
|
||||
export function toWorkspaceScriptCommandError(
|
||||
code: string,
|
||||
action: string,
|
||||
error: unknown,
|
||||
): CommandError {
|
||||
if (error && typeof error === "object" && "code" in error && "message" in error) {
|
||||
return error as CommandError;
|
||||
}
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return { code, message: `Failed to ${action}: ${message}` };
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Command } from "commander";
|
||||
import type { CommandError, SingleResult } from "../../output/index.js";
|
||||
import {
|
||||
connectWorkspaceScriptClient,
|
||||
resolveWorkspaceScriptWorkspaceId,
|
||||
toWorkspaceScriptCommandError,
|
||||
type WorkspaceScriptCommandOptions,
|
||||
} from "./shared.js";
|
||||
import { workspaceScriptSchema, type WorkspaceScriptRow } from "./schema.js";
|
||||
|
||||
export async function runStartCommand(
|
||||
scriptName: string,
|
||||
options: WorkspaceScriptCommandOptions,
|
||||
_command: Command,
|
||||
): Promise<SingleResult<WorkspaceScriptRow>> {
|
||||
const client = await connectWorkspaceScriptClient(options.host);
|
||||
try {
|
||||
const workspaceId = await resolveWorkspaceScriptWorkspaceId(client, options);
|
||||
const payload = await client.startWorkspaceScriptWithStatus(workspaceId, scriptName);
|
||||
if (payload.error || !payload.script) {
|
||||
throw {
|
||||
code: "WORKSPACE_SCRIPT_START_FAILED",
|
||||
message: payload.error ?? `Script '${scriptName}' did not return status metadata`,
|
||||
} satisfies CommandError;
|
||||
}
|
||||
return { type: "single", data: payload.script, schema: workspaceScriptSchema };
|
||||
} catch (error) {
|
||||
throw toWorkspaceScriptCommandError(
|
||||
"WORKSPACE_SCRIPT_START_FAILED",
|
||||
"start workspace script",
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Command } from "commander";
|
||||
import type { CommandError, SingleResult } from "../../output/index.js";
|
||||
import {
|
||||
connectWorkspaceScriptClient,
|
||||
resolveWorkspaceScriptWorkspaceId,
|
||||
toWorkspaceScriptCommandError,
|
||||
type WorkspaceScriptCommandOptions,
|
||||
} from "./shared.js";
|
||||
import { workspaceScriptSchema, type WorkspaceScriptRow } from "./schema.js";
|
||||
|
||||
export async function runStopCommand(
|
||||
scriptName: string,
|
||||
options: WorkspaceScriptCommandOptions,
|
||||
_command: Command,
|
||||
): Promise<SingleResult<WorkspaceScriptRow>> {
|
||||
const client = await connectWorkspaceScriptClient(options.host);
|
||||
try {
|
||||
const workspaceId = await resolveWorkspaceScriptWorkspaceId(client, options);
|
||||
const payload = await client.stopWorkspaceScript(workspaceId, scriptName);
|
||||
if (payload.error || !payload.script) {
|
||||
throw {
|
||||
code: "WORKSPACE_SCRIPT_STOP_FAILED",
|
||||
message: payload.error ?? `Script '${scriptName}' did not return status metadata`,
|
||||
} satisfies CommandError;
|
||||
}
|
||||
return { type: "single", data: payload.script, schema: workspaceScriptSchema };
|
||||
} catch (error) {
|
||||
throw toWorkspaceScriptCommandError(
|
||||
"WORKSPACE_SCRIPT_STOP_FAILED",
|
||||
"stop workspace script",
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user