@rigkit/provider-cmux 0.2.2 → 0.2.4

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 CHANGED
@@ -43,14 +43,13 @@ export default workflow("site", {
43
43
  })
44
44
  .sequence("site")
45
45
  .operation("open", {
46
- requiredHostCapabilities: [cmux.capabilities.open],
47
46
  run: async ({ providers }) => {
48
47
  await providers.cmux.open({
49
48
  name: "site",
50
49
  ssh: {
51
- host: "vm-ssh.freestyle.sh",
52
- username: "vm_123",
53
- auth: { type: "token", token: "token_123" },
50
+ host: "devbox.example.com",
51
+ username: "root",
52
+ sshOptions: ["ServerAliveInterval=15"],
54
53
  },
55
54
  cwd: "/workspace/site",
56
55
  command: "pnpm dev",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigkit/provider-cmux",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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.2",
21
- "@rigkit/engine": "0.2.2"
20
+ "@rigkit/sdk": "0.2.4",
21
+ "@rigkit/engine": "0.2.4"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/bun": "latest",
@@ -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:ed7f74b4fd1101ff87281c0269f9884f85783098d9a727fdfe05491efba2dd28";
4
+ "sha256:671373232fc79a7f75dd01c8c83c0c350af62b349a89bb3cfcc96af2cd76c878";
5
5
 
6
6
  export const CMUX_OPEN_CAPABILITY = {
7
7
  id: CMUX_OPEN_CAPABILITY_ID,
@@ -14,8 +14,6 @@ export type CmuxOpenSshInput = string | {
14
14
  host?: string;
15
15
  port?: number;
16
16
  username?: string;
17
- auth?: { type: "token"; token: string } | { type: "privateKey"; privateKey: string };
18
- command?: string;
19
17
  identity?: string;
20
18
  sshOptions?: readonly string[];
21
19
  remoteCommandArgs?: readonly string[];
package/src/host.ts CHANGED
@@ -13,7 +13,11 @@ import {
13
13
  type CmuxWaitForRemoteOptions,
14
14
  type CmuxWorkspace,
15
15
  } from "./index.ts";
16
- import { defineHostCapability, type HostCapabilityHandler } from "@rigkit/sdk/host";
16
+ import {
17
+ defineHostCapability,
18
+ type HostCapabilityContext,
19
+ type HostCapabilityHandler,
20
+ } from "@rigkit/sdk/host";
17
21
  import {
18
22
  CMUX_OPEN_CAPABILITY,
19
23
  type CmuxOpenInput,
@@ -39,23 +43,19 @@ export type CmuxOpenClient = Pick<
39
43
  export type CmuxOpenHostOptions = {
40
44
  client?: CmuxOpenClient;
41
45
  clientOptions?: CmuxClientOptions;
46
+ logger?: (message: string) => void;
42
47
  };
43
48
 
44
- const freestyleTokenSshOptions = [
45
- "StrictHostKeyChecking=no",
46
- "UserKnownHostsFile=/dev/null",
47
- "LogLevel=ERROR",
48
- "IdentitiesOnly=yes",
49
- "IdentityFile=/dev/null",
50
- "ControlMaster=no",
51
- ] as const;
52
-
53
49
  export function createCmuxOpenHostCapability(
54
50
  options: CmuxOpenHostOptions = {},
55
51
  ): CmuxHostCapabilityHandler {
56
52
  return defineHostCapability(CMUX_OPEN_CAPABILITY.id, {
57
53
  schemaHash: CMUX_OPEN_CAPABILITY.schemaHash,
58
- handle: async (params) => await openCmux(params, options),
54
+ handle: async (params, context) =>
55
+ await openCmux(params, {
56
+ ...options,
57
+ logger: options.logger ?? hostCapabilityLogger(context) ?? options.clientOptions?.logger,
58
+ }),
59
59
  });
60
60
  }
61
61
 
@@ -66,18 +66,26 @@ export async function openCmux(
66
66
  options: CmuxOpenHostOptions = {},
67
67
  ): Promise<CmuxOpenResult> {
68
68
  const input = parseCmuxOpenInput(params);
69
- const cmux = options.client ?? createCmuxClient(options.clientOptions);
69
+ const logger = cmuxOpenLogger(options);
70
+ const cmux = options.client ?? createCmuxClient({
71
+ ...options.clientOptions,
72
+ ...(options.logger ? { logger: options.logger } : {}),
73
+ printCommands: options.clientOptions?.printCommands ?? false,
74
+ });
70
75
  const command = commandForInput(input);
71
76
  let workspace: CmuxWorkspace;
72
77
  let terminalPane: CmuxPane | undefined;
73
78
 
79
+ logger?.(`cmux: opening ${input.name}`);
74
80
  if (input.ssh) {
81
+ logger?.("cmux: connecting remote workspace");
75
82
  workspace = await cmux.ssh({
76
83
  ...cmuxSshOptionsForInput(input.ssh),
77
84
  name: input.name,
78
85
  noFocus: input.focus === false,
79
86
  });
80
87
  } else {
88
+ logger?.("cmux: creating workspace");
81
89
  const workspaceOptions: CmuxNewWorkspaceOptions = {
82
90
  name: input.name,
83
91
  cwd: input.cwd,
@@ -90,6 +98,7 @@ export async function openCmux(
90
98
  const workspaceId = workspace.id ?? workspace.handle;
91
99
 
92
100
  if (input.ssh && command) {
101
+ logger?.(input.cwd ? `cmux: starting command in ${input.cwd}` : "cmux: starting command");
93
102
  const paneOptions: CmuxNewPaneOptions = {
94
103
  workspace: workspaceId,
95
104
  type: "terminal",
@@ -107,10 +116,12 @@ export async function openCmux(
107
116
 
108
117
  const waitOptions = remoteReadyOptionsForInput(input);
109
118
  if (input.ssh && waitOptions) {
119
+ logger?.("cmux: waiting for remote ports");
110
120
  await cmux.waitForRemoteReady(workspaceId, waitOptions);
111
121
  }
112
122
 
113
123
  if (input.ssh && terminalPane?.surface) {
124
+ logger?.("cmux: refreshing remote ports");
114
125
  const kickOptions: CmuxPortsKickOptions = {
115
126
  workspace: workspaceId,
116
127
  surface: terminalPane.surface,
@@ -121,6 +132,7 @@ export async function openCmux(
121
132
 
122
133
  let browserPane: CmuxPane | undefined;
123
134
  if (input.url) {
135
+ logger?.(`cmux: opening ${input.url}`);
124
136
  const browserOptions: CmuxBrowserOpenOptions = {
125
137
  workspace: workspaceId,
126
138
  url: input.url,
@@ -130,9 +142,11 @@ export async function openCmux(
130
142
  }
131
143
 
132
144
  if (input.focus !== false) {
145
+ logger?.("cmux: focusing workspace");
133
146
  await cmux.selectWorkspace(workspaceId);
134
147
  }
135
148
 
149
+ logger?.(`cmux: ready ${input.name}`);
136
150
  return {
137
151
  sessionId: workspaceId,
138
152
  workspaceId,
@@ -144,6 +158,15 @@ export async function openCmux(
144
158
  };
145
159
  }
146
160
 
161
+ function cmuxOpenLogger(options: CmuxOpenHostOptions): ((message: string) => void) | undefined {
162
+ return options.logger ?? options.clientOptions?.logger;
163
+ }
164
+
165
+ function hostCapabilityLogger(context: HostCapabilityContext | undefined): ((message: string) => void) | undefined {
166
+ if (!context) return undefined;
167
+ return (message) => context.log(message, { label: "cmux" });
168
+ }
169
+
147
170
  export function parseCmuxOpenInput(value: unknown): CmuxOpenInput {
148
171
  if (!isRecord(value)) throw new Error(`cmux.open requires an object input`);
149
172
  const name = requiredString(value, "name");
@@ -171,8 +194,6 @@ function parseSshInput(value: unknown): CmuxOpenSshInput {
171
194
  ...optionalStringField(value, "host"),
172
195
  ...optionalNumberField(value, "port"),
173
196
  ...optionalStringField(value, "username"),
174
- ...(value.auth !== undefined ? { auth: parseSshAuth(value.auth) } : {}),
175
- ...optionalStringField(value, "command"),
176
197
  ...optionalStringField(value, "identity"),
177
198
  ...optionalStringArrayField(value, "sshOptions"),
178
199
  ...optionalStringArrayField(value, "remoteCommandArgs"),
@@ -183,17 +204,6 @@ function parseSshInput(value: unknown): CmuxOpenSshInput {
183
204
  };
184
205
  }
185
206
 
186
- function parseSshAuth(value: unknown): Exclude<Extract<CmuxOpenSshInput, object>["auth"], undefined> {
187
- if (!isRecord(value)) throw new Error(`cmux.open ssh.auth must be an object`);
188
- if (value.type === "token") {
189
- return { type: "token", token: requiredString(value, "token") };
190
- }
191
- if (value.type === "privateKey") {
192
- return { type: "privateKey", privateKey: requiredString(value, "privateKey") };
193
- }
194
- throw new Error(`cmux.open ssh.auth.type must be "token" or "privateKey"`);
195
- }
196
-
197
207
  function parseRemoteReadyOptions(value: unknown): boolean | CmuxRemoteReadyOptions {
198
208
  if (typeof value === "boolean") return value;
199
209
  if (!isRecord(value)) throw new Error(`cmux.open waitForRemoteReady must be a boolean or object`);
@@ -208,27 +218,22 @@ function cmuxSshOptionsForInput(ssh: CmuxOpenSshInput): CmuxSshOptions {
208
218
  if (typeof ssh === "string") return { destination: ssh };
209
219
 
210
220
  const destination = ssh.destination ?? sshDestination(ssh);
211
- const sshOptions = [
212
- ...(ssh.auth?.type === "token" ? freestyleTokenSshOptions : []),
213
- ...(ssh.sshOptions ?? []),
214
- ];
215
221
  return {
216
222
  destination,
217
- port: ssh.port,
218
- identity: ssh.identity,
219
- sshOptions,
220
- remoteCommandArgs: ssh.remoteCommandArgs,
221
- initialCommand: ssh.initialCommand,
222
- terminalStartupCommand: ssh.terminalStartupCommand ?? ssh.command,
223
- autoConnect: ssh.autoConnect,
224
- skipDaemonBootstrap: ssh.skipDaemonBootstrap,
223
+ ...(ssh.port !== undefined ? { port: ssh.port } : {}),
224
+ ...(ssh.identity !== undefined ? { identity: ssh.identity } : {}),
225
+ ...(ssh.sshOptions?.length ? { sshOptions: ssh.sshOptions } : {}),
226
+ ...(ssh.remoteCommandArgs !== undefined ? { remoteCommandArgs: ssh.remoteCommandArgs } : {}),
227
+ ...(ssh.initialCommand !== undefined ? { initialCommand: ssh.initialCommand } : {}),
228
+ ...(ssh.terminalStartupCommand !== undefined ? { terminalStartupCommand: ssh.terminalStartupCommand } : {}),
229
+ ...(ssh.autoConnect !== undefined ? { autoConnect: ssh.autoConnect } : {}),
230
+ ...(ssh.skipDaemonBootstrap !== undefined ? { skipDaemonBootstrap: ssh.skipDaemonBootstrap } : {}),
225
231
  };
226
232
  }
227
233
 
228
234
  function sshDestination(ssh: Extract<CmuxOpenSshInput, object>): string {
229
235
  if (!ssh.host) throw new Error(`cmux.open ssh.host is required when ssh.destination is omitted`);
230
236
  if (!ssh.username) throw new Error(`cmux.open ssh.username is required when ssh.destination is omitted`);
231
- if (ssh.auth?.type === "token") return `${ssh.username},${ssh.auth.token}@${ssh.host}`;
232
237
  return `${ssh.username}@${ssh.host}`;
233
238
  }
234
239
 
package/src/index.test.ts CHANGED
@@ -6,7 +6,6 @@ import { describe, expect, test } from "bun:test";
6
6
  import type { ProviderStorage, ProviderStorageRecord } from "@rigkit/engine";
7
7
  import type { JsonValue } from "@rigkit/sdk";
8
8
  import {
9
- CMUX_OPEN_CAPABILITY,
10
9
  CmuxCommandError,
11
10
  cmux,
12
11
  cmuxProviderPlugin,
@@ -403,21 +402,20 @@ describe("cmux sdk", () => {
403
402
 
404
403
  test("handles cmux.open host capability for an ssh workspace", async () => {
405
404
  const calls: Array<{ method: string; params: unknown }> = [];
405
+ const logs: string[] = [];
406
406
  const client = fakeOpenClient(calls);
407
407
 
408
408
  const result = await openCmux({
409
409
  name: "website",
410
410
  ssh: {
411
411
  kind: "ssh",
412
- host: "vm-ssh.freestyle.sh",
413
- username: "vm_123",
414
- auth: { type: "token", token: "token_123" },
415
- command: "ssh vm_123:token_123@vm-ssh.freestyle.sh",
412
+ destination: "vm_123,token_123@vm-ssh.freestyle.sh",
413
+ sshOptions: ["ServerAliveInterval=15"],
416
414
  },
417
415
  cwd: "/workspace/site",
418
416
  command: "pnpm dev",
419
417
  url: "http://localhost:4321",
420
- }, { client });
418
+ }, { client, logger: (message) => logs.push(message) });
421
419
 
422
420
  expect(result).toEqual({
423
421
  sessionId: "workspace-1",
@@ -434,15 +432,7 @@ describe("cmux sdk", () => {
434
432
  params: expect.objectContaining({
435
433
  destination: "vm_123,token_123@vm-ssh.freestyle.sh",
436
434
  name: "website",
437
- terminalStartupCommand: "ssh vm_123:token_123@vm-ssh.freestyle.sh",
438
- sshOptions: [
439
- "StrictHostKeyChecking=no",
440
- "UserKnownHostsFile=/dev/null",
441
- "LogLevel=ERROR",
442
- "IdentitiesOnly=yes",
443
- "IdentityFile=/dev/null",
444
- "ControlMaster=no",
445
- ],
435
+ sshOptions: ["ServerAliveInterval=15"],
446
436
  }),
447
437
  },
448
438
  {
@@ -490,19 +480,54 @@ describe("cmux sdk", () => {
490
480
  params: "workspace-1",
491
481
  },
492
482
  ]);
483
+ expect(calls[0]?.params).not.toHaveProperty("terminalStartupCommand");
484
+ expect(logs).toEqual([
485
+ "cmux: opening website",
486
+ "cmux: connecting remote workspace",
487
+ "cmux: starting command in /workspace/site",
488
+ "cmux: waiting for remote ports",
489
+ "cmux: refreshing remote ports",
490
+ "cmux: opening http://localhost:4321",
491
+ "cmux: focusing workspace",
492
+ "cmux: ready website",
493
+ ]);
494
+ });
495
+
496
+ test("forwards an explicit cmux ssh terminal startup command", async () => {
497
+ const calls: Array<{ method: string; params: unknown }> = [];
498
+ const client = fakeOpenClient(calls);
499
+
500
+ await openCmux({
501
+ name: "website",
502
+ ssh: {
503
+ kind: "ssh",
504
+ destination: "vm_123,token_123@vm-ssh.freestyle.sh",
505
+ terminalStartupCommand: "ssh -tt vm_123:token_123@vm-ssh.freestyle.sh",
506
+ },
507
+ }, { client });
508
+
509
+ expect(calls[0]).toEqual({
510
+ method: "ssh",
511
+ params: expect.objectContaining({
512
+ destination: "vm_123,token_123@vm-ssh.freestyle.sh",
513
+ name: "website",
514
+ terminalStartupCommand: "ssh -tt vm_123:token_123@vm-ssh.freestyle.sh",
515
+ }),
516
+ });
493
517
  });
494
518
 
495
519
  test("exposes a provider facade that requests cmux.open from the local host", async () => {
496
520
  const definition = cmux.provider();
497
521
  expect(definition.providerId).toBe("cmux");
498
522
  expect(definition.plugin).toBe(cmuxProviderPlugin);
499
- expect(cmux.capabilities.open).toBe(CMUX_OPEN_CAPABILITY);
500
523
 
501
524
  const controller = await cmuxProviderPlugin.createProvider({
502
525
  provider: { providerId: "cmux", config: {} },
503
526
  storage: memoryProviderStorage("cmux"),
527
+ hostStorage: memoryProviderStorage("cmux"),
528
+ local: { open: async () => {} },
504
529
  });
505
- const requests: Array<{ capability: string; params: unknown }> = [];
530
+ const requests: Array<{ capability: string; params: unknown; options: unknown }> = [];
506
531
  const runtime = await controller.runtime({
507
532
  workflow: "test",
508
533
  nodePath: "operation.open",
@@ -513,8 +538,8 @@ describe("cmux sdk", () => {
513
538
  metadata: () => {},
514
539
  local: {
515
540
  open: async () => {},
516
- requestCapability: async <Result,>(capability: string, params: unknown) => {
517
- requests.push({ capability, params });
541
+ requestCapability: async <Result,>(capability: string, params: unknown, options: unknown) => {
542
+ requests.push({ capability, params, options });
518
543
  return { sessionId: "workspace-1", workspaceId: "workspace-1" } as Result;
519
544
  },
520
545
  },
@@ -526,6 +551,7 @@ describe("cmux sdk", () => {
526
551
  {
527
552
  capability: "cmux.open",
528
553
  params: { name: "workspace" },
554
+ options: { nodePath: "operation.open" },
529
555
  },
530
556
  ]);
531
557
  expect(session.sessionId).toBe("workspace-1");
@@ -542,6 +568,8 @@ describe("cmux sdk", () => {
542
568
  const controller = await cmuxProviderPlugin.createProvider({
543
569
  provider: { providerId: "cmux", config: {} },
544
570
  storage: memoryProviderStorage("cmux"),
571
+ hostStorage: memoryProviderStorage("cmux"),
572
+ local: { open: async () => {} },
545
573
  });
546
574
  let resolveClosed!: () => void;
547
575
  const runtime = await controller.runtime({
@@ -554,9 +582,10 @@ describe("cmux sdk", () => {
554
582
  metadata: () => {},
555
583
  local: {
556
584
  open: async () => {},
557
- requestCapabilitySession: async <Result,>(capability: string, params: unknown) => {
585
+ requestCapabilitySession: async <Result,>(capability: string, params: unknown, options: unknown) => {
558
586
  expect(capability).toBe("cmux.open");
559
587
  expect(params).toEqual({ name: "workspace" });
588
+ expect(options).toEqual({ nodePath: "operation.open" });
560
589
  return {
561
590
  result: { sessionId: "workspace-1", workspaceId: "workspace-1" } as Result,
562
591
  closed: new Promise<void>((resolve) => {
package/src/index.ts CHANGED
@@ -487,6 +487,8 @@ export {
487
487
  cmux,
488
488
  cmuxProviderPlugin,
489
489
  provider as defineCmuxProvider,
490
+ parseCmuxOpenResult,
491
+ requestCmuxOpen,
490
492
  type CmuxProviderDefinition,
491
493
  type CmuxRuntime,
492
494
  } from "./provider.ts";
package/src/provider.ts CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  } from "@rigkit/sdk";
6
6
  import type { BaseProviderPlugin, WorkflowProviderController } from "@rigkit/engine";
7
7
  import {
8
- CMUX_OPEN_CAPABILITY,
9
8
  CMUX_OPEN_CAPABILITY_ID,
10
9
  type CmuxOpenInput,
11
10
  type CmuxOpenResult,
@@ -30,10 +29,6 @@ export function provider(): CmuxProviderDefinition {
30
29
 
31
30
  export const cmux = {
32
31
  provider,
33
- capability: CMUX_OPEN_CAPABILITY,
34
- capabilities: {
35
- open: CMUX_OPEN_CAPABILITY,
36
- },
37
32
  };
38
33
 
39
34
  export const cmuxProviderPlugin: BaseProviderPlugin = {
@@ -42,37 +37,43 @@ export const cmuxProviderPlugin: BaseProviderPlugin = {
42
37
  return {
43
38
  providerId: CMUX_PROVIDER_ID,
44
39
  runtime(context) {
45
- return createCmuxRuntime(context.local);
40
+ return createCmuxRuntime(context.local, context.nodePath);
46
41
  },
47
42
  };
48
43
  },
49
44
  };
50
45
 
51
- function createCmuxRuntime(local: LocalWorkspaceRuntime): CmuxRuntime {
46
+ function createCmuxRuntime(local: LocalWorkspaceRuntime, nodePath: string): CmuxRuntime {
47
+ return {
48
+ open: async (input) => await requestCmuxOpen(local, input, { nodePath }),
49
+ };
50
+ }
51
+
52
+ export async function requestCmuxOpen(
53
+ local: LocalWorkspaceRuntime,
54
+ input: CmuxOpenInput,
55
+ options: { nodePath?: string } = {},
56
+ ): Promise<CmuxOpenSession> {
57
+ if (local.requestCapabilitySession) {
58
+ const session = await local.requestCapabilitySession<CmuxOpenResult>(CMUX_OPEN_CAPABILITY_ID, input, options);
59
+ return {
60
+ ...parseCmuxOpenResult(session.result),
61
+ closed: session.closed,
62
+ };
63
+ }
64
+ if (!local.requestCapability) {
65
+ throw new Error(`Host capability ${CMUX_OPEN_CAPABILITY_ID} is unavailable in this runtime`);
66
+ }
67
+ const result = parseCmuxOpenResult(
68
+ await local.requestCapability(CMUX_OPEN_CAPABILITY_ID, input, options),
69
+ );
52
70
  return {
53
- async open(input) {
54
- if (local.requestCapabilitySession) {
55
- const session = await local.requestCapabilitySession<CmuxOpenResult>(CMUX_OPEN_CAPABILITY_ID, input);
56
- return {
57
- ...parseCmuxOpenResult(session.result),
58
- closed: session.closed,
59
- };
60
- }
61
- if (!local.requestCapability) {
62
- throw new Error(`Host capability ${CMUX_OPEN_CAPABILITY_ID} is unavailable in this runtime`);
63
- }
64
- const result = parseCmuxOpenResult(
65
- await local.requestCapability(CMUX_OPEN_CAPABILITY_ID, input),
66
- );
67
- return {
68
- ...result,
69
- closed: new Promise<void>(() => {}),
70
- };
71
- },
71
+ ...result,
72
+ closed: new Promise<void>(() => {}),
72
73
  };
73
74
  }
74
75
 
75
- function parseCmuxOpenResult(value: unknown): CmuxOpenResult {
76
+ export function parseCmuxOpenResult(value: unknown): CmuxOpenResult {
76
77
  if (!isRecord(value)) throw new Error(`cmux.open returned a non-object result`);
77
78
  const sessionId = stringField(value, "sessionId");
78
79
  if (!sessionId) throw new Error(`cmux.open result is missing sessionId`);
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const RIGKIT_PROVIDER_CMUX_VERSION = "0.2.2";
1
+ export const RIGKIT_PROVIDER_CMUX_VERSION = "0.2.4";