Skip to content

Commit 41080b3

Browse files
committed
feat: Add rbenv and tests
1 parent 22c4b6a commit 41080b3

6 files changed

Lines changed: 293 additions & 99 deletions

File tree

scripts/runbook.ts

Lines changed: 57 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,62 @@
11
import { query } from "@anthropic-ai/claude-agent-sdk";
22

3-
const toolName = 'uv';
4-
const toolHomepage = 'https://docs.astral.sh/uv/#projects'
3+
const toolName = 'rbenv';
4+
const toolHomepage = 'https://github.com/rbenv/rbenv'
55

6-
const researchResults = [
7-
`Here's a summary of the research and proposed design:
6+
const researchResults: string[] = [];
87

9-
---
10-
11-
## \`uv\` Resource Design
12-
13-
### What was researched
14-
- **uv** is a blazing-fast Python tool from Astral that replaces pip, pyenv, pipx, poetry, and virtualenv in one binary.
15-
16-
---
17-
18-
### Installation
19-
| Platform | Method |
20-
|---|---|
21-
| macOS | \`brew install uv\` (Homebrew, preferred since Codify users likely have it) |
22-
| Linux | \`curl -LsSf https://astral.sh/uv/install.sh \\| sh\` with \`UV_NO_MODIFY_PATH=1\`, then manually add \`~/.local/bin\` to shell rc |
23-
24-
No Rust or Python required. No other OS-level dependencies beyond curl on Linux.
25-
26-
---
27-
28-
### Proposed Resources
29-
30-
**One resource: \`uv\`** (located at \`src/resources/python/uv/\`)
31-
32-
| Parameter | Type | Description |
33-
|---|---|---|
34-
| \`pythonVersions\` | Stateful \`string[]\` | Python versions to install via \`uv python install\` (e.g. \`["3.12", "3.11"]\`) |
35-
| \`tools\` | Stateful \`string[]\` | CLI tools installed globally via \`uv tool install\` (e.g. \`["ruff", "black"]\`) |
36-
37-
This follows the same pattern as **pyenv** (install tool + manage Python versions) and **NVM** (install tool + manage versions as stateful parameter).
38-
39-
---
40-
41-
### Key Design Decisions
42-
43-
1. **Homebrew dependency on macOS** — Declared via \`dependencies: ['homebrew']\` mirroring the \`asdf\` resource pattern.
44-
2. **Two stateful parameters** — \`pythonVersions\` (parsed from \`uv python list --only-installed\`) and \`tools\` (parsed from \`uv tool list\`).
45-
3. **Version prefix matching** — Desired \`"3.12"\` matches installed \`"3.12.3"\` using \`startsWith\` in \`isElementEqual\`.
46-
4. **No sub-resources** — Unlike asdf (which has \`asdf-plugin\` and \`asdf-install\` sub-resources), uv's tool and Python management is simple enough to handle as stateful parameters on the main resource.
47-
`
48-
];
49-
50-
// for await (const message of query({
51-
// prompt:
52-
// `Research and design a Codify resource for ${toolName} (the homepage is: ${toolHomepage})
53-
//
54-
// The research should include:
55-
// ** The installation method **
56-
// - The installation method for the tool of application (in the case ${toolName})
57-
// - The installation method should be the most standard installation method.
58-
// - Find the installation instructions for both macOS and Linux.
59-
//
60-
// ** Dependencies **
61-
// - Any dependencies or prerequisites for installation
62-
//
63-
// ** Configuration **
64-
// - Any configuration options or settings for the tool
65-
// - Any settings that we want the user to manage (which will later be exposed as parameters in the Codify resource)
66-
// - The default values for these settings
67-
//
68-
// ** Usages **
69-
// - Examples of how the tool can be used
70-
// - Any common use cases or scenarios
71-
// - Any use case we want to manage via the Codify resource or sub-resources or stateful parameters
72-
// - For example:
73-
// - The homebrew resource installs homebrew but it also has the formulae and casks stateful parameters that manage installed packages.
74-
// - The asdf resource installs asdf, a tool version manager, but it also has the plugins stateful parameter that manages installed plugins.
75-
// - The asdf resource has sub resources for installing tool plugins and versions.
76-
//
77-
// The purpose of this research is to be used later by Claude to create the resources needed in code. Format the answer so that
78-
// it can be easily understood by Claude.
79-
// `,
80-
// options: {
81-
// settingSources: ['project'],
82-
// allowedTools: ["WebSearch", "WebFetch"],
83-
// mcpServers: {},
84-
// permissionMode: 'plan',
85-
// cwd: '../'
86-
// }
87-
// })) {
88-
// // Print human-readable output
89-
// if (message.type === "assistant" && message.message?.content) {
90-
// for (const block of message.message.content) {
91-
// if ("text" in block) {
92-
// console.log(block.text); // Claude's reasoning
93-
// researchResults.push(block.text);
94-
// } else if ("name" in block) {
95-
// console.log(`Tool: ${block.name}`); // Tool being called
96-
// }
97-
// }
98-
// } else if (message.type === "result") {
99-
// console.log(`Done: ${message.subtype}`); // Final result
100-
// }
101-
// }
8+
for await (const message of query({
9+
prompt:
10+
`Research and design a Codify resource for ${toolName} (the homepage is: ${toolHomepage})
11+
12+
The research should include:
13+
** The installation method **
14+
- The installation method for the tool of application (in the case ${toolName})
15+
- The installation method should be the most standard installation method.
16+
- Find the installation instructions for both macOS and Linux.
17+
18+
** Dependencies **
19+
- Any dependencies or prerequisites for installation
20+
21+
** Configuration **
22+
- Any configuration options or settings for the tool
23+
- Any settings that we want the user to manage (which will later be exposed as parameters in the Codify resource)
24+
- The default values for these settings
25+
26+
** Usages **
27+
- Examples of how the tool can be used
28+
- Any common use cases or scenarios
29+
- Any use case we want to manage via the Codify resource or sub-resources or stateful parameters
30+
- For example:
31+
- The homebrew resource installs homebrew but it also has the formulae and casks stateful parameters that manage installed packages.
32+
- The asdf resource installs asdf, a tool version manager, but it also has the plugins stateful parameter that manages installed plugins.
33+
- The asdf resource has sub resources for installing tool plugins and versions.
34+
35+
The purpose of this research is to be used later by Claude to create the resources needed in code. Format the answer so that
36+
it can be easily understood by Claude.
37+
`,
38+
options: {
39+
settingSources: ['project'],
40+
allowedTools: ["WebSearch", "WebFetch"],
41+
mcpServers: {},
42+
permissionMode: 'plan',
43+
cwd: '../'
44+
}
45+
})) {
46+
// Print human-readable output
47+
if (message.type === "assistant" && message.message?.content) {
48+
for (const block of message.message.content) {
49+
if ("text" in block) {
50+
console.log(block.text); // Claude's reasoning
51+
researchResults.push(block.text);
52+
} else if ("name" in block) {
53+
console.log(`Tool: ${block.name}`); // Tool being called
54+
}
55+
}
56+
} else if (message.type === "result") {
57+
console.log(`Done: ${message.subtype}`); // Final result
58+
}
59+
}
10260

