@rigkit/provider-cmux 0.2.13 → 0.2.14
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/README.md +31 -46
- package/package.json +3 -3
- package/src/capabilities.ts +72 -34
- package/src/host.ts +107 -242
- package/src/index.test.ts +102 -149
- package/src/index.ts +20 -14
- package/src/provider.ts +66 -56
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -1,67 +1,52 @@
|
|
|
1
1
|
# @rigkit/provider-cmux
|
|
2
2
|
|
|
3
|
-
Small SDK and Rigkit provider facade for
|
|
3
|
+
Small SDK and Rigkit provider facade for driving local `cmux`.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import { createCmuxClient } from "@rigkit/provider-cmux";
|
|
7
7
|
|
|
8
8
|
const cmux = createCmuxClient();
|
|
9
9
|
|
|
10
|
-
await cmux.newWorkspace({
|
|
10
|
+
const workspace = await cmux.newWorkspace({
|
|
11
11
|
name: "playground",
|
|
12
|
-
|
|
12
|
+
focus: true,
|
|
13
|
+
});
|
|
14
|
+
await cmux.newSurface({
|
|
15
|
+
workspace: workspace.id ?? workspace.handle,
|
|
16
|
+
type: "terminal",
|
|
13
17
|
focus: true,
|
|
14
18
|
});
|
|
15
19
|
```
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
```text
|
|
20
|
-
$ cmux new-workspace --name playground --command 'echo hello world' --focus true
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
`cmux new-workspace` and `cmux ssh` are socket commands. With cmux's default socket control mode (`cmuxOnly`), run rigkit from a terminal inside cmux so cmux sets `CMUX_SOCKET_PATH` and accepts the process.
|
|
24
|
-
|
|
25
|
-
If you intentionally enable external socket control in cmux, opt in explicitly:
|
|
21
|
+
Workflow operations use raw cmux actions through the provider facade:
|
|
26
22
|
|
|
27
23
|
```ts
|
|
28
|
-
const
|
|
29
|
-
|
|
24
|
+
const workspace = await providers.cmux.ssh({
|
|
25
|
+
destination: "root@devbox.example.com",
|
|
26
|
+
name: "site",
|
|
27
|
+
});
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
await providers.cmux.newSurface({
|
|
30
|
+
workspace: workspace.workspaceId,
|
|
31
|
+
type: "browser",
|
|
32
|
+
url: "http://localhost:3000",
|
|
33
|
+
focus: true,
|
|
34
|
+
});
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
const terminal = await providers.cmux.newSurface({
|
|
37
|
+
workspace: workspace.workspaceId,
|
|
38
|
+
type: "terminal",
|
|
39
|
+
focus: false,
|
|
40
|
+
});
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
await providers.cmux.send({
|
|
43
|
+
workspace: workspace.workspaceId,
|
|
44
|
+
surface: terminal.surfaceId,
|
|
45
|
+
text: "pnpm dev\n",
|
|
46
|
+
});
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
providers: {
|
|
41
|
-
cmux: cmux.provider(),
|
|
42
|
-
},
|
|
43
|
-
})
|
|
44
|
-
.sequence("site")
|
|
45
|
-
.operation("open", {
|
|
46
|
-
run: async ({ providers }) => {
|
|
47
|
-
await providers.cmux.open({
|
|
48
|
-
name: "site",
|
|
49
|
-
ssh: {
|
|
50
|
-
host: "devbox.example.com",
|
|
51
|
-
username: "root",
|
|
52
|
-
sshOptions: ["ServerAliveInterval=15"],
|
|
53
|
-
},
|
|
54
|
-
cwd: "/workspace/site",
|
|
55
|
-
surfaceLayout: "tabs",
|
|
56
|
-
terminals: [{ command: "pnpm dev" }],
|
|
57
|
-
url: "http://localhost:3000",
|
|
58
|
-
});
|
|
59
|
-
},
|
|
60
|
-
});
|
|
48
|
+
await providers.cmux.selectWorkspace(workspace.workspaceId);
|
|
61
49
|
```
|
|
62
50
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
split-pane behavior.
|
|
66
|
-
|
|
67
|
-
Local hosts can import `@rigkit/provider-cmux/host` to register the trusted `cmux.open` handler. The Rigkit CLI registers this handler automatically.
|
|
51
|
+
Local hosts can import `@rigkit/provider-cmux/host` to register the trusted
|
|
52
|
+
`cmux.call` handler. The Rigkit CLI registers this handler automatically.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigkit/provider-cmux",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@rigkit/sdk": "0.2.
|
|
22
|
-
"@rigkit/engine": "0.2.
|
|
21
|
+
"@rigkit/sdk": "0.2.14",
|
|
22
|
+
"@rigkit/engine": "0.2.14"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/bun": "latest",
|
package/src/capabilities.ts
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const CMUX_CALL_CAPABILITY_ID = "cmux.call";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
"sha256:
|
|
3
|
+
export const CMUX_CALL_SCHEMA_HASH =
|
|
4
|
+
"sha256:afcc8ef7251d854c80d1c04d9a98bc9afbd22d2ab15e1ce9fb880452ec17f6cf";
|
|
5
5
|
|
|
6
|
-
export const
|
|
7
|
-
id:
|
|
8
|
-
schemaHash:
|
|
6
|
+
export const CMUX_CALL_CAPABILITY = {
|
|
7
|
+
id: CMUX_CALL_CAPABILITY_ID,
|
|
8
|
+
schemaHash: CMUX_CALL_SCHEMA_HASH,
|
|
9
9
|
} as const;
|
|
10
10
|
|
|
11
|
-
export type
|
|
11
|
+
export type CmuxCallMethod =
|
|
12
|
+
| "newWorkspace"
|
|
13
|
+
| "ssh"
|
|
14
|
+
| "newPane"
|
|
15
|
+
| "newSurface"
|
|
16
|
+
| "browserOpen"
|
|
17
|
+
| "send"
|
|
18
|
+
| "portsKick"
|
|
19
|
+
| "selectWorkspace"
|
|
20
|
+
| "waitForRemoteReady";
|
|
21
|
+
|
|
22
|
+
export type CmuxCallInput = {
|
|
23
|
+
method: CmuxCallMethod;
|
|
24
|
+
params?: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type CmuxSshInput = string | {
|
|
12
28
|
kind?: "ssh";
|
|
13
29
|
destination?: string;
|
|
14
30
|
host?: string;
|
|
@@ -21,50 +37,72 @@ export type CmuxOpenSshInput = string | {
|
|
|
21
37
|
terminalStartupCommand?: string;
|
|
22
38
|
autoConnect?: boolean;
|
|
23
39
|
skipDaemonBootstrap?: boolean;
|
|
40
|
+
name?: string;
|
|
41
|
+
noFocus?: boolean;
|
|
24
42
|
};
|
|
25
43
|
|
|
26
|
-
export type
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
44
|
+
export type CmuxWorkspaceInput = {
|
|
45
|
+
name?: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
cwd?: string;
|
|
48
|
+
command?: string;
|
|
49
|
+
focus?: boolean;
|
|
30
50
|
};
|
|
31
51
|
|
|
32
|
-
export type
|
|
33
|
-
export type
|
|
52
|
+
export type CmuxPaneDirection = "left" | "right" | "up" | "down";
|
|
53
|
+
export type CmuxSurfaceType = "terminal" | "browser";
|
|
34
54
|
|
|
35
|
-
export type
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
direction?:
|
|
55
|
+
export type CmuxNewPaneInput = {
|
|
56
|
+
workspace?: string;
|
|
57
|
+
type?: CmuxSurfaceType;
|
|
58
|
+
direction?: CmuxPaneDirection;
|
|
59
|
+
url?: string;
|
|
39
60
|
focus?: boolean;
|
|
40
61
|
};
|
|
41
62
|
|
|
42
|
-
export type
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
surfaceLayout?: CmuxOpenSurfaceLayout;
|
|
47
|
-
terminals?: readonly CmuxOpenTerminalInput[];
|
|
63
|
+
export type CmuxNewSurfaceInput = {
|
|
64
|
+
workspace?: string;
|
|
65
|
+
pane?: string;
|
|
66
|
+
type?: CmuxSurfaceType;
|
|
48
67
|
url?: string;
|
|
49
68
|
focus?: boolean;
|
|
50
|
-
waitForRemoteReady?: boolean | CmuxRemoteReadyOptions;
|
|
51
69
|
};
|
|
52
70
|
|
|
53
|
-
export type
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
export type CmuxBrowserOpenInput = {
|
|
72
|
+
workspace?: string;
|
|
73
|
+
window?: string;
|
|
74
|
+
url?: string;
|
|
75
|
+
focus?: boolean;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type CmuxSendInput = {
|
|
79
|
+
workspace?: string;
|
|
80
|
+
surface?: string;
|
|
81
|
+
text: string;
|
|
58
82
|
};
|
|
59
83
|
|
|
60
|
-
export type
|
|
84
|
+
export type CmuxPortsKickInput = {
|
|
85
|
+
workspace: string;
|
|
86
|
+
surface?: string;
|
|
87
|
+
reason?: "command" | "refresh";
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type CmuxRemoteReadyInput = {
|
|
91
|
+
workspace: string;
|
|
92
|
+
timeoutMs?: number;
|
|
93
|
+
intervalMs?: number;
|
|
94
|
+
requireProxy?: boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type CmuxWorkspaceResult = {
|
|
61
98
|
sessionId: string;
|
|
62
99
|
workspaceId: string;
|
|
63
100
|
workspaceRef?: string;
|
|
64
|
-
terminalPanes: CmuxOpenPaneResult[];
|
|
65
|
-
browserPane?: CmuxOpenPaneResult;
|
|
66
101
|
};
|
|
67
102
|
|
|
68
|
-
export type
|
|
69
|
-
|
|
103
|
+
export type CmuxPaneResult = {
|
|
104
|
+
paneId?: string;
|
|
105
|
+
paneRef?: string;
|
|
106
|
+
surfaceId?: string;
|
|
107
|
+
surfaceRef?: string;
|
|
70
108
|
};
|
package/src/host.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createCmuxClient,
|
|
3
|
-
|
|
3
|
+
type CmuxBrowserOpenOptions,
|
|
4
4
|
type CmuxClient,
|
|
5
5
|
type CmuxClientOptions,
|
|
6
|
+
type CmuxNewPaneOptions,
|
|
7
|
+
type CmuxNewSurfaceOptions,
|
|
6
8
|
type CmuxNewWorkspaceOptions,
|
|
7
|
-
type CmuxPane,
|
|
8
9
|
type CmuxPortsKickOptions,
|
|
9
10
|
type CmuxSendOptions,
|
|
10
11
|
type CmuxSshOptions,
|
|
11
12
|
type CmuxWaitForRemoteOptions,
|
|
12
|
-
type CmuxWorkspace,
|
|
13
13
|
} from "./index.ts";
|
|
14
14
|
import {
|
|
15
15
|
defineHostCapability,
|
|
@@ -17,19 +17,14 @@ import {
|
|
|
17
17
|
type HostCapabilityHandler,
|
|
18
18
|
} from "@rigkit/sdk/host";
|
|
19
19
|
import {
|
|
20
|
-
|
|
21
|
-
type
|
|
22
|
-
type
|
|
23
|
-
type CmuxOpenResult,
|
|
24
|
-
type CmuxOpenSshInput,
|
|
25
|
-
type CmuxOpenTerminalDirection,
|
|
26
|
-
type CmuxOpenTerminalInput,
|
|
27
|
-
type CmuxRemoteReadyOptions,
|
|
20
|
+
CMUX_CALL_CAPABILITY,
|
|
21
|
+
type CmuxCallInput,
|
|
22
|
+
type CmuxSshInput,
|
|
28
23
|
} from "./capabilities.ts";
|
|
29
24
|
|
|
30
25
|
export type CmuxHostCapabilityHandler = HostCapabilityHandler;
|
|
31
26
|
|
|
32
|
-
export type
|
|
27
|
+
export type CmuxCallClient = Pick<
|
|
33
28
|
CmuxClient,
|
|
34
29
|
| "newWorkspace"
|
|
35
30
|
| "ssh"
|
|
@@ -42,138 +37,69 @@ export type CmuxOpenClient = Pick<
|
|
|
42
37
|
| "waitForRemoteReady"
|
|
43
38
|
>;
|
|
44
39
|
|
|
45
|
-
export type
|
|
46
|
-
client?:
|
|
40
|
+
export type CmuxCallHostOptions = {
|
|
41
|
+
client?: CmuxCallClient;
|
|
47
42
|
clientOptions?: CmuxClientOptions;
|
|
48
43
|
logger?: (message: string) => void;
|
|
49
44
|
};
|
|
50
45
|
|
|
51
|
-
export function
|
|
52
|
-
options:
|
|
46
|
+
export function createCmuxCallHostCapability(
|
|
47
|
+
options: CmuxCallHostOptions = {},
|
|
53
48
|
): CmuxHostCapabilityHandler {
|
|
54
|
-
return defineHostCapability(
|
|
55
|
-
schemaHash:
|
|
49
|
+
return defineHostCapability(CMUX_CALL_CAPABILITY.id, {
|
|
50
|
+
schemaHash: CMUX_CALL_CAPABILITY.schemaHash,
|
|
56
51
|
handle: async (params, context) =>
|
|
57
|
-
await
|
|
52
|
+
await callCmux(params, {
|
|
58
53
|
...options,
|
|
59
54
|
logger: options.logger ?? hostCapabilityLogger(context) ?? options.clientOptions?.logger,
|
|
60
55
|
}),
|
|
61
56
|
});
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
export const cmuxHostCapabilities = [
|
|
59
|
+
export const cmuxHostCapabilities = [createCmuxCallHostCapability()] as const;
|
|
65
60
|
|
|
66
|
-
export async function
|
|
61
|
+
export async function callCmux(
|
|
67
62
|
params: unknown,
|
|
68
|
-
options:
|
|
69
|
-
): Promise<
|
|
70
|
-
const input =
|
|
71
|
-
const logger =
|
|
63
|
+
options: CmuxCallHostOptions = {},
|
|
64
|
+
): Promise<unknown> {
|
|
65
|
+
const input = parseCmuxCallInput(params);
|
|
66
|
+
const logger = cmuxCallLogger(options);
|
|
72
67
|
const cmux = options.client ?? createCmuxClient({
|
|
73
68
|
...options.clientOptions,
|
|
74
69
|
...(options.logger ? { logger: options.logger } : {}),
|
|
75
70
|
printCommands: options.clientOptions?.printCommands ?? false,
|
|
76
71
|
});
|
|
77
|
-
let workspace: CmuxWorkspace;
|
|
78
|
-
const terminalPanes: CmuxPane[] = [];
|
|
79
72
|
|
|
80
|
-
logger?.(`cmux:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
? await cmux.newSurface({
|
|
106
|
-
workspace: workspaceId,
|
|
107
|
-
type: "terminal",
|
|
108
|
-
focus: terminal.focus ?? true,
|
|
109
|
-
})
|
|
110
|
-
: await cmux.newPane({
|
|
111
|
-
workspace: workspaceId,
|
|
112
|
-
type: "terminal",
|
|
113
|
-
direction: terminal.direction ?? "down",
|
|
114
|
-
focus: terminal.focus ?? true,
|
|
115
|
-
});
|
|
116
|
-
terminalPanes.push(terminalPane);
|
|
117
|
-
const sendOptions: CmuxSendOptions = {
|
|
118
|
-
workspace: workspaceId,
|
|
119
|
-
surface: terminalPane.surface,
|
|
120
|
-
text: commandForTerminal(terminal, input),
|
|
121
|
-
};
|
|
122
|
-
await cmux.send(sendOptions);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const waitOptions = remoteReadyOptionsForInput(input);
|
|
126
|
-
if (input.ssh && waitOptions) {
|
|
127
|
-
logger?.("cmux: waiting for remote ports");
|
|
128
|
-
await cmux.waitForRemoteReady(workspaceId, waitOptions);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (input.ssh && terminalPanes.some((pane) => pane.surface)) {
|
|
132
|
-
logger?.("cmux: refreshing remote ports");
|
|
133
|
-
for (const pane of terminalPanes) {
|
|
134
|
-
if (!pane.surface) continue;
|
|
135
|
-
const kickOptions: CmuxPortsKickOptions = {
|
|
136
|
-
workspace: workspaceId,
|
|
137
|
-
surface: pane.surface,
|
|
138
|
-
reason: "command",
|
|
139
|
-
};
|
|
140
|
-
await cmux.portsKick(kickOptions);
|
|
73
|
+
logger?.(`cmux: ${input.method}`);
|
|
74
|
+
switch (input.method) {
|
|
75
|
+
case "newWorkspace":
|
|
76
|
+
return await cmux.newWorkspace(input.params as CmuxNewWorkspaceOptions);
|
|
77
|
+
case "ssh":
|
|
78
|
+
return await cmux.ssh(cmuxSshOptionsForInput(input.params));
|
|
79
|
+
case "newPane":
|
|
80
|
+
return await cmux.newPane(input.params as CmuxNewPaneOptions);
|
|
81
|
+
case "newSurface":
|
|
82
|
+
return await cmux.newSurface(input.params as CmuxNewSurfaceOptions);
|
|
83
|
+
case "browserOpen":
|
|
84
|
+
return await cmux.browserOpen(input.params as CmuxBrowserOpenOptions);
|
|
85
|
+
case "send":
|
|
86
|
+
return await cmux.send(input.params as CmuxSendOptions);
|
|
87
|
+
case "portsKick":
|
|
88
|
+
return await cmux.portsKick(input.params as CmuxPortsKickOptions);
|
|
89
|
+
case "selectWorkspace":
|
|
90
|
+
await cmux.selectWorkspace(requiredString(input.params, "workspace"));
|
|
91
|
+
return "OK";
|
|
92
|
+
case "waitForRemoteReady": {
|
|
93
|
+
const { workspace, ...waitOptions } = input.params;
|
|
94
|
+
if (typeof workspace !== "string" || workspace.trim() === "") {
|
|
95
|
+
throw new Error(`cmux.call waitForRemoteReady requires params.workspace`);
|
|
96
|
+
}
|
|
97
|
+
return await cmux.waitForRemoteReady(workspace, waitOptions as CmuxWaitForRemoteOptions);
|
|
141
98
|
}
|
|
142
99
|
}
|
|
143
|
-
|
|
144
|
-
let browserPane: CmuxPane | undefined;
|
|
145
|
-
if (input.url) {
|
|
146
|
-
logger?.(`cmux: opening ${input.url}`);
|
|
147
|
-
browserPane = useTabLayout
|
|
148
|
-
? await cmux.newSurface({
|
|
149
|
-
workspace: workspaceId,
|
|
150
|
-
type: "browser",
|
|
151
|
-
url: input.url,
|
|
152
|
-
focus: input.focus !== false,
|
|
153
|
-
})
|
|
154
|
-
: await cmux.browserOpen({
|
|
155
|
-
workspace: workspaceId,
|
|
156
|
-
url: input.url,
|
|
157
|
-
focus: input.focus !== false,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (input.focus !== false) {
|
|
162
|
-
logger?.("cmux: focusing workspace");
|
|
163
|
-
await cmux.selectWorkspace(workspaceId);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
logger?.(`cmux: ready ${input.name}`);
|
|
167
|
-
return {
|
|
168
|
-
sessionId: workspaceId,
|
|
169
|
-
workspaceId,
|
|
170
|
-
...(workspace.ref ? { workspaceRef: workspace.ref } : {}),
|
|
171
|
-
terminalPanes: terminalPanes.map(paneResultForCmuxPane),
|
|
172
|
-
...(browserPane ? { browserPane: paneResultForCmuxPane(browserPane) } : {}),
|
|
173
|
-
};
|
|
174
100
|
}
|
|
175
101
|
|
|
176
|
-
function
|
|
102
|
+
function cmuxCallLogger(options: CmuxCallHostOptions): ((message: string) => void) | undefined {
|
|
177
103
|
return options.logger ?? options.clientOptions?.logger;
|
|
178
104
|
}
|
|
179
105
|
|
|
@@ -182,28 +108,56 @@ function hostCapabilityLogger(context: HostCapabilityContext | undefined): ((mes
|
|
|
182
108
|
return (message) => context.log(message, { label: "cmux" });
|
|
183
109
|
}
|
|
184
110
|
|
|
185
|
-
export function
|
|
186
|
-
if (!isRecord(value)) throw new Error(`cmux.
|
|
187
|
-
const
|
|
111
|
+
export function parseCmuxCallInput(value: unknown): CmuxCallInput & { params: Record<string, unknown> } {
|
|
112
|
+
if (!isRecord(value)) throw new Error(`cmux.call requires an object input`);
|
|
113
|
+
const method = requiredString(value, "method");
|
|
114
|
+
if (!isCmuxCallMethod(method)) {
|
|
115
|
+
throw new Error(`cmux.call method is not supported: ${method}`);
|
|
116
|
+
}
|
|
117
|
+
const params = value.params === undefined ? {} : value.params;
|
|
118
|
+
if (!isRecord(params)) throw new Error(`cmux.call params must be an object`);
|
|
119
|
+
return { method, params };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isCmuxCallMethod(value: string): value is CmuxCallInput["method"] {
|
|
123
|
+
return [
|
|
124
|
+
"newWorkspace",
|
|
125
|
+
"ssh",
|
|
126
|
+
"newPane",
|
|
127
|
+
"newSurface",
|
|
128
|
+
"browserOpen",
|
|
129
|
+
"send",
|
|
130
|
+
"portsKick",
|
|
131
|
+
"selectWorkspace",
|
|
132
|
+
"waitForRemoteReady",
|
|
133
|
+
].includes(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function cmuxSshOptionsForInput(ssh: Record<string, unknown>): CmuxSshOptions {
|
|
137
|
+
const parsed = parseSshInput(ssh);
|
|
138
|
+
if (typeof parsed === "string") return { destination: parsed };
|
|
139
|
+
|
|
140
|
+
const destination = parsed.destination ?? sshDestination(parsed);
|
|
188
141
|
return {
|
|
189
|
-
|
|
190
|
-
...(
|
|
191
|
-
...
|
|
192
|
-
...
|
|
193
|
-
...(
|
|
194
|
-
...
|
|
195
|
-
...
|
|
196
|
-
...(
|
|
197
|
-
|
|
198
|
-
|
|
142
|
+
destination,
|
|
143
|
+
...(parsed.port !== undefined ? { port: parsed.port } : {}),
|
|
144
|
+
...(parsed.identity !== undefined ? { identity: parsed.identity } : {}),
|
|
145
|
+
...(parsed.sshOptions?.length ? { sshOptions: parsed.sshOptions } : {}),
|
|
146
|
+
...(parsed.remoteCommandArgs !== undefined ? { remoteCommandArgs: parsed.remoteCommandArgs } : {}),
|
|
147
|
+
...(parsed.initialCommand !== undefined ? { initialCommand: parsed.initialCommand } : {}),
|
|
148
|
+
...(parsed.terminalStartupCommand !== undefined ? { terminalStartupCommand: parsed.terminalStartupCommand } : {}),
|
|
149
|
+
...(parsed.autoConnect !== undefined ? { autoConnect: parsed.autoConnect } : {}),
|
|
150
|
+
...(parsed.skipDaemonBootstrap !== undefined ? { skipDaemonBootstrap: parsed.skipDaemonBootstrap } : {}),
|
|
151
|
+
...(parsed.name !== undefined ? { name: parsed.name } : {}),
|
|
152
|
+
...(parsed.noFocus !== undefined ? { noFocus: parsed.noFocus } : {}),
|
|
199
153
|
};
|
|
200
154
|
}
|
|
201
155
|
|
|
202
|
-
function parseSshInput(value: unknown):
|
|
156
|
+
function parseSshInput(value: unknown): CmuxSshInput {
|
|
203
157
|
if (typeof value === "string" && value.trim()) return value.trim();
|
|
204
|
-
if (!isRecord(value)) throw new Error(`cmux.
|
|
158
|
+
if (!isRecord(value)) throw new Error(`cmux.call ssh params must be an object`);
|
|
205
159
|
if (value.kind !== undefined && value.kind !== "ssh") {
|
|
206
|
-
throw new Error(`cmux.
|
|
160
|
+
throw new Error(`cmux.call ssh.kind must be "ssh"`);
|
|
207
161
|
}
|
|
208
162
|
return {
|
|
209
163
|
...optionalStringField(value, "destination"),
|
|
@@ -217,84 +171,21 @@ function parseSshInput(value: unknown): CmuxOpenSshInput {
|
|
|
217
171
|
...optionalStringField(value, "terminalStartupCommand"),
|
|
218
172
|
...optionalBooleanField(value, "autoConnect"),
|
|
219
173
|
...optionalBooleanField(value, "skipDaemonBootstrap"),
|
|
174
|
+
...optionalStringField(value, "name"),
|
|
175
|
+
...optionalBooleanField(value, "noFocus"),
|
|
220
176
|
};
|
|
221
177
|
}
|
|
222
178
|
|
|
223
|
-
function
|
|
224
|
-
if (
|
|
225
|
-
if (!
|
|
226
|
-
return {
|
|
227
|
-
...optionalNumberField(value, "timeoutMs"),
|
|
228
|
-
...optionalNumberField(value, "intervalMs"),
|
|
229
|
-
...optionalBooleanField(value, "requireProxy"),
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function cmuxSshOptionsForInput(ssh: CmuxOpenSshInput): CmuxSshOptions {
|
|
234
|
-
if (typeof ssh === "string") return { destination: ssh };
|
|
235
|
-
|
|
236
|
-
const destination = ssh.destination ?? sshDestination(ssh);
|
|
237
|
-
return {
|
|
238
|
-
destination,
|
|
239
|
-
...(ssh.port !== undefined ? { port: ssh.port } : {}),
|
|
240
|
-
...(ssh.identity !== undefined ? { identity: ssh.identity } : {}),
|
|
241
|
-
...(ssh.sshOptions?.length ? { sshOptions: ssh.sshOptions } : {}),
|
|
242
|
-
...(ssh.remoteCommandArgs !== undefined ? { remoteCommandArgs: ssh.remoteCommandArgs } : {}),
|
|
243
|
-
...(ssh.initialCommand !== undefined ? { initialCommand: ssh.initialCommand } : {}),
|
|
244
|
-
...(ssh.terminalStartupCommand !== undefined ? { terminalStartupCommand: ssh.terminalStartupCommand } : {}),
|
|
245
|
-
...(ssh.autoConnect !== undefined ? { autoConnect: ssh.autoConnect } : {}),
|
|
246
|
-
...(ssh.skipDaemonBootstrap !== undefined ? { skipDaemonBootstrap: ssh.skipDaemonBootstrap } : {}),
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function sshDestination(ssh: Extract<CmuxOpenSshInput, object>): string {
|
|
251
|
-
if (!ssh.host) throw new Error(`cmux.open ssh.host is required when ssh.destination is omitted`);
|
|
252
|
-
if (!ssh.username) throw new Error(`cmux.open ssh.username is required when ssh.destination is omitted`);
|
|
179
|
+
function sshDestination(ssh: Extract<CmuxSshInput, object>): string {
|
|
180
|
+
if (!ssh.host) throw new Error(`cmux.call ssh.host is required when ssh.destination is omitted`);
|
|
181
|
+
if (!ssh.username) throw new Error(`cmux.call ssh.username is required when ssh.destination is omitted`);
|
|
253
182
|
return `${ssh.username}@${ssh.host}`;
|
|
254
183
|
}
|
|
255
184
|
|
|
256
|
-
function parseTerminalInputs(value: unknown): CmuxOpenTerminalInput[] {
|
|
257
|
-
if (!Array.isArray(value)) throw new Error(`cmux.open terminals must be an array`);
|
|
258
|
-
return value.map((item, index) => parseTerminalInput(item, index));
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function parseTerminalInput(value: unknown, index: number): CmuxOpenTerminalInput {
|
|
262
|
-
if (!isRecord(value)) throw new Error(`cmux.open terminals[${index}] must be an object`);
|
|
263
|
-
return {
|
|
264
|
-
command: requiredString(value, "command"),
|
|
265
|
-
...optionalStringField(value, "cwd"),
|
|
266
|
-
...optionalTerminalDirectionField(value, "direction"),
|
|
267
|
-
...optionalBooleanField(value, "focus"),
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function commandForTerminal(terminal: CmuxOpenTerminalInput, input: CmuxOpenInput): string {
|
|
272
|
-
const cwd = terminal.cwd ?? input.cwd;
|
|
273
|
-
const prefix = cwd ? `${formatShellCommand(["cd", cwd])} && ` : "";
|
|
274
|
-
return `${prefix}${terminal.command}\n`;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function paneResultForCmuxPane(pane: CmuxPane): CmuxOpenPaneResult {
|
|
278
|
-
return {
|
|
279
|
-
...(pane.pane ? { paneId: pane.pane } : {}),
|
|
280
|
-
...(pane.paneRef ? { paneRef: pane.paneRef } : {}),
|
|
281
|
-
...(pane.surface ? { surfaceId: pane.surface } : {}),
|
|
282
|
-
...(pane.surfaceRef ? { surfaceRef: pane.surfaceRef } : {}),
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function remoteReadyOptionsForInput(input: CmuxOpenInput): CmuxWaitForRemoteOptions | false {
|
|
287
|
-
if (input.waitForRemoteReady === false) return false;
|
|
288
|
-
if (input.waitForRemoteReady === true || input.waitForRemoteReady === undefined) {
|
|
289
|
-
return input.url ? {} : false;
|
|
290
|
-
}
|
|
291
|
-
return input.waitForRemoteReady;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
185
|
function requiredString(record: Record<string, unknown>, key: string): string {
|
|
295
186
|
const value = record[key];
|
|
296
187
|
if (typeof value !== "string" || value.trim() === "") {
|
|
297
|
-
throw new Error(`cmux.
|
|
188
|
+
throw new Error(`cmux.call ${key} must be a non-empty string`);
|
|
298
189
|
}
|
|
299
190
|
return value.trim();
|
|
300
191
|
}
|
|
@@ -302,34 +193,32 @@ function requiredString(record: Record<string, unknown>, key: string): string {
|
|
|
302
193
|
function optionalStringField(
|
|
303
194
|
record: Record<string, unknown>,
|
|
304
195
|
key: string,
|
|
305
|
-
expected?: string,
|
|
306
196
|
): Record<string, string> {
|
|
307
197
|
const value = record[key];
|
|
308
198
|
if (value === undefined) return {};
|
|
309
199
|
if (typeof value !== "string" || value.trim() === "") {
|
|
310
|
-
throw new Error(`cmux.
|
|
200
|
+
throw new Error(`cmux.call ${key} must be a non-empty string`);
|
|
311
201
|
}
|
|
312
|
-
|
|
313
|
-
if (expected !== undefined && normalized !== expected) {
|
|
314
|
-
throw new Error(`cmux.open ${key} must be "${expected}"`);
|
|
315
|
-
}
|
|
316
|
-
return { [key]: normalized };
|
|
202
|
+
return { [key]: value.trim() };
|
|
317
203
|
}
|
|
318
204
|
|
|
319
|
-
function optionalStringArrayField(
|
|
205
|
+
function optionalStringArrayField(
|
|
206
|
+
record: Record<string, unknown>,
|
|
207
|
+
key: string,
|
|
208
|
+
): Record<string, string[]> {
|
|
320
209
|
const value = record[key];
|
|
321
210
|
if (value === undefined) return {};
|
|
322
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
323
|
-
throw new Error(`cmux.
|
|
211
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
212
|
+
throw new Error(`cmux.call ${key} must be an array of strings`);
|
|
324
213
|
}
|
|
325
|
-
return { [key]: value };
|
|
214
|
+
return { [key]: value.map((item) => item.trim()) };
|
|
326
215
|
}
|
|
327
216
|
|
|
328
217
|
function optionalNumberField(record: Record<string, unknown>, key: string): Record<string, number> {
|
|
329
218
|
const value = record[key];
|
|
330
219
|
if (value === undefined) return {};
|
|
331
220
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
332
|
-
throw new Error(`cmux.
|
|
221
|
+
throw new Error(`cmux.call ${key} must be a finite number`);
|
|
333
222
|
}
|
|
334
223
|
return { [key]: value };
|
|
335
224
|
}
|
|
@@ -337,31 +226,7 @@ function optionalNumberField(record: Record<string, unknown>, key: string): Reco
|
|
|
337
226
|
function optionalBooleanField(record: Record<string, unknown>, key: string): Record<string, boolean> {
|
|
338
227
|
const value = record[key];
|
|
339
228
|
if (value === undefined) return {};
|
|
340
|
-
if (typeof value !== "boolean") throw new Error(`cmux.
|
|
341
|
-
return { [key]: value };
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function optionalTerminalDirectionField(
|
|
345
|
-
record: Record<string, unknown>,
|
|
346
|
-
key: string,
|
|
347
|
-
): Record<string, CmuxOpenTerminalDirection> {
|
|
348
|
-
const value = record[key];
|
|
349
|
-
if (value === undefined) return {};
|
|
350
|
-
if (value !== "left" && value !== "right" && value !== "up" && value !== "down") {
|
|
351
|
-
throw new Error(`cmux.open ${key} must be "left", "right", "up", or "down"`);
|
|
352
|
-
}
|
|
353
|
-
return { [key]: value };
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function optionalSurfaceLayoutField(
|
|
357
|
-
record: Record<string, unknown>,
|
|
358
|
-
key: string,
|
|
359
|
-
): Record<string, "splits" | "tabs"> {
|
|
360
|
-
const value = record[key];
|
|
361
|
-
if (value === undefined) return {};
|
|
362
|
-
if (value !== "splits" && value !== "tabs") {
|
|
363
|
-
throw new Error(`cmux.open ${key} must be "splits" or "tabs"`);
|
|
364
|
-
}
|
|
229
|
+
if (typeof value !== "boolean") throw new Error(`cmux.call ${key} must be a boolean`);
|
|
365
230
|
return { [key]: value };
|
|
366
231
|
}
|
|
367
232
|
|
package/src/index.test.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type CmuxRpcResult,
|
|
19
19
|
type CmuxRuntime,
|
|
20
20
|
} from "./index.ts";
|
|
21
|
-
import {
|
|
21
|
+
import { callCmux, type CmuxCallClient } from "./host.ts";
|
|
22
22
|
|
|
23
23
|
describe("cmux sdk", () => {
|
|
24
24
|
test("parses workspace refs from cmux text output", () => {
|
|
@@ -439,52 +439,53 @@ describe("cmux sdk", () => {
|
|
|
439
439
|
expect(() => cmux.run(["bad"])).toThrow(CmuxCommandError);
|
|
440
440
|
});
|
|
441
441
|
|
|
442
|
-
test("handles cmux.
|
|
442
|
+
test("handles raw cmux.call host capability requests", async () => {
|
|
443
443
|
const calls: Array<{ method: string; params: unknown }> = [];
|
|
444
444
|
const logs: string[] = [];
|
|
445
445
|
const client = fakeOpenClient(calls);
|
|
446
446
|
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
447
|
+
const workspace = await callCmux({
|
|
448
|
+
method: "ssh",
|
|
449
|
+
params: {
|
|
450
450
|
kind: "ssh",
|
|
451
451
|
destination: "vm_123,token_123@vm-ssh.freestyle.sh",
|
|
452
|
+
name: "website",
|
|
452
453
|
sshOptions: ["ServerAliveInterval=15"],
|
|
453
454
|
},
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
paneRef: "pane:1",
|
|
471
|
-
surfaceId: "surface-1",
|
|
472
|
-
surfaceRef: "surface:1",
|
|
473
|
-
},
|
|
474
|
-
{
|
|
475
|
-
paneId: "pane-2",
|
|
476
|
-
paneRef: "pane:2",
|
|
477
|
-
surfaceId: "surface-2",
|
|
478
|
-
surfaceRef: "surface:2",
|
|
479
|
-
},
|
|
480
|
-
],
|
|
481
|
-
browserPane: {
|
|
482
|
-
paneId: "pane-3",
|
|
483
|
-
paneRef: "pane:3",
|
|
484
|
-
surfaceId: "surface-3",
|
|
485
|
-
surfaceRef: "surface:3",
|
|
455
|
+
}, { client, logger: (message: string) => logs.push(message) });
|
|
456
|
+
const browser = await callCmux({
|
|
457
|
+
method: "newSurface",
|
|
458
|
+
params: {
|
|
459
|
+
workspace: "workspace-1",
|
|
460
|
+
type: "browser",
|
|
461
|
+
url: "http://localhost:4321",
|
|
462
|
+
focus: true,
|
|
463
|
+
},
|
|
464
|
+
}, { client, logger: (message: string) => logs.push(message) });
|
|
465
|
+
const terminal = await callCmux({
|
|
466
|
+
method: "newSurface",
|
|
467
|
+
params: {
|
|
468
|
+
workspace: "workspace-1",
|
|
469
|
+
type: "terminal",
|
|
470
|
+
focus: false,
|
|
486
471
|
},
|
|
487
|
-
});
|
|
472
|
+
}, { client, logger: (message: string) => logs.push(message) });
|
|
473
|
+
await callCmux({
|
|
474
|
+
method: "send",
|
|
475
|
+
params: {
|
|
476
|
+
workspace: "workspace-1",
|
|
477
|
+
surface: "surface-2",
|
|
478
|
+
text: "pnpm dev\n",
|
|
479
|
+
},
|
|
480
|
+
}, { client, logger: (message: string) => logs.push(message) });
|
|
481
|
+
await callCmux({
|
|
482
|
+
method: "selectWorkspace",
|
|
483
|
+
params: { workspace: "workspace-1" },
|
|
484
|
+
}, { client, logger: (message: string) => logs.push(message) });
|
|
485
|
+
|
|
486
|
+
expect(workspace).toEqual({ handle: "workspace-1", id: "workspace-1", ref: "workspace:1" });
|
|
487
|
+
expect(browser).toMatchObject({ surface: "surface-1" });
|
|
488
|
+
expect(terminal).toMatchObject({ surface: "surface-2" });
|
|
488
489
|
expect(calls).toEqual([
|
|
489
490
|
{
|
|
490
491
|
method: "ssh",
|
|
@@ -498,18 +499,11 @@ describe("cmux sdk", () => {
|
|
|
498
499
|
method: "newSurface",
|
|
499
500
|
params: {
|
|
500
501
|
workspace: "workspace-1",
|
|
501
|
-
type: "
|
|
502
|
+
type: "browser",
|
|
503
|
+
url: "http://localhost:4321",
|
|
502
504
|
focus: true,
|
|
503
505
|
},
|
|
504
506
|
},
|
|
505
|
-
{
|
|
506
|
-
method: "send",
|
|
507
|
-
params: {
|
|
508
|
-
workspace: "workspace-1",
|
|
509
|
-
surface: "surface-1",
|
|
510
|
-
text: "cd /workspace/site && pnpm dev\n",
|
|
511
|
-
},
|
|
512
|
-
},
|
|
513
507
|
{
|
|
514
508
|
method: "newSurface",
|
|
515
509
|
params: {
|
|
@@ -523,39 +517,7 @@ describe("cmux sdk", () => {
|
|
|
523
517
|
params: {
|
|
524
518
|
workspace: "workspace-1",
|
|
525
519
|
surface: "surface-2",
|
|
526
|
-
text: "
|
|
527
|
-
},
|
|
528
|
-
},
|
|
529
|
-
{
|
|
530
|
-
method: "waitForRemoteReady",
|
|
531
|
-
params: {
|
|
532
|
-
workspace: "workspace-1",
|
|
533
|
-
options: {},
|
|
534
|
-
},
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
method: "portsKick",
|
|
538
|
-
params: {
|
|
539
|
-
workspace: "workspace-1",
|
|
540
|
-
surface: "surface-1",
|
|
541
|
-
reason: "command",
|
|
542
|
-
},
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
method: "portsKick",
|
|
546
|
-
params: {
|
|
547
|
-
workspace: "workspace-1",
|
|
548
|
-
surface: "surface-2",
|
|
549
|
-
reason: "command",
|
|
550
|
-
},
|
|
551
|
-
},
|
|
552
|
-
{
|
|
553
|
-
method: "newSurface",
|
|
554
|
-
params: {
|
|
555
|
-
workspace: "workspace-1",
|
|
556
|
-
type: "browser",
|
|
557
|
-
url: "http://localhost:4321",
|
|
558
|
-
focus: true,
|
|
520
|
+
text: "pnpm dev\n",
|
|
559
521
|
},
|
|
560
522
|
},
|
|
561
523
|
{
|
|
@@ -563,17 +525,12 @@ describe("cmux sdk", () => {
|
|
|
563
525
|
params: "workspace-1",
|
|
564
526
|
},
|
|
565
527
|
]);
|
|
566
|
-
expect(calls[0]?.params).not.toHaveProperty("terminalStartupCommand");
|
|
567
528
|
expect(logs).toEqual([
|
|
568
|
-
"cmux:
|
|
569
|
-
"cmux:
|
|
570
|
-
"cmux:
|
|
571
|
-
"cmux:
|
|
572
|
-
"cmux:
|
|
573
|
-
"cmux: refreshing remote ports",
|
|
574
|
-
"cmux: opening http://localhost:4321",
|
|
575
|
-
"cmux: focusing workspace",
|
|
576
|
-
"cmux: ready website",
|
|
529
|
+
"cmux: ssh",
|
|
530
|
+
"cmux: newSurface",
|
|
531
|
+
"cmux: newSurface",
|
|
532
|
+
"cmux: send",
|
|
533
|
+
"cmux: selectWorkspace",
|
|
577
534
|
]);
|
|
578
535
|
});
|
|
579
536
|
|
|
@@ -581,11 +538,12 @@ describe("cmux sdk", () => {
|
|
|
581
538
|
const calls: Array<{ method: string; params: unknown }> = [];
|
|
582
539
|
const client = fakeOpenClient(calls);
|
|
583
540
|
|
|
584
|
-
await
|
|
585
|
-
|
|
586
|
-
|
|
541
|
+
await callCmux({
|
|
542
|
+
method: "ssh",
|
|
543
|
+
params: {
|
|
587
544
|
kind: "ssh",
|
|
588
545
|
destination: "vm_123,token_123@vm-ssh.freestyle.sh",
|
|
546
|
+
name: "website",
|
|
589
547
|
terminalStartupCommand: "ssh -tt vm_123:token_123@vm-ssh.freestyle.sh",
|
|
590
548
|
},
|
|
591
549
|
}, { client });
|
|
@@ -600,7 +558,7 @@ describe("cmux sdk", () => {
|
|
|
600
558
|
});
|
|
601
559
|
});
|
|
602
560
|
|
|
603
|
-
test("exposes a provider facade that requests cmux
|
|
561
|
+
test("exposes a provider facade that requests raw cmux calls from the local host", async () => {
|
|
604
562
|
const definition = cmux.provider();
|
|
605
563
|
expect(definition.providerId).toBe("cmux");
|
|
606
564
|
expect(definition.plugin).toBe(cmuxProviderPlugin);
|
|
@@ -624,77 +582,72 @@ describe("cmux sdk", () => {
|
|
|
624
582
|
open: async () => {},
|
|
625
583
|
requestCapability: async <Result,>(capability: string, params: unknown, options: unknown) => {
|
|
626
584
|
requests.push({ capability, params, options });
|
|
585
|
+
const method = (params as { method?: string }).method;
|
|
586
|
+
if (method === "newSurface") {
|
|
587
|
+
return { surface: "surface-1", pane: "pane-1" } as Result;
|
|
588
|
+
}
|
|
589
|
+
if (method === "send") return "OK" as Result;
|
|
627
590
|
return { sessionId: "workspace-1", workspaceId: "workspace-1" } as Result;
|
|
628
591
|
},
|
|
629
592
|
},
|
|
630
593
|
}) as CmuxRuntime;
|
|
631
594
|
|
|
632
|
-
const
|
|
595
|
+
const workspace = await runtime.ssh({
|
|
596
|
+
destination: "vm_123,token_123@vm-ssh.freestyle.sh",
|
|
597
|
+
name: "workspace",
|
|
598
|
+
});
|
|
599
|
+
const terminal = await runtime.newSurface({
|
|
600
|
+
workspace: workspace.workspaceId,
|
|
601
|
+
type: "terminal",
|
|
602
|
+
focus: true,
|
|
603
|
+
});
|
|
604
|
+
await runtime.send({
|
|
605
|
+
workspace: workspace.workspaceId,
|
|
606
|
+
surface: terminal.surfaceId,
|
|
607
|
+
text: "git status\n",
|
|
608
|
+
});
|
|
633
609
|
|
|
634
610
|
expect(requests).toEqual([
|
|
635
611
|
{
|
|
636
|
-
capability: "cmux.
|
|
637
|
-
params: {
|
|
612
|
+
capability: "cmux.call",
|
|
613
|
+
params: {
|
|
614
|
+
method: "ssh",
|
|
615
|
+
params: {
|
|
616
|
+
destination: "vm_123,token_123@vm-ssh.freestyle.sh",
|
|
617
|
+
name: "workspace",
|
|
618
|
+
},
|
|
619
|
+
},
|
|
638
620
|
options: { nodePath: "operation.open" },
|
|
639
621
|
},
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
test("uses host capability close reporting when the runtime provides it", async () => {
|
|
652
|
-
const controller = await cmuxProviderPlugin.createProvider({
|
|
653
|
-
provider: { providerId: "cmux", config: {} },
|
|
654
|
-
storage: memoryProviderStorage("cmux"),
|
|
655
|
-
hostStorage: memoryProviderStorage("cmux"),
|
|
656
|
-
local: { open: async () => {} },
|
|
657
|
-
});
|
|
658
|
-
let resolveClosed!: () => void;
|
|
659
|
-
const runtime = await controller.runtime({
|
|
660
|
-
workflow: "test",
|
|
661
|
-
nodePath: "operation.open",
|
|
662
|
-
emit: () => {},
|
|
663
|
-
interaction: {
|
|
664
|
-
present: async <Result,>() => undefined as Result,
|
|
622
|
+
{
|
|
623
|
+
capability: "cmux.call",
|
|
624
|
+
params: {
|
|
625
|
+
method: "newSurface",
|
|
626
|
+
params: {
|
|
627
|
+
workspace: "workspace-1",
|
|
628
|
+
type: "terminal",
|
|
629
|
+
focus: true,
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
options: { nodePath: "operation.open" },
|
|
665
633
|
},
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
closed: new Promise<void>((resolve) => {
|
|
676
|
-
resolveClosed = resolve;
|
|
677
|
-
}),
|
|
678
|
-
};
|
|
634
|
+
{
|
|
635
|
+
capability: "cmux.call",
|
|
636
|
+
params: {
|
|
637
|
+
method: "send",
|
|
638
|
+
params: {
|
|
639
|
+
workspace: "workspace-1",
|
|
640
|
+
surface: "surface-1",
|
|
641
|
+
text: "git status\n",
|
|
642
|
+
},
|
|
679
643
|
},
|
|
644
|
+
options: { nodePath: "operation.open" },
|
|
680
645
|
},
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const session = await runtime.open({ name: "workspace" });
|
|
684
|
-
let closed = false;
|
|
685
|
-
void session.closed.then(() => {
|
|
686
|
-
closed = true;
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
await Bun.sleep(5);
|
|
690
|
-
expect(closed).toBe(false);
|
|
691
|
-
resolveClosed();
|
|
692
|
-
await session.closed;
|
|
693
|
-
expect(closed).toBe(true);
|
|
646
|
+
]);
|
|
694
647
|
});
|
|
695
648
|
});
|
|
696
649
|
|
|
697
|
-
function fakeOpenClient(calls: Array<{ method: string; params: unknown }>):
|
|
650
|
+
function fakeOpenClient(calls: Array<{ method: string; params: unknown }>): CmuxCallClient {
|
|
698
651
|
let terminalPaneIndex = 0;
|
|
699
652
|
return {
|
|
700
653
|
async newWorkspace(params) {
|
package/src/index.ts
CHANGED
|
@@ -492,26 +492,32 @@ export function formatShellCommand(args: readonly string[]): string {
|
|
|
492
492
|
|
|
493
493
|
export { RIGKIT_PROVIDER_CMUX_VERSION } from "./version.ts";
|
|
494
494
|
export {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
type
|
|
499
|
-
type
|
|
500
|
-
type
|
|
501
|
-
type
|
|
502
|
-
type
|
|
503
|
-
type
|
|
504
|
-
type
|
|
505
|
-
type
|
|
506
|
-
type
|
|
495
|
+
CMUX_CALL_CAPABILITY,
|
|
496
|
+
CMUX_CALL_CAPABILITY_ID,
|
|
497
|
+
CMUX_CALL_SCHEMA_HASH,
|
|
498
|
+
type CmuxBrowserOpenInput,
|
|
499
|
+
type CmuxCallInput,
|
|
500
|
+
type CmuxCallMethod,
|
|
501
|
+
type CmuxNewPaneInput,
|
|
502
|
+
type CmuxNewSurfaceInput,
|
|
503
|
+
type CmuxPaneDirection,
|
|
504
|
+
type CmuxPaneResult,
|
|
505
|
+
type CmuxPortsKickInput,
|
|
506
|
+
type CmuxRemoteReadyInput,
|
|
507
|
+
type CmuxSendInput,
|
|
508
|
+
type CmuxSshInput,
|
|
509
|
+
type CmuxSurfaceType,
|
|
510
|
+
type CmuxWorkspaceInput,
|
|
511
|
+
type CmuxWorkspaceResult,
|
|
507
512
|
} from "./capabilities.ts";
|
|
508
513
|
export {
|
|
509
514
|
CMUX_PROVIDER_ID,
|
|
510
515
|
cmux,
|
|
511
516
|
cmuxProviderPlugin,
|
|
512
517
|
provider as defineCmuxProvider,
|
|
513
|
-
|
|
514
|
-
|
|
518
|
+
parseCmuxPaneResult,
|
|
519
|
+
parseCmuxWorkspaceResult,
|
|
520
|
+
requestCmuxCall,
|
|
515
521
|
type CmuxProviderDefinition,
|
|
516
522
|
type CmuxRuntime,
|
|
517
523
|
} from "./provider.ts";
|
package/src/provider.ts
CHANGED
|
@@ -5,17 +5,33 @@ import {
|
|
|
5
5
|
} from "@rigkit/sdk";
|
|
6
6
|
import type { BaseProviderPlugin, WorkflowProviderController } from "@rigkit/engine";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
type
|
|
11
|
-
type
|
|
12
|
-
type
|
|
8
|
+
CMUX_CALL_CAPABILITY_ID,
|
|
9
|
+
type CmuxBrowserOpenInput,
|
|
10
|
+
type CmuxCallInput,
|
|
11
|
+
type CmuxNewPaneInput,
|
|
12
|
+
type CmuxNewSurfaceInput,
|
|
13
|
+
type CmuxPaneResult,
|
|
14
|
+
type CmuxPortsKickInput,
|
|
15
|
+
type CmuxRemoteReadyInput,
|
|
16
|
+
type CmuxSendInput,
|
|
17
|
+
type CmuxSshInput,
|
|
18
|
+
type CmuxWorkspaceInput,
|
|
19
|
+
type CmuxWorkspaceResult,
|
|
13
20
|
} from "./capabilities.ts";
|
|
14
21
|
|
|
15
22
|
export const CMUX_PROVIDER_ID = "cmux";
|
|
16
23
|
|
|
17
24
|
export type CmuxRuntime = {
|
|
18
|
-
|
|
25
|
+
call(input: CmuxCallInput): Promise<unknown>;
|
|
26
|
+
newWorkspace(input?: CmuxWorkspaceInput): Promise<CmuxWorkspaceResult>;
|
|
27
|
+
ssh(input: CmuxSshInput): Promise<CmuxWorkspaceResult>;
|
|
28
|
+
newPane(input?: CmuxNewPaneInput): Promise<CmuxPaneResult>;
|
|
29
|
+
newSurface(input?: CmuxNewSurfaceInput): Promise<CmuxPaneResult>;
|
|
30
|
+
browserOpen(input?: CmuxBrowserOpenInput): Promise<CmuxPaneResult>;
|
|
31
|
+
send(input: CmuxSendInput): Promise<string>;
|
|
32
|
+
portsKick(input: CmuxPortsKickInput): Promise<string>;
|
|
33
|
+
selectWorkspace(workspace: string): Promise<void>;
|
|
34
|
+
waitForRemoteReady(input: CmuxRemoteReadyInput): Promise<unknown>;
|
|
19
35
|
};
|
|
20
36
|
|
|
21
37
|
export type CmuxProviderDefinition = WorkflowProviderDefinition<
|
|
@@ -45,75 +61,69 @@ export const cmuxProviderPlugin: BaseProviderPlugin = {
|
|
|
45
61
|
};
|
|
46
62
|
|
|
47
63
|
function createCmuxRuntime(local: LocalWorkspaceRuntime, nodePath: string): CmuxRuntime {
|
|
64
|
+
const request = (method: CmuxCallInput["method"], params?: Record<string, unknown>) =>
|
|
65
|
+
requestCmuxCall(local, { method, ...(params ? { params } : {}) }, { nodePath });
|
|
66
|
+
|
|
48
67
|
return {
|
|
49
|
-
|
|
68
|
+
call: async (input) => await requestCmuxCall(local, input, { nodePath }),
|
|
69
|
+
newWorkspace: async (input = {}) =>
|
|
70
|
+
parseCmuxWorkspaceResult(await request("newWorkspace", input)),
|
|
71
|
+
ssh: async (input) =>
|
|
72
|
+
parseCmuxWorkspaceResult(await request("ssh", typeof input === "string" ? { destination: input } : input)),
|
|
73
|
+
newPane: async (input = {}) => parseCmuxPaneResult(await request("newPane", input)),
|
|
74
|
+
newSurface: async (input = {}) => parseCmuxPaneResult(await request("newSurface", input)),
|
|
75
|
+
browserOpen: async (input = {}) => parseCmuxPaneResult(await request("browserOpen", input)),
|
|
76
|
+
send: async (input) => String(await request("send", input)),
|
|
77
|
+
portsKick: async (input) => String(await request("portsKick", input)),
|
|
78
|
+
selectWorkspace: async (workspace) => {
|
|
79
|
+
await request("selectWorkspace", { workspace });
|
|
80
|
+
},
|
|
81
|
+
waitForRemoteReady: async (input) => await request("waitForRemoteReady", input),
|
|
50
82
|
};
|
|
51
83
|
}
|
|
52
84
|
|
|
53
|
-
export async function
|
|
85
|
+
export async function requestCmuxCall(
|
|
54
86
|
local: LocalWorkspaceRuntime,
|
|
55
|
-
input:
|
|
87
|
+
input: CmuxCallInput,
|
|
56
88
|
options: { nodePath?: string } = {},
|
|
57
|
-
): Promise<
|
|
58
|
-
if (local.requestCapabilitySession) {
|
|
59
|
-
const session = await local.requestCapabilitySession<CmuxOpenResult>(CMUX_OPEN_CAPABILITY_ID, input, options);
|
|
60
|
-
return {
|
|
61
|
-
...parseCmuxOpenResult(session.result),
|
|
62
|
-
closed: session.closed,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
89
|
+
): Promise<unknown> {
|
|
65
90
|
if (!local.requestCapability) {
|
|
66
|
-
throw new Error(`Host capability ${
|
|
91
|
+
throw new Error(`Host capability ${CMUX_CALL_CAPABILITY_ID} is unavailable in this runtime`);
|
|
67
92
|
}
|
|
68
|
-
|
|
69
|
-
await local.requestCapability(CMUX_OPEN_CAPABILITY_ID, input, options),
|
|
70
|
-
);
|
|
71
|
-
return {
|
|
72
|
-
...result,
|
|
73
|
-
closed: new Promise<void>(() => {}),
|
|
74
|
-
};
|
|
93
|
+
return await local.requestCapability(CMUX_CALL_CAPABILITY_ID, input, options);
|
|
75
94
|
}
|
|
76
95
|
|
|
77
|
-
export function
|
|
78
|
-
if (!isRecord(value)) throw new Error(`cmux.
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
81
|
-
const workspaceId = stringField(value, "workspaceId") ?? sessionId;
|
|
96
|
+
export function parseCmuxWorkspaceResult(value: unknown): CmuxWorkspaceResult {
|
|
97
|
+
if (!isRecord(value)) throw new Error(`cmux.call returned a non-object workspace result`);
|
|
98
|
+
const workspaceId = stringField(value, "workspaceId") ?? stringField(value, "id") ?? stringField(value, "handle");
|
|
99
|
+
if (!workspaceId) throw new Error(`cmux.call workspace result is missing workspace id`);
|
|
82
100
|
return {
|
|
83
|
-
sessionId,
|
|
101
|
+
sessionId: stringField(value, "sessionId") ?? workspaceId,
|
|
84
102
|
workspaceId,
|
|
85
|
-
...
|
|
86
|
-
|
|
87
|
-
|
|
103
|
+
...(stringField(value, "workspaceRef") ?? stringField(value, "ref")
|
|
104
|
+
? { workspaceRef: stringField(value, "workspaceRef") ?? stringField(value, "ref") }
|
|
105
|
+
: {}),
|
|
88
106
|
};
|
|
89
107
|
}
|
|
90
108
|
|
|
91
|
-
function
|
|
92
|
-
if (!isRecord(value)) throw new Error(`cmux.
|
|
109
|
+
export function parseCmuxPaneResult(value: unknown): CmuxPaneResult {
|
|
110
|
+
if (!isRecord(value)) throw new Error(`cmux.call returned a non-object pane result`);
|
|
93
111
|
return {
|
|
94
|
-
...
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
...
|
|
112
|
+
...(stringField(value, "paneId") ?? stringField(value, "pane")
|
|
113
|
+
? { paneId: stringField(value, "paneId") ?? stringField(value, "pane") }
|
|
114
|
+
: {}),
|
|
115
|
+
...(stringField(value, "paneRef") ?? stringField(value, "paneRef")
|
|
116
|
+
? { paneRef: stringField(value, "paneRef") ?? stringField(value, "paneRef") }
|
|
117
|
+
: {}),
|
|
118
|
+
...(stringField(value, "surfaceId") ?? stringField(value, "surface")
|
|
119
|
+
? { surfaceId: stringField(value, "surfaceId") ?? stringField(value, "surface") }
|
|
120
|
+
: {}),
|
|
121
|
+
...(stringField(value, "surfaceRef") ?? stringField(value, "surfaceRef")
|
|
122
|
+
? { surfaceRef: stringField(value, "surfaceRef") ?? stringField(value, "surfaceRef") }
|
|
123
|
+
: {}),
|
|
98
124
|
};
|
|
99
125
|
}
|
|
100
126
|
|
|
101
|
-
function arrayField<Item>(
|
|
102
|
-
record: Record<string, unknown>,
|
|
103
|
-
key: string,
|
|
104
|
-
parseItem: (value: unknown) => Item,
|
|
105
|
-
): Item[] {
|
|
106
|
-
const value = record[key];
|
|
107
|
-
if (value === undefined) return [];
|
|
108
|
-
if (!Array.isArray(value)) throw new Error(`cmux.open result ${key} must be an array`);
|
|
109
|
-
return value.map(parseItem);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function optionalStringField(record: Record<string, unknown>, key: string): Record<string, string> {
|
|
113
|
-
const value = stringField(record, key);
|
|
114
|
-
return value ? { [key]: value } : {};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
127
|
function stringField(record: Record<string, unknown>, key: string): string | undefined {
|
|
118
128
|
const value = record[key];
|
|
119
129
|
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const RIGKIT_PROVIDER_CMUX_VERSION = "0.2.
|
|
1
|
+
export const RIGKIT_PROVIDER_CMUX_VERSION = "0.2.14";
|