Skip to content

Commit 22c4b6a

Browse files
committed
feat: Add uv and tests
1 parent 857e9a2 commit 22c4b6a

File tree

9 files changed

+426
-11846
lines changed

9 files changed

+426
-11846
lines changed

package-lock.json

Lines changed: 0 additions & 11841 deletions
This file was deleted.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"trash": "^10.0.0"
5555
},
5656
"devDependencies": {
57+
"@anthropic-ai/claude-agent-sdk": "^0.2.97",
5758
"@apidevtools/json-schema-ref-parser": "^11.7.2",
5859
"@codifycli/plugin-test": "^1.0.0",
5960
"@fastify/merge-json-schemas": "^0.2.0",
@@ -70,14 +71,13 @@
7071
"@types/debug": "4.1.12",
7172
"@types/lodash.isequal": "^4.5.8",
7273
"@types/mock-fs": "^4.13.4",
73-
"@types/uuid": "10.0.0",
7474
"@types/node": "^18",
7575
"@types/plist": "^3.0.5",
7676
"@types/semver": "^7.5.4",
77+
"@types/uuid": "10.0.0",
7778
"commander": "^12.1.0",
7879
"eslint": "^10.0.3",
79-
"eslint-config-oclif": "^6.0.146",
80-
"eslint-config-oclif-typescript": "^3.1.14",
80+
"eslint-config-oclif": "^6.0.156",
8181
"eslint-config-prettier": "^10.1.8",
8282
"glob": "^11.0.0",
8383
"merge-json-schemas": "^1.0.0",

scripts/runbook.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { query } from "@anthropic-ai/claude-agent-sdk";
2+
3+
const toolName = 'uv';
4+
const toolHomepage = 'https://docs.astral.sh/uv/#projects'
5+
6+
const researchResults = [
7+
`Here's a summary of the research and proposed design:
8+
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+
// }
102+
103+
// Checkout a new git branch
104+
// Launch a new docker container
105+
106+
for await (const message of query({
107+
prompt: `Use the research results to design a Codify resource for ${toolName} (the homepage is: ${toolHomepage}).
108+
109+
Guidelines:
110+
- Follow the other tools in the project under @src/resources/** as a guideline
111+
- Prefer to use Zod over JSON Schema
112+
- Remember to write tests, follow the other test examples under @test/** as a guideline
113+
- Keep the resource simple and focused on the core functionality of ${toolName}
114+
- Use the research to guide the software design
115+
- Remember to split up functions if they get too long and complicated to understand. Create helper functions instead with idiomatic names.
116+
117+
Steps:
118+
- Write code to fulfill the requirements laid out in the research.
119+
- Add the resource to @src/index.ts so that it is visible
120+
- Write tests for the code to test ${toolName}
121+
- 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.
124+
125+
Research:
126+
${researchResults.join('\n\n')}
127+
`,
128+
options: {
129+
settingSources: ['project'],
130+
permissionMode: "bypassPermissions", // Auto-approve file edits
131+
cwd: '../'
132+
}
133+
})) {
134+
// Print human-readable output
135+
if (message.type === "assistant" && message.message?.content) {
136+
for (const block of message.message.content) {
137+
if ("text" in block) {
138+
console.log(block.text); // Claude's reasoning
139+
} else if ("name" in block) {
140+
console.log(`Tool: ${block.name}`); // Tool being called
141+
}
142+
}
143+
} else if (message.type === "result") {
144+
console.log(`Done: ${message.subtype}`); // Final result
145+
}
146+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { PgcliResource } from './resources/pgcli/pgcli.js';
2626
import { Pip } from './resources/python/pip/pip.js';
2727
import { PipSync } from './resources/python/pip-sync/pip-sync.js';
2828
import { PyenvResource } from './resources/python/pyenv/pyenv.js';
29+
import { UvResource } from './resources/python/uv/uv.js';
2930
import { VenvProject } from './resources/python/venv/venv-project.js';
3031
import { Virtualenv } from './resources/python/virtualenv/virtualenv.js';
3132
import { VirtualenvProject } from './resources/python/virtualenv/virtualenv-project.js';
@@ -54,6 +55,7 @@ runPlugin(Plugin.create(
5455
new AliasesResource(),
5556
new HomebrewResource(),
5657
new PyenvResource(),
58+
new UvResource(),
5759
new GitLfsResource(),
5860
new AwsCliResource(),
5961
new AwsProfileResource(),
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { ArrayParameterSetting, ArrayStatefulParameter, getPty, SpawnStatus } from '@codifycli/plugin-core';
2+
3+
import { UvConfig } from './uv.js';
4+
5+
/**
6+
* uv python list --only-installed output example:
7+
* cpython-3.12.3-macos-aarch64-none
8+
* cpython-3.11.9-macos-aarch64-none
9+
*
10+
* We extract the version string (e.g. "3.12.3") from each line and match
11+
* against the user-specified prefix (e.g. "3.12").
12+
*/
13+
export class UvPythonVersionsParameter extends ArrayStatefulParameter<UvConfig, string> {
14+
getSettings(): ArrayParameterSetting {
15+
return {
16+
type: 'array',
17+
// desired "3.12" matches installed "3.12.3" via startsWith
18+
isElementEqual: (desired, current) => current.startsWith(desired),
19+
};
20+
}
21+
22+
override async refresh(desired: string[] | null): Promise<string[] | null> {
23+
const $ = getPty();
24+
25+
const { status, data } = await $.spawnSafe('uv python list --only-installed');
26+
if (status === SpawnStatus.ERROR) {
27+
return null;
28+
}
29+
30+
const installedVersions = parseInstalledPythonVersions(data);
31+
32+
// Replace full versions with the matching desired prefix so the framework
33+
// can treat them as equal (e.g. "3.12.3" → "3.12" when desired is "3.12").
34+
return normalizeToDesiredPrefixes(installedVersions, desired ?? []);
35+
}
36+
37+
override async addItem(version: string): Promise<void> {
38+
const $ = getPty();
39+
await $.spawn(`uv python install ${version}`, { interactive: true });
40+
}
41+
42+
override async removeItem(version: string): Promise<void> {
43+
const $ = getPty();
44+
await $.spawn(`uv python uninstall ${version}`, { interactive: true });
45+
}
46+
}
47+
48+
/** Extract semver strings like "3.12.3" from lines such as "cpython-3.12.3-macos-aarch64-none" */
49+
function parseInstalledPythonVersions(output: string): string[] {
50+
return output
51+
.split('\n')
52+
.map((line) => {
53+
const match = line.match(/cpython-(\d+\.\d+\.\d+)/);
54+
return match ? match[1] : null;
55+
})
56+
.filter((v): v is string => v !== null);
57+
}
58+
59+
/**
60+
* For each installed full version (e.g. "3.12.3"), if a desired prefix matches
61+
* it (e.g. "3.12"), replace the full version entry with the prefix so the
62+
* framework sees them as equal.
63+
*/
64+
function normalizeToDesiredPrefixes(installed: string[], desired: string[]): string[] {
65+
return installed.map((fullVersion) => {
66+
const matchedPrefix = desired.find((prefix) => fullVersion.startsWith(prefix));
67+
return matchedPrefix ?? fullVersion;
68+
});
69+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ArrayStatefulParameter, getPty, SpawnStatus } from '@codifycli/plugin-core';
2+
3+
import { UvConfig } from './uv.js';
4+
5+
/**
6+
* uv tool list output example:
7+
* ruff v0.4.4
8+
* - ruff
9+
* black v24.4.2
10+
* - black
11+
* - blackd
12+
*
13+
* We extract the tool names from lines that do NOT start with whitespace (the
14+
* header lines), taking everything before the first space.
15+
*/
16+
export class UvToolsParameter extends ArrayStatefulParameter<UvConfig, string> {
17+
override async refresh(desired: string[] | null): Promise<string[] | null> {
18+
const $ = getPty();
19+
20+
const { status, data } = await $.spawnSafe('uv tool list');
21+
if (status === SpawnStatus.ERROR) {
22+
return null;
23+
}
24+
25+
return parseInstalledTools(data);
26+
}
27+
28+
override async addItem(tool: string): Promise<void> {
29+
const $ = getPty();
30+
await $.spawn(`uv tool install ${tool}`, { interactive: true });
31+
}
32+
33+
override async removeItem(tool: string): Promise<void> {
34+
const $ = getPty();
35+
await $.spawn(`uv tool uninstall ${tool}`, { interactive: true });
36+
}
37+
}
38+
39+
/** Extract tool names from the header lines of `uv tool list` output */
40+
function parseInstalledTools(output: string): string[] {
41+
return output
42+
.split('\n')
43+
.filter((line) => line.length > 0 && !/^\s/.test(line))
44+
.map((line) => line.split(' ')[0].trim())
45+
.filter(Boolean);
46+
}

0 commit comments

Comments
 (0)