@cuewright/skills 0.1.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cuewright/skills",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "CLI installer for the Cuewright skills framework",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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)) {
@@ -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);
@@ -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
 
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
+
32
42
  // Determine target version — always bare (no leading v)
33
- const targetVersion = ((options.to || await resolveLatestTag(frameworkDir))).replace(/^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 isOverride = await isRealDirectory(skillPath);
79
- if (isOverride) {
80
- console.log(chalk.yellow(` warning: skill '${name}' was removed from the framework but you have a local override at .claude/skills/${name}/. It has been left in place.`));
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
- // It's a symlink to a now-missing target — clean it up
103
+ // Symlink to now-missing target — clean it up
83
104
  if (!dryRun) {
84
105
  await unlink(skillPath);
85
106
  }
86
- console.log(` Removed broken symlink: ${name}`);
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
- * Resolve the latest semver tag from the installed framework's git history.
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
- throw new Error('Could not determine latest framework version. Use --to <version> to specify one.');
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
 
@@ -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
- await git.clone(FRAMEWORK_REPO, targetDir, ['--depth', '1', ...(tag ? ['--branch', tag] : [])]);
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
  }
@@ -5,7 +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.14.33) or prefixed (v0.14.33) — normalized internally.
8
+ * Tag may be bare (0.15.0) or prefixed (v0.15.0) — normalized internally.
9
9
  */
10
10
  export async function addSubmodule(projectRoot, submodulePath, tag) {
11
11
  const git = simpleGit(projectRoot);
@@ -23,20 +23,27 @@ export async function addSubmodule(projectRoot, submodulePath, tag) {
23
23
 
24
24
  /**
25
25
  * Update an existing submodule to a specific tag (or latest if no tag given).
26
- * "Latest" means the most recent semver tag on the remote.
26
+ * Caller is responsible for fetching tags before calling this.
27
27
  */
28
28
  export async function updateSubmodule(projectRoot, submodulePath, tag) {
29
29
  const subGit = simpleGit(`${projectRoot}/${submodulePath}`);
30
-
31
- await subGit.fetch(['--tags']);
32
-
33
30
  const gitTag = tag ? toGitTag(tag) : toGitTag(await getLatestTag(subGit));
34
31
  await subGit.checkout(gitTag);
35
32
  }
36
33
 
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']);
41
+ }
42
+
37
43
  /**
38
44
  * Return the latest semver tag as a bare version string (no leading v).
39
- * e.g. "0.14.33" not "v0.14.33"
45
+ * e.g. "0.15.0" not "v0.15.0"
46
+ * Uses local tags — call fetchTags() first if stale.
40
47
  */
41
48
  export async function getLatestTag(git) {
42
49
  const result = await git.tags(['--sort=-version:refname']);
@@ -47,9 +54,42 @@ export async function getLatestTag(git) {
47
54
  return semverTags[0].replace(/^v/, '');
48
55
  }
49
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
+
50
90
  /**
51
91
  * Normalize a version string to a git tag with v prefix.
52
- * "0.14.33" → "v0.14.33", "v0.14.33" → "v0.14.33"
92
+ * "0.15.0" → "v0.15.0", "v0.15.0" → "v0.15.0"
53
93
  */
54
94
  function toGitTag(version) {
55
95
  return version.startsWith('v') ? version : `v${version}`;
@@ -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
+ }
@@ -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
- * For each skill name:
9
- * - If a real directory already exists at <skillsDir>/<name>: skip (override preserved).
10
- * - If a valid symlink already exists: skip.
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
- skipped.push(name); // real directory overridenever touch
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
- * For each skill name:
60
- * - If a real directory exists at <skillsDir>/<name> WITHOUT the auto-gen header: skip (override).
61
- * - If a copy with the header already exists: skip (already wired).
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 treat as override directory
87
- skipped.push(name);
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
- skipped.push(name); // real overridenever touch
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');