@rigkit/fragments 0.0.0-canary-20260518T014918-c5bc0c2

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 ADDED
@@ -0,0 +1,143 @@
1
+ # @rigkit/fragments
2
+
3
+ Reusable workflow fragments for Rigkit configs.
4
+
5
+ The package currently exports `freestyleCompanyBaseFragment` and
6
+ `withFreestyleCompanyBase`, an opinionated global Freestyle base fragment and a
7
+ wrapper that runs a local GitHub auth check after repo-specific setup. The base
8
+ creates a Freestyle VM, installs common development tooling, initializes the
9
+ enabled interactive CLIs, snapshots the VM, and stores that snapshot in Rigkit's
10
+ global fragment cache. Repos can then build their project-specific setup on top
11
+ of the same base snapshot.
12
+
13
+ Installed by default:
14
+
15
+ - Git and common build packages
16
+ - GitHub CLI
17
+ - Node.js 22 and npm
18
+ - Bun
19
+ - Codex CLI
20
+ - Claude Code
21
+
22
+ ```ts
23
+ import { freestyle } from "@rigkit/provider-freestyle";
24
+ import {
25
+ withFreestyleCompanyBase,
26
+ type FreestyleCompanyBaseFragmentContext,
27
+ } from "@rigkit/fragments";
28
+ import { workflow } from "@rigkit/sdk";
29
+
30
+ const app = workflow("my-app", {
31
+ providers: {
32
+ freestyle: freestyle.provider(),
33
+ terminal: freestyle.terminal(),
34
+ },
35
+ });
36
+
37
+ const repoSetup = app
38
+ .sequence<FreestyleCompanyBaseFragmentContext>("repo-setup")
39
+ .task("clone-repo", async ({ freestyle, step }) => {
40
+ const created = await freestyle.client.vms.create({
41
+ snapshotId: step.ctx.snapshotId,
42
+ idleTimeoutSeconds: step.ctx.freestyleCompanyBase.idleTimeoutSeconds,
43
+ logger: console.log,
44
+ });
45
+ const { vm, vmId } = created;
46
+ try {
47
+ await vm.exec("git clone https://github.com/acme/app.git /workspace/app");
48
+ const snapshot = await vm.snapshot();
49
+ return { ctx: { ...step.ctx, snapshotId: snapshot.snapshotId, repoPath: "/workspace/app" } };
50
+ } finally {
51
+ await freestyle.client.vms.delete({ vmId });
52
+ }
53
+ });
54
+
55
+ export default app
56
+ .sequence("my-app")
57
+ .add(withFreestyleCompanyBase(repoSetup));
58
+ ```
59
+
60
+ Repos can pass environment-backed overrides when individual developers need to
61
+ choose their own tool set or VM size. See
62
+ `examples/base-freestyle-fragment/rig.config.ts` for that pattern.
63
+
64
+ `freestyleCompanyBaseFragment(...)` and `withFreestyleCompanyBase(...)`
65
+ intentionally expose a small API: `github`, `codex`, `claude`, and VM sizing.
66
+ Those options are normalized into `.configure(...)`, so they are part of the
67
+ global fragment fingerprint. For example, enabling Claude and disabling Claude
68
+ produce different global cache fragments.
69
+
70
+ ## `withFreestyleCompanyBase` Execution Model
71
+
72
+ `withFreestyleCompanyBase(repoSetup)` is a wrapper sequence. It does not just
73
+ prepend the base fragment. It also appends a company-owned check after the
74
+ sequence you pass in.
75
+
76
+ ```mermaid
77
+ flowchart LR
78
+ wrapper["withFreestyleCompanyBase(repoSetup)"]
79
+ base["1. freestyle-company-base\nbefore: global base snapshot"]
80
+ repo["2. repoSetup\nyour sequence"]
81
+ check["3. freestyle-company-base-auth-check\nafter: local uncached check"]
82
+
83
+ wrapper --> base
84
+ base --> repo
85
+ repo --> check
86
+ ```
87
+
88
+ Conceptually, the wrapper expands to:
89
+
90
+ ```ts
91
+ sequence("with-freestyle-company-base")
92
+ .add(freestyleCompanyBaseFragment(options))
93
+ .add(repoSetup)
94
+ .add(freestyleCompanyBaseAuthCheckFragment(options));
95
+ ```
96
+
97
+ In execution order:
98
+
99
+ 1. `freestyle-company-base` creates or reuses the shared global base snapshot.
100
+ 2. `repoSetup` starts from that base snapshot and adds repo-specific state.
101
+ 3. `freestyle-company-base-auth-check` runs after repo setup and can invalidate
102
+ stale global auth before Rigkit finishes the composed workflow.
103
+
104
+ `withFreestyleCompanyBase(repoSetup)` requires the wrapped setup to preserve
105
+ `freestyleCompanyBase` in its returned ctx. That lets the trailing auth check
106
+ use the base snapshot context after repo-specific setup has added its own
107
+ fields. The supporting TypeScript types are intentionally stricter than a
108
+ minimal fragment: they make ctx preservation a compile-time contract while
109
+ carrying forward any repo-specific ctx fields.
110
+
111
+ The trailing check currently verifies GitHub with `gh auth status`. If GitHub
112
+ auth is stale, it invalidates the global `github-auth` task and replays the
113
+ wrapped local setup from the refreshed base.
114
+
115
+ ## Advanced Rigkit Use Cases
116
+
117
+ This wrapper is the advanced part of the example. It is useful when a company
118
+ wants a reusable base fragment that runs before repo setup and still owns checks
119
+ after repo setup. That is a different pattern than asking every repo to add
120
+ fragments one after another in its root sequence.
121
+
122
+ Use this pattern when a reusable Rigkit fragment needs to:
123
+
124
+ - sandwich repo setup between company-managed preparation and validation
125
+ - combine global cached setup with local uncached health checks
126
+ - thread a shared ctx contract through repo-specific setup without losing
127
+ repo-specific fields
128
+ - invalidate an earlier global task from a later local task when credentials
129
+ or other long-lived state are stale
130
+ - publish a company base fragment that teams can consume without copying the
131
+ full workflow shape into every repo config
132
+
133
+ Enabling a tool installs it and runs its auth/init task:
134
+
135
+ - `github: true` installs GitHub CLI, runs `gh auth login`, and configures Git
136
+ author identity from the authenticated account.
137
+ - `codex: true` installs Codex CLI and opens `codex` in a Freestyle terminal for
138
+ login/initialization.
139
+ - `claude: true` installs Claude Code and opens `claude` in a Freestyle terminal
140
+ for login/initialization.
141
+
142
+ Authenticated global fragments can contain developer or org credentials. Use
143
+ them only when that is the intended cache boundary.
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@rigkit/fragments",
3
+ "version": "0.0.0-canary-20260518T014918-c5bc0c2",
4
+ "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/freestyle-sh/rigkit.git",
8
+ "directory": "packages/fragments"
9
+ },
10
+ "exports": {
11
+ ".": "./src/index.ts",
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "src",
16
+ "README.md"
17
+ ],
18
+ "dependencies": {
19
+ "@rigkit/provider-freestyle": "0.0.0-canary-20260518T014918-c5bc0c2",
20
+ "@rigkit/sdk": "0.0.0-canary-20260518T014918-c5bc0c2"
21
+ },
22
+ "devDependencies": {
23
+ "@types/bun": "latest",
24
+ "typescript": "latest"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc --noEmit",
31
+ "typecheck": "tsc --noEmit",
32
+ "test": "bun test"
33
+ }
34
+ }
@@ -0,0 +1,141 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { freestyle } from "@rigkit/provider-freestyle";
3
+ import { workflow } from "@rigkit/sdk";
4
+ import {
5
+ freestyleCompanyBaseFragment,
6
+ withFreestyleCompanyBase,
7
+ type FreestyleCompanyBaseFragmentContext,
8
+ } from "./index.ts";
9
+
10
+ describe("freestyleCompanyBaseFragment", () => {
11
+ test("creates a global fragment with resolved tool config", () => {
12
+ const fragment = freestyleCompanyBaseFragment({
13
+ claude: false,
14
+ vm: {
15
+ home: "/home/runner",
16
+ memSizeGb: 8,
17
+ },
18
+ });
19
+
20
+ expect(fragment.name).toBe("freestyle-company-base");
21
+ expect(fragment.cacheScope).toBe("global");
22
+ expect(fragment.config).toMatchObject({
23
+ github: true,
24
+ codex: true,
25
+ claude: false,
26
+ bun: true,
27
+ nodeMajor: 22,
28
+ npmPackages: ["@openai/codex"],
29
+ vm: {
30
+ home: "/home/runner",
31
+ idleTimeoutSeconds: 600,
32
+ memSizeGb: 8,
33
+ vcpuCount: 4,
34
+ rootfsSizeGb: 24,
35
+ },
36
+ });
37
+ expect(fragment.config?.systemPackages).toContain("git");
38
+ });
39
+
40
+ test("tool toggles control install and auth tasks together", () => {
41
+ const fragment = freestyleCompanyBaseFragment({
42
+ github: false,
43
+ codex: false,
44
+ claude: true,
45
+ });
46
+
47
+ expect(fragment.name).toBe("freestyle-company-base");
48
+ expect(fragment.config).toMatchObject({
49
+ github: false,
50
+ codex: false,
51
+ claude: true,
52
+ npmPackages: ["@anthropic-ai/claude-code"],
53
+ });
54
+ });
55
+
56
+ test("composes with a Freestyle workflow and a typed dependent sequence", () => {
57
+ const app = workflow("example", {
58
+ providers: {
59
+ freestyle: freestyle.provider(),
60
+ terminal: freestyle.terminal(),
61
+ },
62
+ });
63
+
64
+ const repoSetup = app
65
+ .sequence<FreestyleCompanyBaseFragmentContext>("repo-setup")
66
+ .task("repo-ready", async ({ step }) => ({
67
+ ctx: {
68
+ ...step.ctx,
69
+ repoPath: "/workspace/app",
70
+ },
71
+ }));
72
+
73
+ const root = app
74
+ .sequence("root")
75
+ .add(freestyleCompanyBaseFragment({ claude: false }))
76
+ .add(repoSetup);
77
+
78
+ expect(root.children.map((child) => child.name)).toEqual([
79
+ "freestyle-company-base",
80
+ "repo-setup",
81
+ ]);
82
+ });
83
+
84
+ test("wraps a dependent sequence with the base fragment and auth check", () => {
85
+ const app = workflow("wrapped-example", {
86
+ providers: {
87
+ freestyle: freestyle.provider(),
88
+ terminal: freestyle.terminal(),
89
+ },
90
+ });
91
+
92
+ const repoSetup = app
93
+ .sequence<FreestyleCompanyBaseFragmentContext>("repo-setup")
94
+ .task("repo-ready", async ({ step }) => ({
95
+ ctx: {
96
+ ...step.ctx,
97
+ repoPath: "/workspace/app",
98
+ },
99
+ }));
100
+
101
+ const wrapped = withFreestyleCompanyBase(repoSetup, { claude: false });
102
+
103
+ expect(wrapped.name).toBe("with-freestyle-company-base");
104
+ expect(wrapped.nodeKind).toBe("sequence");
105
+ expect((wrapped as any).children.map((child: { name: string }) => child.name)).toEqual([
106
+ "freestyle-company-base",
107
+ "repo-setup",
108
+ "freestyle-company-base-auth-check",
109
+ ]);
110
+ });
111
+ });
112
+
113
+ if (false) {
114
+ const app = workflow("wrapped-typecheck", {
115
+ providers: {
116
+ freestyle: freestyle.provider(),
117
+ terminal: freestyle.terminal(),
118
+ },
119
+ });
120
+
121
+ const preservingSetup = app
122
+ .sequence<FreestyleCompanyBaseFragmentContext>("preserving-setup")
123
+ .task("preserve", async ({ step }) => ({
124
+ ctx: {
125
+ ...step.ctx,
126
+ repoPath: "/workspace/app",
127
+ },
128
+ }));
129
+ withFreestyleCompanyBase(preservingSetup);
130
+
131
+ const droppingSetup = app
132
+ .sequence<FreestyleCompanyBaseFragmentContext>("dropping-setup")
133
+ .task("drop", async () => ({
134
+ ctx: {
135
+ repoPath: "/workspace/app",
136
+ },
137
+ }));
138
+
139
+ // @ts-expect-error wrapped setup must preserve freestyleCompanyBase in ctx
140
+ withFreestyleCompanyBase(droppingSetup);
141
+ }
@@ -0,0 +1,472 @@
1
+ import {
2
+ VmSpec,
3
+ type FreestyleProviderDefinition,
4
+ type FreestyleTerminalProviderDefinition,
5
+ } from "@rigkit/provider-freestyle";
6
+ import {
7
+ sequence,
8
+ type JsonObject,
9
+ type WorkflowNodeInput,
10
+ type WorkflowNodeOutput,
11
+ type WorkflowNodeDefinition,
12
+ type WorkflowProviderMap,
13
+ } from "@rigkit/sdk";
14
+
15
+ export type FreestyleCompanyBaseFragmentOptions = {
16
+ github?: boolean;
17
+ codex?: boolean;
18
+ claude?: boolean;
19
+ vm?: {
20
+ home?: string;
21
+ memSizeGb?: number;
22
+ vcpuCount?: number;
23
+ rootfsSizeGb?: number;
24
+ };
25
+ };
26
+
27
+ export type FreestyleCompanyBaseFragmentProviderMap = WorkflowProviderMap & {
28
+ freestyle: FreestyleProviderDefinition;
29
+ terminal: FreestyleTerminalProviderDefinition;
30
+ };
31
+
32
+ type FreestyleCompanyBaseFragmentConfig = JsonObject & {
33
+ github: boolean;
34
+ codex: boolean;
35
+ claude: boolean;
36
+ bun: boolean;
37
+ nodeMajor: number;
38
+ codexPackage: string;
39
+ claudePackage: string;
40
+ systemPackages: string[];
41
+ npmPackages: string[];
42
+ vm: {
43
+ home: string;
44
+ idleTimeoutSeconds: number;
45
+ memSizeGb: number;
46
+ vcpuCount: number;
47
+ rootfsSizeGb: number;
48
+ };
49
+ };
50
+
51
+ export type FreestyleCompanyBaseFragmentContext = JsonObject & {
52
+ snapshotId: string;
53
+ freestyleCompanyBase: {
54
+ snapshotId: string;
55
+ home: string;
56
+ idleTimeoutSeconds: number;
57
+ tools: {
58
+ github: boolean;
59
+ codex: boolean;
60
+ claude: boolean;
61
+ bun: boolean;
62
+ nodeMajor: number;
63
+ };
64
+ systemPackages: string[];
65
+ npmPackages: string[];
66
+ authenticated: {
67
+ github: boolean;
68
+ codex: boolean;
69
+ claude: boolean;
70
+ };
71
+ };
72
+ };
73
+
74
+ export type FreestyleCompanyBaseFragment = WorkflowNodeDefinition<FreestyleCompanyBaseFragmentProviderMap, {}, FreestyleCompanyBaseFragmentContext>;
75
+
76
+ /**
77
+ * `withFreestyleCompanyBase` is an intentionally advanced wrapper pattern.
78
+ *
79
+ * A company base fragment usually needs to do two things that are easy to lose
80
+ * in simpler examples:
81
+ *
82
+ * - seed the workflow with a global, shared base snapshot
83
+ * - let repo-specific setup add fields to ctx while preserving the base ctx
84
+ * needed by later company checks
85
+ *
86
+ * The types below encode that contract. They reject a child setup that drops
87
+ * `freestyleCompanyBase`, but keep any extra fields the child adds so callers
88
+ * can continue with their repo-specific ctx shape.
89
+ */
90
+ export type FreestyleCompanyBaseWrappedFragment<Context extends FreestyleCompanyBaseFragmentContext> =
91
+ WorkflowNodeDefinition<FreestyleCompanyBaseFragmentProviderMap, {}, Context>;
92
+
93
+ type FreestyleCompanyBasePreservingChild<Child extends WorkflowNodeDefinition<FreestyleCompanyBaseFragmentProviderMap, any, any>> =
94
+ FreestyleCompanyBaseFragmentContext extends WorkflowNodeInput<Child>
95
+ ? WorkflowNodeOutput<Child> extends FreestyleCompanyBaseFragmentContext
96
+ ? Child
97
+ : never
98
+ : never;
99
+
100
+ type FreestyleCompanyBasePreservedOutput<Child extends WorkflowNodeDefinition<FreestyleCompanyBaseFragmentProviderMap, any, any>> =
101
+ WorkflowNodeOutput<Child> extends FreestyleCompanyBaseFragmentContext
102
+ ? WorkflowNodeOutput<Child>
103
+ : never;
104
+
105
+ const defaultSystemPackages = [
106
+ "build-essential",
107
+ "ca-certificates",
108
+ "curl",
109
+ "git",
110
+ "gnupg",
111
+ "jq",
112
+ "pkg-config",
113
+ "python3",
114
+ "python3-pip",
115
+ "ripgrep",
116
+ "unzip",
117
+ "xz-utils",
118
+ ] as const;
119
+
120
+ const bun = true;
121
+ const nodeMajor = 22;
122
+ const codexPackage = "@openai/codex";
123
+ const claudePackage = "@anthropic-ai/claude-code";
124
+ const idleTimeoutSeconds = 600;
125
+
126
+ export function freestyleCompanyBaseFragment(options: FreestyleCompanyBaseFragmentOptions = {}): FreestyleCompanyBaseFragment {
127
+ const github = options.github ?? true;
128
+ const codex = options.codex ?? true;
129
+ const claude = options.claude ?? true;
130
+ const systemPackages = [...defaultSystemPackages];
131
+ const npmPackages = [
132
+ ...(codex ? [codexPackage] : []),
133
+ ...(claude ? [claudePackage] : []),
134
+ ];
135
+
136
+ const config = {
137
+ github,
138
+ codex,
139
+ claude,
140
+ bun,
141
+ nodeMajor,
142
+ codexPackage,
143
+ claudePackage,
144
+ systemPackages,
145
+ npmPackages,
146
+ vm: {
147
+ home: options.vm?.home ?? "/root",
148
+ idleTimeoutSeconds,
149
+ memSizeGb: options.vm?.memSizeGb ?? 16,
150
+ vcpuCount: options.vm?.vcpuCount ?? 4,
151
+ rootfsSizeGb: options.vm?.rootfsSizeGb ?? 24,
152
+ },
153
+ } satisfies FreestyleCompanyBaseFragmentConfig;
154
+
155
+ return sequence<FreestyleCompanyBaseFragmentProviderMap, {}>("freestyle-company-base")
156
+ .global()
157
+ .configure(config)
158
+ .task(
159
+ "install-tooling",
160
+ { version: "freestyle-company-base-tooling-v1" },
161
+ async ({ config, freestyle, step }) => {
162
+ const { vm, vmId } = await freestyle.client.vms.create({
163
+ spec: createVmSpec(config),
164
+ logger: console.log,
165
+ });
166
+ try {
167
+ const snapshot = await vm.snapshot();
168
+ return {
169
+ ctx: {
170
+ snapshotId: snapshot.snapshotId,
171
+ freestyleCompanyBase: {
172
+ snapshotId: snapshot.snapshotId,
173
+ home: config.vm.home,
174
+ idleTimeoutSeconds: config.vm.idleTimeoutSeconds,
175
+ tools: {
176
+ github: config.github,
177
+ codex: config.codex,
178
+ claude: config.claude,
179
+ bun: config.bun,
180
+ nodeMajor: config.nodeMajor,
181
+ },
182
+ systemPackages: config.systemPackages,
183
+ npmPackages: config.npmPackages,
184
+ authenticated: {
185
+ github: false,
186
+ codex: false,
187
+ claude: false,
188
+ },
189
+ },
190
+ },
191
+ };
192
+ } finally {
193
+ await freestyle.client.vms.delete({ vmId });
194
+ }
195
+ },
196
+ )
197
+ .task(
198
+ "github-auth",
199
+ { version: "freestyle-company-base-github-auth-v1" },
200
+ async ({ config, freestyle, terminal, step }) => {
201
+ if (!config.github) return { ctx: { ...step.ctx } };
202
+
203
+ const created = await freestyle.client.vms.create({
204
+ snapshotId: step.ctx.snapshotId,
205
+ idleTimeoutSeconds: config.vm.idleTimeoutSeconds,
206
+ logger: console.log,
207
+ });
208
+ const { vm, vmId } = created;
209
+ try {
210
+ const authenticated = await vm.exec(withHome(config.vm.home, "gh auth status -h github.com >/dev/null 2>&1"));
211
+ if ((authenticated.statusCode ?? 0) !== 0) {
212
+ await terminal.open("Log in to GitHub", {
213
+ ssh: await freestyle.createSSHOptions({ vmId }),
214
+ command: "gh auth login --hostname github.com --git-protocol https --web",
215
+ keepOpenAfterCommand: true,
216
+ instructions:
217
+ "Complete the GitHub browser login in this terminal. After gh succeeds, type exit to continue.",
218
+ });
219
+
220
+ const verified = await vm.exec(withHome(config.vm.home, "gh auth status -h github.com >/dev/null 2>&1"));
221
+ if ((verified.statusCode ?? 0) !== 0) {
222
+ const status = await vm.exec(withHome(config.vm.home, "gh auth status -h github.com 2>&1"));
223
+ throw new Error(
224
+ `GitHub CLI is not authenticated:\n${status.stdout || status.stderr}`.trim(),
225
+ );
226
+ }
227
+ }
228
+
229
+ const gitIdentity = await vm.exec({
230
+ command: configureGitIdentityCommand(config.vm.home),
231
+ timeoutMs: 60 * 1000,
232
+ });
233
+ if ((gitIdentity.statusCode ?? 0) !== 0) {
234
+ throw new Error(
235
+ `Git author identity configuration failed:\n${gitIdentity.stdout ?? ""}${gitIdentity.stderr ?? ""}`.trim(),
236
+ );
237
+ }
238
+
239
+ const snapshot = await vm.snapshot();
240
+ return { ctx: updateCompanyBaseSnapshot(step.ctx, snapshot.snapshotId, { github: true }) };
241
+ } finally {
242
+ await freestyle.client.vms.delete({ vmId });
243
+ }
244
+ },
245
+ )
246
+ .task(
247
+ "codex-auth",
248
+ { version: "freestyle-company-base-codex-auth-v1" },
249
+ async ({ config, freestyle, terminal, step }) => {
250
+ if (!config.codex) return { ctx: { ...step.ctx } };
251
+
252
+ const { vm, vmId } = await freestyle.client.vms.create({
253
+ snapshotId: step.ctx.snapshotId,
254
+ idleTimeoutSeconds: config.vm.idleTimeoutSeconds,
255
+ logger: console.log,
256
+ });
257
+ try {
258
+ await terminal.open("Initialize Codex CLI", {
259
+ ssh: await freestyle.createSSHOptions({ vmId }),
260
+ command: agentCliInitCommand(config.vm.home, "codex"),
261
+ keepOpenAfterCommand: true,
262
+ instructions:
263
+ "Complete Codex login and initialization in this terminal. Exit Codex, then type exit to continue.",
264
+ });
265
+
266
+ const snapshot = await vm.snapshot();
267
+ return { ctx: updateCompanyBaseSnapshot(step.ctx, snapshot.snapshotId, { codex: true }) };
268
+ } finally {
269
+ await freestyle.client.vms.delete({ vmId });
270
+ }
271
+ },
272
+ )
273
+ .task(
274
+ "claude-auth",
275
+ { version: "freestyle-company-base-claude-auth-v1" },
276
+ async ({ config, freestyle, terminal, step }) => {
277
+ if (!config.claude) return { ctx: { ...step.ctx } };
278
+
279
+ const { vm, vmId } = await freestyle.client.vms.create({
280
+ snapshotId: step.ctx.snapshotId,
281
+ idleTimeoutSeconds: config.vm.idleTimeoutSeconds,
282
+ logger: console.log,
283
+ });
284
+ try {
285
+ await terminal.open("Initialize Claude CLI", {
286
+ ssh: await freestyle.createSSHOptions({ vmId }),
287
+ command: agentCliInitCommand(config.vm.home, "claude"),
288
+ keepOpenAfterCommand: true,
289
+ instructions:
290
+ "Complete Claude login and initialization in this terminal. Exit Claude, then type exit to continue.",
291
+ });
292
+
293
+ const snapshot = await vm.snapshot();
294
+ return { ctx: updateCompanyBaseSnapshot(step.ctx, snapshot.snapshotId, { claude: true }) };
295
+ } finally {
296
+ await freestyle.client.vms.delete({ vmId });
297
+ }
298
+ },
299
+ ) as unknown as FreestyleCompanyBaseFragment;
300
+ }
301
+
302
+ export function withFreestyleCompanyBase<
303
+ Child extends WorkflowNodeDefinition<FreestyleCompanyBaseFragmentProviderMap, any, any>,
304
+ >(
305
+ child: FreestyleCompanyBasePreservingChild<Child>,
306
+ options: FreestyleCompanyBaseFragmentOptions = {},
307
+ ): FreestyleCompanyBaseWrappedFragment<FreestyleCompanyBasePreservedOutput<Child>> {
308
+ return sequence<FreestyleCompanyBaseFragmentProviderMap, {}>("with-freestyle-company-base")
309
+ .add(freestyleCompanyBaseFragment(options))
310
+ .add(child as any)
311
+ .add(freestyleCompanyBaseAuthCheckFragment<FreestyleCompanyBasePreservedOutput<Child>>(options) as any) as unknown as FreestyleCompanyBaseWrappedFragment<FreestyleCompanyBasePreservedOutput<Child>>;
312
+ }
313
+
314
+ function freestyleCompanyBaseAuthCheckFragment<Context extends FreestyleCompanyBaseFragmentContext>(
315
+ options: FreestyleCompanyBaseFragmentOptions,
316
+ ): WorkflowNodeDefinition<FreestyleCompanyBaseFragmentProviderMap, Context, Context> {
317
+ const handler = async ({ freestyle, step }: any) => {
318
+ const { vm, vmId } = await freestyle.client.vms.create({
319
+ snapshotId: step.ctx.snapshotId,
320
+ idleTimeoutSeconds: step.ctx.freestyleCompanyBase.idleTimeoutSeconds,
321
+ logger: console.log,
322
+ });
323
+ try {
324
+ if (options.github ?? true) {
325
+ const github = await vm.exec(withHome(step.ctx.freestyleCompanyBase.home, "gh auth status -h github.com >/dev/null 2>&1"));
326
+ if ((github.statusCode ?? 0) !== 0) {
327
+ return step.invalidate("github-auth" as never);
328
+ }
329
+ }
330
+
331
+ return { ctx: { ...step.ctx } as Context };
332
+ } finally {
333
+ await freestyle.client.vms.delete({ vmId });
334
+ }
335
+ };
336
+
337
+ return sequence<FreestyleCompanyBaseFragmentProviderMap, Context>("freestyle-company-base-auth-check")
338
+ .local()
339
+ .task("check-auth", { cacheTTL: 0 }, handler as any) as unknown as WorkflowNodeDefinition<FreestyleCompanyBaseFragmentProviderMap, Context, Context>;
340
+ }
341
+
342
+ function createVmSpec(config: FreestyleCompanyBaseFragmentConfig): VmSpec {
343
+ return new VmSpec()
344
+ .runCommands(installToolingCommand(config))
345
+ .memSizeGb(config.vm.memSizeGb)
346
+ .vcpuCount(config.vm.vcpuCount)
347
+ .rootfsSizeGb(config.vm.rootfsSizeGb)
348
+ .idleTimeoutSeconds(config.vm.idleTimeoutSeconds);
349
+ }
350
+
351
+ function installToolingCommand(config: FreestyleCompanyBaseFragmentConfig): string {
352
+ const aptPackages = [...config.systemPackages];
353
+ if (config.github && !aptPackages.includes("gh")) aptPackages.push("gh");
354
+ if (!aptPackages.includes("nodejs")) aptPackages.push("nodejs");
355
+
356
+ const lines = [
357
+ "set -e",
358
+ "export DEBIAN_FRONTEND=noninteractive",
359
+ `export HOME=${shellQuote(config.vm.home)}`,
360
+ "apt-get update -qq",
361
+ "apt-get install -y -qq ca-certificates curl gnupg",
362
+ "mkdir -p /etc/apt/keyrings",
363
+ ];
364
+
365
+ if (config.github) {
366
+ lines.push(
367
+ "curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /etc/apt/keyrings/githubcli-archive-keyring.gpg",
368
+ "chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg",
369
+ "printf 'deb [arch=%s signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\\n' \"$(dpkg --print-architecture)\" > /etc/apt/sources.list.d/github-cli.list",
370
+ );
371
+ }
372
+
373
+ lines.push(
374
+ `curl -fsSL https://deb.nodesource.com/setup_${config.nodeMajor}.x | bash -`,
375
+ `apt-get install -y -qq ${aptPackages.map(shellQuote).join(" ")}`,
376
+ "corepack enable || true",
377
+ "npm config set prefix /usr/local",
378
+ );
379
+
380
+ const backgroundInstalls: string[] = [];
381
+ if (config.bun) {
382
+ backgroundInstalls.push("curl -fsSL https://bun.sh/install | BUN_INSTALL=/opt/bun bash");
383
+ }
384
+ for (const npmPackage of config.npmPackages) {
385
+ backgroundInstalls.push(`npm install -g ${shellQuote(npmPackage)}`);
386
+ }
387
+
388
+ backgroundInstalls.forEach((command, index) => {
389
+ const pid = `install_pid_${index}`;
390
+ lines.push(`${command} &`, `${pid}=$!`);
391
+ });
392
+ backgroundInstalls.forEach((_, index) => {
393
+ lines.push(`wait "$install_pid_${index}"`);
394
+ });
395
+
396
+ if (config.bun) {
397
+ lines.push(
398
+ "ln -sf /opt/bun/bin/bun /usr/local/bin/bun",
399
+ "ln -sf /opt/bun/bin/bunx /usr/local/bin/bunx",
400
+ );
401
+ }
402
+ if (config.codex) {
403
+ lines.push(
404
+ `mkdir -p ${shellQuote(`${config.vm.home}/.codex`)}`,
405
+ `printf 'cli_auth_credentials_store = "file"\\n' > ${shellQuote(`${config.vm.home}/.codex/config.toml`)}`,
406
+ );
407
+ }
408
+
409
+ lines.push(
410
+ "git config --system init.defaultBranch main",
411
+ "git --version",
412
+ ...(config.github ? ["gh --version"] : []),
413
+ "node --version",
414
+ "npm --version",
415
+ ...(config.bun ? ["bun --version"] : []),
416
+ ...(config.codex ? ["codex --version"] : []),
417
+ ...(config.claude ? ["claude --version"] : []),
418
+ "rm -rf /var/lib/apt/lists/*",
419
+ );
420
+
421
+ return lines.join("\n");
422
+ }
423
+
424
+ function withHome(home: string, command: string): string {
425
+ return `HOME=${shellQuote(home)} ${command}`;
426
+ }
427
+
428
+ function agentCliInitCommand(home: string, command: "codex" | "claude"): string {
429
+ return [
430
+ "set -e",
431
+ `export HOME=${shellQuote(home)}`,
432
+ command,
433
+ ].join("\n");
434
+ }
435
+
436
+ function configureGitIdentityCommand(home: string): string {
437
+ return [
438
+ "set -e",
439
+ `export HOME=${shellQuote(home)}`,
440
+ "login=$(gh api user --jq '.login')",
441
+ "name=$(gh api user --jq '.name // empty')",
442
+ "id=$(gh api user --jq '.id')",
443
+ "email=$(gh api user --jq '.email // empty')",
444
+ 'if [ -z "$name" ]; then name="$login"; fi',
445
+ 'if [ -z "$email" ]; then email="${id}+${login}@users.noreply.github.com"; fi',
446
+ 'git config --global user.name "$name"',
447
+ 'git config --global user.email "$email"',
448
+ ].join("\n");
449
+ }
450
+
451
+ function updateCompanyBaseSnapshot(
452
+ ctx: Readonly<FreestyleCompanyBaseFragmentContext>,
453
+ snapshotId: string,
454
+ authenticated: Partial<FreestyleCompanyBaseFragmentContext["freestyleCompanyBase"]["authenticated"]>,
455
+ ): FreestyleCompanyBaseFragmentContext {
456
+ return {
457
+ ...ctx,
458
+ snapshotId,
459
+ freestyleCompanyBase: {
460
+ ...ctx.freestyleCompanyBase,
461
+ snapshotId,
462
+ authenticated: {
463
+ ...ctx.freestyleCompanyBase.authenticated,
464
+ ...authenticated,
465
+ },
466
+ },
467
+ };
468
+ }
469
+
470
+ function shellQuote(value: string): string {
471
+ return `'${value.replaceAll("'", `'\\''`)}'`;
472
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./freestyleCompanyBaseFragment/index.ts";
package/src/version.ts ADDED
@@ -0,0 +1 @@
1
+ export const RIGKIT_FRAGMENTS_VERSION = "0.0.0-canary-20260518T014918-c5bc0c2";