10361
// Checkout a new git branch
10462
// Launch a new docker container
@@ -119,8 +77,8 @@ Steps:
11977
- Add the resource to @src/index.ts so that it is visible
12078
- Write tests for the code to test ${toolName}
12179
- Ensure typescript is correct using tsx
122-
- Run the test using 'npm run test:integration:dev -- $PathToTheTestFile'
123-
- Do not try to test the code in any other ways. It may brick the current computer if you do.
80+
// - Run the test using 'npm run test:integration:dev -- $PathToTheTestFile'. Make sure the $PathToTheTestFile is replaced with the relative path to the test file.
81+
// - Do not try to test the code in any other ways. It may brick the current computer if you do.
12482
12583
Research:
12684
${researchResults.join('\n\n')}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { UvResource } from './resources/python/uv/uv.js';
3030
import { VenvProject } from './resources/python/venv/venv-project.js';
3131
import { Virtualenv } from './resources/python/virtualenv/virtualenv.js';
3232
import { VirtualenvProject } from './resources/python/virtualenv/virtualenv-project.js';
33+
import { RbenvResource } from './resources/ruby/rbenv/rbenv.js';
3334
import { ActionResource } from './resources/scripting/action.js';
3435
import { AliasResource } from './resources/shell/alias/alias-resource.js';
3536
import { AliasesResource } from './resources/shell/aliases/aliases-resource.js';
@@ -92,5 +93,6 @@ runPlugin(Plugin.create(
9293
new SnapResource(),
9394
new TartResource(),
9495
new TartVmResource(),
96+
new RbenvResource(),
9597
])
9698
)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { getPty, ParameterSetting, SpawnStatus, StatefulParameter } from '@codifycli/plugin-core';
2+
3+
import { RbenvConfig } from './rbenv.js';
4+
5+
export class RbenvGlobalParameter extends StatefulParameter<RbenvConfig, string> {
6+
getSettings(): ParameterSetting {
7+
return {
8+
type: 'version',
9+
};
10+
}
11+
12+
override async refresh(): Promise<string | null> {
13+
const $ = getPty();
14+
const { data, status } = await $.spawnSafe('rbenv global');
15+
16+
if (status === SpawnStatus.ERROR) {
17+
return null;
18+
}
19+
20+
return parseGlobalVersion(data);
21+
}
22+
23+
override async add(valueToAdd: string): Promise<void> {
24+
const $ = getPty();
25+
await $.spawn(`rbenv global ${valueToAdd}`, { interactive: true });
26+
}
27+
28+
override async modify(newValue: string): Promise<void> {
29+
const $ = getPty();
30+
await $.spawn(`rbenv global ${newValue}`, { interactive: true });
31+
}
32+
33+
override async remove(): Promise<void> {
34+
const $ = getPty();
35+
await $.spawn('rbenv global system', { interactive: true });
36+
}
37+
}
38+
39+
/**
40+
* Parse the output of `rbenv global`.
41+
* Returns null when rbenv reports "system" (no user-managed version set).
42+
*/
43+
function parseGlobalVersion(output: string): string | null {
44+
const version = output.trim();
45+
return version === 'system' ? null : version;
46+
}

