@cuewright/skills 0.1.0 → 0.2.0
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 +2 -2
- package/src/commands/doctor.js +35 -1
- package/src/commands/init.js +15 -1
- package/src/commands/update.js +97 -12
- package/src/storage/copy.js +2 -1
- package/src/storage/submodule.js +58 -13
- package/src/utils/project-config.js +25 -0
- package/src/utils/symlink.js +11 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cuewright/skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI installer for the Cuewright skills framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"license": "MIT",
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
|
40
|
-
"url": "https://github.com/scriswell/cuewright-cli.git"
|
|
40
|
+
"url": "git+https://github.com/scriswell/cuewright-cli.git"
|
|
41
41
|
},
|
|
42
42
|
"homepage": "https://github.com/scriswell/cuewright-cli#readme"
|
|
43
43
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -63,7 +63,7 @@ export async function doctor() {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
if (brokenLinks.length === 0) {
|
|
66
|
-
checks.push(pass(`${wiredCount}/${skills.length} skills wired`));
|
|
66
|
+
checks.push(pass(`${wiredCount}/${skills.length} skills wired (cw- prefix)`));
|
|
67
67
|
} else {
|
|
68
68
|
allPassed = false;
|
|
69
69
|
checks.push(fail(
|
|
@@ -75,6 +75,32 @@ export async function doctor() {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// 4b. Check for legacy unprefixed skill directories (old install, needs migration)
|
|
79
|
+
const legacySkillNames = skills.map(s => s.replace(/^cw-/, ''));
|
|
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
|
+
}
|
|
92
|
+
|
|
93
|
+
if (legacyFound.length > 0) {
|
|
94
|
+
allPassed = false;
|
|
95
|
+
checks.push(fail(
|
|
96
|
+
`${legacyFound.length} legacy skill director${legacyFound.length === 1 ? 'y' : 'ies'} found (pre-v0.15.0 install)`,
|
|
97
|
+
'Run `cuewright update` to migrate to cw- prefix and move reference files to arch/references/.'
|
|
98
|
+
));
|
|
99
|
+
for (const name of legacyFound) {
|
|
100
|
+
checks.push(fail(` .claude/skills/${name}/`, null));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
78
104
|
// 5. arch/project-config.md exists
|
|
79
105
|
const configPath = join(projectRoot, '.claude', 'skills', 'arch', 'project-config.md');
|
|
80
106
|
if (!await exists(configPath)) {
|
|
@@ -107,6 +133,14 @@ export async function doctor() {
|
|
|
107
133
|
}
|
|
108
134
|
}
|
|
109
135
|
|
|
136
|
+
// 6b. arch/references/ exists
|
|
137
|
+
const referencesDir = join(projectRoot, '.claude', 'skills', 'arch', 'references');
|
|
138
|
+
if (!await exists(referencesDir)) {
|
|
139
|
+
checks.push(warn('arch/references/ not found — skill reference files have no home. Run `cuewright update` to scaffold it.'));
|
|
140
|
+
} else {
|
|
141
|
+
checks.push(pass('arch/references/ found'));
|
|
142
|
+
}
|
|
143
|
+
|
|
110
144
|
// 7. Baseline compatibility check (if baseline present)
|
|
111
145
|
const baselineDir = join(projectRoot, '.claude', 'skills', '_baseline');
|
|
112
146
|
if (await exists(baselineDir)) {
|
package/src/commands/init.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join } from 'path';
|
|
|
4
4
|
import { cwd } from 'process';
|
|
5
5
|
import { readManifest, listSkills, getVersion } from '../utils/manifest.js';
|
|
6
6
|
import { syncSymlinks, syncCopies, platformSupportsSymlinks } from '../utils/symlink.js';
|
|
7
|
-
import { addSubmodule } from '../storage/submodule.js';
|
|
7
|
+
import { addSubmodule, configureSparseCheckout } from '../storage/submodule.js';
|
|
8
8
|
import { cloneFramework } from '../storage/copy.js';
|
|
9
9
|
import { getSkillsDir, getFrameworkDir, getSubmodulePath } from '../adapters/claude.js';
|
|
10
10
|
|
|
@@ -76,6 +76,7 @@ async function freshInstall(projectRoot, frameworkDir, skillsDir, options) {
|
|
|
76
76
|
if (useSubmodule) {
|
|
77
77
|
console.log('Adding git submodule...');
|
|
78
78
|
await addSubmodule(projectRoot, getSubmodulePath(), tag);
|
|
79
|
+
await configureSparseCheckout(frameworkDir);
|
|
79
80
|
} else {
|
|
80
81
|
console.log('Cloning framework (copy mode)...');
|
|
81
82
|
await cloneFramework(frameworkDir, tag);
|
|
@@ -96,6 +97,7 @@ async function freshInstall(projectRoot, frameworkDir, skillsDir, options) {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
await scaffoldConfigIfMissing(projectRoot, frameworkDir);
|
|
100
|
+
await scaffoldArchDirectories(projectRoot);
|
|
99
101
|
|
|
100
102
|
console.log(chalk.green(`\nDone.`));
|
|
101
103
|
console.log(` Framework: v${version}`);
|
|
@@ -127,6 +129,18 @@ async function scaffoldConfigIfMissing(projectRoot, frameworkDir) {
|
|
|
127
129
|
console.log(' Scaffolded arch/project-config.md');
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Scaffold arch/references/ and arch/templates/ subdirectories if they don't exist.
|
|
134
|
+
* These are the project customization roots under the cw- prefix model.
|
|
135
|
+
*/
|
|
136
|
+
async function scaffoldArchDirectories(projectRoot) {
|
|
137
|
+
const archDir = join(projectRoot, '.claude', 'skills', 'arch');
|
|
138
|
+
await mkdir(join(archDir, 'references'), { recursive: true });
|
|
139
|
+
await mkdir(join(archDir, 'templates', 'page'), { recursive: true });
|
|
140
|
+
await mkdir(join(archDir, 'templates', 'component'), { recursive: true });
|
|
141
|
+
await mkdir(join(archDir, 'templates', 'guide'), { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
|
|
130
144
|
async function exists(path) {
|
|
131
145
|
try {
|
|
132
146
|
await access(path);
|
package/src/commands/update.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { access, readFile, unlink } from 'fs/promises';
|
|
2
|
+
import { access, readFile, unlink, readdir, rename, 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
6
|
import { syncSymlinks, syncCopies, platformSupportsSymlinks, buildHeader } from '../utils/symlink.js';
|
|
7
|
-
import { updateSubmodule, getLatestTag } from '../storage/submodule.js';
|
|
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';
|
|
10
|
+
import { updateProjectConfigVersion } from '../utils/project-config.js';
|
|
10
11
|
import simpleGit from 'simple-git';
|
|
11
12
|
|
|
12
13
|
export async function update(options) {
|
|
@@ -29,8 +30,17 @@ export async function update(options) {
|
|
|
29
30
|
const isSubmodule = await exists(join(projectRoot, '.gitmodules'))
|
|
30
31
|
&& await isSubmoduleInstall(projectRoot, frameworkDir);
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
-
|
|
33
|
+
// Fetch remote tags before resolving latest — ensures local tag list is current
|
|
34
|
+
if (isSubmodule && !options.to) {
|
|
35
|
+
try {
|
|
36
|
+
await fetchTags(frameworkDir);
|
|
37
|
+
} catch {
|
|
38
|
+
// Non-fatal — fallback to remote query below
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Determine target version — always bare (no leading v)
|
|
43
|
+
const targetVersion = (options.to || await resolveLatestTag(frameworkDir, isSubmodule)).replace(/^v/, '');
|
|
34
44
|
|
|
35
45
|
if (targetVersion === oldVersion) {
|
|
36
46
|
console.log(chalk.cyan(`Already at v${oldVersion} (latest). Nothing to update.`));
|
|
@@ -46,6 +56,7 @@ export async function update(options) {
|
|
|
46
56
|
if (!dryRun) {
|
|
47
57
|
if (isSubmodule) {
|
|
48
58
|
await updateSubmodule(projectRoot, '.claude/skills/_framework', targetVersion);
|
|
59
|
+
await configureSparseCheckout(frameworkDir);
|
|
49
60
|
} else {
|
|
50
61
|
await cloneFramework(frameworkDir, targetVersion);
|
|
51
62
|
}
|
|
@@ -75,20 +86,39 @@ export async function update(options) {
|
|
|
75
86
|
for (const name of removed) {
|
|
76
87
|
const skillPath = join(skillsDir, name);
|
|
77
88
|
if (await exists(skillPath)) {
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
|
|
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
|
+
}
|
|
81
102
|
} else {
|
|
82
|
-
//
|
|
103
|
+
// Symlink to now-missing target — clean it up
|
|
83
104
|
if (!dryRun) {
|
|
84
105
|
await unlink(skillPath);
|
|
85
106
|
}
|
|
86
|
-
console.log(` Removed
|
|
107
|
+
console.log(` Removed stale symlink: ${name}`);
|
|
87
108
|
}
|
|
88
109
|
}
|
|
89
110
|
}
|
|
90
111
|
}
|
|
91
112
|
|
|
113
|
+
// Scaffold arch/references/ and arch/templates/ if missing
|
|
114
|
+
if (!dryRun) {
|
|
115
|
+
const archDir = join(projectRoot, '.claude', 'skills', 'arch');
|
|
116
|
+
await mkdir(join(archDir, 'references'), { recursive: true });
|
|
117
|
+
await mkdir(join(archDir, 'templates', 'page'), { recursive: true });
|
|
118
|
+
await mkdir(join(archDir, 'templates', 'component'), { recursive: true });
|
|
119
|
+
await mkdir(join(archDir, 'templates', 'guide'), { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
92
122
|
// Print changelog for the updated range
|
|
93
123
|
const changelogPath = join(frameworkDir, 'CHANGELOG.md');
|
|
94
124
|
if (await exists(changelogPath)) {
|
|
@@ -100,6 +130,12 @@ export async function update(options) {
|
|
|
100
130
|
}
|
|
101
131
|
}
|
|
102
132
|
|
|
133
|
+
// Update version in project-config.md
|
|
134
|
+
if (!dryRun) {
|
|
135
|
+
const configPath = join(projectRoot, '.claude', 'skills', 'arch', 'project-config.md');
|
|
136
|
+
await updateProjectConfigVersion(configPath, targetVersion);
|
|
137
|
+
}
|
|
138
|
+
|
|
103
139
|
if (!dryRun) {
|
|
104
140
|
console.log(chalk.green(`\nUpdated to v${targetVersion}.`));
|
|
105
141
|
} else {
|
|
@@ -108,14 +144,63 @@ export async function update(options) {
|
|
|
108
144
|
}
|
|
109
145
|
|
|
110
146
|
/**
|
|
111
|
-
*
|
|
147
|
+
* Migrate an old skill directory during the cw- rename:
|
|
148
|
+
* - Move any non-SKILL.md files to arch/references/<name>/
|
|
149
|
+
* - Remove the SKILL.md (it was a framework copy, overrides are gone)
|
|
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.`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolve the latest semver tag from the installed framework.
|
|
112
192
|
*/
|
|
113
|
-
async function resolveLatestTag(frameworkDir) {
|
|
193
|
+
async function resolveLatestTag(frameworkDir, isSubmodule) {
|
|
114
194
|
try {
|
|
115
195
|
const git = simpleGit(frameworkDir);
|
|
116
196
|
return await getLatestTag(git);
|
|
117
197
|
} catch {
|
|
118
|
-
|
|
198
|
+
// Shallow clone or no local tags — query remote directly
|
|
199
|
+
try {
|
|
200
|
+
return await getLatestTagFromRemote();
|
|
201
|
+
} catch {
|
|
202
|
+
throw new Error('Could not determine latest framework version. Use --to <version> to specify one.');
|
|
203
|
+
}
|
|
119
204
|
}
|
|
120
205
|
}
|
|
121
206
|
|
package/src/storage/copy.js
CHANGED
|
@@ -14,5 +14,6 @@ export async function cloneFramework(targetDir, tag) {
|
|
|
14
14
|
await mkdir(targetDir, { recursive: true });
|
|
15
15
|
|
|
16
16
|
const git = simpleGit();
|
|
17
|
-
|
|
17
|
+
const gitTag = tag ? (tag.startsWith('v') ? tag : `v${tag}`) : undefined;
|
|
18
|
+
await git.clone(FRAMEWORK_REPO, targetDir, ['--depth', '1', ...(gitTag ? ['--branch', gitTag] : [])]);
|
|
18
19
|
}
|
package/src/storage/submodule.js
CHANGED
|
@@ -5,6 +5,7 @@ const FRAMEWORK_REPO = 'https://github.com/scriswell/cuewright';
|
|
|
5
5
|
/**
|
|
6
6
|
* Add the framework as a git submodule.
|
|
7
7
|
* If tag is provided, checks out that tag after adding.
|
|
8
|
+
* Tag may be bare (0.15.0) or prefixed (v0.15.0) — normalized internally.
|
|
8
9
|
*/
|
|
9
10
|
export async function addSubmodule(projectRoot, submodulePath, tag) {
|
|
10
11
|
const git = simpleGit(projectRoot);
|
|
@@ -13,7 +14,7 @@ export async function addSubmodule(projectRoot, submodulePath, tag) {
|
|
|
13
14
|
|
|
14
15
|
if (tag) {
|
|
15
16
|
const subGit = simpleGit(`${projectRoot}/${submodulePath}`);
|
|
16
|
-
await subGit.checkout(tag);
|
|
17
|
+
await subGit.checkout(toGitTag(tag));
|
|
17
18
|
|
|
18
19
|
// Update the superproject's submodule pointer to this commit
|
|
19
20
|
await git.submoduleUpdate(['--init', submodulePath]);
|
|
@@ -22,24 +23,27 @@ export async function addSubmodule(projectRoot, submodulePath, tag) {
|
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Update an existing submodule to a specific tag (or latest if no tag given).
|
|
25
|
-
*
|
|
26
|
+
* Caller is responsible for fetching tags before calling this.
|
|
26
27
|
*/
|
|
27
28
|
export async function updateSubmodule(projectRoot, submodulePath, tag) {
|
|
28
29
|
const subGit = simpleGit(`${projectRoot}/${submodulePath}`);
|
|
30
|
+
const gitTag = tag ? toGitTag(tag) : toGitTag(await getLatestTag(subGit));
|
|
31
|
+
await subGit.checkout(gitTag);
|
|
32
|
+
}
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
await subGit.checkout(latestTag);
|
|
38
|
-
}
|
|
34
|
+
/**
|
|
35
|
+
* Fetch all tags from the remote into the submodule's local repo.
|
|
36
|
+
* Call this before resolveLatestTag to ensure local tags are current.
|
|
37
|
+
*/
|
|
38
|
+
export async function fetchTags(frameworkDir) {
|
|
39
|
+
const git = simpleGit(frameworkDir);
|
|
40
|
+
await git.fetch(['--tags']);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
|
-
* Return the latest semver tag
|
|
44
|
+
* Return the latest semver tag as a bare version string (no leading v).
|
|
45
|
+
* e.g. "0.15.0" not "v0.15.0"
|
|
46
|
+
* Uses local tags — call fetchTags() first if stale.
|
|
43
47
|
*/
|
|
44
48
|
export async function getLatestTag(git) {
|
|
45
49
|
const result = await git.tags(['--sort=-version:refname']);
|
|
@@ -47,5 +51,46 @@ export async function getLatestTag(git) {
|
|
|
47
51
|
if (semverTags.length === 0) {
|
|
48
52
|
throw new Error('No semver tags found in framework repo');
|
|
49
53
|
}
|
|
50
|
-
return semverTags[0];
|
|
54
|
+
return semverTags[0].replace(/^v/, '');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the latest semver tag directly from the remote.
|
|
59
|
+
* Fallback for copy-mode installs where local tags are unavailable.
|
|
60
|
+
*/
|
|
61
|
+
export async function getLatestTagFromRemote() {
|
|
62
|
+
const git = simpleGit();
|
|
63
|
+
const result = await git.listRemote(['--tags', '--sort=-version:refname', FRAMEWORK_REPO]);
|
|
64
|
+
const tags = result.split('\n')
|
|
65
|
+
.map(line => line.match(/refs\/tags\/(v\d+\.\d+\.\d+)$/))
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.map(m => m[1]);
|
|
68
|
+
if (tags.length === 0) throw new Error('No semver tags found on remote');
|
|
69
|
+
return tags[0].replace(/^v/, '');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Configure sparse checkout on a submodule to include only the directories
|
|
74
|
+
* that consuming projects need: skills/, targets/, core/, plus key root files.
|
|
75
|
+
* Excludes docs/, .github/, CHANGELOG.md, etc.
|
|
76
|
+
*/
|
|
77
|
+
export async function configureSparseCheckout(submoduleAbsPath) {
|
|
78
|
+
const git = simpleGit(submoduleAbsPath);
|
|
79
|
+
await git.raw(['sparse-checkout', 'init', '--no-cone']);
|
|
80
|
+
await git.raw(['sparse-checkout', 'set',
|
|
81
|
+
'skills/',
|
|
82
|
+
'targets/',
|
|
83
|
+
'core/',
|
|
84
|
+
'manifest.json',
|
|
85
|
+
'LICENSE',
|
|
86
|
+
'README.md',
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Normalize a version string to a git tag with v prefix.
|
|
92
|
+
* "0.15.0" → "v0.15.0", "v0.15.0" → "v0.15.0"
|
|
93
|
+
*/
|
|
94
|
+
function toGitTag(version) {
|
|
95
|
+
return version.startsWith('v') ? version : `v${version}`;
|
|
51
96
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFile, writeFile, access } from 'fs/promises';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Update the Framework Version line in arch/project-config.md.
|
|
5
|
+
* Looks for a markdown table row like: | Framework Version | v0.8.0 |
|
|
6
|
+
* Updates the version value in place. No-ops if the file doesn't exist
|
|
7
|
+
* or doesn't contain a recognizable version line.
|
|
8
|
+
*/
|
|
9
|
+
export async function updateProjectConfigVersion(configPath, newVersion) {
|
|
10
|
+
try {
|
|
11
|
+
await access(configPath);
|
|
12
|
+
} catch {
|
|
13
|
+
return; // No project-config — nothing to update
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const content = await readFile(configPath, 'utf8');
|
|
17
|
+
const updated = content.replace(
|
|
18
|
+
/^(\|\s*(?:Framework\s+Version|framework_version)\s*\|\s*)v?\d+\.\d+\.\d+(\s*\|)/im,
|
|
19
|
+
`$1v${newVersion}$2`
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (updated !== content) {
|
|
23
|
+
await writeFile(configPath, updated, 'utf8');
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/utils/symlink.js
CHANGED
|
@@ -5,11 +5,9 @@ import { join } from 'path';
|
|
|
5
5
|
* Ensure all skills in the manifest are wired into the skills discovery dir
|
|
6
6
|
* using directory symlinks.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - If a broken symlink exists: re-create it.
|
|
12
|
-
* - Otherwise: create a directory symlink pointing to <frameworkDir>/skills/<name>.
|
|
8
|
+
* Framework skills use the cw- prefix and are always symlinks — no local
|
|
9
|
+
* directory overrides. If a real directory is found at a cw-* path, a warning
|
|
10
|
+
* is emitted and the entry is skipped.
|
|
13
11
|
*
|
|
14
12
|
* Returns a summary: { wired: string[], skipped: string[], errors: string[] }
|
|
15
13
|
*/
|
|
@@ -36,7 +34,8 @@ export async function syncSymlinks(frameworkDir, skillsDir, skillNames) {
|
|
|
36
34
|
wired.push(name);
|
|
37
35
|
}
|
|
38
36
|
} else if (stat.isDirectory()) {
|
|
39
|
-
|
|
37
|
+
// Real directory at a cw-* path — should not exist
|
|
38
|
+
errors.push(`${name}: real directory found — framework skills cannot be overridden locally. Remove .claude/skills/${name}/ to restore the framework version.`);
|
|
40
39
|
} else {
|
|
41
40
|
errors.push(`${name}: unexpected file at ${linkPath}`);
|
|
42
41
|
}
|
|
@@ -56,10 +55,9 @@ export async function syncSymlinks(frameworkDir, skillsDir, skillNames) {
|
|
|
56
55
|
* Copies <frameworkDir>/skills/<name>/SKILL.md → <skillsDir>/<name>/SKILL.md,
|
|
57
56
|
* prepending an auto-generated header so consumers know not to edit the file directly.
|
|
58
57
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* - Otherwise: create <skillsDir>/<name>/ and write SKILL.md with header.
|
|
58
|
+
* Framework skills (cw-* prefix) are always auto-generated copies. If a copy
|
|
59
|
+
* already exists with the auto-generated header, it is refreshed. If a real
|
|
60
|
+
* override without the header is found, an error is emitted.
|
|
63
61
|
*
|
|
64
62
|
* Returns a summary: { wired: string[], skipped: string[], errors: string[] }
|
|
65
63
|
*/
|
|
@@ -78,20 +76,19 @@ export async function syncCopies(frameworkDir, skillsDir, skillNames, version) {
|
|
|
78
76
|
try {
|
|
79
77
|
const stat = await lstat(destDir);
|
|
80
78
|
if (stat.isDirectory()) {
|
|
81
|
-
// Check if it's an auto-generated copy or a real override
|
|
82
79
|
let existingContent = '';
|
|
83
80
|
try {
|
|
84
81
|
existingContent = await readFile(destFile, 'utf8');
|
|
85
82
|
} catch {
|
|
86
|
-
// No SKILL.md
|
|
87
|
-
|
|
83
|
+
// No SKILL.md in a cw-* dir — unexpected
|
|
84
|
+
errors.push(`${name}: real directory without SKILL.md — framework skills cannot be overridden locally.`);
|
|
88
85
|
continue;
|
|
89
86
|
}
|
|
90
87
|
|
|
91
88
|
if (existingContent.startsWith('<!-- AUTO-GENERATED')) {
|
|
92
89
|
skipped.push(name); // already wired as a copy
|
|
93
90
|
} else {
|
|
94
|
-
|
|
91
|
+
errors.push(`${name}: real directory with custom SKILL.md found — framework skills cannot be overridden locally. Remove .claude/skills/${name}/ to restore the framework version.`);
|
|
95
92
|
}
|
|
96
93
|
continue;
|
|
97
94
|
}
|
|
@@ -120,7 +117,6 @@ export function buildHeader(skillName, version) {
|
|
|
120
117
|
return [
|
|
121
118
|
`<!-- AUTO-GENERATED: Copied from _framework/skills/${skillName}/SKILL.md (v${version}) -->`,
|
|
122
119
|
`<!-- Do not edit directly. Run \`npx @cuewright/skills update\` to refresh. -->`,
|
|
123
|
-
`<!-- To override: delete this file and create your own SKILL.md in this directory. -->`,
|
|
124
120
|
'',
|
|
125
121
|
'',
|
|
126
122
|
].join('\n');
|