@rigkit/provider-freestyle 0.1.8
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 +10 -0
- package/package.json +36 -0
- package/src/auth.ts +24 -0
- package/src/index.ts +166 -0
- package/src/provider.test.ts +135 -0
- package/src/provider.ts +500 -0
- package/src/store.test.ts +38 -0
- package/src/store.ts +142 -0
- package/src/terminal-session.test.ts +182 -0
- package/src/terminal-session.ts +821 -0
- package/src/version.ts +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @rigkit/provider-freestyle
|
|
2
|
+
|
|
3
|
+
Freestyle provider integration for `rig`.
|
|
4
|
+
|
|
5
|
+
This package supplies:
|
|
6
|
+
|
|
7
|
+
- `freestyle.provider(...)` for Freestyle VM/snapshot workflow tasks
|
|
8
|
+
- `freestyle.terminal()` for provider-owned browser terminal sessions targeting Freestyle VMs
|
|
9
|
+
- `createFreestyleProvider(...)` for low-level Freestyle VM operations
|
|
10
|
+
- Freestyle-specific JSON state helpers backed by the Rigkit-owned provider storage table
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rigkit/provider-freestyle",
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/freestyle-sh/rigkit.git",
|
|
8
|
+
"directory": "packages/provider-freestyle"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"freestyle": "latest",
|
|
20
|
+
"zod": "^4",
|
|
21
|
+
"@rigkit/sdk": "0.1.8",
|
|
22
|
+
"@rigkit/engine": "0.1.8"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/bun": "latest",
|
|
26
|
+
"typescript": "latest"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc --noEmit",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"test": "bun test"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare const freestyleIdentityIdBrand: unique symbol;
|
|
2
|
+
declare const freestyleTokenIdBrand: unique symbol;
|
|
3
|
+
declare const freestyleTokenBrand: unique symbol;
|
|
4
|
+
|
|
5
|
+
export type FreestyleIdentityId = string & { readonly [freestyleIdentityIdBrand]: true };
|
|
6
|
+
export type FreestyleTokenId = string & { readonly [freestyleTokenIdBrand]: true };
|
|
7
|
+
export type FreestyleToken = string & { readonly [freestyleTokenBrand]: true };
|
|
8
|
+
|
|
9
|
+
export function freestyleIdentityId(value: string): FreestyleIdentityId {
|
|
10
|
+
return nonEmpty(value, "Freestyle identity id") as FreestyleIdentityId;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function freestyleTokenId(value: string): FreestyleTokenId {
|
|
14
|
+
return nonEmpty(value, "Freestyle token id") as FreestyleTokenId;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function freestyleToken(value: string): FreestyleToken {
|
|
18
|
+
return nonEmpty(value, "Freestyle token") as FreestyleToken;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function nonEmpty(value: string, label: string): string {
|
|
22
|
+
if (!value) throw new Error(`${label} must be a non-empty string`);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineProvider,
|
|
3
|
+
type WorkflowProviderDefinition,
|
|
4
|
+
} from "@rigkit/sdk";
|
|
5
|
+
import type { BaseProviderPlugin } from "@rigkit/engine";
|
|
6
|
+
import type { WorkflowProviderController } from "@rigkit/engine";
|
|
7
|
+
import { Freestyle } from "freestyle";
|
|
8
|
+
import * as z from "zod/v4-mini";
|
|
9
|
+
import { freestyleIdentityId, freestyleToken, freestyleTokenId } from "./auth.ts";
|
|
10
|
+
import {
|
|
11
|
+
FREESTYLE_PROVIDER_ID,
|
|
12
|
+
FREESTYLE_TERMINAL_PROVIDER_ID,
|
|
13
|
+
createFreestyleTerminalController,
|
|
14
|
+
createFreestyleWorkflowProvider,
|
|
15
|
+
isFreestyleVmSnapshotRef,
|
|
16
|
+
} from "./provider.ts";
|
|
17
|
+
import type { FreestyleRuntime, FreestyleTerminalRuntime, FreestyleWorkspaceContext } from "./provider.ts";
|
|
18
|
+
import { createFreestyleStore } from "./store.ts";
|
|
19
|
+
|
|
20
|
+
const freestyleProviderConfigSchema = z.object({
|
|
21
|
+
apiKey: z.string().check(z.minLength(1)),
|
|
22
|
+
image: z.string().check(z.minLength(1)),
|
|
23
|
+
cpu: z.optional(z.number()),
|
|
24
|
+
memory: z.optional(z.union([z.string(), z.number()])),
|
|
25
|
+
disk: z.optional(z.union([z.string(), z.number()])),
|
|
26
|
+
idleTimeoutSeconds: z.optional(z.nullable(z.number())),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type FreestyleProviderConfig = z.output<typeof freestyleProviderConfigSchema>;
|
|
30
|
+
|
|
31
|
+
export type FreestyleProviderDefinition = WorkflowProviderDefinition<
|
|
32
|
+
typeof FREESTYLE_PROVIDER_ID,
|
|
33
|
+
FreestyleProviderConfig,
|
|
34
|
+
FreestyleRuntime,
|
|
35
|
+
FreestyleWorkspaceContext
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
export type FreestyleTerminalProviderDefinition = WorkflowProviderDefinition<
|
|
39
|
+
typeof FREESTYLE_TERMINAL_PROVIDER_ID,
|
|
40
|
+
{},
|
|
41
|
+
FreestyleTerminalRuntime
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
export function provider(
|
|
45
|
+
config: FreestyleProviderDefinition["config"],
|
|
46
|
+
): FreestyleProviderDefinition {
|
|
47
|
+
return defineProvider(FREESTYLE_PROVIDER_ID, config, freestyleProviderPlugin);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function terminal(): FreestyleTerminalProviderDefinition {
|
|
51
|
+
return defineProvider(FREESTYLE_TERMINAL_PROVIDER_ID, {}, freestyleTerminalPlugin);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const freestyle = {
|
|
55
|
+
provider,
|
|
56
|
+
terminal,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const defineFreestyleProvider = provider;
|
|
60
|
+
|
|
61
|
+
export const freestyleProviderPlugin: BaseProviderPlugin = {
|
|
62
|
+
providerId: FREESTYLE_PROVIDER_ID,
|
|
63
|
+
createProvider({ provider, storage }) {
|
|
64
|
+
const config = parseFreestyleProviderConfig(provider.config);
|
|
65
|
+
const { apiKey, ...vm } = config;
|
|
66
|
+
let controller: Promise<WorkflowProviderController<FreestyleRuntime, FreestyleWorkspaceContext>> | undefined;
|
|
67
|
+
|
|
68
|
+
const load = async () => {
|
|
69
|
+
controller ??= create();
|
|
70
|
+
return await controller;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const create = async () => {
|
|
74
|
+
const store = createFreestyleStore(storage);
|
|
75
|
+
const savedIdentity = store.getIdentity();
|
|
76
|
+
if (savedIdentity) {
|
|
77
|
+
return createFreestyleWorkflowProvider({
|
|
78
|
+
apiKey,
|
|
79
|
+
identityId: savedIdentity.identityId,
|
|
80
|
+
token: savedIdentity.token,
|
|
81
|
+
vm,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const client = new Freestyle({ apiKey });
|
|
86
|
+
const { identity, identityId } = await client.identities.create();
|
|
87
|
+
const { token, tokenId } = await identity.tokens.create();
|
|
88
|
+
const createdIdentity = store.saveIdentity({
|
|
89
|
+
identityId: freestyleIdentityId(identityId),
|
|
90
|
+
tokenId: freestyleTokenId(tokenId),
|
|
91
|
+
token: freestyleToken(token),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return createFreestyleWorkflowProvider({
|
|
95
|
+
apiKey,
|
|
96
|
+
identityId: createdIdentity.identityId,
|
|
97
|
+
token: createdIdentity.token,
|
|
98
|
+
vm,
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
providerId: FREESTYLE_PROVIDER_ID,
|
|
104
|
+
runtime: async (context) => await (await load()).runtime(context),
|
|
105
|
+
validateArtifact: (ref) => isFreestyleVmSnapshotRef(ref),
|
|
106
|
+
workspace: {
|
|
107
|
+
canUse: (ref) => isFreestyleVmSnapshotRef(ref),
|
|
108
|
+
createWorkspace: async (sourceRef, input) =>
|
|
109
|
+
await (await load()).workspace!.createWorkspace(sourceRef, input),
|
|
110
|
+
deleteWorkspace: async (workspace) =>
|
|
111
|
+
await (await load()).workspace!.deleteWorkspace(workspace),
|
|
112
|
+
snapshotWorkspace: async (workspace) =>
|
|
113
|
+
await (await load()).workspace!.snapshotWorkspace(workspace),
|
|
114
|
+
ssh: async (workspaceOrResourceId, options) =>
|
|
115
|
+
await (await load()).workspace!.ssh(workspaceOrResourceId, options),
|
|
116
|
+
workspaceContext: async (workspace) =>
|
|
117
|
+
await (await load()).workspace!.workspaceContext!(workspace),
|
|
118
|
+
},
|
|
119
|
+
} satisfies WorkflowProviderController<FreestyleRuntime, FreestyleWorkspaceContext>;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const freestyleTerminalPlugin: BaseProviderPlugin = {
|
|
124
|
+
providerId: FREESTYLE_TERMINAL_PROVIDER_ID,
|
|
125
|
+
createProvider() {
|
|
126
|
+
return createFreestyleTerminalController();
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
freestyleIdentityId,
|
|
132
|
+
freestyleToken,
|
|
133
|
+
freestyleTokenId,
|
|
134
|
+
type FreestyleIdentityId,
|
|
135
|
+
type FreestyleToken,
|
|
136
|
+
type FreestyleTokenId,
|
|
137
|
+
} from "./auth.ts";
|
|
138
|
+
export {
|
|
139
|
+
FREESTYLE_PROVIDER_ID,
|
|
140
|
+
FREESTYLE_TERMINAL_PROVIDER_ID,
|
|
141
|
+
createFreestyleProvider,
|
|
142
|
+
createFreestyleTerminalController,
|
|
143
|
+
createFreestyleWorkflowController,
|
|
144
|
+
createFreestyleWorkflowProvider,
|
|
145
|
+
isFreestyleVmSnapshotRef,
|
|
146
|
+
} from "./provider.ts";
|
|
147
|
+
export { createFreestyleStore } from "./store.ts";
|
|
148
|
+
export { createFreestyleTerminalSession } from "./terminal-session.ts";
|
|
149
|
+
export { RIGKIT_PROVIDER_FREESTYLE_VERSION } from "./version.ts";
|
|
150
|
+
export type {
|
|
151
|
+
FreestyleRuntime,
|
|
152
|
+
FreestyleTerminalRuntime,
|
|
153
|
+
FreestyleVmConfig,
|
|
154
|
+
FreestyleVmRuntime,
|
|
155
|
+
FreestyleVmSnapshotRef,
|
|
156
|
+
FreestyleWorkspaceContext,
|
|
157
|
+
} from "./provider.ts";
|
|
158
|
+
export type { FreestyleGitRelationship, FreestyleIdentity } from "./store.ts";
|
|
159
|
+
|
|
160
|
+
function parseFreestyleProviderConfig(value: unknown): FreestyleProviderConfig {
|
|
161
|
+
const result = z.safeParse(freestyleProviderConfigSchema, value);
|
|
162
|
+
if (!result.success) {
|
|
163
|
+
throw new Error(`Invalid Freestyle provider config:\n${z.prettifyError(result.error)}`);
|
|
164
|
+
}
|
|
165
|
+
return result.data;
|
|
166
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ExecOptions, ExecOutputChunk, ExecResult } from "@rigkit/sdk";
|
|
3
|
+
import type {
|
|
4
|
+
BaseDevMachineProvider,
|
|
5
|
+
ProviderRuntimeContext,
|
|
6
|
+
SshConnection,
|
|
7
|
+
VmHandle,
|
|
8
|
+
WorkflowEvent,
|
|
9
|
+
} from "@rigkit/engine";
|
|
10
|
+
import { createFreestyleWorkflowController, wrapCommand } from "./provider.ts";
|
|
11
|
+
import type { FreestyleWorkspaceContext } from "./provider.ts";
|
|
12
|
+
|
|
13
|
+
describe("Freestyle provider command wrapper", () => {
|
|
14
|
+
test("sets a root HOME fallback for exec commands", () => {
|
|
15
|
+
expect(wrapCommand("printf '%s\n' \"$HOME\"")).toContain("export HOME=${HOME:-/root}");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("allows callers to override HOME explicitly", () => {
|
|
19
|
+
const wrapped = wrapCommand("pwd", {
|
|
20
|
+
env: { HOME: "/workspace/home" },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(wrapped).toContain("export HOME=${HOME:-/root}");
|
|
24
|
+
expect(wrapped).toContain("export HOME='\\''/workspace/home'\\''");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("streams VM command output through run events without replaying buffered output", async () => {
|
|
28
|
+
const chunks: ExecOutputChunk[] = [];
|
|
29
|
+
const events: WorkflowEvent[] = [];
|
|
30
|
+
const provider = new StreamingProvider();
|
|
31
|
+
const controller = createFreestyleWorkflowController(provider);
|
|
32
|
+
const runtime = await controller.runtime(providerContext(events));
|
|
33
|
+
const vm = runtime.vms.fromWorkspace({ resourceId: "vm-stream" });
|
|
34
|
+
|
|
35
|
+
const result = await vm.exec("printf ready", {
|
|
36
|
+
name: "stream command",
|
|
37
|
+
onOutput: (chunk) => {
|
|
38
|
+
chunks.push(chunk);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(result.stdout).toBe("ready\n");
|
|
43
|
+
expect(chunks).toEqual([{ stream: "stdout", data: "ready\n" }]);
|
|
44
|
+
expect(events).toEqual([
|
|
45
|
+
{
|
|
46
|
+
type: "command.started",
|
|
47
|
+
nodePath: "workflow.step",
|
|
48
|
+
commandName: "stream command",
|
|
49
|
+
command: "printf ready",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: "command.output",
|
|
53
|
+
nodePath: "workflow.step",
|
|
54
|
+
commandName: "stream command",
|
|
55
|
+
stream: "stdout",
|
|
56
|
+
data: "ready\n",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: "command.completed",
|
|
60
|
+
nodePath: "workflow.step",
|
|
61
|
+
commandName: "stream command",
|
|
62
|
+
exitCode: 0,
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const sshConnection: SshConnection = {
|
|
69
|
+
kind: "ssh",
|
|
70
|
+
host: "localhost",
|
|
71
|
+
username: "root",
|
|
72
|
+
auth: { type: "token", token: "token" },
|
|
73
|
+
command: "ssh vm-stream",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
class StreamingProvider implements BaseDevMachineProvider<FreestyleWorkspaceContext> {
|
|
77
|
+
readonly providerId = "freestyle";
|
|
78
|
+
|
|
79
|
+
async createVm(): Promise<VmHandle> {
|
|
80
|
+
return { vmId: "vm-stream" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async createVmFromSnapshot(): Promise<VmHandle> {
|
|
84
|
+
return { vmId: "vm-stream" };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async exec(_vm: VmHandle, _command: string, options?: ExecOptions): Promise<ExecResult> {
|
|
88
|
+
await options?.onOutput?.({ stream: "stdout", data: "ready\n" });
|
|
89
|
+
return { stdout: "ready\n", stderr: "", exitCode: 0, ok: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async readFile(): Promise<string> {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async writeFile(): Promise<void> {}
|
|
97
|
+
|
|
98
|
+
async snapshot(): Promise<{ snapshotId: string; sourceVmId: string }> {
|
|
99
|
+
return { snapshotId: "snap-stream", sourceVmId: "vm-stream" };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async ssh(): Promise<SshConnection> {
|
|
103
|
+
return sshConnection;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async workspaceContext(): Promise<FreestyleWorkspaceContext> {
|
|
107
|
+
return {
|
|
108
|
+
ssh: sshConnection,
|
|
109
|
+
host: sshConnection.host,
|
|
110
|
+
username: sshConnection.username,
|
|
111
|
+
vscodeAuthority: "root@localhost",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async deleteVm(): Promise<void> {}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function providerContext(events: WorkflowEvent[]): ProviderRuntimeContext {
|
|
119
|
+
return {
|
|
120
|
+
workflow: "workflow",
|
|
121
|
+
nodePath: "workflow.step",
|
|
122
|
+
emit: (event) => {
|
|
123
|
+
events.push(event);
|
|
124
|
+
},
|
|
125
|
+
interaction: {
|
|
126
|
+
present: async () => {
|
|
127
|
+
throw new Error("unexpected interaction");
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
local: {
|
|
131
|
+
open: async () => {},
|
|
132
|
+
},
|
|
133
|
+
metadata: () => {},
|
|
134
|
+
};
|
|
135
|
+
}
|