src/resources/ruby/rbenv/rbenv.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { FileUtils, getPty, Resource, ResourceSettings, SpawnStatus, Utils, z } from '@codifycli/plugin-core';
2+
import { OS } from '@codifycli/schemas';
3+
import os from 'node:os';
4+
import path from 'node:path';
5+
6+
import { RbenvGlobalParameter } from './global-parameter.js';
7+
import { RubyVersionsParameter } from './ruby-versions-parameter.js';
8+
9+
const RBENV_ROOT = path.join(os.homedir(), '.rbenv');
10+
const RBENV_PATH_EXPORT = 'export PATH="$HOME/.rbenv/bin:$PATH"';
11+
const RBENV_INIT = 'eval "$(rbenv init -)"';
12+
13+
const schema = z.object({
14+
rubyVersions: z
15+
.array(z.string())
16+
.describe('Ruby versions to install via rbenv (e.g. ["3.3.0", "3.2.4"])')
17+
.optional(),
18+
global: z
19+
.string()
20+
.describe('The global Ruby version set by rbenv.')
21+
.optional(),
22+
})
23+
.describe('rbenv resource — install and manage multiple Ruby versions');
24+
25+
export type RbenvConfig = z.infer<typeof schema>;
26+
27+
export class RbenvResource extends Resource<RbenvConfig> {
28+
getSettings(): ResourceSettings<RbenvConfig> {
29+
return {
30+
id: 'rbenv',
31+
operatingSystems: [OS.Darwin, OS.Linux],
32+
schema,
33+
parameterSettings: {
34+
rubyVersions: { type: 'stateful', definition: new RubyVersionsParameter(), order: 1 },
35+
global: { type: 'stateful', definition: new RbenvGlobalParameter(), order: 2 },
36+
},
37+
};
38+
}
39+
40+
override async refresh(): Promise<Partial<RbenvConfig> | null> {
41+
const $ = getPty();
42+
const { status } = await $.spawnSafe('rbenv --version');
43+
return status === SpawnStatus.SUCCESS ? {} : null;
44+
}
45+
46+
override async create(): Promise<void> {
47+
if (Utils.isMacOS()) {
48+
await installOnMacOS();
49+
} else {
50+
await installOnLinux();
51+
}
52+
}
53+
54+
override async destroy(): Promise<void> {
55+
if (Utils.isMacOS()) {
56+
await uninstallOnMacOS();
57+
} else {
58+
await uninstallOnLinux();
59+
}
60+
}
61+
}
62+
63+
async function installOnMacOS(): Promise<void> {
64+
const $ = getPty();
65+
await $.spawn('brew install rbenv ruby-build', {
66+
interactive: true,
67+
env: { HOMEBREW_NO_AUTO_UPDATE: '1' },
68+
});
69+
await FileUtils.addToShellRc(RBENV_INIT);
70+
}
71+
72+
async function installOnLinux(): Promise<void> {
73+
const $ = getPty();
74+
75+
await $.spawn(`git clone https://github.com/rbenv/rbenv.git ${RBENV_ROOT}`, { interactive: true });
76+
77+
const rubyBuildPath = path.join(RBENV_ROOT, 'plugins', 'ruby-build');
78+
await $.spawn(`git clone https://github.com/rbenv/ruby-build.git ${rubyBuildPath}`, { interactive: true });
79+
80+
await Utils.installViaPkgMgr(
81+
'autoconf patch build-essential rustc libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev libdb-dev uuid-dev'
82+
);
83+
84+
await FileUtils.addAllToShellRc([RBENV_PATH_EXPORT, RBENV_INIT]);
85+
}
86+
87+
async function uninstallOnMacOS(): Promise<void> {
88+
const $ = getPty();
89+
await $.spawn('brew uninstall rbenv ruby-build', {
90+
interactive: true,
91+
env: { HOMEBREW_NO_AUTO_UPDATE: '1' },
92+
});
93+
await removeRbenvFromShellRc([RBENV_INIT]);
94+
}
95+
96+
async function uninstallOnLinux(): Promise<void> {
97+
const $ = getPty();
98+
await $.spawn(`rm -rf ${RBENV_ROOT}`);
99+
await removeRbenvFromShellRc([RBENV_PATH_EXPORT, RBENV_INIT]);
100+
}
101+
102+
/**
103+
* Removes rbenv-related lines from the shell RC file.
104+
* Skips gracefully if the shell RC file does not exist (e.g. rbenv was
105+
* already present on the machine before Codify managed it and was never
106+
* added to the RC in the first place).
107+
*/
108+
async function removeRbenvFromShellRc(lines: string[]): Promise<void> {
109+
const shellRc = Utils.getPrimaryShellRc();
110+
if (!(await FileUtils.fileExists(shellRc))) {
111+
return;
112+
}
113+
for (const line of lines) {
114+
await FileUtils.removeLineFromShellRc(line);
115+
}
116+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ArrayStatefulParameter, getPty } from '@codifycli/plugin-core';
2+
3+
import { RbenvConfig } from './rbenv.js';
4+
5+
export class RubyVersionsParameter extends ArrayStatefulParameter<RbenvConfig, string> {
6+
override async refresh(_desired: string[] | null): Promise<string[] | null> {
7+
const $ = getPty();
8+
const { data } = await $.spawnSafe('rbenv versions --bare');
9+
10+
return parseInstalledVersions(data);
11+
}
12+
13+
override async addItem(version: string): Promise<void> {
14+
const $ = getPty();
15+
await $.spawn(`rbenv install ${version}`, { interactive: true });
16+
}
17+
18+
override async removeItem(version: string): Promise<void> {
19+
const $ = getPty();
20+
await $.spawn(`rbenv uninstall --force ${version}`, { interactive: true });
21+
}
22+
}
23+
24+
function parseInstalledVersions(output: string): string[] {
25+
return output
26+
.split('\n')
27+
.map((line) => line.trim())
28+
.filter(Boolean);
29+
}

0 commit comments

Comments
 (0)