Merge branch 'main' into storage-terminal-ui-dev
This commit is contained in:
@@ -8,6 +8,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 { createTerminalCommand } from "./commands/terminal/index.js";
|
||||
import { createWorktreeCommand } from "./commands/worktree/index.js";
|
||||
import { startCommand as daemonStartCommand } from "./commands/daemon/start.js";
|
||||
import { runStatusCommand as runDaemonStatusCommand } from "./commands/daemon/status.js";
|
||||
@@ -143,6 +144,9 @@ export function createCli(): Command {
|
||||
// Chat commands
|
||||
program.addCommand(createChatCommand());
|
||||
|
||||
// Terminal commands
|
||||
program.addCommand(createTerminalCommand());
|
||||
|
||||
// Loop commands
|
||||
program.addCommand(createLoopCommand());
|
||||
|
||||
|
||||
@@ -124,11 +124,16 @@ function resolveDaemonRunnerEntry(): string {
|
||||
try {
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { name?: string };
|
||||
if (packageJson.name === "@getpaseo/server") {
|
||||
const distRunner = path.join(currentDir, "dist", "scripts", "daemon-runner.js");
|
||||
const distRunner = path.join(
|
||||
currentDir,
|
||||
"dist",
|
||||
"scripts",
|
||||
"supervisor-entrypoint.js",
|
||||
);
|
||||
if (existsSync(distRunner)) {
|
||||
return distRunner;
|
||||
}
|
||||
return path.join(currentDir, "scripts", "daemon-runner.ts");
|
||||
return path.join(currentDir, "scripts", "supervisor-entrypoint.ts");
|
||||
}
|
||||
} catch {
|
||||
// Continue searching up if package.json exists but is invalid.
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import type { Command } from "commander";
|
||||
import type { CommandError, CommandOptions, ListResult, OutputSchema } from "../../output/index.js";
|
||||
import { connectToDaemon } from "../../utils/client.js";
|
||||
|
||||
interface SpeechDownloadRow {
|
||||
modelId: string;
|
||||
status: "downloaded";
|
||||
}
|
||||
|
||||
const speechDownloadSchema: OutputSchema<SpeechDownloadRow> = {
|
||||
idField: "modelId",
|
||||
columns: [
|
||||
{ header: "MODEL", field: "modelId", width: 36 },
|
||||
{ header: "STATUS", field: "status", width: 12, color: () => "green" },
|
||||
],
|
||||
};
|
||||
|
||||
export type SpeechDownloadResult = ListResult<SpeechDownloadRow>;
|
||||
|
||||
export interface SpeechDownloadOptions extends CommandOptions {
|
||||
host?: string;
|
||||
model?: string[];
|
||||
}
|
||||
|
||||
export async function runSpeechDownloadCommand(
|
||||
options: SpeechDownloadOptions,
|
||||
_command: Command,
|
||||
): Promise<SpeechDownloadResult> {
|
||||
const client = await connectToDaemon({ host: options.host });
|
||||
try {
|
||||
const response = await client.downloadSpeechModels({
|
||||
modelIds: options.model && options.model.length > 0 ? options.model : undefined,
|
||||
});
|
||||
if (response.error) {
|
||||
const commandError: CommandError = {
|
||||
code: "SPEECH_MODELS_DOWNLOAD_FAILED",
|
||||
message: response.error,
|
||||
};
|
||||
throw commandError;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "list",
|
||||
data: response.downloadedModelIds.map((modelId) => ({
|
||||
modelId,
|
||||
status: "downloaded" as const,
|
||||
})),
|
||||
schema: speechDownloadSchema,
|
||||
};
|
||||
} catch (error) {
|
||||
if (typeof error === "object" && error && "code" in error && "message" in error) {
|
||||
throw error;
|
||||
}
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const commandError: CommandError = {
|
||||
code: "SPEECH_MODELS_DOWNLOAD_FAILED",
|
||||
message: `Failed to download speech models: ${message}`,
|
||||
};
|
||||
throw commandError;
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,5 @@
|
||||
import { Command } from "commander";
|
||||
import { withOutput } from "../../output/index.js";
|
||||
import { runSpeechModelsCommand } from "./models.js";
|
||||
import { runSpeechDownloadCommand } from "./download.js";
|
||||
import { addJsonAndDaemonHostOptions, collectMultiple } from "../../utils/command-options.js";
|
||||
|
||||
export function createSpeechCommand(): Command {
|
||||
const speech = new Command("speech").description("Manage local speech models");
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
speech.command("models").description("List local speech model download status"),
|
||||
).action(withOutput(runSpeechModelsCommand));
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
speech
|
||||
.command("download")
|
||||
.description("Download local speech models")
|
||||
.option("--model <id>", "Model ID to download (repeatable)", collectMultiple, []),
|
||||
).action(withOutput(runSpeechDownloadCommand));
|
||||
|
||||
return speech;
|
||||
return new Command("speech").description("Speech commands");
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import type { Command } from "commander";
|
||||
import type { CommandError, CommandOptions, ListResult, OutputSchema } from "../../output/index.js";
|
||||
import { connectToDaemon } from "../../utils/client.js";
|
||||
|
||||
interface SpeechModelListItem {
|
||||
id: string;
|
||||
kind: string;
|
||||
status: "downloaded" | "missing";
|
||||
modelDir: string;
|
||||
missingFiles: string;
|
||||
}
|
||||
|
||||
const speechModelsSchema: OutputSchema<SpeechModelListItem> = {
|
||||
idField: "id",
|
||||
columns: [
|
||||
{ header: "MODEL", field: "id", width: 36 },
|
||||
{ header: "KIND", field: "kind", width: 12 },
|
||||
{
|
||||
header: "STATUS",
|
||||
field: "status",
|
||||
width: 12,
|
||||
color: (value) => (value === "downloaded" ? "green" : "yellow"),
|
||||
},
|
||||
{ header: "MODEL DIR", field: "modelDir", width: 44 },
|
||||
{ header: "MISSING FILES", field: "missingFiles", width: 40 },
|
||||
],
|
||||
};
|
||||
|
||||
export type SpeechModelsResult = ListResult<SpeechModelListItem>;
|
||||
|
||||
export interface SpeechModelsOptions extends CommandOptions {
|
||||
host?: string;
|
||||
}
|
||||
|
||||
export async function runSpeechModelsCommand(
|
||||
options: SpeechModelsOptions,
|
||||
_command: Command,
|
||||
): Promise<SpeechModelsResult> {
|
||||
const client = await connectToDaemon({ host: options.host });
|
||||
try {
|
||||
const response = await client.listSpeechModels();
|
||||
const rows: SpeechModelListItem[] = response.models
|
||||
.slice()
|
||||
.sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id))
|
||||
.map((model) => ({
|
||||
id: model.id,
|
||||
kind: model.kind,
|
||||
status: model.isDownloaded ? "downloaded" : "missing",
|
||||
modelDir: model.modelDir,
|
||||
missingFiles: model.missingFiles?.join(", ") ?? "",
|
||||
}));
|
||||
return {
|
||||
type: "list",
|
||||
data: rows,
|
||||
schema: speechModelsSchema,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const commandError: CommandError = {
|
||||
code: "SPEECH_MODELS_LIST_FAILED",
|
||||
message: `Failed to list speech models: ${message}`,
|
||||
};
|
||||
throw commandError;
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { Command } from "commander";
|
||||
import { renderError, toCommandError } from "../../output/render.js";
|
||||
import {
|
||||
connectTerminalClient,
|
||||
resolveTerminalId,
|
||||
toTerminalCommandError,
|
||||
type TerminalCommandOptions,
|
||||
} from "./shared.js";
|
||||
|
||||
export interface TerminalCaptureOptions extends TerminalCommandOptions {
|
||||
start?: string;
|
||||
end?: string;
|
||||
scrollback?: boolean;
|
||||
ansi?: boolean;
|
||||
}
|
||||
|
||||
export async function runCaptureCommand(
|
||||
terminalId: string,
|
||||
_options: TerminalCaptureOptions,
|
||||
command: Command,
|
||||
): Promise<void> {
|
||||
const options = command.optsWithGlobals() as TerminalCaptureOptions;
|
||||
|
||||
try {
|
||||
const payload = await executeCaptureCommand(terminalId, options);
|
||||
if (options.json) {
|
||||
process.stdout.write(
|
||||
JSON.stringify(
|
||||
{
|
||||
terminalId: payload.terminalId,
|
||||
lines: payload.lines,
|
||||
totalLines: payload.totalLines,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.lines.length > 0) {
|
||||
process.stdout.write(payload.lines.join("\n") + "\n");
|
||||
}
|
||||
} catch (err) {
|
||||
const output = renderError(toCommandError(err), {
|
||||
format: options.json ? "json" : "table",
|
||||
noColor: options.color === false,
|
||||
});
|
||||
process.stderr.write(output + "\n");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function executeCaptureCommand(
|
||||
terminalId: string,
|
||||
options: TerminalCaptureOptions,
|
||||
): Promise<{ terminalId: string; lines: string[]; totalLines: number }> {
|
||||
const { client } = await connectTerminalClient(options.host);
|
||||
|
||||
try {
|
||||
const resolvedId = await resolveTerminalId(client, terminalId);
|
||||
if (!resolvedId) {
|
||||
throw {
|
||||
code: "TERMINAL_NOT_FOUND",
|
||||
message: `No terminal found matching: ${terminalId}`,
|
||||
details: "Use `paseo terminal ls --all` to list available terminals.",
|
||||
};
|
||||
}
|
||||
|
||||
const start = options.scrollback ? 0 : parseLineNumber("--start", options.start);
|
||||
const end = parseLineNumber("--end", options.end);
|
||||
|
||||
return await client.captureTerminal(resolvedId, {
|
||||
...(start === undefined ? {} : { start }),
|
||||
...(end === undefined ? {} : { end }),
|
||||
stripAnsi: !options.ansi,
|
||||
});
|
||||
} catch (err) {
|
||||
throw toTerminalCommandError("TERMINAL_CAPTURE_FAILED", "capture terminal output", err);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function parseLineNumber(flag: string, value?: string): number | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isInteger(parsed)) {
|
||||
throw {
|
||||
code: "INVALID_LINE_NUMBER",
|
||||
message: `Invalid ${flag} value: ${value}`,
|
||||
details: "Use an integer line number.",
|
||||
};
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Command } from "commander";
|
||||
import type { SingleResult, CommandError } from "../../output/index.js";
|
||||
import {
|
||||
connectTerminalClient,
|
||||
toTerminalCommandError,
|
||||
type TerminalCommandOptions,
|
||||
} from "./shared.js";
|
||||
import { terminalSchema, type TerminalRow, toTerminalRow } from "./schema.js";
|
||||
|
||||
export interface TerminalCreateOptions extends TerminalCommandOptions {
|
||||
cwd?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export async function runCreateCommand(
|
||||
options: TerminalCreateOptions,
|
||||
_command: Command,
|
||||
): Promise<SingleResult<TerminalRow>> {
|
||||
const { client } = await connectTerminalClient(options.host);
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
|
||||
try {
|
||||
const payload = await client.createTerminal(cwd, options.name);
|
||||
if (!payload.terminal) {
|
||||
const error: CommandError = {
|
||||
code: "TERMINAL_CREATE_FAILED",
|
||||
message: payload.error ?? "Failed to create terminal",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
type: "single",
|
||||
data: toTerminalRow(payload.terminal),
|
||||
schema: terminalSchema,
|
||||
};
|
||||
} catch (err) {
|
||||
throw toTerminalCommandError("TERMINAL_CREATE_FAILED", "create terminal", err);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Command } from "commander";
|
||||
import { withOutput } from "../../output/index.js";
|
||||
import { addDaemonHostOption, addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
||||
import { runCaptureCommand } from "./capture.js";
|
||||
import { runCreateCommand } from "./create.js";
|
||||
import { runKillCommand } from "./kill.js";
|
||||
import { runLsCommand } from "./ls.js";
|
||||
import { runSendKeysCommand } from "./send-keys.js";
|
||||
|
||||
export function createTerminalCommand(): Command {
|
||||
const terminal = new Command("terminal").description("Manage workspace terminals");
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
terminal
|
||||
.command("ls")
|
||||
.description("List terminals")
|
||||
.option("--all", "List terminals across all workspaces")
|
||||
.option("--cwd <path>", "Workspace directory"),
|
||||
).action(withOutput(runLsCommand));
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
terminal
|
||||
.command("create")
|
||||
.description("Create a terminal")
|
||||
.option("--cwd <path>", "Workspace directory")
|
||||
.option("--name <name>", "Terminal name"),
|
||||
).action(withOutput(runCreateCommand));
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
terminal
|
||||
.command("kill")
|
||||
.description("Kill a terminal")
|
||||
.argument("<terminal-id>", "Terminal ID, ID prefix, or name"),
|
||||
).action(withOutput(runKillCommand));
|
||||
|
||||
addDaemonHostOption(
|
||||
terminal
|
||||
.command("capture")
|
||||
.description("Capture terminal output")
|
||||
.argument("<terminal-id>", "Terminal ID, ID prefix, or name")
|
||||
.option("--start <n>", "Capture start line")
|
||||
.option("--end <n>", "Capture end line")
|
||||
.option("-S, --scrollback", "Capture from the beginning of scrollback")
|
||||
.option("--ansi", "Preserve ANSI escape codes")
|
||||
.option("--json", "Output in JSON format"),
|
||||
).action(runCaptureCommand);
|
||||
|
||||
addDaemonHostOption(
|
||||
terminal
|
||||
.command("send-keys")
|
||||
.description("Send keys to a terminal")
|
||||
.argument("<terminal-id>", "Terminal ID, ID prefix, or name")
|
||||
.argument("<keys...>", "Keys to send")
|
||||
.option("-l, --literal", "Send raw keys without interpreting special tokens")
|
||||
.option("--json", "Output in JSON format"),
|
||||
).action(runSendKeysCommand);
|
||||
|
||||
return terminal;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Command } from "commander";
|
||||
import type { CommandError, SingleResult } from "../../output/index.js";
|
||||
import {
|
||||
connectTerminalClient,
|
||||
resolveTerminalId,
|
||||
toTerminalCommandError,
|
||||
type TerminalCommandOptions,
|
||||
} from "./shared.js";
|
||||
import { terminalKillSchema, type TerminalKillRow } from "./schema.js";
|
||||
|
||||
export async function runKillCommand(
|
||||
terminalId: string,
|
||||
options: TerminalCommandOptions,
|
||||
_command: Command,
|
||||
): Promise<SingleResult<TerminalKillRow>> {
|
||||
const { client } = await connectTerminalClient(options.host);
|
||||
|
||||
try {
|
||||
const resolvedId = await requireTerminalId(client, terminalId);
|
||||
const payload = await client.killTerminal(resolvedId);
|
||||
return {
|
||||
type: "single",
|
||||
data: {
|
||||
terminalId: payload.terminalId,
|
||||
success: payload.success,
|
||||
},
|
||||
schema: terminalKillSchema,
|
||||
};
|
||||
} catch (err) {
|
||||
throw toTerminalCommandError("TERMINAL_KILL_FAILED", "kill terminal", err);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async function requireTerminalId(
|
||||
client: Awaited<ReturnType<typeof connectTerminalClient>>["client"],
|
||||
terminalId: string,
|
||||
): Promise<string> {
|
||||
const resolvedId = await resolveTerminalId(client, terminalId);
|
||||
if (resolvedId) {
|
||||
return resolvedId;
|
||||
}
|
||||
|
||||
const error: CommandError = {
|
||||
code: "TERMINAL_NOT_FOUND",
|
||||
message: `No terminal found matching: ${terminalId}`,
|
||||
details: "Use `paseo terminal ls --all` to list available terminals.",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Command } from "commander";
|
||||
import type { ListResult } from "../../output/index.js";
|
||||
import {
|
||||
connectTerminalClient,
|
||||
toTerminalCommandError,
|
||||
type TerminalCommandOptions,
|
||||
} from "./shared.js";
|
||||
import { terminalSchema, type TerminalRow, toTerminalRow } from "./schema.js";
|
||||
|
||||
export interface TerminalLsOptions extends TerminalCommandOptions {
|
||||
all?: boolean;
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
export async function runLsCommand(
|
||||
options: TerminalLsOptions,
|
||||
_command: Command,
|
||||
): Promise<ListResult<TerminalRow>> {
|
||||
const { client } = await connectTerminalClient(options.host);
|
||||
const cwd = options.all ? undefined : (options.cwd ?? process.cwd());
|
||||
|
||||
try {
|
||||
const payload = cwd === undefined ? await client.listTerminals() : await client.listTerminals(cwd);
|
||||
return {
|
||||
type: "list",
|
||||
data: payload.terminals.map((terminal) => toTerminalRow(terminal, payload.cwd ?? cwd)),
|
||||
schema: terminalSchema,
|
||||
};
|
||||
} catch (err) {
|
||||
throw toTerminalCommandError("TERMINAL_LIST_FAILED", "list terminals", err);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { OutputSchema } from "../../output/index.js";
|
||||
|
||||
export interface TerminalRow {
|
||||
id: string;
|
||||
name: string;
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
export interface TerminalKillRow {
|
||||
terminalId: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export const terminalSchema: OutputSchema<TerminalRow> = {
|
||||
idField: "id",
|
||||
columns: [
|
||||
{ header: "ID", field: (row) => row.id.slice(0, 8), width: 8 },
|
||||
{ header: "NAME", field: "name", width: 24 },
|
||||
{ header: "CWD", field: "cwd", width: 48 },
|
||||
],
|
||||
};
|
||||
|
||||
export const terminalKillSchema: OutputSchema<TerminalKillRow> = {
|
||||
idField: "terminalId",
|
||||
columns: [
|
||||
{ header: "ID", field: (row) => row.terminalId.slice(0, 8), width: 8 },
|
||||
{ header: "SUCCESS", field: "success", width: 8 },
|
||||
],
|
||||
};
|
||||
|
||||
export function toTerminalRow(
|
||||
terminal: {
|
||||
id: string;
|
||||
name: string;
|
||||
cwd?: string;
|
||||
},
|
||||
cwd?: string,
|
||||
): TerminalRow {
|
||||
return {
|
||||
id: terminal.id,
|
||||
name: terminal.name,
|
||||
cwd: terminal.cwd ?? cwd ?? "-",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { Command } from "commander";
|
||||
import { renderError, toCommandError } from "../../output/render.js";
|
||||
import {
|
||||
connectTerminalClient,
|
||||
resolveTerminalId,
|
||||
toTerminalCommandError,
|
||||
type TerminalCommandOptions,
|
||||
} from "./shared.js";
|
||||
|
||||
export interface TerminalSendKeysOptions extends TerminalCommandOptions {
|
||||
literal?: boolean;
|
||||
}
|
||||
|
||||
export async function runSendKeysCommand(
|
||||
terminalId: string,
|
||||
keys: string[],
|
||||
_options: TerminalSendKeysOptions,
|
||||
command: Command,
|
||||
): Promise<void> {
|
||||
const options = command.optsWithGlobals() as TerminalSendKeysOptions;
|
||||
|
||||
try {
|
||||
const payload = await executeSendKeysCommand(terminalId, keys, options);
|
||||
if (options.json) {
|
||||
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
||||
}
|
||||
} catch (err) {
|
||||
const output = renderError(toCommandError(err), {
|
||||
format: options.json ? "json" : "table",
|
||||
noColor: options.color === false,
|
||||
});
|
||||
process.stderr.write(output + "\n");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function executeSendKeysCommand(
|
||||
terminalId: string,
|
||||
keys: string[],
|
||||
options: TerminalSendKeysOptions,
|
||||
): Promise<{ terminalId: string; keysSent: number }> {
|
||||
const { client } = await connectTerminalClient(options.host);
|
||||
|
||||
try {
|
||||
const resolvedId = await resolveTerminalId(client, terminalId);
|
||||
if (!resolvedId) {
|
||||
throw {
|
||||
code: "TERMINAL_NOT_FOUND",
|
||||
message: `No terminal found matching: ${terminalId}`,
|
||||
details: "Use `paseo terminal ls --all` to list available terminals.",
|
||||
};
|
||||
}
|
||||
|
||||
const data = keys.map((key) => resolveKeyToken(key, options.literal === true)).join("");
|
||||
client.sendTerminalInput(resolvedId, { type: "input", data });
|
||||
|
||||
return {
|
||||
terminalId: resolvedId,
|
||||
keysSent: data.length,
|
||||
};
|
||||
} catch (err) {
|
||||
throw toTerminalCommandError("TERMINAL_SEND_KEYS_FAILED", "send terminal keys", err);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function resolveKeyToken(key: string, literal: boolean): string {
|
||||
if (literal) {
|
||||
return key;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "Enter":
|
||||
return "\r";
|
||||
case "Tab":
|
||||
return "\t";
|
||||
case "Escape":
|
||||
return "\u001b";
|
||||
case "Space":
|
||||
return " ";
|
||||
case "BSpace":
|
||||
return "\u007f";
|
||||
case "C-c":
|
||||
return "\u0003";
|
||||
case "C-d":
|
||||
return "\u0004";
|
||||
case "C-z":
|
||||
return "\u001a";
|
||||
case "C-l":
|
||||
return "\u000c";
|
||||
case "C-a":
|
||||
return "\u0001";
|
||||
case "C-e":
|
||||
return "\u0005";
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||
import type { CommandError, CommandOptions } from "../../output/index.js";
|
||||
|
||||
export interface TerminalCommandOptions extends CommandOptions {
|
||||
host?: string;
|
||||
}
|
||||
|
||||
interface TerminalLike {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
export async function connectTerminalClient(host?: string) {
|
||||
const daemonHost = getDaemonHost({ host });
|
||||
try {
|
||||
const client = await connectToDaemon({ host });
|
||||
return { client, daemonHost };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const error: CommandError = {
|
||||
code: "DAEMON_NOT_RUNNING",
|
||||
message: `Cannot connect to daemon at ${daemonHost}: ${message}`,
|
||||
details: "Start the daemon with: paseo daemon start",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function toTerminalCommandError(code: string, action: string, err: unknown): CommandError {
|
||||
if (err && typeof err === "object" && "code" in err && "message" in err) {
|
||||
return err as CommandError;
|
||||
}
|
||||
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const rpcCode =
|
||||
typeof err === "object" && err !== null && "code" in err && typeof err.code === "string"
|
||||
? err.code
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
code: rpcCode ?? code,
|
||||
message: `Failed to ${action}: ${message}`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveTerminalId(
|
||||
client: Awaited<ReturnType<typeof connectToDaemon>>,
|
||||
idOrName: string,
|
||||
): Promise<string | null> {
|
||||
const payload = await client.listTerminals();
|
||||
return resolveTerminalIdentifier(idOrName, payload.terminals);
|
||||
}
|
||||
|
||||
function resolveTerminalIdentifier(idOrName: string, terminals: TerminalLike[]): string | null {
|
||||
if (!idOrName || terminals.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = idOrName.toLowerCase();
|
||||
|
||||
const exactMatch = terminals.find((terminal) => terminal.id === idOrName);
|
||||
if (exactMatch) {
|
||||
return exactMatch.id;
|
||||
}
|
||||
|
||||
const prefixMatches = terminals.filter((terminal) => terminal.id.toLowerCase().startsWith(query));
|
||||
if (prefixMatches.length === 1 && prefixMatches[0]) {
|
||||
return prefixMatches[0].id;
|
||||
}
|
||||
if (prefixMatches.length > 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nameMatches = terminals.filter((terminal) => terminal.name?.toLowerCase() === query);
|
||||
if (nameMatches.length === 1 && nameMatches[0]) {
|
||||
return nameMatches[0].id;
|
||||
}
|
||||
|
||||
const partialNameMatches = terminals.filter((terminal) =>
|
||||
terminal.name?.toLowerCase().includes(query),
|
||||
);
|
||||
if (partialNameMatches.length === 1 && partialNameMatches[0]) {
|
||||
return partialNameMatches[0].id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user