@ax-llm/ax 18.0.0 → 18.0.2

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/cli/index.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  * CLI for @ax-llm/ax
5
5
  *
6
6
  * Commands:
7
- * setup-claude [--force] Install/upgrade Claude Code skills to .claude/skills/ax/ (project-local)
7
+ * setup-claude [--force] Install/upgrade Claude Code skills to .claude/skills/<name>/SKILL.md (project-local)
8
8
  * remove-claude Remove all Claude Code skills
9
9
  *
10
10
  * Usage:
@@ -21,7 +21,7 @@ import {
21
21
  rmSync,
22
22
  writeFileSync,
23
23
  } from 'node:fs';
24
- import { dirname, join } from 'node:path';
24
+ import { basename, dirname, join } from 'node:path';
25
25
  import { fileURLToPath } from 'node:url';
26
26
 
27
27
  const __filename = fileURLToPath(import.meta.url);
@@ -30,8 +30,8 @@ const __dirname = dirname(__filename);
30
30
  // Skill files directory in the package
31
31
  const SKILLS_SOURCE_DIR = join(__dirname, '..', 'skills');
32
32
 
33
- // Target location in current working directory (project-local)
34
- const SKILL_TARGET_DIR = join(process.cwd(), '.claude', 'skills', 'ax');
33
+ // Base target location for skills in current working directory (project-local)
34
+ const SKILLS_BASE_DIR = join(process.cwd(), '.claude', 'skills');
35
35
 
