@rigkit/provider-cmux 0.2.3 → 0.2.5
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 +6 -1
- package/package.json +3 -3
- package/src/capabilities.ts +22 -6
- package/src/host.ts +135 -39
- package/src/index.test.ts +138 -23
- package/src/index.ts +23 -0
- package/src/provider.ts +30 -9
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -52,11 +52,16 @@ export default workflow("site", {
|
|
|
52
52
|
sshOptions: ["ServerAliveInterval=15"],
|
|
53
53
|
},
|
|
54
54
|
cwd: "/workspace/site",
|
|
55
|
-
|
|
55
|
+
surfaceLayout: "tabs",
|
|
56
|
+
terminals: [{ command: "pnpm dev" }],
|
|
56
57
|
url: "http://localhost:3000",
|
|
57
58
|
});
|
|
58
59
|
},
|
|
59
60
|
});
|
|
60
61
|
```
|
|
61
62
|
|
|
63
|
+
Set `surfaceLayout: "tabs"` to open terminals and the optional browser URL as
|
|
64
|
+
tabs in the same cmux pane. Omit it, or set `"splits"`, to keep the default
|
|
65
|
+
split-pane behavior.
|
|
66
|
+
|
|
62
67
|
Local hosts can import `@rigkit/provider-cmux/host` to register the trusted `cmux.open` 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.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@rigkit/sdk": "0.2.
|
|
21
|
-
"@rigkit/engine": "0.2.
|
|
20
|
+
"@rigkit/sdk": "0.2.5",
|
|
21
|
+
"@rigkit/engine": "0.2.5"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/bun": "latest",
|
package/src/capabilities.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const CMUX_OPEN_CAPABILITY_ID = "cmux.open";
|
|
2
2
|
|
|
3
3
|
export const CMUX_OPEN_SCHEMA_HASH =
|
|
4
|
-
"sha256:
|
|
4
|
+
"sha256:3a2975cfe53089c6a607da751b55575dc8806bd90132242d4b7e9065a26ae3af";
|
|
5
5
|
|
|
6
6
|
export const CMUX_OPEN_CAPABILITY = {
|
|
7
7
|
id: CMUX_OPEN_CAPABILITY_ID,
|
|
@@ -29,24 +29,40 @@ export type CmuxRemoteReadyOptions = {
|
|
|
29
29
|
requireProxy?: boolean;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
export type CmuxOpenTerminalDirection = "left" | "right" | "up" | "down";
|
|
33
|
+
export type CmuxOpenSurfaceLayout = "splits" | "tabs";
|
|
34
|
+
|
|
35
|
+
export type CmuxOpenTerminalInput = {
|
|
36
|
+
command: string;
|
|
37
|
+
cwd?: string;
|
|
38
|
+
direction?: CmuxOpenTerminalDirection;
|
|
39
|
+
focus?: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
32
42
|
export type CmuxOpenInput = {
|
|
33
43
|
name: string;
|
|
34
44
|
ssh?: CmuxOpenSshInput;
|
|
35
45
|
cwd?: string;
|
|
36
|
-
|
|
46
|
+
surfaceLayout?: CmuxOpenSurfaceLayout;
|
|
47
|
+
terminals?: readonly CmuxOpenTerminalInput[];
|
|
37
48
|
url?: string;
|
|
38
49
|
focus?: boolean;
|
|
39
50
|
waitForRemoteReady?: boolean | CmuxRemoteReadyOptions;
|
|
40
51
|
};
|
|
41
52
|
|
|
53
|
+
export type CmuxOpenPaneResult = {
|
|
54
|
+
paneId?: string;
|
|
55
|
+
paneRef?: string;
|
|
56
|
+
surfaceId?: string;
|
|
57
|
+
surfaceRef?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
42
60
|
export type CmuxOpenResult = {
|
|
43
61
|
sessionId: string;
|
|
44
62
|
workspaceId: string;
|
|
45
63
|
workspaceRef?: string;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
browserPaneId?: string;
|
|
49
|
-
browserSurfaceId?: string;
|
|
64
|
+
terminalPanes: CmuxOpenPaneResult[];
|
|
65
|
+
browserPane?: CmuxOpenPaneResult;
|
|
50
66
|
};
|
|
51
67
|
|
|
52
68
|
export type CmuxOpenSession = CmuxOpenResult & {
|
package/src/host.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createCmuxClient,
|
|
3
3
|
formatShellCommand,
|
|
4
|
-
type CmuxBrowserOpenOptions,
|
|
5
4
|
type CmuxClient,
|
|
6
5
|
type CmuxClientOptions,
|
|
7
|
-
type CmuxNewPaneOptions,
|
|
8
6
|
type CmuxNewWorkspaceOptions,
|
|
9
7
|
type CmuxPane,
|
|
10
8
|
type CmuxPortsKickOptions,
|
|
@@ -13,12 +11,19 @@ import {
|
|
|
13
11
|
type CmuxWaitForRemoteOptions,
|
|
14
12
|
type CmuxWorkspace,
|
|
15
13
|
} from "./index.ts";
|
|
16
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
defineHostCapability,
|
|
16
|
+
type HostCapabilityContext,
|
|
17
|
+
type HostCapabilityHandler,
|
|
18
|
+
} from "@rigkit/sdk/host";
|
|
17
19
|
import {
|
|
18
20
|
CMUX_OPEN_CAPABILITY,
|
|
19
21
|
type CmuxOpenInput,
|
|
22
|
+
type CmuxOpenPaneResult,
|
|
20
23
|
type CmuxOpenResult,
|
|
21
24
|
type CmuxOpenSshInput,
|
|
25
|
+
type CmuxOpenTerminalDirection,
|
|
26
|
+
type CmuxOpenTerminalInput,
|
|
22
27
|
type CmuxRemoteReadyOptions,
|
|
23
28
|
} from "./capabilities.ts";
|
|
24
29
|
|
|
@@ -29,6 +34,7 @@ export type CmuxOpenClient = Pick<
|
|
|
29
34
|
| "newWorkspace"
|
|
30
35
|
| "ssh"
|
|
31
36
|
| "newPane"
|
|
37
|
+
| "newSurface"
|
|
32
38
|
| "send"
|
|
33
39
|
| "portsKick"
|
|
34
40
|
| "browserOpen"
|
|
@@ -39,6 +45,7 @@ export type CmuxOpenClient = Pick<
|
|
|
39
45
|
export type CmuxOpenHostOptions = {
|
|
40
46
|
client?: CmuxOpenClient;
|
|
41
47
|
clientOptions?: CmuxClientOptions;
|
|
48
|
+
logger?: (message: string) => void;
|
|
42
49
|
};
|
|
43
50
|
|
|
44
51
|
export function createCmuxOpenHostCapability(
|
|
@@ -46,7 +53,11 @@ export function createCmuxOpenHostCapability(
|
|
|
46
53
|
): CmuxHostCapabilityHandler {
|
|
47
54
|
return defineHostCapability(CMUX_OPEN_CAPABILITY.id, {
|
|
48
55
|
schemaHash: CMUX_OPEN_CAPABILITY.schemaHash,
|
|
49
|
-
handle: async (params) =>
|
|
56
|
+
handle: async (params, context) =>
|
|
57
|
+
await openCmux(params, {
|
|
58
|
+
...options,
|
|
59
|
+
logger: options.logger ?? hostCapabilityLogger(context) ?? options.clientOptions?.logger,
|
|
60
|
+
}),
|
|
50
61
|
});
|
|
51
62
|
}
|
|
52
63
|
|
|
@@ -57,84 +68,120 @@ export async function openCmux(
|
|
|
57
68
|
options: CmuxOpenHostOptions = {},
|
|
58
69
|
): Promise<CmuxOpenResult> {
|
|
59
70
|
const input = parseCmuxOpenInput(params);
|
|
60
|
-
const
|
|
61
|
-
const
|
|
71
|
+
const logger = cmuxOpenLogger(options);
|
|
72
|
+
const cmux = options.client ?? createCmuxClient({
|
|
73
|
+
...options.clientOptions,
|
|
74
|
+
...(options.logger ? { logger: options.logger } : {}),
|
|
75
|
+
printCommands: options.clientOptions?.printCommands ?? false,
|
|
76
|
+
});
|
|
62
77
|
let workspace: CmuxWorkspace;
|
|
63
|
-
|
|
78
|
+
const terminalPanes: CmuxPane[] = [];
|
|
64
79
|
|
|
80
|
+
logger?.(`cmux: opening ${input.name}`);
|
|
65
81
|
if (input.ssh) {
|
|
82
|
+
logger?.("cmux: connecting remote workspace");
|
|
66
83
|
workspace = await cmux.ssh({
|
|
67
84
|
...cmuxSshOptionsForInput(input.ssh),
|
|
68
85
|
name: input.name,
|
|
69
86
|
noFocus: input.focus === false,
|
|
70
87
|
});
|
|
71
88
|
} else {
|
|
89
|
+
logger?.("cmux: creating workspace");
|
|
72
90
|
const workspaceOptions: CmuxNewWorkspaceOptions = {
|
|
73
91
|
name: input.name,
|
|
74
92
|
cwd: input.cwd,
|
|
75
|
-
command: input.command,
|
|
76
93
|
focus: input.focus,
|
|
77
94
|
};
|
|
78
95
|
workspace = await cmux.newWorkspace(workspaceOptions);
|
|
79
96
|
}
|
|
80
97
|
|
|
81
98
|
const workspaceId = workspace.id ?? workspace.handle;
|
|
99
|
+
const useTabLayout = input.surfaceLayout === "tabs";
|
|
82
100
|
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
for (const terminal of input.terminals ?? []) {
|
|
102
|
+
const cwd = terminal.cwd ?? input.cwd;
|
|
103
|
+
logger?.(cwd ? `cmux: starting terminal in ${cwd}` : "cmux: starting terminal");
|
|
104
|
+
const terminalPane = useTabLayout
|
|
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);
|
|
91
117
|
const sendOptions: CmuxSendOptions = {
|
|
92
118
|
workspace: workspaceId,
|
|
93
119
|
surface: terminalPane.surface,
|
|
94
|
-
text:
|
|
120
|
+
text: commandForTerminal(terminal, input),
|
|
95
121
|
};
|
|
96
122
|
await cmux.send(sendOptions);
|
|
97
123
|
}
|
|
98
124
|
|
|
99
125
|
const waitOptions = remoteReadyOptionsForInput(input);
|
|
100
126
|
if (input.ssh && waitOptions) {
|
|
127
|
+
logger?.("cmux: waiting for remote ports");
|
|
101
128
|
await cmux.waitForRemoteReady(workspaceId, waitOptions);
|
|
102
129
|
}
|
|
103
130
|
|
|
104
|
-
if (input.ssh &&
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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);
|
|
141
|
+
}
|
|
111
142
|
}
|
|
112
143
|
|
|
113
144
|
let browserPane: CmuxPane | undefined;
|
|
114
145
|
if (input.url) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
});
|
|
121
159
|
}
|
|
122
160
|
|
|
123
161
|
if (input.focus !== false) {
|
|
162
|
+
logger?.("cmux: focusing workspace");
|
|
124
163
|
await cmux.selectWorkspace(workspaceId);
|
|
125
164
|
}
|
|
126
165
|
|
|
166
|
+
logger?.(`cmux: ready ${input.name}`);
|
|
127
167
|
return {
|
|
128
168
|
sessionId: workspaceId,
|
|
129
169
|
workspaceId,
|
|
130
170
|
...(workspace.ref ? { workspaceRef: workspace.ref } : {}),
|
|
131
|
-
|
|
132
|
-
...(
|
|
133
|
-
...(browserPane?.pane ? { browserPaneId: browserPane.pane } : {}),
|
|
134
|
-
...(browserPane?.surface ? { browserSurfaceId: browserPane.surface } : {}),
|
|
171
|
+
terminalPanes: terminalPanes.map(paneResultForCmuxPane),
|
|
172
|
+
...(browserPane ? { browserPane: paneResultForCmuxPane(browserPane) } : {}),
|
|
135
173
|
};
|
|
136
174
|
}
|
|
137
175
|
|
|
176
|
+
function cmuxOpenLogger(options: CmuxOpenHostOptions): ((message: string) => void) | undefined {
|
|
177
|
+
return options.logger ?? options.clientOptions?.logger;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hostCapabilityLogger(context: HostCapabilityContext | undefined): ((message: string) => void) | undefined {
|
|
181
|
+
if (!context) return undefined;
|
|
182
|
+
return (message) => context.log(message, { label: "cmux" });
|
|
183
|
+
}
|
|
184
|
+
|
|
138
185
|
export function parseCmuxOpenInput(value: unknown): CmuxOpenInput {
|
|
139
186
|
if (!isRecord(value)) throw new Error(`cmux.open requires an object input`);
|
|
140
187
|
const name = requiredString(value, "name");
|
|
@@ -142,7 +189,8 @@ export function parseCmuxOpenInput(value: unknown): CmuxOpenInput {
|
|
|
142
189
|
name,
|
|
143
190
|
...(value.ssh !== undefined ? { ssh: parseSshInput(value.ssh) } : {}),
|
|
144
191
|
...optionalStringField(value, "cwd"),
|
|
145
|
-
...
|
|
192
|
+
...optionalSurfaceLayoutField(value, "surfaceLayout"),
|
|
193
|
+
...(value.terminals !== undefined ? { terminals: parseTerminalInputs(value.terminals) } : {}),
|
|
146
194
|
...optionalStringField(value, "url"),
|
|
147
195
|
...optionalBooleanField(value, "focus"),
|
|
148
196
|
...(value.waitForRemoteReady !== undefined
|
|
@@ -205,10 +253,34 @@ function sshDestination(ssh: Extract<CmuxOpenSshInput, object>): string {
|
|
|
205
253
|
return `${ssh.username}@${ssh.host}`;
|
|
206
254
|
}
|
|
207
255
|
|
|
208
|
-
function
|
|
209
|
-
if (!
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
};
|
|
212
284
|
}
|
|
213
285
|
|
|
214
286
|
function remoteReadyOptionsForInput(input: CmuxOpenInput): CmuxWaitForRemoteOptions | false {
|
|
@@ -269,6 +341,30 @@ function optionalBooleanField(record: Record<string, unknown>, key: string): Rec
|
|
|
269
341
|
return { [key]: value };
|
|
270
342
|
}
|
|
271
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
|
+
}
|
|
365
|
+
return { [key]: value };
|
|
366
|
+
}
|
|
367
|
+
|
|
272
368
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
273
369
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
274
370
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -135,7 +135,7 @@ describe("cmux sdk", () => {
|
|
|
135
135
|
]);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
test("creates panes, opens browsers, and sends terminal text", async () => {
|
|
138
|
+
test("creates panes, surfaces, opens browsers, and sends terminal text", async () => {
|
|
139
139
|
const calls: Array<{ method: string; params: CmuxRpcParams }> = [];
|
|
140
140
|
const cmux = createCmuxClient({
|
|
141
141
|
printCommands: false,
|
|
@@ -161,6 +161,16 @@ describe("cmux sdk", () => {
|
|
|
161
161
|
pane_ref: "pane:11",
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
|
+
if (method === "surface.create") {
|
|
165
|
+
return {
|
|
166
|
+
workspace_id: "00000000-0000-0000-0000-000000000009",
|
|
167
|
+
workspace_ref: "workspace:9",
|
|
168
|
+
surface_id: "00000000-0000-0000-0000-000000000012",
|
|
169
|
+
surface_ref: "surface:12",
|
|
170
|
+
pane_id: "00000000-0000-0000-0000-000000000008",
|
|
171
|
+
pane_ref: "pane:8",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
164
174
|
return {};
|
|
165
175
|
},
|
|
166
176
|
});
|
|
@@ -181,6 +191,17 @@ describe("cmux sdk", () => {
|
|
|
181
191
|
surface: pane.surface,
|
|
182
192
|
reason: "refresh",
|
|
183
193
|
});
|
|
194
|
+
const surface = await cmux.newSurface({
|
|
195
|
+
workspace: "00000000-0000-0000-0000-000000000009",
|
|
196
|
+
pane: pane.pane,
|
|
197
|
+
type: "terminal",
|
|
198
|
+
focus: false,
|
|
199
|
+
});
|
|
200
|
+
await cmux.send({
|
|
201
|
+
workspace: "00000000-0000-0000-0000-000000000009",
|
|
202
|
+
surface: surface.surface,
|
|
203
|
+
text: "codex\\n",
|
|
204
|
+
});
|
|
184
205
|
await cmux.browserOpen({
|
|
185
206
|
workspace: "00000000-0000-0000-0000-000000000009",
|
|
186
207
|
url: "http://localhost:3000",
|
|
@@ -188,6 +209,7 @@ describe("cmux sdk", () => {
|
|
|
188
209
|
});
|
|
189
210
|
|
|
190
211
|
expect(pane.surface).toBe("00000000-0000-0000-0000-000000000007");
|
|
212
|
+
expect(surface.surface).toBe("00000000-0000-0000-0000-000000000012");
|
|
191
213
|
expect(calls).toEqual([
|
|
192
214
|
{
|
|
193
215
|
method: "pane.create",
|
|
@@ -214,6 +236,23 @@ describe("cmux sdk", () => {
|
|
|
214
236
|
reason: "refresh",
|
|
215
237
|
},
|
|
216
238
|
},
|
|
239
|
+
{
|
|
240
|
+
method: "surface.create",
|
|
241
|
+
params: {
|
|
242
|
+
type: "terminal",
|
|
243
|
+
pane_id: "00000000-0000-0000-0000-000000000008",
|
|
244
|
+
workspace_id: "00000000-0000-0000-0000-000000000009",
|
|
245
|
+
focus: false,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
method: "surface.send_text",
|
|
250
|
+
params: {
|
|
251
|
+
workspace_id: "00000000-0000-0000-0000-000000000009",
|
|
252
|
+
surface_id: "00000000-0000-0000-0000-000000000012",
|
|
253
|
+
text: "codex\\n",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
217
256
|
{
|
|
218
257
|
method: "browser.open_split",
|
|
219
258
|
params: {
|
|
@@ -400,8 +439,9 @@ describe("cmux sdk", () => {
|
|
|
400
439
|
expect(() => cmux.run(["bad"])).toThrow(CmuxCommandError);
|
|
401
440
|
});
|
|
402
441
|
|
|
403
|
-
test("handles cmux.open host capability for an ssh workspace", async () => {
|
|
442
|
+
test("handles cmux.open host capability for an ssh workspace with tabs", async () => {
|
|
404
443
|
const calls: Array<{ method: string; params: unknown }> = [];
|
|
444
|
+
const logs: string[] = [];
|
|
405
445
|
const client = fakeOpenClient(calls);
|
|
406
446
|
|
|
407
447
|
const result = await openCmux({
|
|
@@ -412,18 +452,38 @@ describe("cmux sdk", () => {
|
|
|
412
452
|
sshOptions: ["ServerAliveInterval=15"],
|
|
413
453
|
},
|
|
414
454
|
cwd: "/workspace/site",
|
|
415
|
-
|
|
455
|
+
surfaceLayout: "tabs",
|
|
456
|
+
terminals: [
|
|
457
|
+
{ command: "pnpm dev" },
|
|
458
|
+
{ command: "codex", focus: false },
|
|
459
|
+
],
|
|
416
460
|
url: "http://localhost:4321",
|
|
417
|
-
}, { client });
|
|
461
|
+
}, { client, logger: (message) => logs.push(message) });
|
|
418
462
|
|
|
419
463
|
expect(result).toEqual({
|
|
420
464
|
sessionId: "workspace-1",
|
|
421
465
|
workspaceId: "workspace-1",
|
|
422
466
|
workspaceRef: "workspace:1",
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
467
|
+
terminalPanes: [
|
|
468
|
+
{
|
|
469
|
+
paneId: "pane-1",
|
|
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",
|
|
486
|
+
},
|
|
427
487
|
});
|
|
428
488
|
expect(calls).toEqual([
|
|
429
489
|
{
|
|
@@ -435,11 +495,10 @@ describe("cmux sdk", () => {
|
|
|
435
495
|
}),
|
|
436
496
|
},
|
|
437
497
|
{
|
|
438
|
-
method: "
|
|
498
|
+
method: "newSurface",
|
|
439
499
|
params: {
|
|
440
500
|
workspace: "workspace-1",
|
|
441
501
|
type: "terminal",
|
|
442
|
-
direction: "down",
|
|
443
502
|
focus: true,
|
|
444
503
|
},
|
|
445
504
|
},
|
|
@@ -451,6 +510,22 @@ describe("cmux sdk", () => {
|
|
|
451
510
|
text: "cd /workspace/site && pnpm dev\n",
|
|
452
511
|
},
|
|
453
512
|
},
|
|
513
|
+
{
|
|
514
|
+
method: "newSurface",
|
|
515
|
+
params: {
|
|
516
|
+
workspace: "workspace-1",
|
|
517
|
+
type: "terminal",
|
|
518
|
+
focus: false,
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
method: "send",
|
|
523
|
+
params: {
|
|
524
|
+
workspace: "workspace-1",
|
|
525
|
+
surface: "surface-2",
|
|
526
|
+
text: "cd /workspace/site && codex\n",
|
|
527
|
+
},
|
|
528
|
+
},
|
|
454
529
|
{
|
|
455
530
|
method: "waitForRemoteReady",
|
|
456
531
|
params: {
|
|
@@ -467,9 +542,18 @@ describe("cmux sdk", () => {
|
|
|
467
542
|
},
|
|
468
543
|
},
|
|
469
544
|
{
|
|
470
|
-
method: "
|
|
545
|
+
method: "portsKick",
|
|
546
|
+
params: {
|
|
547
|
+
workspace: "workspace-1",
|
|
548
|
+
surface: "surface-2",
|
|
549
|
+
reason: "command",
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
method: "newSurface",
|
|
471
554
|
params: {
|
|
472
555
|
workspace: "workspace-1",
|
|
556
|
+
type: "browser",
|
|
473
557
|
url: "http://localhost:4321",
|
|
474
558
|
focus: true,
|
|
475
559
|
},
|
|
@@ -480,6 +564,17 @@ describe("cmux sdk", () => {
|
|
|
480
564
|
},
|
|
481
565
|
]);
|
|
482
566
|
expect(calls[0]?.params).not.toHaveProperty("terminalStartupCommand");
|
|
567
|
+
expect(logs).toEqual([
|
|
568
|
+
"cmux: opening website",
|
|
569
|
+
"cmux: connecting remote workspace",
|
|
570
|
+
"cmux: starting terminal in /workspace/site",
|
|
571
|
+
"cmux: starting terminal in /workspace/site",
|
|
572
|
+
"cmux: waiting for remote ports",
|
|
573
|
+
"cmux: refreshing remote ports",
|
|
574
|
+
"cmux: opening http://localhost:4321",
|
|
575
|
+
"cmux: focusing workspace",
|
|
576
|
+
"cmux: ready website",
|
|
577
|
+
]);
|
|
483
578
|
});
|
|
484
579
|
|
|
485
580
|
test("forwards an explicit cmux ssh terminal startup command", async () => {
|
|
@@ -513,8 +608,10 @@ describe("cmux sdk", () => {
|
|
|
513
608
|
const controller = await cmuxProviderPlugin.createProvider({
|
|
514
609
|
provider: { providerId: "cmux", config: {} },
|
|
515
610
|
storage: memoryProviderStorage("cmux"),
|
|
611
|
+
hostStorage: memoryProviderStorage("cmux"),
|
|
612
|
+
local: { open: async () => {} },
|
|
516
613
|
});
|
|
517
|
-
const requests: Array<{ capability: string; params: unknown }> = [];
|
|
614
|
+
const requests: Array<{ capability: string; params: unknown; options: unknown }> = [];
|
|
518
615
|
const runtime = await controller.runtime({
|
|
519
616
|
workflow: "test",
|
|
520
617
|
nodePath: "operation.open",
|
|
@@ -525,8 +622,8 @@ describe("cmux sdk", () => {
|
|
|
525
622
|
metadata: () => {},
|
|
526
623
|
local: {
|
|
527
624
|
open: async () => {},
|
|
528
|
-
requestCapability: async <Result,>(capability: string, params: unknown) => {
|
|
529
|
-
requests.push({ capability, params });
|
|
625
|
+
requestCapability: async <Result,>(capability: string, params: unknown, options: unknown) => {
|
|
626
|
+
requests.push({ capability, params, options });
|
|
530
627
|
return { sessionId: "workspace-1", workspaceId: "workspace-1" } as Result;
|
|
531
628
|
},
|
|
532
629
|
},
|
|
@@ -538,6 +635,7 @@ describe("cmux sdk", () => {
|
|
|
538
635
|
{
|
|
539
636
|
capability: "cmux.open",
|
|
540
637
|
params: { name: "workspace" },
|
|
638
|
+
options: { nodePath: "operation.open" },
|
|
541
639
|
},
|
|
542
640
|
]);
|
|
543
641
|
expect(session.sessionId).toBe("workspace-1");
|
|
@@ -554,6 +652,8 @@ describe("cmux sdk", () => {
|
|
|
554
652
|
const controller = await cmuxProviderPlugin.createProvider({
|
|
555
653
|
provider: { providerId: "cmux", config: {} },
|
|
556
654
|
storage: memoryProviderStorage("cmux"),
|
|
655
|
+
hostStorage: memoryProviderStorage("cmux"),
|
|
656
|
+
local: { open: async () => {} },
|
|
557
657
|
});
|
|
558
658
|
let resolveClosed!: () => void;
|
|
559
659
|
const runtime = await controller.runtime({
|
|
@@ -566,9 +666,10 @@ describe("cmux sdk", () => {
|
|
|
566
666
|
metadata: () => {},
|
|
567
667
|
local: {
|
|
568
668
|
open: async () => {},
|
|
569
|
-
requestCapabilitySession: async <Result,>(capability: string, params: unknown) => {
|
|
669
|
+
requestCapabilitySession: async <Result,>(capability: string, params: unknown, options: unknown) => {
|
|
570
670
|
expect(capability).toBe("cmux.open");
|
|
571
671
|
expect(params).toEqual({ name: "workspace" });
|
|
672
|
+
expect(options).toEqual({ nodePath: "operation.open" });
|
|
572
673
|
return {
|
|
573
674
|
result: { sessionId: "workspace-1", workspaceId: "workspace-1" } as Result,
|
|
574
675
|
closed: new Promise<void>((resolve) => {
|
|
@@ -594,6 +695,7 @@ describe("cmux sdk", () => {
|
|
|
594
695
|
});
|
|
595
696
|
|
|
596
697
|
function fakeOpenClient(calls: Array<{ method: string; params: unknown }>): CmuxOpenClient {
|
|
698
|
+
let terminalPaneIndex = 0;
|
|
597
699
|
return {
|
|
598
700
|
async newWorkspace(params) {
|
|
599
701
|
calls.push({ method: "newWorkspace", params });
|
|
@@ -605,13 +707,26 @@ function fakeOpenClient(calls: Array<{ method: string; params: unknown }>): Cmux
|
|
|
605
707
|
},
|
|
606
708
|
async newPane(params) {
|
|
607
709
|
calls.push({ method: "newPane", params });
|
|
710
|
+
terminalPaneIndex += 1;
|
|
711
|
+
return {
|
|
712
|
+
workspace: "workspace-1",
|
|
713
|
+
workspaceRef: "workspace:1",
|
|
714
|
+
pane: `pane-${terminalPaneIndex}`,
|
|
715
|
+
paneRef: `pane:${terminalPaneIndex}`,
|
|
716
|
+
surface: `surface-${terminalPaneIndex}`,
|
|
717
|
+
surfaceRef: `surface:${terminalPaneIndex}`,
|
|
718
|
+
};
|
|
719
|
+
},
|
|
720
|
+
async newSurface(params) {
|
|
721
|
+
calls.push({ method: "newSurface", params });
|
|
722
|
+
terminalPaneIndex += 1;
|
|
608
723
|
return {
|
|
609
724
|
workspace: "workspace-1",
|
|
610
725
|
workspaceRef: "workspace:1",
|
|
611
|
-
pane:
|
|
612
|
-
paneRef:
|
|
613
|
-
surface:
|
|
614
|
-
surfaceRef:
|
|
726
|
+
pane: `pane-${terminalPaneIndex}`,
|
|
727
|
+
paneRef: `pane:${terminalPaneIndex}`,
|
|
728
|
+
surface: `surface-${terminalPaneIndex}`,
|
|
729
|
+
surfaceRef: `surface:${terminalPaneIndex}`,
|
|
615
730
|
};
|
|
616
731
|
},
|
|
617
732
|
async send(params) {
|
|
@@ -627,10 +742,10 @@ function fakeOpenClient(calls: Array<{ method: string; params: unknown }>): Cmux
|
|
|
627
742
|
return {
|
|
628
743
|
workspace: "workspace-1",
|
|
629
744
|
workspaceRef: "workspace:1",
|
|
630
|
-
pane: "pane-
|
|
631
|
-
paneRef: "pane:
|
|
632
|
-
surface: "surface-
|
|
633
|
-
surfaceRef: "surface:
|
|
745
|
+
pane: "browser-pane-1",
|
|
746
|
+
paneRef: "pane:browser-1",
|
|
747
|
+
surface: "browser-surface-1",
|
|
748
|
+
surfaceRef: "surface:browser-1",
|
|
634
749
|
};
|
|
635
750
|
},
|
|
636
751
|
async selectWorkspace(workspace) {
|
package/src/index.ts
CHANGED
|
@@ -103,6 +103,14 @@ export type CmuxNewPaneOptions = {
|
|
|
103
103
|
focus?: boolean;
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
export type CmuxNewSurfaceOptions = {
|
|
107
|
+
workspace?: string;
|
|
108
|
+
pane?: string;
|
|
109
|
+
type?: "terminal" | "browser";
|
|
110
|
+
url?: string;
|
|
111
|
+
focus?: boolean;
|
|
112
|
+
};
|
|
113
|
+
|
|
106
114
|
export type CmuxPane = {
|
|
107
115
|
workspace?: string;
|
|
108
116
|
workspaceRef?: string;
|
|
@@ -277,6 +285,17 @@ export class CmuxClient {
|
|
|
277
285
|
return paneFromResult(await this.rpc("pane.create", params));
|
|
278
286
|
}
|
|
279
287
|
|
|
288
|
+
async newSurface(options: CmuxNewSurfaceOptions = {}): Promise<CmuxPane> {
|
|
289
|
+
const params: CmuxRpcParams = {};
|
|
290
|
+
if (options.type) params.type = options.type;
|
|
291
|
+
if (options.pane) params.pane_id = options.pane;
|
|
292
|
+
if (options.workspace) params.workspace_id = options.workspace;
|
|
293
|
+
if (options.url) params.url = options.url;
|
|
294
|
+
if (options.focus !== undefined) params.focus = options.focus;
|
|
295
|
+
|
|
296
|
+
return paneFromResult(await this.rpc("surface.create", params));
|
|
297
|
+
}
|
|
298
|
+
|
|
280
299
|
async listWorkspaces(): Promise<CmuxWorkspaceStatus[]> {
|
|
281
300
|
const result = await this.rpc("workspace.list");
|
|
282
301
|
const workspaces = Array.isArray(result.workspaces)
|
|
@@ -477,9 +496,13 @@ export {
|
|
|
477
496
|
CMUX_OPEN_CAPABILITY_ID,
|
|
478
497
|
CMUX_OPEN_SCHEMA_HASH,
|
|
479
498
|
type CmuxOpenInput,
|
|
499
|
+
type CmuxOpenPaneResult,
|
|
480
500
|
type CmuxOpenResult,
|
|
481
501
|
type CmuxOpenSession,
|
|
502
|
+
type CmuxOpenSurfaceLayout,
|
|
482
503
|
type CmuxOpenSshInput,
|
|
504
|
+
type CmuxOpenTerminalDirection,
|
|
505
|
+
type CmuxOpenTerminalInput,
|
|
483
506
|
type CmuxRemoteReadyOptions,
|
|
484
507
|
} from "./capabilities.ts";
|
|
485
508
|
export {
|
package/src/provider.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { BaseProviderPlugin, WorkflowProviderController } from "@rigkit/eng
|
|
|
7
7
|
import {
|
|
8
8
|
CMUX_OPEN_CAPABILITY_ID,
|
|
9
9
|
type CmuxOpenInput,
|
|
10
|
+
type CmuxOpenPaneResult,
|
|
10
11
|
type CmuxOpenResult,
|
|
11
12
|
type CmuxOpenSession,
|
|
12
13
|
} from "./capabilities.ts";
|
|
@@ -37,24 +38,25 @@ export const cmuxProviderPlugin: BaseProviderPlugin = {
|
|
|
37
38
|
return {
|
|
38
39
|
providerId: CMUX_PROVIDER_ID,
|
|
39
40
|
runtime(context) {
|
|
40
|
-
return createCmuxRuntime(context.local);
|
|
41
|
+
return createCmuxRuntime(context.local, context.nodePath);
|
|
41
42
|
},
|
|
42
43
|
};
|
|
43
44
|
},
|
|
44
45
|
};
|
|
45
46
|
|
|
46
|
-
function createCmuxRuntime(local: LocalWorkspaceRuntime): CmuxRuntime {
|
|
47
|
+
function createCmuxRuntime(local: LocalWorkspaceRuntime, nodePath: string): CmuxRuntime {
|
|
47
48
|
return {
|
|
48
|
-
open: async (input) => await requestCmuxOpen(local, input),
|
|
49
|
+
open: async (input) => await requestCmuxOpen(local, input, { nodePath }),
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export async function requestCmuxOpen(
|
|
53
54
|
local: LocalWorkspaceRuntime,
|
|
54
55
|
input: CmuxOpenInput,
|
|
56
|
+
options: { nodePath?: string } = {},
|
|
55
57
|
): Promise<CmuxOpenSession> {
|
|
56
58
|
if (local.requestCapabilitySession) {
|
|
57
|
-
const session = await local.requestCapabilitySession<CmuxOpenResult>(CMUX_OPEN_CAPABILITY_ID, input);
|
|
59
|
+
const session = await local.requestCapabilitySession<CmuxOpenResult>(CMUX_OPEN_CAPABILITY_ID, input, options);
|
|
58
60
|
return {
|
|
59
61
|
...parseCmuxOpenResult(session.result),
|
|
60
62
|
closed: session.closed,
|
|
@@ -64,7 +66,7 @@ export async function requestCmuxOpen(
|
|
|
64
66
|
throw new Error(`Host capability ${CMUX_OPEN_CAPABILITY_ID} is unavailable in this runtime`);
|
|
65
67
|
}
|
|
66
68
|
const result = parseCmuxOpenResult(
|
|
67
|
-
await local.requestCapability(CMUX_OPEN_CAPABILITY_ID, input),
|
|
69
|
+
await local.requestCapability(CMUX_OPEN_CAPABILITY_ID, input, options),
|
|
68
70
|
);
|
|
69
71
|
return {
|
|
70
72
|
...result,
|
|
@@ -81,13 +83,32 @@ export function parseCmuxOpenResult(value: unknown): CmuxOpenResult {
|
|
|
81
83
|
sessionId,
|
|
82
84
|
workspaceId,
|
|
83
85
|
...optionalStringField(value, "workspaceRef"),
|
|
84
|
-
|
|
85
|
-
...
|
|
86
|
-
...optionalStringField(value, "browserPaneId"),
|
|
87
|
-
...optionalStringField(value, "browserSurfaceId"),
|
|
86
|
+
terminalPanes: arrayField(value, "terminalPanes", parseCmuxOpenPaneResult),
|
|
87
|
+
...(value.browserPane !== undefined ? { browserPane: parseCmuxOpenPaneResult(value.browserPane) } : {}),
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function parseCmuxOpenPaneResult(value: unknown): CmuxOpenPaneResult {
|
|
92
|
+
if (!isRecord(value)) throw new Error(`cmux.open returned a non-object pane result`);
|
|
93
|
+
return {
|
|
94
|
+
...optionalStringField(value, "paneId"),
|
|
95
|
+
...optionalStringField(value, "paneRef"),
|
|
96
|
+
...optionalStringField(value, "surfaceId"),
|
|
97
|
+
...optionalStringField(value, "surfaceRef"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
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
|
+
|
|
91
112
|
function optionalStringField(record: Record<string, unknown>, key: string): Record<string, string> {
|
|
92
113
|
const value = stringField(record, key);
|
|
93
114
|
return value ? { [key]: value } : {};
|
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.5";
|