@cuewright/skills 0.2.0 → 0.2.1
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 +1 -1
- package/src/commands/doctor.js +14 -16
- package/src/commands/update.js +19 -62
- package/src/utils/symlink.js +141 -1
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { access, readFile, lstat } from 'fs/promises';
|
|
2
|
+
import { access, readFile, lstat } from 'fs/promises'; // lstat used in checks 3-4
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { cwd } from 'process';
|
|
5
5
|
import { readManifest, listSkills, getVersion } from '../utils/manifest.js';
|
|
6
|
+
import { inventorySkillsDir } from '../utils/symlink.js';
|
|
6
7
|
import { getSkillsDir, getFrameworkDir } from '../adapters/claude.js';
|
|
7
8
|
|
|
8
9
|
export async function doctor() {
|
|
@@ -75,32 +76,29 @@ export async function doctor() {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
// 4b.
|
|
79
|
-
const
|
|
80
|
-
const legacyFound = [];
|
|
81
|
-
for (const name of legacySkillNames) {
|
|
82
|
-
const legacyPath = join(skillsDir, name);
|
|
83
|
-
try {
|
|
84
|
-
const stat = await lstat(legacyPath);
|
|
85
|
-
if (stat.isDirectory()) {
|
|
86
|
-
legacyFound.push(name);
|
|
87
|
-
}
|
|
88
|
-
} catch {
|
|
89
|
-
// Not present — good
|
|
90
|
-
}
|
|
91
|
-
}
|
|
79
|
+
// 4b. Inventory skills directory — detect legacy and orphaned entries
|
|
80
|
+
const { legacy: legacyFound, orphaned: orphanedFound } = await inventorySkillsDir(skillsDir, skills);
|
|
92
81
|
|
|
93
82
|
if (legacyFound.length > 0) {
|
|
94
83
|
allPassed = false;
|
|
95
84
|
checks.push(fail(
|
|
96
85
|
`${legacyFound.length} legacy skill director${legacyFound.length === 1 ? 'y' : 'ies'} found (pre-v0.15.0 install)`,
|
|
97
|
-
'Run `cuewright update` to
|
|
86
|
+
'Run `cuewright update` to clean up automatically.'
|
|
98
87
|
));
|
|
99
88
|
for (const name of legacyFound) {
|
|
100
89
|
checks.push(fail(` .claude/skills/${name}/`, null));
|
|
101
90
|
}
|
|
102
91
|
}
|
|
103
92
|
|
|
93
|
+
if (orphanedFound.length > 0) {
|
|
94
|
+
checks.push(warn(
|
|
95
|
+
`${orphanedFound.length} unknown director${orphanedFound.length === 1 ? 'y' : 'ies'} in .claude/skills/ (third-party or project-local — no action needed)`
|
|
96
|
+
));
|
|
97
|
+
for (const name of orphanedFound) {
|
|
98
|
+
checks.push(warn(` .claude/skills/${name}/`));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
104
102
|
// 5. arch/project-config.md exists
|
|
105
103
|
const configPath = join(projectRoot, '.claude', 'skills', 'arch', 'project-config.md');
|
|
106
104
|
if (!await exists(configPath)) {
|
package/src/commands/update.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { access, readFile, unlink,
|
|
2
|
+
import { access, readFile, unlink, mkdir } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { cwd } from 'process';
|
|
5
5
|
import { readManifest, listSkills, getVersion } from '../utils/manifest.js';
|
|
6
|
-
import { syncSymlinks, syncCopies, platformSupportsSymlinks,
|
|
6
|
+
import { syncSymlinks, syncCopies, platformSupportsSymlinks, cleanupLegacyDirectories } from '../utils/symlink.js';
|
|
7
7
|
import { updateSubmodule, fetchTags, getLatestTag, getLatestTagFromRemote, configureSparseCheckout } from '../storage/submodule.js';
|
|
8
8
|
import { cloneFramework } from '../storage/copy.js';
|
|
9
9
|
import { getSkillsDir, getFrameworkDir } from '../adapters/claude.js';
|
|
@@ -43,7 +43,8 @@ export async function update(options) {
|
|
|
43
43
|
const targetVersion = (options.to || await resolveLatestTag(frameworkDir, isSubmodule)).replace(/^v/, '');
|
|
44
44
|
|
|
45
45
|
if (targetVersion === oldVersion) {
|
|
46
|
-
console.log(chalk.cyan(`Already at v${oldVersion} (latest)
|
|
46
|
+
console.log(chalk.cyan(`Already at v${oldVersion} (latest).`));
|
|
47
|
+
await runCleanup(skillsDir, listSkills(oldManifest), projectRoot, dryRun);
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -74,11 +75,10 @@ export async function update(options) {
|
|
|
74
75
|
console.log(`New skills: ${added.join(', ')}`);
|
|
75
76
|
if (!dryRun) {
|
|
76
77
|
const useCopies = !platformSupportsSymlinks();
|
|
77
|
-
|
|
78
|
-
await syncCopies(frameworkDir, skillsDir, added, targetVersion)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
78
|
+
const { errors: syncErrors } = useCopies
|
|
79
|
+
? await syncCopies(frameworkDir, skillsDir, added, targetVersion)
|
|
80
|
+
: await syncSymlinks(frameworkDir, skillsDir, added);
|
|
81
|
+
for (const e of syncErrors) console.log(chalk.yellow(` warning: ${e}`));
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -87,29 +87,22 @@ export async function update(options) {
|
|
|
87
87
|
const skillPath = join(skillsDir, name);
|
|
88
88
|
if (await exists(skillPath)) {
|
|
89
89
|
const isRealDir = await isRealDirectory(skillPath);
|
|
90
|
-
if (isRealDir) {
|
|
91
|
-
// Check if this is a cw- rename migration (old unprefixed name → cw-name)
|
|
92
|
-
const isMigrationRename = newSkills.has(`cw-${name}`);
|
|
93
|
-
if (isMigrationRename) {
|
|
94
|
-
if (!dryRun) {
|
|
95
|
-
await migrateSkillDirectory(skillPath, name, skillsDir, projectRoot);
|
|
96
|
-
} else {
|
|
97
|
-
console.log(chalk.yellow(` migrate: ${name}/ → arch/references/${name}/ (reference files) + remove SKILL.md`));
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
console.log(chalk.yellow(` warning: '${name}' removed from framework. Local directory at .claude/skills/${name}/ left in place.`));
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
90
|
+
if (!isRealDir) {
|
|
103
91
|
// Symlink to now-missing target — clean it up
|
|
104
92
|
if (!dryRun) {
|
|
105
93
|
await unlink(skillPath);
|
|
106
94
|
}
|
|
107
95
|
console.log(` Removed stale symlink: ${name}`);
|
|
96
|
+
} else {
|
|
97
|
+
console.log(chalk.yellow(` warning: '${name}' removed from framework. Local directory at .claude/skills/${name}/ left in place.`));
|
|
108
98
|
}
|
|
109
99
|
}
|
|
110
100
|
}
|
|
111
101
|
}
|
|
112
102
|
|
|
103
|
+
// Always run legacy cleanup — catches dirs that survived the diff-based migration
|
|
104
|
+
await runCleanup(skillsDir, [...newSkills], projectRoot, dryRun);
|
|
105
|
+
|
|
113
106
|
// Scaffold arch/references/ and arch/templates/ if missing
|
|
114
107
|
if (!dryRun) {
|
|
115
108
|
const archDir = join(projectRoot, '.claude', 'skills', 'arch');
|
|
@@ -143,48 +136,12 @@ export async function update(options) {
|
|
|
143
136
|
}
|
|
144
137
|
}
|
|
145
138
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
* - Remove the now-empty directory
|
|
151
|
-
*/
|
|
152
|
-
async function migrateSkillDirectory(skillPath, name, skillsDir, projectRoot) {
|
|
153
|
-
const archReferencesDir = join(projectRoot, '.claude', 'skills', 'arch', 'references', name);
|
|
154
|
-
|
|
155
|
-
let entries;
|
|
156
|
-
try {
|
|
157
|
-
entries = await readdir(skillPath);
|
|
158
|
-
} catch {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const referenceFiles = entries.filter(e => e !== 'SKILL.md');
|
|
163
|
-
|
|
164
|
-
if (referenceFiles.length > 0) {
|
|
165
|
-
await mkdir(archReferencesDir, { recursive: true });
|
|
166
|
-
for (const file of referenceFiles) {
|
|
167
|
-
const src = join(skillPath, file);
|
|
168
|
-
const dest = join(archReferencesDir, file);
|
|
169
|
-
await rename(src, dest);
|
|
170
|
-
}
|
|
171
|
-
console.log(` Moved ${referenceFiles.length} reference file(s) to arch/references/${name}/`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Remove SKILL.md and the directory
|
|
175
|
-
try {
|
|
176
|
-
await unlink(join(skillPath, 'SKILL.md'));
|
|
177
|
-
} catch {
|
|
178
|
-
// Already gone or never existed
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
const { rmdir } = await import('fs/promises');
|
|
183
|
-
await rmdir(skillPath);
|
|
184
|
-
console.log(` Removed legacy skill directory: ${name}/`);
|
|
185
|
-
} catch {
|
|
186
|
-
console.log(chalk.yellow(` warning: Could not remove .claude/skills/${name}/ — remove it manually.`));
|
|
139
|
+
async function runCleanup(skillsDir, manifestSkillNames, projectRoot, dryRun) {
|
|
140
|
+
const { cleaned, errors } = await cleanupLegacyDirectories(skillsDir, manifestSkillNames, projectRoot, { dryRun });
|
|
141
|
+
if (cleaned.length > 0) {
|
|
142
|
+
console.log(chalk.green(` Cleaned up ${cleaned.length} legacy director${cleaned.length === 1 ? 'y' : 'ies'}: ${cleaned.join(', ')}`));
|
|
187
143
|
}
|
|
144
|
+
for (const e of errors) console.log(chalk.yellow(` warning: ${e}`));
|
|
188
145
|
}
|
|
189
146
|
|
|
190
147
|
/**
|
package/src/utils/symlink.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { symlink, mkdir, access, lstat, readFile, writeFile } from 'fs/promises';
|
|
1
|
+
import { symlink, mkdir, access, lstat, readFile, writeFile, readdir, unlink, rename } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -129,3 +129,143 @@ export function buildHeader(skillName, version) {
|
|
|
129
129
|
export function platformSupportsSymlinks() {
|
|
130
130
|
return process.platform !== 'win32';
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Remove old unprefixed skill directories that correspond to cw-* framework skills.
|
|
135
|
+
*
|
|
136
|
+
* This runs independently of the manifest diff, so it catches directories that
|
|
137
|
+
* survived the initial migration (e.g., rmdir failed due to non-empty content)
|
|
138
|
+
* as well as directories from even older installs that were never in a manifest.
|
|
139
|
+
*
|
|
140
|
+
* For each cw-* skill in manifestSkillNames, checks for a real directory or
|
|
141
|
+
* symlink at skillsDir/<unprefixed-name>/ and cleans it up:
|
|
142
|
+
* - Symlink: unlinked (was an old framework pointer)
|
|
143
|
+
* - Real directory: reference files moved to arch/references/<name>/, SKILL.md
|
|
144
|
+
* removed, directory rmdir'd
|
|
145
|
+
*
|
|
146
|
+
* Returns { cleaned: string[], errors: string[] }
|
|
147
|
+
*/
|
|
148
|
+
export async function cleanupLegacyDirectories(skillsDir, manifestSkillNames, projectRoot, { dryRun = false } = {}) {
|
|
149
|
+
const cleaned = [];
|
|
150
|
+
const errors = [];
|
|
151
|
+
const { join } = await import('path');
|
|
152
|
+
const { rmdir } = await import('fs/promises');
|
|
153
|
+
|
|
154
|
+
for (const cwName of manifestSkillNames) {
|
|
155
|
+
if (!cwName.startsWith('cw-')) continue;
|
|
156
|
+
const unprefixed = cwName.slice(3);
|
|
157
|
+
const legacyPath = join(skillsDir, unprefixed);
|
|
158
|
+
|
|
159
|
+
let stat;
|
|
160
|
+
try {
|
|
161
|
+
stat = await lstat(legacyPath);
|
|
162
|
+
} catch {
|
|
163
|
+
continue; // Does not exist — nothing to do
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (stat.isSymbolicLink()) {
|
|
167
|
+
if (!dryRun) {
|
|
168
|
+
try {
|
|
169
|
+
await unlink(legacyPath);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
errors.push(`${unprefixed}: could not remove symlink — ${err.message}`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
cleaned.push(unprefixed);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!stat.isDirectory()) continue;
|
|
180
|
+
|
|
181
|
+
// Real directory — migrate reference files, then remove
|
|
182
|
+
let entries;
|
|
183
|
+
try {
|
|
184
|
+
entries = await readdir(legacyPath);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
errors.push(`${unprefixed}: could not read directory — ${err.message}`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const referenceFiles = entries.filter(e => e !== 'SKILL.md');
|
|
191
|
+
|
|
192
|
+
if (referenceFiles.length > 0 && !dryRun) {
|
|
193
|
+
const refDir = join(projectRoot, '.claude', 'skills', 'arch', 'references', unprefixed);
|
|
194
|
+
try {
|
|
195
|
+
await mkdir(refDir, { recursive: true });
|
|
196
|
+
for (const file of referenceFiles) {
|
|
197
|
+
await rename(join(legacyPath, file), join(refDir, file));
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
errors.push(`${unprefixed}: could not migrate reference files — ${err.message}`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!dryRun) {
|
|
206
|
+
try {
|
|
207
|
+
await unlink(join(legacyPath, 'SKILL.md'));
|
|
208
|
+
} catch {
|
|
209
|
+
// Already gone or never existed — fine
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
await rmdir(legacyPath);
|
|
214
|
+
} catch {
|
|
215
|
+
errors.push(`${unprefixed}: directory not empty after migration — remove .claude/skills/${unprefixed}/ manually`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
cleaned.push(unprefixed);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { cleaned, errors };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Scan skillsDir and categorize every entry.
|
|
228
|
+
*
|
|
229
|
+
* Returns:
|
|
230
|
+
* {
|
|
231
|
+
* legacy: string[], // unprefixed dirs matching a cw-* manifest skill
|
|
232
|
+
* orphaned: string[], // dirs that don't match any manifest skill or reserved name
|
|
233
|
+
* }
|
|
234
|
+
*
|
|
235
|
+
* Reserved names (skipped): _framework, _baseline, arch, worktrees
|
|
236
|
+
* Framework skills (cw-*) are not included in the return — they're already
|
|
237
|
+
* checked by the main doctor health checks.
|
|
238
|
+
*/
|
|
239
|
+
export async function inventorySkillsDir(skillsDir, manifestSkillNames) {
|
|
240
|
+
const { join } = await import('path');
|
|
241
|
+
const legacy = [];
|
|
242
|
+
const orphaned = [];
|
|
243
|
+
|
|
244
|
+
const RESERVED = new Set(['_framework', '_baseline', 'arch', 'worktrees']);
|
|
245
|
+
const unprefixedNames = new Set(
|
|
246
|
+
manifestSkillNames.filter(n => n.startsWith('cw-')).map(n => n.slice(3))
|
|
247
|
+
);
|
|
248
|
+
const cwNames = new Set(manifestSkillNames);
|
|
249
|
+
|
|
250
|
+
let entries;
|
|
251
|
+
try {
|
|
252
|
+
entries = await readdir(skillsDir, { withFileTypes: true });
|
|
253
|
+
} catch {
|
|
254
|
+
return { legacy, orphaned };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const entry of entries) {
|
|
258
|
+
const name = entry.name;
|
|
259
|
+
if (RESERVED.has(name)) continue;
|
|
260
|
+
if (cwNames.has(name)) continue; // framework skill — handled by main checks
|
|
261
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
262
|
+
|
|
263
|
+
if (unprefixedNames.has(name)) {
|
|
264
|
+
legacy.push(name);
|
|
265
|
+
} else {
|
|
266
|
+
orphaned.push(name);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { legacy, orphaned };
|
|
271
|
+
}
|