36
36
  /**
37
37
  * Get all *.md skill files from the source directory
@@ -43,6 +43,20 @@ function getSkillFiles() {
43
43
  return readdirSync(SKILLS_SOURCE_DIR).filter((f) => f.endsWith('.md'));
44
44
  }
45
45
 
46
+ /**
47
+ * Get the skill name from YAML frontmatter, falling back to filename without .md
48
+ */
49
+ function getSkillName(filePath) {
50
+ try {
51
+ const content = readFileSync(filePath, 'utf8');
52
+ const match = content.match(/^name:\s*["']?([^"'\n\r]+)/m);
53
+ if (match) return match[1].trim();
54
+ } catch {
55
+ // Fall through to fallback
56
+ }
57
+ return basename(filePath, '.md');
58
+ }
59
+
46
60
  /**
47
61
  * Compare semver versions
48
62
  * Returns: 1 if a > b, -1 if a < b, 0 if equal
@@ -122,11 +136,28 @@ function setupClaude(force = false) {
122
136
  process.exit(1);
123
137
  }
124
138
 
139
+ // Clean up legacy flat file structure (.claude/skills/ax/*.md except SKILL.md)
140
+ const legacyDir = join(SKILLS_BASE_DIR, 'ax');
141
+ if (existsSync(legacyDir)) {
142
+ try {
143
+ const legacyFiles = readdirSync(legacyDir).filter(
144
+ (f) => f.endsWith('.md') && f !== 'SKILL.md'
145
+ );
146
+ for (const f of legacyFiles) {
147
+ rmSync(join(legacyDir, f), { force: true });
148
+ }
149
+ } catch {
150
+ // Ignore cleanup errors
151
+ }
152
+ }
153
+
125
154
  let allUpToDate = true;
126
155
 
127
156
  for (const file of skillFiles) {
128
157
  const skillSource = join(SKILLS_SOURCE_DIR, file);
129
- const skillTarget = join(SKILL_TARGET_DIR, file);
158
+ const skillName = getSkillName(skillSource);
159
+ const skillDir = join(SKILLS_BASE_DIR, skillName);
160
+ const skillTarget = join(skillDir, 'SKILL.md');
130
161
 
131
162
  const packageVersion = getPackageVersion(skillSource);
132
163
  const installedVersion = getInstalledVersion(skillTarget);
@@ -146,11 +177,11 @@ function setupClaude(force = false) {
146
177
  shouldInstall = true;
147
178
  action = 'Upgraded';
148
179
  } else if (comparison === 0) {
149
- console.log(`${file} is up to date (v${installedVersion})`);
180
+ console.log(`${skillName} is up to date (v${installedVersion})`);
150
181
  continue;
151
182
  } else {
152
183
  console.log(
153
- `${file} is already at v${installedVersion} (package has v${packageVersion})`
184
+ `${skillName} is already at v${installedVersion} (package has v${packageVersion})`
154
185
  );
155
186
  continue;
156
187
  }
@@ -160,15 +191,17 @@ function setupClaude(force = false) {
160
191
  }
161
192
 
162
193
  if (!shouldInstall) {
163
- console.log(`${file} is up to date (v${installedVersion || 'unknown'})`);
194
+ console.log(
195
+ `${skillName} is up to date (v${installedVersion || 'unknown'})`
196
+ );
164
197
  continue;
165
198
  }
166
199
 
167
200
  allUpToDate = false;
168
201
 
169
202
  // Create target directory if it doesn't exist
170
- if (!existsSync(SKILL_TARGET_DIR)) {
171
- mkdirSync(SKILL_TARGET_DIR, { recursive: true });
203
+ if (!existsSync(skillDir)) {
204
+ mkdirSync(skillDir, { recursive: true });
172
205
  }
173
206
 
174
207
  try {
@@ -177,15 +210,15 @@ function setupClaude(force = false) {
177
210
 
178
211
  if (action === 'Upgraded' && installedVersion && packageVersion) {
179
212
  console.log(
180
- `\u2713 ${action} ${file} (v${installedVersion} \u2192 v${packageVersion})`
213
+ `\u2713 ${action} ${skillName} (v${installedVersion} \u2192 v${packageVersion})`
181
214
  );
182
215
  } else {
183
216
  console.log(
184
- `\u2713 ${action} ${file} (v${packageVersion || 'unknown'})`
217
+ `\u2713 ${action} ${skillName} (v${packageVersion || 'unknown'})`
185
218
  );
186
219
  }
187
220
  } catch (err) {
188
- console.error(`Error installing ${file}: ${err.message}`);
221
+ console.error(`Error installing ${skillName}: ${err.message}`);
189
222
  process.exit(1);
190
223
  }
191
224
  }
@@ -199,39 +232,54 @@ function setupClaude(force = false) {
199
232
  * Remove all Claude Code skills
200
233
  */
201
234
  function removeClaude() {
202
- if (!existsSync(SKILL_TARGET_DIR)) {
203
- console.log('Ax Claude Code skills are not installed.');
235
+ const skillFiles = getSkillFiles();
236
+
237
+ if (skillFiles.length === 0) {
238
+ console.log('No skill definitions found in package.');
204
239
  return;
205
240
  }
206
241
 
207
242
  try {
208
- const installedFiles = readdirSync(SKILL_TARGET_DIR).filter((f) =>
209
- f.endsWith('.md')
210
- );
211
-
212
- if (installedFiles.length === 0) {
213
- console.log('Ax Claude Code skills are not installed.');
214
- return;
215
- }
216
-
217
- for (const file of installedFiles) {
218
- rmSync(join(SKILL_TARGET_DIR, file), { force: true });
243
+ let removedCount = 0;
244
+
245
+ for (const file of skillFiles) {
246
+ const skillName = getSkillName(join(SKILLS_SOURCE_DIR, file));
247
+ const skillDir = join(SKILLS_BASE_DIR, skillName);
248
+ const skillTarget = join(skillDir, 'SKILL.md');
249
+
250
+ if (existsSync(skillTarget)) {
251
+ rmSync(skillTarget, { force: true });
252
+ removedCount++;
253
+
254
+ // Try to remove the directory if empty
255
+ try {
256
+ const remaining = readdirSync(skillDir);
257
+ if (remaining.length === 0) {
258
+ rmSync(skillDir, { recursive: true, force: true });
259
+ }
260
+ } catch {
261
+ // Ignore errors when trying to clean up directory
262
+ }
263
+ }
219
264
  }
220
265
 
221
- // Try to remove the directory if empty
222
- try {
223
- const remaining = readdirSync(SKILL_TARGET_DIR);
224
- if (remaining.length === 0) {
225
- rmSync(SKILL_TARGET_DIR, { recursive: true, force: true });
266
+ // Also clean up legacy flat files from old .claude/skills/ax/ structure
267
+ const legacyDir = join(SKILLS_BASE_DIR, 'ax');
268
+ if (existsSync(legacyDir)) {
269
+ const legacyFiles = readdirSync(legacyDir).filter(
270
+ (f) => f.endsWith('.md') && f !== 'SKILL.md'
271
+ );
272
+ for (const f of legacyFiles) {
273
+ rmSync(join(legacyDir, f), { force: true });
226
274
  }
227
- } catch {
228
- // Ignore errors when trying to clean up directory
229
275
  }
230
276
 
231
- const noun = installedFiles.length === 1 ? 'skill' : 'skills';
232
- console.log(
233
- `\u2713 Removed ${installedFiles.length} Ax Claude Code ${noun}`
234
- );
277
+ if (removedCount > 0) {
278
+ const noun = removedCount === 1 ? 'skill' : 'skills';
279
+ console.log(`\u2713 Removed ${removedCount} Ax Claude Code ${noun}`);
280
+ } else {
281
+ console.log('Ax Claude Code skills are not installed.');
282
+ }
235
283
  } catch (err) {
236
284
  console.error(`Error removing skills: ${err.message}`);
237
285
  process.exit(1);