@getpaseo/cli 0.6.1 → 0.7.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent/ls.js +15 -7
- package/dist/commands/plugin/index.d.ts +3 -0
- package/dist/commands/plugin/index.js +68 -8
- package/dist/commands/plugin/scaffold.js +105 -0
- package/dist/commands/plugin/shared.d.ts +1 -0
- package/dist/commands/plugin/shared.js +4 -0
- package/dist/ssh/ssh-tunnel.d.ts +9 -0
- package/dist/ssh/ssh-tunnel.js +79 -0
- package/dist/utils/client.js +18 -1
- package/dist/utils/command-options.js +1 -1
- package/package.json +4 -4
|
@@ -78,6 +78,20 @@ function toListItem(agent) {
|
|
|
78
78
|
created: relativeTime(agent.createdAt),
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
|
+
function daemonConnectionFailure(host, cause) {
|
|
82
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
83
|
+
const isSsh = host.trim().startsWith("ssh://");
|
|
84
|
+
return {
|
|
85
|
+
code: "DAEMON_NOT_RUNNING",
|
|
86
|
+
message: `Cannot reach the daemon at ${host}: ${reason}`,
|
|
87
|
+
details: isSsh
|
|
88
|
+
? "Start the Paseo daemon on the SSH host; SSH transport does not install or start it."
|
|
89
|
+
: [
|
|
90
|
+
"Start a local daemon with: paseo daemon start",
|
|
91
|
+
"To use another daemon, pass --host <host:port> or set PASEO_HOST.",
|
|
92
|
+
].join("\n"),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
81
95
|
function parseLabelFilters(labels) {
|
|
82
96
|
const labelFilters = {};
|
|
83
97
|
for (const labelStr of labels ?? []) {
|
|
@@ -126,13 +140,7 @@ export async function runLsCommand(options, _command) {
|
|
|
126
140
|
client = await connectToDaemon({ host: options.host });
|
|
127
141
|
}
|
|
128
142
|
catch (err) {
|
|
129
|
-
|
|
130
|
-
const error = {
|
|
131
|
-
code: "DAEMON_NOT_RUNNING",
|
|
132
|
-
message: `Cannot connect to daemon at ${host}: ${message}`,
|
|
133
|
-
details: "Start the daemon with: paseo daemon start\nFor a remote daemon, pass --host <host:port> or set PASEO_HOST.",
|
|
134
|
-
};
|
|
135
|
-
throw error;
|
|
143
|
+
throw daemonConnectionFailure(host, err);
|
|
136
144
|
}
|
|
137
145
|
try {
|
|
138
146
|
const normalizedThinkingOptionId = options.thinking?.trim();
|
|
@@ -5,6 +5,9 @@ import { type PluginScaffold } from "./scaffold.js";
|
|
|
5
5
|
interface PluginOptions extends CommandOptions {
|
|
6
6
|
host?: string;
|
|
7
7
|
id?: string;
|
|
8
|
+
ref?: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
all?: boolean;
|
|
8
11
|
}
|
|
9
12
|
export declare function runPluginInitCommand(directory: string, options: PluginOptions, _command: Command): Promise<SingleResult<PluginScaffold>>;
|
|
10
13
|
export declare function runPluginListCommand(options: PluginOptions, _command: Command): Promise<ListResult<PluginListItem>>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { withOutput } from "../../output/index.js";
|
|
3
4
|
import { addJsonAndDaemonHostOptions, addJsonOption } from "../../utils/command-options.js";
|
|
4
5
|
import { scaffoldPluginDirectory } from "./scaffold.js";
|
|
5
|
-
import { withPluginLogsClient, withPluginManagementClient } from "./shared.js";
|
|
6
|
+
import { withPluginLogsClient, withPluginManagementClient, withPluginSourceClient, } from "./shared.js";
|
|
6
7
|
const pluginSchema = {
|
|
7
8
|
idField: "id",
|
|
8
9
|
columns: [
|
|
@@ -28,6 +29,30 @@ const pluginLogsSchema = {
|
|
|
28
29
|
{ header: "MESSAGE", field: "message", width: 80 },
|
|
29
30
|
],
|
|
30
31
|
};
|
|
32
|
+
function shortCommit(commit) {
|
|
33
|
+
return commit?.slice(0, 12) ?? "-";
|
|
34
|
+
}
|
|
35
|
+
const pluginStatusSchema = {
|
|
36
|
+
idField: "id",
|
|
37
|
+
columns: [
|
|
38
|
+
{ header: "PLUGIN", field: "id", width: 20 },
|
|
39
|
+
{ header: "SOURCE", field: "source", width: 10 },
|
|
40
|
+
{ header: "CURRENT", field: (plugin) => shortCommit(plugin.currentCommit), width: 14 },
|
|
41
|
+
{ header: "LATEST", field: (plugin) => shortCommit(plugin.latestCommit), width: 14 },
|
|
42
|
+
{ header: "COMMITS", field: (plugin) => String(plugin.commitsBehind ?? 0), width: 8 },
|
|
43
|
+
{ header: "REF", field: (plugin) => plugin.ref ?? "-", width: 24 },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
const pluginUpdateSchema = {
|
|
47
|
+
idField: "id",
|
|
48
|
+
columns: [
|
|
49
|
+
{ header: "PLUGIN", field: "id", width: 20 },
|
|
50
|
+
{ header: "PREVIOUS", field: (plugin) => shortCommit(plugin.previousCommit), width: 14 },
|
|
51
|
+
{ header: "CURRENT", field: (plugin) => shortCommit(plugin.currentCommit), width: 14 },
|
|
52
|
+
{ header: "COMMITS", field: (plugin) => String(plugin.commits), width: 8 },
|
|
53
|
+
{ header: "UPDATED", field: (plugin) => (plugin.updated ? "yes" : "no"), width: 8 },
|
|
54
|
+
],
|
|
55
|
+
};
|
|
31
56
|
export async function runPluginInitCommand(directory, options, _command) {
|
|
32
57
|
return {
|
|
33
58
|
type: "single",
|
|
@@ -43,10 +68,36 @@ export async function runPluginLogsCommand(pluginId, options, _command) {
|
|
|
43
68
|
const data = await withPluginLogsClient(options.host, (client) => client.getPluginLogs(pluginId));
|
|
44
69
|
return { type: "list", data, schema: pluginLogsSchema };
|
|
45
70
|
}
|
|
46
|
-
async function install(
|
|
47
|
-
const
|
|
71
|
+
async function install(source, options, _command) {
|
|
72
|
+
const isExplicitPath = path.isAbsolute(source) ||
|
|
73
|
+
source === "." ||
|
|
74
|
+
source === ".." ||
|
|
75
|
+
source.startsWith("./") ||
|
|
76
|
+
source.startsWith("../") ||
|
|
77
|
+
source.startsWith(".\\") ||
|
|
78
|
+
source.startsWith("..\\");
|
|
79
|
+
const canUseLegacyDirectoryInstall = isExplicitPath && !options.ref && !options.path;
|
|
80
|
+
const data = canUseLegacyDirectoryInstall
|
|
81
|
+
? await withPluginManagementClient(options.host, (client) => client.installDirectoryPlugin(source, options.id))
|
|
82
|
+
: await withPluginSourceClient(options.host, (client) => client.installPluginSource({
|
|
83
|
+
source,
|
|
84
|
+
...(options.id ? { id: options.id } : {}),
|
|
85
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
86
|
+
...(options.path ? { pluginPath: options.path } : {}),
|
|
87
|
+
}));
|
|
48
88
|
return { type: "single", data, schema: pluginSchema };
|
|
49
89
|
}
|
|
90
|
+
async function status(pluginId, options, _command) {
|
|
91
|
+
const data = await withPluginSourceClient(options.host, (client) => client.getPluginSourceStatus(pluginId));
|
|
92
|
+
return { type: "list", data, schema: pluginStatusSchema };
|
|
93
|
+
}
|
|
94
|
+
async function update(pluginId, options, _command) {
|
|
95
|
+
if ((pluginId === undefined) === (options.all !== true)) {
|
|
96
|
+
throw new Error("Choose one plugin ID or pass --all");
|
|
97
|
+
}
|
|
98
|
+
const data = await withPluginSourceClient(options.host, (client) => client.updatePluginSources(pluginId));
|
|
99
|
+
return { type: "list", data, schema: pluginUpdateSchema };
|
|
100
|
+
}
|
|
50
101
|
async function act(action, pluginId, options) {
|
|
51
102
|
const data = await withPluginManagementClient(options.host, (client) => client[`${action}Plugin`](pluginId));
|
|
52
103
|
return { type: "single", data, schema: pluginSchema };
|
|
@@ -62,7 +113,7 @@ async function remove(pluginId, options, _command) {
|
|
|
62
113
|
return { type: "single", data, schema: pluginSchema };
|
|
63
114
|
}
|
|
64
115
|
export function createPluginCommand() {
|
|
65
|
-
const plugin = new Command("plugin").description("Manage trusted
|
|
116
|
+
const plugin = new Command("plugin").description("Manage trusted plugins");
|
|
66
117
|
addJsonOption(plugin
|
|
67
118
|
.command("init")
|
|
68
119
|
.description("Create a typecheckable local plugin")
|
|
@@ -72,11 +123,20 @@ export function createPluginCommand() {
|
|
|
72
123
|
addJsonAndDaemonHostOptions(plugin.command("logs").description("Show recent plugin output").argument("<id>")).action(withOutput(runPluginLogsCommand));
|
|
73
124
|
addJsonAndDaemonHostOptions(plugin
|
|
74
125
|
.command("install")
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
.
|
|
126
|
+
.alias("add")
|
|
127
|
+
.description("Install a plugin from a directory or Git repository")
|
|
128
|
+
.argument("<source>", "Host directory, owner/repo shorthand, or Git URL")
|
|
129
|
+
.option("--id <id>", "Runtime plugin ID (defaults to paseo-plugin.json id)")
|
|
130
|
+
.option("--ref <ref>", "Git branch, tag, or commit")
|
|
131
|
+
.option("--path <path>", "Plugin directory within the repository")).action(withOutput(install));
|
|
132
|
+
addJsonAndDaemonHostOptions(plugin.command("status").description("Check plugin source updates").argument("[id]")).action(withOutput(status));
|
|
133
|
+
addJsonAndDaemonHostOptions(plugin
|
|
134
|
+
.command("update")
|
|
135
|
+
.description("Update a Git-managed plugin")
|
|
136
|
+
.argument("[id]")
|
|
137
|
+
.option("--all", "Update every Git-managed plugin")).action(withOutput(update));
|
|
78
138
|
for (const action of ["reload", "enable", "disable"]) {
|
|
79
|
-
addJsonAndDaemonHostOptions(plugin.command(action).description(`${action} a
|
|
139
|
+
addJsonAndDaemonHostOptions(plugin.command(action).description(`${action} a plugin`).argument("<id>")).action(withOutput((id, options, _command) => act(action, id, options)));
|
|
80
140
|
}
|
|
81
141
|
addJsonAndDaemonHostOptions(plugin.command("remove").description("Remove plugin configuration").argument("<id>")).action(withOutput(remove));
|
|
82
142
|
return plugin;
|
|
@@ -55,9 +55,50 @@ const SDK_DECLARATIONS = `declare module "@getpaseo/plugin/server" {
|
|
|
55
55
|
export const PluginAttachmentSearchPayloadSchema: import("zod").ZodType<PluginAttachmentSearchPayload>;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
declare module "@getpaseo/plugin/react-native" {
|
|
59
|
+
import type { ComponentType, FunctionComponent, ReactNode } from "react";
|
|
60
|
+
|
|
61
|
+
export interface PluginIconProps {
|
|
62
|
+
name: string;
|
|
63
|
+
size?: number;
|
|
64
|
+
color?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ModalProps {
|
|
68
|
+
title: string;
|
|
69
|
+
icon?: ReactNode;
|
|
70
|
+
open: boolean;
|
|
71
|
+
onOpenChange(open: boolean): void;
|
|
72
|
+
children: ReactNode;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ModalContentProps {
|
|
76
|
+
children: ReactNode;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ModalComponent extends FunctionComponent<ModalProps> {
|
|
80
|
+
Content: ComponentType<ModalContentProps>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type ToastVariant = "default" | "info" | "success" | "warning" | "error";
|
|
84
|
+
export interface ToastOptions {
|
|
85
|
+
variant?: ToastVariant;
|
|
86
|
+
durationMs?: number;
|
|
87
|
+
}
|
|
88
|
+
export interface ToastApi {
|
|
89
|
+
show(message: string, options?: ToastOptions): void;
|
|
90
|
+
error(message: string): void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const Icon: ComponentType<PluginIconProps>;
|
|
94
|
+
export const Modal: ModalComponent;
|
|
95
|
+
export function useToast(): ToastApi;
|
|
96
|
+
}
|
|
97
|
+
|
|
58
98
|
declare module "@getpaseo/plugin" {
|
|
59
99
|
import type { ComponentType } from "react";
|
|
60
100
|
import type { PaseoApi } from "@getpaseo/client";
|
|
101
|
+
import type { AgentTimelineItem } from "@getpaseo/protocol/agent-types";
|
|
61
102
|
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
|
|
62
103
|
import type {
|
|
63
104
|
PluginAttachmentSourceContribution,
|
|
@@ -80,10 +121,15 @@ declare module "@getpaseo/plugin" {
|
|
|
80
121
|
export interface PluginTheme {
|
|
81
122
|
readonly colors: {
|
|
82
123
|
readonly surface0: string;
|
|
124
|
+
readonly surface1: string;
|
|
125
|
+
readonly surface2: string;
|
|
126
|
+
readonly border: string;
|
|
83
127
|
readonly foreground: string;
|
|
84
128
|
readonly foregroundMuted: string;
|
|
85
129
|
readonly accent: string;
|
|
86
130
|
readonly accentForeground: string;
|
|
131
|
+
readonly statusSuccess: string;
|
|
132
|
+
readonly statusWarning: string;
|
|
87
133
|
readonly statusDanger: string;
|
|
88
134
|
};
|
|
89
135
|
}
|
|
@@ -96,6 +142,12 @@ declare module "@getpaseo/plugin" {
|
|
|
96
142
|
|
|
97
143
|
export interface PluginSurfaceProps extends PluginHostProps {}
|
|
98
144
|
|
|
145
|
+
export interface PluginIconProps {
|
|
146
|
+
name: string;
|
|
147
|
+
size?: number;
|
|
148
|
+
color?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
99
151
|
export interface PluginWorkspaceSnapshot {
|
|
100
152
|
readonly id: string;
|
|
101
153
|
readonly projectId: string;
|
|
@@ -142,8 +194,26 @@ declare module "@getpaseo/plugin" {
|
|
|
142
194
|
agentId: string;
|
|
143
195
|
}
|
|
144
196
|
|
|
197
|
+
export interface PluginComposerPillProps extends PluginHostProps {
|
|
198
|
+
workspaceId: string;
|
|
199
|
+
agentId: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface PluginComposerPillContribution {
|
|
203
|
+
id: string;
|
|
204
|
+
title: string;
|
|
205
|
+
workspaceId: string;
|
|
206
|
+
agentId: string;
|
|
207
|
+
Component: ComponentType<PluginComposerPillProps>;
|
|
208
|
+
onPress(): void | Promise<void>;
|
|
209
|
+
}
|
|
210
|
+
|
|
145
211
|
export type PluginPanelLocation = "workspace" | "explorer";
|
|
146
212
|
export interface PluginOpenPanelOptions { location?: PluginPanelLocation; }
|
|
213
|
+
export interface PluginClientOpenPanelOptions extends PluginOpenPanelOptions {
|
|
214
|
+
workspaceId: string;
|
|
215
|
+
agentId?: string;
|
|
216
|
+
}
|
|
147
217
|
|
|
148
218
|
export type PluginWorkspacePanelContribution =
|
|
149
219
|
| { id: string; title: string; icon: string; locations?: readonly PluginPanelLocation[]; context: "workspace"; Component: ComponentType<PluginWorkspacePanelProps> }
|
|
@@ -179,6 +249,29 @@ declare module "@getpaseo/plugin" {
|
|
|
179
249
|
Component: ComponentType<PluginSurfaceProps>;
|
|
180
250
|
}
|
|
181
251
|
|
|
252
|
+
export type PluginTimelineData = null | boolean | number | string | PluginTimelineData[] | { [key: string]: PluginTimelineData };
|
|
253
|
+
export interface PluginTimelineItem { type: "plugin"; kind: string; version: number; data: PluginTimelineData; }
|
|
254
|
+
export interface PluginTimelineTransformResult { items: PluginTimelineItem[]; }
|
|
255
|
+
export type PluginTimelineTransformerContribution<ItemType extends AgentTimelineItem["type"] = AgentTimelineItem["type"]> =
|
|
256
|
+
ItemType extends AgentTimelineItem["type"]
|
|
257
|
+
? {
|
|
258
|
+
id: string;
|
|
259
|
+
query: { itemType: ItemType };
|
|
260
|
+
transform(input: { item: Extract<AgentTimelineItem, { type: ItemType }> }): PluginTimelineTransformResult | undefined;
|
|
261
|
+
}
|
|
262
|
+
: never;
|
|
263
|
+
export interface PluginTimelineItemProps<Data = unknown> extends PluginHostProps {
|
|
264
|
+
agentId: string;
|
|
265
|
+
item: { type: "plugin"; kind: string; version: number; data: Data };
|
|
266
|
+
timestamp: Date;
|
|
267
|
+
}
|
|
268
|
+
export interface PluginTimelineRendererContribution<Schema extends ZodType = ZodType> {
|
|
269
|
+
kind: string;
|
|
270
|
+
version: number;
|
|
271
|
+
schema: Schema;
|
|
272
|
+
Component: ComponentType<PluginTimelineItemProps<ZodOutput<Schema>>>;
|
|
273
|
+
}
|
|
274
|
+
|
|
182
275
|
export interface PluginCommandCapabilities {
|
|
183
276
|
paseo: PaseoApi;
|
|
184
277
|
rpc<InputSchema extends ZodType, OutputSchema extends ZodType>(
|
|
@@ -205,6 +298,12 @@ declare module "@getpaseo/plugin" {
|
|
|
205
298
|
openPanel(id: string, options?: PluginOpenPanelOptions): void;
|
|
206
299
|
}
|
|
207
300
|
|
|
301
|
+
export interface PluginClientContext extends PluginCommandCapabilities {
|
|
302
|
+
addComposerPill(contribution: PluginComposerPillContribution): PluginCleanup;
|
|
303
|
+
openPanel(id: string, options: PluginClientOpenPanelOptions): void;
|
|
304
|
+
}
|
|
305
|
+
export type PluginClientContribution = (client: PluginClientContext) => PluginCleanup;
|
|
306
|
+
|
|
208
307
|
export type PluginCommandCenterItemContribution =
|
|
209
308
|
| { id: string; title: string; icon: string; keywords?: readonly string[]; context: "global"; onSelect(context: PluginGlobalCommandContext): void | Promise<void> }
|
|
210
309
|
| { id: string; title: string; icon: string; keywords?: readonly string[]; context: "workspace"; onSelect(context: PluginWorkspaceCommandContext): void | Promise<void> }
|
|
@@ -222,13 +321,18 @@ declare module "@getpaseo/plugin" {
|
|
|
222
321
|
addSidebarItem(contribution: PluginSidebarContribution): void;
|
|
223
322
|
addWorkspacePanel(contribution: PluginWorkspacePanelContribution): void;
|
|
224
323
|
addCommandCenterItem(contribution: PluginCommandCenterItemContribution): void;
|
|
324
|
+
addClientSide(contribution: PluginClientContribution): void;
|
|
225
325
|
addAttachmentSource(contribution: PluginAttachmentSourceContribution): void;
|
|
226
326
|
addTheme(contribution: PluginThemeContribution): void;
|
|
327
|
+
addTimelineTransformer<ItemType extends AgentTimelineItem["type"]>(contribution: PluginTimelineTransformerContribution<ItemType>): void;
|
|
328
|
+
addTimelineRenderer<Schema extends ZodType>(contribution: PluginTimelineRendererContribution<Schema>): void;
|
|
227
329
|
}
|
|
228
330
|
|
|
229
331
|
export type PluginCleanup = () => void | Promise<void>;
|
|
230
332
|
export type PluginContribution = (plugin: PluginContext) => PluginCleanup;
|
|
231
333
|
|
|
334
|
+
export const Icon: ComponentType<PluginIconProps>;
|
|
335
|
+
|
|
232
336
|
export function useRpc<InputSchema extends ZodType, OutputSchema extends ZodType>(
|
|
233
337
|
contract: PluginRpcContract<InputSchema, OutputSchema>,
|
|
234
338
|
): (input: ZodInput<InputSchema>) => Promise<ZodOutput<OutputSchema>>;
|
|
@@ -307,6 +411,7 @@ export async function scaffoldPluginDirectory(targetDirectory, requestedId) {
|
|
|
307
411
|
scripts: { typecheck: "tsc --noEmit" },
|
|
308
412
|
devDependencies: {
|
|
309
413
|
"@getpaseo/client": "^0.4.0",
|
|
414
|
+
"@getpaseo/protocol": "^0.6.1",
|
|
310
415
|
"@tanstack/react-query": "^5.90.11",
|
|
311
416
|
"@types/react": "~19.2.0",
|
|
312
417
|
react: "19.1.0",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
|
2
2
|
export declare function withPluginManagementClient<T>(host: string | undefined, run: (client: DaemonClient) => Promise<T>): Promise<T>;
|
|
3
3
|
export declare function withPluginLogsClient<T>(host: string | undefined, run: (client: DaemonClient) => Promise<T>): Promise<T>;
|
|
4
|
+
export declare function withPluginSourceClient<T>(host: string | undefined, run: (client: DaemonClient) => Promise<T>): Promise<T>;
|
|
4
5
|
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -23,4 +23,8 @@ export async function withPluginLogsClient(host, run) {
|
|
|
23
23
|
// COMPAT(pluginLogs): added in v0.4.0, remove gate after 2027-08-16.
|
|
24
24
|
return withPluginClient(host, "pluginLogs", "Update the host to view plugin logs.", run);
|
|
25
25
|
}
|
|
26
|
+
export async function withPluginSourceClient(host, run) {
|
|
27
|
+
// COMPAT(pluginGitManagement): added in v0.7.0, remove gate after 2027-08-26.
|
|
28
|
+
return withPluginClient(host, "pluginGitManagement", "Update the host to install and update Git plugins.", run);
|
|
29
|
+
}
|
|
26
30
|
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type SshTransportTarget } from "@getpaseo/protocol/ssh-transport";
|
|
2
|
+
export interface SshTunnel {
|
|
3
|
+
endpoint: string;
|
|
4
|
+
close(): void;
|
|
5
|
+
failureDetail(): string | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveSshFailureDetail(failure: string | null, stderr: string): string | null;
|
|
8
|
+
export declare function createSshTunnel(target: SshTransportTarget): Promise<SshTunnel>;
|
|
9
|
+
//# sourceMappingURL=ssh-tunnel.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { buildSshTunnelArgs } from "@getpaseo/protocol/ssh-transport";
|
|
4
|
+
const SSH_STDERR_LIMIT = 8192;
|
|
5
|
+
function formatSshFailure(stderr, code, signal) {
|
|
6
|
+
const detail = stderr.trim();
|
|
7
|
+
if (detail)
|
|
8
|
+
return detail;
|
|
9
|
+
if (signal)
|
|
10
|
+
return `ssh exited with signal ${signal}`;
|
|
11
|
+
return `ssh exited with code ${code ?? "unknown"}`;
|
|
12
|
+
}
|
|
13
|
+
export function resolveSshFailureDetail(failure, stderr) {
|
|
14
|
+
return failure ?? (stderr.trim() || null);
|
|
15
|
+
}
|
|
16
|
+
export function createSshTunnel(target) {
|
|
17
|
+
let server = null;
|
|
18
|
+
let socket = null;
|
|
19
|
+
let child = null;
|
|
20
|
+
let stderr = "";
|
|
21
|
+
let failure = null;
|
|
22
|
+
function close() {
|
|
23
|
+
server?.close();
|
|
24
|
+
server = null;
|
|
25
|
+
socket?.destroy();
|
|
26
|
+
socket = null;
|
|
27
|
+
if (child && !child.killed)
|
|
28
|
+
child.kill();
|
|
29
|
+
child = null;
|
|
30
|
+
}
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
server = createServer((acceptedSocket) => {
|
|
33
|
+
socket = acceptedSocket;
|
|
34
|
+
server?.close();
|
|
35
|
+
server = null;
|
|
36
|
+
child = spawn("ssh", buildSshTunnelArgs(target), {
|
|
37
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
38
|
+
windowsHide: true,
|
|
39
|
+
});
|
|
40
|
+
child.stderr.on("data", (chunk) => {
|
|
41
|
+
stderr = `${stderr}${chunk.toString()}`.slice(-SSH_STDERR_LIMIT);
|
|
42
|
+
});
|
|
43
|
+
child.on("error", (error) => {
|
|
44
|
+
failure = error.message;
|
|
45
|
+
acceptedSocket.destroy(error);
|
|
46
|
+
});
|
|
47
|
+
child.on("exit", (code, signal) => {
|
|
48
|
+
if (code !== 0 || signal)
|
|
49
|
+
failure = formatSshFailure(stderr, code, signal);
|
|
50
|
+
acceptedSocket.destroy(failure ? new Error(failure) : undefined);
|
|
51
|
+
});
|
|
52
|
+
acceptedSocket.on("error", () => undefined);
|
|
53
|
+
acceptedSocket.on("close", () => {
|
|
54
|
+
if (child && !child.killed)
|
|
55
|
+
child.kill();
|
|
56
|
+
});
|
|
57
|
+
acceptedSocket.pipe(child.stdin);
|
|
58
|
+
child.stdout.pipe(acceptedSocket);
|
|
59
|
+
});
|
|
60
|
+
server.once("error", (error) => {
|
|
61
|
+
close();
|
|
62
|
+
reject(error);
|
|
63
|
+
});
|
|
64
|
+
server.listen(0, "127.0.0.1", () => {
|
|
65
|
+
const address = server?.address();
|
|
66
|
+
if (!address || typeof address === "string") {
|
|
67
|
+
close();
|
|
68
|
+
reject(new Error("Failed to allocate the SSH tunnel port"));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
resolve({
|
|
72
|
+
endpoint: `127.0.0.1:${address.port}`,
|
|
73
|
+
close,
|
|
74
|
+
failureDetail: () => resolveSshFailureDetail(failure, stderr),
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=ssh-tunnel.js.map
|
package/dist/utils/client.js
CHANGED
|
@@ -2,11 +2,13 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { loadConfig, resolvePaseoHome } from "@getpaseo/server";
|
|
3
3
|
import { buildDaemonWebSocketUrl, buildRelayWebSocketUrl, normalizeHostPort, parseConnectionUri, shouldUseTlsForDefaultHostedRelay, } from "@getpaseo/protocol/daemon-endpoints";
|
|
4
4
|
import { parseConnectionOfferFromUrl, } from "@getpaseo/protocol/connection-offer";
|
|
5
|
+
import { parseSshTransportUri } from "@getpaseo/protocol/ssh-transport";
|
|
5
6
|
import { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { WebSocket } from "ws";
|
|
8
9
|
import { getOrCreateCliClientId } from "./client-id.js";
|
|
9
10
|
import { resolveCliVersion } from "../version.js";
|
|
11
|
+
import { createSshTunnel } from "../ssh/ssh-tunnel.js";
|
|
10
12
|
const DEFAULT_HOST = "localhost:6767";
|
|
11
13
|
const DEFAULT_TIMEOUT = 15000;
|
|
12
14
|
const PID_FILENAME = "paseo.pid";
|
|
@@ -22,7 +24,9 @@ export function buildDaemonConnectionCommandError(options) {
|
|
|
22
24
|
return {
|
|
23
25
|
code: "DAEMON_NOT_RUNNING",
|
|
24
26
|
message: `Cannot connect to daemon at ${host}: ${message}`,
|
|
25
|
-
details: "
|
|
27
|
+
details: host.trim().startsWith("ssh://")
|
|
28
|
+
? "Start the Paseo daemon on the SSH host; SSH transport does not install or start it."
|
|
29
|
+
: "Start the daemon with: paseo daemon start",
|
|
26
30
|
};
|
|
27
31
|
}
|
|
28
32
|
export function normalizeDaemonHost(raw) {
|
|
@@ -266,6 +270,19 @@ export async function connectToDaemon(options) {
|
|
|
266
270
|
const clientId = await getOrCreateCliClientId();
|
|
267
271
|
const nodeWebSocketFactory = createNodeWebSocketFactory();
|
|
268
272
|
const explicitHost = options?.host ?? process.env.PASEO_HOST;
|
|
273
|
+
if (explicitHost?.trim().startsWith("ssh://")) {
|
|
274
|
+
const target = parseSshTransportUri(explicitHost.trim());
|
|
275
|
+
const tunnel = await createSshTunnel(target);
|
|
276
|
+
const password = resolveDaemonPassword(explicitHost);
|
|
277
|
+
const result = await tryConnectHost(tunnel.endpoint, password, clientId, timeout, nodeWebSocketFactory);
|
|
278
|
+
if ("client" in result)
|
|
279
|
+
return result.client;
|
|
280
|
+
const failure = tunnel.failureDetail();
|
|
281
|
+
tunnel.close();
|
|
282
|
+
if (failure)
|
|
283
|
+
throw new Error(`SSH connection failed: ${failure}`, { cause: result.error });
|
|
284
|
+
throw result.error;
|
|
285
|
+
}
|
|
269
286
|
const offer = parseHostOfferOrNull(explicitHost);
|
|
270
287
|
if (offer) {
|
|
271
288
|
return connectViaRelayOffer(offer, clientId, timeout, nodeWebSocketFactory);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const JSON_OPTION_DESCRIPTION = "Output in JSON format";
|
|
2
|
-
const DAEMON_HOST_OPTION_DESCRIPTION = "Daemon host target: host:port
|
|
2
|
+
const DAEMON_HOST_OPTION_DESCRIPTION = "Daemon host target: host:port, tcp://host:port, or ssh://user@host (default: local socket/pipe, then localhost:6767)";
|
|
3
3
|
export function collectMultiple(value, previous) {
|
|
4
4
|
return previous.concat([value]);
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpaseo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-beta.2",
|
|
4
4
|
"description": "Paseo CLI - control your AI coding agents from the command line",
|
|
5
5
|
"bin": {
|
|
6
6
|
"paseo": "bin/paseo"
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@clack/prompts": "^1.0.0",
|
|
31
|
-
"@getpaseo/client": "0.
|
|
32
|
-
"@getpaseo/protocol": "0.
|
|
33
|
-
"@getpaseo/server": "0.
|
|
31
|
+
"@getpaseo/client": "0.7.0-beta.2",
|
|
32
|
+
"@getpaseo/protocol": "0.7.0-beta.2",
|
|
33
|
+
"@getpaseo/server": "0.7.0-beta.2",
|
|
34
34
|
"chalk": "^5.3.0",
|
|
35
35
|
"commander": "^12.0.0",
|
|
36
36
|
"mime-types": "^2.1.35",
|