@getpaseo/cli 0.7.0-beta.1 → 0.7.0-beta.3
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 +7 -4
- package/dist/commands/hub/connect.d.ts +2 -0
- package/dist/commands/hub/connect.js +12 -2
- package/dist/commands/hub/daemon-client.d.ts +8 -2
- package/dist/commands/hub/export.d.ts +28 -0
- package/dist/commands/hub/export.js +80 -0
- package/dist/commands/hub/hub-client/index.d.ts +3 -2
- package/dist/commands/hub/hub-client/index.js +13 -1
- package/dist/commands/hub/hub-client/internal/contracts.d.ts +23 -0
- package/dist/commands/hub/hub-client/internal/contracts.js +10 -0
- package/dist/commands/hub/index.js +13 -0
- package/dist/commands/hub/init.js +28 -28
- package/dist/commands/hub/permissions.d.ts +21 -0
- package/dist/commands/hub/permissions.js +61 -0
- package/dist/commands/hub/status-output.d.ts +1 -1
- package/dist/commands/hub/status-output.js +2 -2
- package/dist/commands/plugin/scaffold.js +76 -3
- 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
|
@@ -80,13 +80,16 @@ function toListItem(agent) {
|
|
|
80
80
|
}
|
|
81
81
|
function daemonConnectionFailure(host, cause) {
|
|
82
82
|
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
83
|
+
const isSsh = host.trim().startsWith("ssh://");
|
|
83
84
|
return {
|
|
84
85
|
code: "DAEMON_NOT_RUNNING",
|
|
85
86
|
message: `Cannot reach the daemon at ${host}: ${reason}`,
|
|
86
|
-
details:
|
|
87
|
-
"Start
|
|
88
|
-
|
|
89
|
-
|
|
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"),
|
|
90
93
|
};
|
|
91
94
|
}
|
|
92
95
|
function parseLabelFilters(labels) {
|
|
@@ -15,8 +15,14 @@ export async function runHubConnect(originInput, options, dependencies) {
|
|
|
15
15
|
reportHubProgress(dependencies.reporter, options, `Connecting this daemon to ${origin}`);
|
|
16
16
|
const credential = resolveHubCredential({ ...resolution, origin });
|
|
17
17
|
const token = await dependencies.hub.issueEnrollmentToken(origin, credential);
|
|
18
|
+
const permissions = options.permissions ?? options.permission ?? [];
|
|
18
19
|
return withHubDaemon(dependencies.daemon, options.host, async (daemon) => {
|
|
19
|
-
const response = await daemon.connectHub(origin, token);
|
|
20
|
+
const response = await daemon.connectHub(origin, token, permissions);
|
|
21
|
+
if (response.status.hubOrigin !== null &&
|
|
22
|
+
!samePermissions(response.status.permissions, permissions)) {
|
|
23
|
+
await daemon.disconnectHub(false).catch(() => undefined);
|
|
24
|
+
throw new Error("The daemon did not honor the requested Hub access. Update Paseo before connecting it.");
|
|
25
|
+
}
|
|
20
26
|
return hubStatusResult(response.status);
|
|
21
27
|
});
|
|
22
28
|
}
|
|
@@ -25,10 +31,14 @@ export function addHubConnectCommand(parent, dependencies) {
|
|
|
25
31
|
.command("connect")
|
|
26
32
|
.description("Enroll this daemon with a Paseo Hub")
|
|
27
33
|
.argument("[origin]", "Paseo Hub origin")
|
|
28
|
-
.option("--api-key <secret>", "Organization API key")
|
|
34
|
+
.option("--api-key <secret>", "Organization API key")
|
|
35
|
+
.option("--permission <permission...>", "Grant daemon permission during connection"))).action(withOutput(async (...args) => {
|
|
29
36
|
const origin = args[0];
|
|
30
37
|
const options = args.at(-2);
|
|
31
38
|
return runHubConnect(origin, options, dependencies);
|
|
32
39
|
}));
|
|
33
40
|
}
|
|
41
|
+
function samePermissions(actual, expected) {
|
|
42
|
+
return actual.length === expected.length && expected.every((scope) => actual.includes(scope));
|
|
43
|
+
}
|
|
34
44
|
//# sourceMappingURL=connect.js.map
|
|
@@ -3,7 +3,7 @@ export interface HubStatus {
|
|
|
3
3
|
state: string;
|
|
4
4
|
daemonId: string | null;
|
|
5
5
|
hubOrigin: string | null;
|
|
6
|
-
|
|
6
|
+
permissions: string[];
|
|
7
7
|
connectedAt: string | null;
|
|
8
8
|
lastError: string | null;
|
|
9
9
|
}
|
|
@@ -11,7 +11,13 @@ export interface HubProvidersSnapshotOptions {
|
|
|
11
11
|
cwd?: string;
|
|
12
12
|
}
|
|
13
13
|
export interface HubDaemonClient {
|
|
14
|
-
connectHub(url: string, token: string): Promise<{
|
|
14
|
+
connectHub(url: string, token: string, permissions?: readonly string[]): Promise<{
|
|
15
|
+
status: HubStatus;
|
|
16
|
+
}>;
|
|
17
|
+
updateHubPermissions(input: {
|
|
18
|
+
grant?: readonly string[];
|
|
19
|
+
revoke?: readonly string[];
|
|
20
|
+
}): Promise<{
|
|
15
21
|
status: HubStatus;
|
|
16
22
|
}>;
|
|
17
23
|
getHubStatus(): Promise<{
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { type SingleResult } from "../../output/index.js";
|
|
3
|
+
import type { HubCredentialStore } from "./credentials.js";
|
|
4
|
+
import type { HubHttpClient } from "./hub-client/index.js";
|
|
5
|
+
import { type HubReporter } from "./reporter.js";
|
|
6
|
+
interface HubExportResult {
|
|
7
|
+
origin: string;
|
|
8
|
+
directory: string;
|
|
9
|
+
exported: number;
|
|
10
|
+
unchanged: number;
|
|
11
|
+
}
|
|
12
|
+
export interface HubExportOptions {
|
|
13
|
+
hub?: string;
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
force?: boolean;
|
|
16
|
+
json?: boolean;
|
|
17
|
+
}
|
|
18
|
+
interface HubExportDependencies {
|
|
19
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
20
|
+
credentials: HubCredentialStore;
|
|
21
|
+
hub: Pick<HubHttpClient, "listTriggers">;
|
|
22
|
+
reporter: HubReporter;
|
|
23
|
+
cwd(): string;
|
|
24
|
+
}
|
|
25
|
+
export declare function runHubExport(directoryInput: string | undefined, options: HubExportOptions, dependencies: HubExportDependencies): Promise<SingleResult<HubExportResult>>;
|
|
26
|
+
export declare function addHubExportCommand(parent: Command, dependencies: HubExportDependencies): void;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=export.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { withOutput } from "../../output/index.js";
|
|
4
|
+
import { addJsonOption } from "../../utils/command-options.js";
|
|
5
|
+
import { resolveHubCredential, resolveHubOrigin } from "./authority.js";
|
|
6
|
+
import { HubCommandError } from "./error.js";
|
|
7
|
+
import { addHubResolutionHelp } from "./help.js";
|
|
8
|
+
import { reportHubProgress } from "./reporter.js";
|
|
9
|
+
const schema = {
|
|
10
|
+
idField: "directory",
|
|
11
|
+
columns: [
|
|
12
|
+
{ header: "DIRECTORY", field: "directory" },
|
|
13
|
+
{ header: "EXPORTED", field: "exported" },
|
|
14
|
+
{ header: "UNCHANGED", field: "unchanged" },
|
|
15
|
+
{ header: "HUB", field: "origin" },
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
export async function runHubExport(directoryInput, options, dependencies) {
|
|
19
|
+
const resolution = {
|
|
20
|
+
options: { origin: options.hub, apiKey: options.apiKey },
|
|
21
|
+
env: dependencies.env,
|
|
22
|
+
credentials: dependencies.credentials,
|
|
23
|
+
};
|
|
24
|
+
const origin = resolveHubOrigin(resolution);
|
|
25
|
+
const credential = resolveHubCredential({ ...resolution, origin });
|
|
26
|
+
const directory = path.resolve(dependencies.cwd(), directoryInput ?? ".paseo/triggers");
|
|
27
|
+
reportHubProgress(dependencies.reporter, options, `Exporting triggers from ${origin}`);
|
|
28
|
+
const triggers = await dependencies.hub.listTriggers(origin, credential);
|
|
29
|
+
await mkdir(directory, { recursive: true });
|
|
30
|
+
const files = await Promise.all(triggers.map(async (trigger) => {
|
|
31
|
+
const destination = path.join(directory, `${trigger.name}.yml`);
|
|
32
|
+
return { destination, trigger, existing: await readOptionalFile(destination) };
|
|
33
|
+
}));
|
|
34
|
+
const conflict = files.find(({ existing, trigger }) => existing !== undefined && existing !== trigger.yaml && options.force !== true);
|
|
35
|
+
if (conflict !== undefined) {
|
|
36
|
+
throw new HubCommandError("HUB_EXPORT_CONFLICT", `${conflict.destination} already exists with different contents. Pass --force to replace it.`);
|
|
37
|
+
}
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
if (file.existing === file.trigger.yaml)
|
|
40
|
+
continue;
|
|
41
|
+
await writeFile(file.destination, file.trigger.yaml, file.existing === undefined ? { flag: "wx" } : undefined);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
type: "single",
|
|
45
|
+
data: {
|
|
46
|
+
origin,
|
|
47
|
+
directory,
|
|
48
|
+
exported: files.filter(({ existing, trigger }) => existing !== trigger.yaml).length,
|
|
49
|
+
unchanged: files.filter(({ existing, trigger }) => existing === trigger.yaml).length,
|
|
50
|
+
},
|
|
51
|
+
schema,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function addHubExportCommand(parent, dependencies) {
|
|
55
|
+
addJsonOption(addHubResolutionHelp(parent
|
|
56
|
+
.command("export")
|
|
57
|
+
.description("Export active Hub triggers as one YAML file per trigger")
|
|
58
|
+
.argument("[directory]", "Destination directory", ".paseo/triggers")
|
|
59
|
+
.option("--hub <origin>", "Paseo Hub origin")
|
|
60
|
+
.option("--api-key <secret>", "Organization API key")
|
|
61
|
+
.option("--force", "Replace trigger files with different contents"))).action(withOutput(async (...args) => {
|
|
62
|
+
const directory = args[0];
|
|
63
|
+
const options = args.at(-2);
|
|
64
|
+
return runHubExport(directory, options, dependencies);
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
async function readOptionalFile(filePath) {
|
|
68
|
+
try {
|
|
69
|
+
return await readFile(filePath, "utf8");
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (isMissingFile(error))
|
|
73
|
+
return undefined;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function isMissingFile(error) {
|
|
78
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=export.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HubBundleFile } from "../deploy-bundle.js";
|
|
2
|
-
import { type CliAuthorization, type CliAuthorizationPoll, type HubInstallResult, type HubConfigurationResources, type HubSetupResources, type HubProject, type HubValidationResult } from "./internal/contracts.js";
|
|
3
|
-
export type { CliAuthorization, CliAuthorizationPoll, HubInstallResult, HubConfigurationResources, HubSetupResources, HubProject, HubValidationResult, } from "./internal/contracts.js";
|
|
2
|
+
import { type CliAuthorization, type CliAuthorizationPoll, type HubInstallResult, type HubConfigurationResources, type HubSetupResources, type HubProject, type HubTrigger, type HubValidationResult } from "./internal/contracts.js";
|
|
3
|
+
export type { CliAuthorization, CliAuthorizationPoll, HubInstallResult, HubConfigurationResources, HubSetupResources, HubProject, HubTrigger, HubValidationResult, } from "./internal/contracts.js";
|
|
4
4
|
interface HubConfigurationInput {
|
|
5
5
|
origin: string;
|
|
6
6
|
apiKey: string;
|
|
@@ -11,6 +11,7 @@ export declare class HubHttpClient {
|
|
|
11
11
|
startCliAuthorization(origin: string): Promise<CliAuthorization>;
|
|
12
12
|
pollCliAuthorization(origin: string, deviceCode: string, timeoutMilliseconds: number): Promise<CliAuthorizationPoll>;
|
|
13
13
|
listProjects(origin: string, apiKey: string): Promise<HubProject[]>;
|
|
14
|
+
listTriggers(origin: string, apiKey: string): Promise<HubTrigger[]>;
|
|
14
15
|
listConfigurationResources(origin: string, apiKey: string): Promise<HubConfigurationResources>;
|
|
15
16
|
listSetupResources(origin: string, apiKey: string): Promise<HubSetupResources>;
|
|
16
17
|
installConfiguration(input: HubConfigurationInput): Promise<HubInstallResult>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HubCommandError } from "../error.js";
|
|
2
|
-
import { authorizationPollSchema, authorizationSchema, configurationResourcesSchema, enrollmentTokenSchema, installResponseSchema, projectsResponseSchema, validationResponseSchema, setupResourcesSchema, } from "./internal/contracts.js";
|
|
2
|
+
import { authorizationPollSchema, authorizationSchema, configurationResourcesSchema, enrollmentTokenSchema, installResponseSchema, projectsResponseSchema, triggersResponseSchema, validationResponseSchema, setupResourcesSchema, } from "./internal/contracts.js";
|
|
3
3
|
import { requestHub } from "./internal/transport.js";
|
|
4
4
|
export class HubHttpClient {
|
|
5
5
|
startCliAuthorization(origin) {
|
|
@@ -45,6 +45,18 @@ export class HubHttpClient {
|
|
|
45
45
|
});
|
|
46
46
|
return response.projects;
|
|
47
47
|
}
|
|
48
|
+
async listTriggers(origin, apiKey) {
|
|
49
|
+
const response = await requestHub({
|
|
50
|
+
origin,
|
|
51
|
+
path: "/api/v1/triggers",
|
|
52
|
+
method: "GET",
|
|
53
|
+
apiKey,
|
|
54
|
+
successStatus: 200,
|
|
55
|
+
schema: triggersResponseSchema,
|
|
56
|
+
failureMessage: "Hub trigger export failed",
|
|
57
|
+
});
|
|
58
|
+
return response.triggers;
|
|
59
|
+
}
|
|
48
60
|
listConfigurationResources(origin, apiKey) {
|
|
49
61
|
return requestHub({
|
|
50
62
|
origin,
|
|
@@ -42,6 +42,28 @@ export declare const projectsResponseSchema: z.ZodObject<{
|
|
|
42
42
|
name: z.ZodString;
|
|
43
43
|
}, z.core.$strict>>;
|
|
44
44
|
}, z.core.$strict>;
|
|
45
|
+
declare const triggerSchema: z.ZodObject<{
|
|
46
|
+
id: z.ZodString;
|
|
47
|
+
name: z.ZodString;
|
|
48
|
+
enabled: z.ZodBoolean;
|
|
49
|
+
format: z.ZodEnum<{
|
|
50
|
+
single_run: "single_run";
|
|
51
|
+
legacy_multistep: "legacy_multistep";
|
|
52
|
+
}>;
|
|
53
|
+
yaml: z.ZodString;
|
|
54
|
+
}, z.core.$strict>;
|
|
55
|
+
export declare const triggersResponseSchema: z.ZodObject<{
|
|
56
|
+
triggers: z.ZodArray<z.ZodObject<{
|
|
57
|
+
id: z.ZodString;
|
|
58
|
+
name: z.ZodString;
|
|
59
|
+
enabled: z.ZodBoolean;
|
|
60
|
+
format: z.ZodEnum<{
|
|
61
|
+
single_run: "single_run";
|
|
62
|
+
legacy_multistep: "legacy_multistep";
|
|
63
|
+
}>;
|
|
64
|
+
yaml: z.ZodString;
|
|
65
|
+
}, z.core.$strict>>;
|
|
66
|
+
}, z.core.$strict>;
|
|
45
67
|
export declare const configurationResourcesSchema: z.ZodObject<{
|
|
46
68
|
daemons: z.ZodArray<z.ZodObject<{
|
|
47
69
|
id: z.ZodString;
|
|
@@ -95,6 +117,7 @@ export declare const enrollmentTokenSchema: z.ZodObject<{
|
|
|
95
117
|
export type CliAuthorization = z.infer<typeof authorizationSchema>;
|
|
96
118
|
export type CliAuthorizationPoll = z.infer<typeof authorizationPollSchema>;
|
|
97
119
|
export type HubProject = z.infer<typeof projectSchema>;
|
|
120
|
+
export type HubTrigger = z.infer<typeof triggerSchema>;
|
|
98
121
|
export type HubConfigurationResources = z.infer<typeof configurationResourcesSchema>;
|
|
99
122
|
export type HubSetupResources = z.infer<typeof setupResourcesSchema>;
|
|
100
123
|
export type HubInstallResult = z.infer<typeof installResponseSchema>;
|
|
@@ -30,6 +30,16 @@ const projectSchema = z
|
|
|
30
30
|
.object({ id: z.string().uuid(), slug: z.string().min(1), name: z.string().min(1) })
|
|
31
31
|
.strict();
|
|
32
32
|
export const projectsResponseSchema = z.object({ projects: z.array(projectSchema) }).strict();
|
|
33
|
+
const triggerSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
id: z.string().uuid(),
|
|
36
|
+
name: z.string().regex(/^[a-z][a-z0-9_-]*$/u),
|
|
37
|
+
enabled: z.boolean(),
|
|
38
|
+
format: z.enum(["single_run", "legacy_multistep"]),
|
|
39
|
+
yaml: z.string(),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
42
|
+
export const triggersResponseSchema = z.object({ triggers: z.array(triggerSchema) }).strict();
|
|
33
43
|
export const configurationResourcesSchema = z
|
|
34
44
|
.object({
|
|
35
45
|
daemons: z.array(z.object({ id: z.string().uuid(), slug: z.string().min(1) }).strict()),
|
|
@@ -11,10 +11,12 @@ import { createCliLoginFlow } from "./login-flow.js";
|
|
|
11
11
|
import { addHubLoginCommand } from "./login.js";
|
|
12
12
|
import { addHubLogoutCommand, productionLogoutPrompt } from "./logout.js";
|
|
13
13
|
import { addHubProjectsCommand } from "./projects.js";
|
|
14
|
+
import { addHubExportCommand } from "./export.js";
|
|
14
15
|
import { processHubReporter } from "./reporter.js";
|
|
15
16
|
import { hubStatusResult } from "./status-output.js";
|
|
16
17
|
import { addHubResolutionHelp } from "./help.js";
|
|
17
18
|
import { addHubInitCommand, continueHubGuidedSetup } from "./init.js";
|
|
19
|
+
import { addHubPermissionsCommand } from "./permissions.js";
|
|
18
20
|
function productionEnvironment() {
|
|
19
21
|
const env = process.env;
|
|
20
22
|
const hub = new HubHttpClient();
|
|
@@ -56,12 +58,23 @@ export function createHubCommand(overrides = {}) {
|
|
|
56
58
|
daemon: environment.daemon,
|
|
57
59
|
reporter: environment.reporter,
|
|
58
60
|
});
|
|
61
|
+
addHubPermissionsCommand(hub, {
|
|
62
|
+
daemon: environment.daemon,
|
|
63
|
+
reporter: environment.reporter,
|
|
64
|
+
});
|
|
59
65
|
addHubProjectsCommand(hub, {
|
|
60
66
|
env: environment.env,
|
|
61
67
|
credentials: environment.credentials,
|
|
62
68
|
hub: environment.hub,
|
|
63
69
|
reporter: environment.reporter,
|
|
64
70
|
});
|
|
71
|
+
addHubExportCommand(hub, {
|
|
72
|
+
env: environment.env,
|
|
73
|
+
credentials: environment.credentials,
|
|
74
|
+
hub: environment.hub,
|
|
75
|
+
reporter: environment.reporter,
|
|
76
|
+
cwd: environment.cwd,
|
|
77
|
+
});
|
|
65
78
|
addHubDeployCommand(hub, {
|
|
66
79
|
env: environment.env,
|
|
67
80
|
credentials: environment.credentials,
|
|
@@ -112,31 +112,28 @@ export async function runHubGuidedSetup(environment, state = {}) {
|
|
|
112
112
|
outro(deploy ? "Hub is ready" : "Hub bundle is ready");
|
|
113
113
|
}
|
|
114
114
|
export async function continueHubGuidedSetup(origin, environment) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
outro("Connect an app to continue");
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
environment.prompts.message(error.message);
|
|
135
|
-
}
|
|
136
|
-
return;
|
|
115
|
+
const currentStatus = await withHubDaemon(environment.daemon, undefined, async (daemon) => daemon.getHubStatus().then((response) => response.status));
|
|
116
|
+
const current = resolveHubInitConnection(currentStatus, origin);
|
|
117
|
+
if (current.kind === "connected") {
|
|
118
|
+
reportMessage(environment, `This daemon is already connected to ${origin}. Permissions: ${currentStatus.permissions.join(", ") || "None"}.`);
|
|
119
|
+
}
|
|
120
|
+
else if (current.kind === "pending") {
|
|
121
|
+
await waitForDaemonReady(origin, environment.daemon);
|
|
122
|
+
}
|
|
123
|
+
else if (current.kind === "conflict") {
|
|
124
|
+
reportMessage(environment, `This daemon is connected to ${current.origin}. Disconnect it before connecting to ${origin}.`);
|
|
125
|
+
}
|
|
126
|
+
else if (await requiredConfirm(environment, "Connect this daemon to Paseo Hub?\n\nConnecting lets Hub identify this daemon and show whether it is online.\nIt does not allow Hub to create workspaces or run agents.", true)) {
|
|
127
|
+
const grantExecution = await requiredConfirm(environment, "Allow Hub automations to run agents on this daemon?\n\nThis lets workflows triggered from GitHub, Slack, Discord, Linear, and other integrations create workspaces and run agents here.\n\nAgents can access files and run commands allowed by their workspace runtime.", false);
|
|
128
|
+
await ensureDaemonConnection(origin, environment, true, grantExecution ? ["hub.execute"] : []);
|
|
129
|
+
if (!grantExecution) {
|
|
130
|
+
reportMessage(environment, "Daemon connected with no permissions.\n\nEnable Hub automations later:\n paseo hub permissions grant hub.execute");
|
|
137
131
|
}
|
|
138
|
-
throw error;
|
|
139
132
|
}
|
|
133
|
+
else {
|
|
134
|
+
reportMessage(environment, `Skipped daemon connection. Connect later with: ${hubLoginResumeCommand("connect", origin)}`);
|
|
135
|
+
}
|
|
136
|
+
reportMessage(environment, `Configure triggers in Hub: ${new URL("/triggers", origin).toString()}\nOr scaffold triggers as code: ${hubLoginResumeCommand("init", origin)}`);
|
|
140
137
|
}
|
|
141
138
|
async function ensureLogin(activeOrigin, environment) {
|
|
142
139
|
const endpoint = await requiredSelect(environment, {
|
|
@@ -175,10 +172,13 @@ async function ensureLogin(activeOrigin, environment) {
|
|
|
175
172
|
log.success(`Logged in to ${normalizedOrigin}`);
|
|
176
173
|
return normalizedOrigin;
|
|
177
174
|
}
|
|
178
|
-
async function ensureDaemonConnection(origin, environment, confirmed = false) {
|
|
175
|
+
async function ensureDaemonConnection(origin, environment, confirmed = false, permissions = ["hub.execute"]) {
|
|
179
176
|
const status = await withHubDaemon(environment.daemon, undefined, async (daemon) => daemon.getHubStatus().then((response) => response.status));
|
|
180
177
|
const connection = resolveHubInitConnection(status, origin);
|
|
181
178
|
if (connection.kind === "connected") {
|
|
179
|
+
if (permissions.includes("hub.execute") && !status.permissions.includes("hub.execute")) {
|
|
180
|
+
throw new HubCommandError("HUB_DAEMON_EXECUTION_NOT_ALLOWED", "This daemon is connected to Hub but cannot run Hub automations. Run `paseo hub permissions grant hub.execute`, then run Hub init again.");
|
|
181
|
+
}
|
|
182
182
|
return connection.daemonId;
|
|
183
183
|
}
|
|
184
184
|
if (connection.kind === "pending") {
|
|
@@ -188,13 +188,13 @@ async function ensureDaemonConnection(origin, environment, confirmed = false) {
|
|
|
188
188
|
throw new HubCommandError("HUB_DAEMON_ALREADY_CONNECTED", `This daemon is connected to ${connection.origin}. Disconnect it before running Hub init for ${origin}.`);
|
|
189
189
|
}
|
|
190
190
|
if (!confirmed &&
|
|
191
|
-
!(await requiredConfirm(environment, `Connect this daemon to ${origin}?`, true))) {
|
|
191
|
+
!(await requiredConfirm(environment, `Connect this daemon to ${origin} and allow Hub workflows to create and control workspaces and agents?`, true))) {
|
|
192
192
|
throw new HubInitCancelledError("A connected daemon is required to create the bundle.");
|
|
193
193
|
}
|
|
194
|
-
return connectDaemon(origin, environment);
|
|
194
|
+
return connectDaemon(origin, environment, permissions);
|
|
195
195
|
}
|
|
196
|
-
async function connectDaemon(origin, environment) {
|
|
197
|
-
await runHubConnect(origin, {}, {
|
|
196
|
+
async function connectDaemon(origin, environment, permissions = ["hub.execute"]) {
|
|
197
|
+
await runHubConnect(origin, { permissions }, {
|
|
198
198
|
env: environment.env,
|
|
199
199
|
credentials: environment.credentials,
|
|
200
200
|
hub: environment.hub,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { type ListResult } from "../../output/index.js";
|
|
3
|
+
import type { HubDaemonConnection } from "./daemon-client.js";
|
|
4
|
+
import { type HubReporter } from "./reporter.js";
|
|
5
|
+
interface HubPermissionsOptions {
|
|
6
|
+
host?: string;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface HubPermissionsDependencies {
|
|
10
|
+
daemon: HubDaemonConnection;
|
|
11
|
+
reporter: HubReporter;
|
|
12
|
+
}
|
|
13
|
+
interface PermissionRow {
|
|
14
|
+
permission: string;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function runHubPermissionsList(options: HubPermissionsOptions, dependencies: HubPermissionsDependencies): Promise<ListResult<PermissionRow>>;
|
|
18
|
+
export declare function runHubPermissionChange(operation: "grant" | "revoke", permission: string, options: HubPermissionsOptions, dependencies: HubPermissionsDependencies): Promise<ListResult<import("./status-output.js").HubRow>>;
|
|
19
|
+
export declare function addHubPermissionsCommand(parent: Command, dependencies: HubPermissionsDependencies): void;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { withOutput } from "../../output/index.js";
|
|
2
|
+
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
|
3
|
+
import { withHubDaemon } from "./daemon-client.js";
|
|
4
|
+
import { reportHubProgress } from "./reporter.js";
|
|
5
|
+
import { hubStatusResult } from "./status-output.js";
|
|
6
|
+
const schema = {
|
|
7
|
+
idField: "permission",
|
|
8
|
+
columns: [
|
|
9
|
+
{ header: "PERMISSION", field: "permission" },
|
|
10
|
+
{ header: "DESCRIPTION", field: "description" },
|
|
11
|
+
],
|
|
12
|
+
};
|
|
13
|
+
export function runHubPermissionsList(options, dependencies) {
|
|
14
|
+
return withHubDaemon(dependencies.daemon, options.host, async (client) => {
|
|
15
|
+
const status = (await client.getHubStatus()).status;
|
|
16
|
+
requireConnectedHub(status);
|
|
17
|
+
return {
|
|
18
|
+
type: "list",
|
|
19
|
+
data: status.permissions.map((permission) => ({
|
|
20
|
+
permission,
|
|
21
|
+
description: describePermission(permission),
|
|
22
|
+
})),
|
|
23
|
+
schema,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function runHubPermissionChange(operation, permission, options, dependencies) {
|
|
28
|
+
return withHubDaemon(dependencies.daemon, options.host, async (client) => {
|
|
29
|
+
const current = (await client.getHubStatus()).status;
|
|
30
|
+
requireConnectedHub(current);
|
|
31
|
+
const response = await client.updateHubPermissions(operation === "grant" ? { grant: [permission] } : { revoke: [permission] });
|
|
32
|
+
reportHubProgress(dependencies.reporter, options, `${operation === "grant" ? "Granted" : "Revoked"} ${permission} ${operation === "grant" ? "to" : "from"} ${response.status.hubOrigin}`);
|
|
33
|
+
return hubStatusResult(response.status);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function addHubPermissionsCommand(parent, dependencies) {
|
|
37
|
+
const permissions = parent
|
|
38
|
+
.command("permissions")
|
|
39
|
+
.description("Manage this Hub's daemon permissions");
|
|
40
|
+
addJsonAndDaemonHostOptions(permissions.command("list")).action(withOutput(async (...args) => {
|
|
41
|
+
const options = args.at(-2);
|
|
42
|
+
return runHubPermissionsList(options, dependencies);
|
|
43
|
+
}));
|
|
44
|
+
for (const operation of ["grant", "revoke"]) {
|
|
45
|
+
addJsonAndDaemonHostOptions(permissions.command(operation).argument("<permission>", "Daemon permission")).action(withOutput(async (...args) => {
|
|
46
|
+
const permission = args[0];
|
|
47
|
+
const options = args.at(-2);
|
|
48
|
+
return runHubPermissionChange(operation, permission, options, dependencies);
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function requireConnectedHub(status) {
|
|
53
|
+
if (status.hubOrigin === null ||
|
|
54
|
+
(status.state !== "connected" && status.state !== "reconnecting")) {
|
|
55
|
+
throw new Error("This daemon is not connected to a Hub");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function describePermission(permission) {
|
|
59
|
+
return permission === "hub.execute" ? "Run agents for Hub automations" : permission;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=permissions.js.map
|
|
@@ -4,7 +4,7 @@ const schema = {
|
|
|
4
4
|
{ header: "STATE", field: "state" },
|
|
5
5
|
{ header: "HUB", field: "hub" },
|
|
6
6
|
{ header: "DAEMON", field: "daemonId" },
|
|
7
|
-
{ header: "
|
|
7
|
+
{ header: "PERMISSIONS", field: "permissions" },
|
|
8
8
|
{ header: "CONNECTED", field: "connectedAt" },
|
|
9
9
|
{ header: "ERROR", field: "error" },
|
|
10
10
|
{ header: "WARNING", field: "warning" },
|
|
@@ -18,7 +18,7 @@ export function hubStatusResult(status, warning, reportedHubOrigin = status.hubO
|
|
|
18
18
|
state: status.state,
|
|
19
19
|
daemonId: status.daemonId,
|
|
20
20
|
hub: reportedHubOrigin,
|
|
21
|
-
|
|
21
|
+
permissions: status.permissions.join(", "),
|
|
22
22
|
connectedAt: status.connectedAt,
|
|
23
23
|
error: status.lastError,
|
|
24
24
|
warning,
|
|
@@ -55,6 +55,46 @@ 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";
|
|
@@ -100,7 +140,15 @@ declare module "@getpaseo/plugin" {
|
|
|
100
140
|
layout: { compact: boolean; platform: "ios" | "android" | "web" };
|
|
101
141
|
}
|
|
102
142
|
|
|
103
|
-
|
|
143
|
+
interface PluginNavigableHostProps extends PluginHostProps {
|
|
144
|
+
/** Client-owned navigation. Undefined on older hosts; hide dependent affordances when absent. */
|
|
145
|
+
readonly navigation?: {
|
|
146
|
+
readonly openAgent: (input: { readonly agentId: string }) => void;
|
|
147
|
+
readonly openWorkspace: (input: { readonly workspaceId: string }) => void;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface PluginSurfaceProps extends PluginNavigableHostProps {}
|
|
104
152
|
|
|
105
153
|
export interface PluginIconProps {
|
|
106
154
|
name: string;
|
|
@@ -143,19 +191,37 @@ declare module "@getpaseo/plugin" {
|
|
|
143
191
|
readonly labels: Readonly<Record<string, string>>;
|
|
144
192
|
}
|
|
145
193
|
|
|
146
|
-
export interface PluginWorkspacePanelProps extends
|
|
194
|
+
export interface PluginWorkspacePanelProps extends PluginNavigableHostProps {
|
|
147
195
|
context: "workspace";
|
|
148
196
|
workspaceId: string;
|
|
149
197
|
}
|
|
150
198
|
|
|
151
|
-
export interface PluginAgentPanelProps extends
|
|
199
|
+
export interface PluginAgentPanelProps extends PluginNavigableHostProps {
|
|
152
200
|
context: "agent";
|
|
153
201
|
workspaceId: string;
|
|
154
202
|
agentId: string;
|
|
155
203
|
}
|
|
156
204
|
|
|
205
|
+
export interface PluginComposerPillProps extends PluginHostProps {
|
|
206
|
+
workspaceId: string;
|
|
207
|
+
agentId: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface PluginComposerPillContribution {
|
|
211
|
+
id: string;
|
|
212
|
+
title: string;
|
|
213
|
+
workspaceId: string;
|
|
214
|
+
agentId: string;
|
|
215
|
+
Component: ComponentType<PluginComposerPillProps>;
|
|
216
|
+
onPress(): void | Promise<void>;
|
|
217
|
+
}
|
|
218
|
+
|
|
157
219
|
export type PluginPanelLocation = "workspace" | "explorer";
|
|
158
220
|
export interface PluginOpenPanelOptions { location?: PluginPanelLocation; }
|
|
221
|
+
export interface PluginClientOpenPanelOptions extends PluginOpenPanelOptions {
|
|
222
|
+
workspaceId: string;
|
|
223
|
+
agentId?: string;
|
|
224
|
+
}
|
|
159
225
|
|
|
160
226
|
export type PluginWorkspacePanelContribution =
|
|
161
227
|
| { id: string; title: string; icon: string; locations?: readonly PluginPanelLocation[]; context: "workspace"; Component: ComponentType<PluginWorkspacePanelProps> }
|
|
@@ -240,6 +306,12 @@ declare module "@getpaseo/plugin" {
|
|
|
240
306
|
openPanel(id: string, options?: PluginOpenPanelOptions): void;
|
|
241
307
|
}
|
|
242
308
|
|
|
309
|
+
export interface PluginClientContext extends PluginCommandCapabilities {
|
|
310
|
+
addComposerPill(contribution: PluginComposerPillContribution): PluginCleanup;
|
|
311
|
+
openPanel(id: string, options: PluginClientOpenPanelOptions): void;
|
|
312
|
+
}
|
|
313
|
+
export type PluginClientContribution = (client: PluginClientContext) => PluginCleanup;
|
|
314
|
+
|
|
243
315
|
export type PluginCommandCenterItemContribution =
|
|
244
316
|
| { id: string; title: string; icon: string; keywords?: readonly string[]; context: "global"; onSelect(context: PluginGlobalCommandContext): void | Promise<void> }
|
|
245
317
|
| { id: string; title: string; icon: string; keywords?: readonly string[]; context: "workspace"; onSelect(context: PluginWorkspaceCommandContext): void | Promise<void> }
|
|
@@ -257,6 +329,7 @@ declare module "@getpaseo/plugin" {
|
|
|
257
329
|
addSidebarItem(contribution: PluginSidebarContribution): void;
|
|
258
330
|
addWorkspacePanel(contribution: PluginWorkspacePanelContribution): void;
|
|
259
331
|
addCommandCenterItem(contribution: PluginCommandCenterItemContribution): void;
|
|
332
|
+
addClientSide(contribution: PluginClientContribution): void;
|
|
260
333
|
addAttachmentSource(contribution: PluginAttachmentSourceContribution): void;
|
|
261
334
|
addTheme(contribution: PluginThemeContribution): void;
|
|
262
335
|
addTimelineTransformer<ItemType extends AgentTimelineItem["type"]>(contribution: PluginTimelineTransformerContribution<ItemType>): void;
|
|
@@ -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.7.0-beta.
|
|
3
|
+
"version": "0.7.0-beta.3",
|
|
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.7.0-beta.
|
|
32
|
-
"@getpaseo/protocol": "0.7.0-beta.
|
|
33
|
-
"@getpaseo/server": "0.7.0-beta.
|
|
31
|
+
"@getpaseo/client": "0.7.0-beta.3",
|
|
32
|
+
"@getpaseo/protocol": "0.7.0-beta.3",
|
|
33
|
+
"@getpaseo/server": "0.7.0-beta.3",
|
|
34
34
|
"chalk": "^5.3.0",
|
|
35
35
|
"commander": "^12.0.0",
|
|
36
36
|
"mime-types": "^2.1.35",
|