evalify-cli 0.1.2 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evalify-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CLI tool for the Evalify eval criteria registry",
5
5
  "homepage": "https://evalify.sh",
6
6
  "repository": "https://github.com/AppVerse-cc/evalify",
@@ -14,11 +14,13 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "chalk": "^5.4.1",
17
- "commander": "^12.1.0"
17
+ "commander": "^12.1.0",
18
+ "prompts": "^2.4.2"
18
19
  },
19
20
  "devDependencies": {
20
21
  "@evalify/frameworks": "workspace:*",
21
22
  "@types/node": "^22.0.0",
23
+ "@types/prompts": "^2.4.9",
22
24
  "tsup": "^8.5.1",
23
25
  "typescript": "^5.7.0"
24
26
  }
@@ -1,9 +1,34 @@
1
1
  import path from "node:path";
2
+ import os from "node:os";
2
3
  import fs from "node:fs/promises";
4
+ import prompts from "prompts";
3
5
  import { header, success, info, dim, error } from "../format.js";
4
6
 
5
7
  const REGISTRY_URL = "https://evalify.sh/api/registry";
6
8
 
9
+ async function detectSkills(skillsDir: string): Promise<{ name: string; evalCount: number }[]> {
10
+ try {
11
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true });
12
+ const skills: { name: string; evalCount: number }[] = [];
13
+
14
+ for (const entry of entries) {
15
+ if (!entry.isDirectory()) continue;
16
+ const evalsFile = path.join(skillsDir, entry.name, "evals.json");
17
+ try {
18
+ const content = await fs.readFile(evalsFile, "utf-8");
19
+ const parsed = JSON.parse(content);
20
+ skills.push({ name: entry.name, evalCount: (parsed.evals ?? []).length });
21
+ } catch {
22
+ // Folder exists but no valid evals.json — skip
23
+ }
24
+ }
25
+
26
+ return skills;
27
+ } catch {
28
+ return [];
29
+ }
30
+ }
31
+
7
32
  export async function pull(slug: string): Promise<void> {
8
33
  header();
9
34
 
@@ -47,9 +72,142 @@ export async function pull(slug: string): Promise<void> {
47
72
  return;
48
73
  }
49
74
 
50
- const targetDir = path.resolve(process.cwd(), "evals", slug);
75
+ // Ask install location
76
+ const locationResponse = await prompts({
77
+ type: "select",
78
+ name: "location",
79
+ message: "Where do you want to install?",
80
+ choices: [
81
+ {
82
+ title: "Current directory",
83
+ description: `evals/${pack.slug}/evals.json`,
84
+ value: "current",
85
+ },
86
+ {
87
+ title: "Project",
88
+ description: ".claude/skills/<name>/evals.json",
89
+ value: "project",
90
+ },
91
+ {
92
+ title: "Global",
93
+ description: "~/.claude/skills/<name>/evals.json",
94
+ value: "global",
95
+ },
96
+ ],
97
+ initial: 0,
98
+ });
99
+
100
+ if (!locationResponse.location) {
101
+ console.log();
102
+ dim("Cancelled.");
103
+ console.log();
104
+ return;
105
+ }
106
+
107
+ const location: "current" | "project" | "global" = locationResponse.location;
108
+
109
+ let skillName = pack.slug;
110
+
111
+ if (location === "project" || location === "global") {
112
+ const skillsDir =
113
+ location === "project"
114
+ ? path.resolve(process.cwd(), ".claude", "skills")
115
+ : path.resolve(os.homedir(), ".claude", "skills");
116
+
117
+ const existing = await detectSkills(skillsDir);
118
+
119
+ const baseChoices = existing.map((s) => ({
120
+ title: s.name,
121
+ description: `${s.evalCount} eval${s.evalCount !== 1 ? "s" : ""}`,
122
+ value: s.name,
123
+ }));
124
+
125
+ const pickResponse = await prompts({
126
+ type: "autocomplete",
127
+ name: "skill",
128
+ message:
129
+ existing.length > 0
130
+ ? `Skill folder (${existing.length} found — type to filter or create new):`
131
+ : "Skill folder:",
132
+ choices: baseChoices,
133
+ initial: pack.slug,
134
+ suggest: async (input: string, choices: any[]) => {
135
+ const term = (input || "").toLowerCase();
136
+ const filtered = choices.filter((c) => c.title.toLowerCase().includes(term));
137
+ const exactMatch = choices.find((c) => c.title === (input || pack.slug));
138
+ if (!exactMatch) {
139
+ filtered.push({ title: input || pack.slug, value: input || pack.slug });
140
+ }
141
+ return filtered;
142
+ },
143
+ });
144
+
145
+ if (!pickResponse.skill) {
146
+ console.log();
147
+ dim("Cancelled.");
148
+ console.log();
149
+ return;
150
+ }
151
+
152
+ skillName = (pickResponse.skill as string).trim();
153
+ }
154
+
155
+ let targetDir: string;
156
+ if (location === "current") {
157
+ targetDir = path.resolve(process.cwd(), "evals", pack.slug);
158
+ } else if (location === "project") {
159
+ targetDir = path.resolve(process.cwd(), ".claude", "skills", skillName);
160
+ } else {
161
+ targetDir = path.resolve(os.homedir(), ".claude", "skills", skillName);
162
+ }
163
+
51
164
  const targetFile = path.join(targetDir, "evals.json");
52
165
 
166
+ // Check for existing evals.json and ask append vs override
167
+ let writeMode: "override" | "append" = "override";
168
+ let existingEvals: { prompt: string; expectations: string[] }[] = [];
169
+
170
+ try {
171
+ const existing = await fs.readFile(targetFile, "utf-8");
172
+ const parsed = JSON.parse(existing);
173
+ existingEvals = parsed.evals ?? [];
174
+
175
+ if (existingEvals.length > 0) {
176
+ const conflictResponse = await prompts({
177
+ type: "select",
178
+ name: "mode",
179
+ message: `Found ${existingEvals.length} existing eval${existingEvals.length !== 1 ? "s" : ""} in ${skillName}. What do you want to do?`,
180
+ choices: [
181
+ {
182
+ title: "Append",
183
+ description: `Add ${pack.evals.length} new eval${pack.evals.length !== 1 ? "s" : ""} to the existing ${existingEvals.length}`,
184
+ value: "append",
185
+ },
186
+ {
187
+ title: "Override",
188
+ description: "Replace all existing evals with the pulled set",
189
+ value: "override",
190
+ },
191
+ ],
192
+ initial: 0,
193
+ });
194
+
195
+ if (!conflictResponse.mode) {
196
+ console.log();
197
+ dim("Cancelled.");
198
+ console.log();
199
+ return;
200
+ }
201
+
202
+ writeMode = conflictResponse.mode;
203
+ }
204
+ } catch {
205
+ // No existing file — fresh write
206
+ }
207
+
208
+ const evalsToWrite =
209
+ writeMode === "append" ? [...existingEvals, ...pack.evals] : pack.evals;
210
+
53
211
  try {
54
212
  await fs.mkdir(targetDir, { recursive: true });
55
213
 
@@ -61,21 +219,26 @@ export async function pull(slug: string): Promise<void> {
61
219
  domain: pack.domain,
62
220
  author: pack.author,
63
221
  tags: pack.tags,
64
- evals: pack.evals,
222
+ evals: evalsToWrite,
65
223
  };
66
224
 
67
225
  await fs.writeFile(targetFile, JSON.stringify(output, null, 2) + "\n");
68
226
 
227
+ const action = writeMode === "append" ? "Appended" : "Wrote";
228
+ console.log();
69
229
  success(`Pulled ${pack.displayName} v${pack.version}`);
70
- success(`Wrote ${pack.evals.length} eval${pack.evals.length !== 1 ? "s" : ""} to evals/${slug}/evals.json`);
230
+ success(
231
+ `${action} ${pack.evals.length} eval${pack.evals.length !== 1 ? "s" : ""}` +
232
+ (writeMode === "append" ? ` (${evalsToWrite.length} total)` : "") +
233
+ ` to ${targetFile}`
234
+ );
71
235
  console.log();
72
236
  dim(`Author: ${pack.author}`);
73
237
  dim(`Domain: ${pack.domain}`);
74
- dim(`Location: ${targetFile}`);
75
- dim(`To validate: evalify validate evals/${slug}`);
238
+ dim(`To validate: evalify validate ${targetDir}`);
76
239
  } catch (err) {
77
240
  error(`Failed to write file: ${(err as Error).message}`);
78
241
  }
79
242
 
80
243
  console.log();
81
- }
244
+ }