@rigkit/fragments 0.2.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 +143 -0
- package/package.json +34 -0
- package/src/freestyleCompanyBaseFragment/index.test.ts +141 -0
- package/src/freestyleCompanyBaseFragment/index.ts +472 -0
- package/src/index.ts +1 -0
- package/src/version.ts +1 -0
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.2.8",
|
|
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.2.8",
|
|
20
|
+
"@rigkit/sdk": "0.2.8"
|
|
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.2.8";
|