skillshop 0.0.0-stage → 2.1.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.
Files changed (63) hide show
  1. package/README.md +280 -2
  2. package/agents-content/000_communication-writing.block.md +11 -0
  3. package/agents-content/001_external-actions.block.md +9 -0
  4. package/agents-content/AGENTS.md +11 -0
  5. package/package.json +28 -4
  6. package/skills-content/architecture-scanner/SKILL.md +274 -0
  7. package/skills-content/architecture-scanner/files.json +10 -0
  8. package/skills-content/architecture-scanner/scan.sh +823 -0
  9. package/skills-content/change-review/SKILL.md +76 -0
  10. package/skills-content/change-review/agents/openai.yaml +4 -0
  11. package/skills-content/changelog/SKILL.md +99 -0
  12. package/skills-content/changeset/changeset.md +80 -0
  13. package/skills-content/code-buddy/SKILL.md +136 -0
  14. package/skills-content/design-system-catalog/SKILL.md +106 -0
  15. package/skills-content/design-system-catalog/extract-components.mjs +441 -0
  16. package/skills-content/design-system-catalog/generate-catalog.sh +255 -0
  17. package/skills-content/flare/SKILL.md +96 -0
  18. package/skills-content/grilling/SKILL.md +31 -0
  19. package/skills-content/install-sigmap/SKILL.md +97 -0
  20. package/skills-content/one-shot/SKILL.md +51 -0
  21. package/skills-content/release/SKILL.md +128 -0
  22. package/skills-content/shadow-clone/SKILL.md +228 -0
  23. package/skills-content/shadow-clone/extract-signatures.mjs +202 -0
  24. package/skills-content/shadow-clone/scan-repo.mjs +343 -0
  25. package/skills-content/ship/SKILL.md +95 -0
  26. package/skills-content/shots/SKILL.md +89 -0
  27. package/skills-content/shots/agents/openai.yaml +4 -0
  28. package/skills-content/skillshop/SKILL.md +104 -0
  29. package/skills-content/skillz/SKILL.md +15 -0
  30. package/skills-content/transcript-clean/SKILL.md +55 -0
  31. package/skills-content/transcript-edit/SKILL.md +80 -0
  32. package/skills-content/vitest/GENERATION.md +5 -0
  33. package/skills-content/vitest/SKILL.md +52 -0
  34. package/skills-content/vitest/references/advanced-environments.md +264 -0
  35. package/skills-content/vitest/references/advanced-projects.md +300 -0
  36. package/skills-content/vitest/references/advanced-type-testing.md +237 -0
  37. package/skills-content/vitest/references/advanced-vi.md +249 -0
  38. package/skills-content/vitest/references/core-cli.md +166 -0
  39. package/skills-content/vitest/references/core-config.md +174 -0
  40. package/skills-content/vitest/references/core-describe.md +193 -0
  41. package/skills-content/vitest/references/core-expect.md +219 -0
  42. package/skills-content/vitest/references/core-hooks.md +244 -0
  43. package/skills-content/vitest/references/core-test-api.md +233 -0
  44. package/skills-content/vitest/references/features-concurrency.md +250 -0
  45. package/skills-content/vitest/references/features-context.md +238 -0
  46. package/skills-content/vitest/references/features-coverage.md +207 -0
  47. package/skills-content/vitest/references/features-filtering.md +211 -0
  48. package/skills-content/vitest/references/features-mocking.md +265 -0
  49. package/skills-content/vitest/references/features-snapshots.md +207 -0
  50. package/skills-content/workleaf/SKILL.md +77 -0
  51. package/skills-content/worklog/SKILL.md +145 -0
  52. package/skills-content/worklog/references/summary-format.md +114 -0
  53. package/skills-content/worktree/SKILL.md +71 -0
  54. package/src/agents-instructions.js +219 -0
  55. package/src/cli.js +160 -0
  56. package/src/export.js +113 -0
  57. package/src/import.js +139 -0
  58. package/src/index.js +5 -0
  59. package/src/legacy-agents-content.js +36 -0
  60. package/src/legacy-cli.js +1234 -0
  61. package/src/library.js +259 -0
  62. package/src/specifier.js +27 -0
  63. package/src/user-install.js +48 -0
package/src/library.js ADDED
@@ -0,0 +1,259 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { normalizeSkillSpecifier, parseSkillSpecifier } from './specifier.js';
5
+
6
+ export const SKILLSHOP_LIBRARY_FILE = 'skillshop.library.json';
7
+ export const SKILLSHOP_MANIFEST_FILE = 'skillshop.manifest.mjs';
8
+ export const SKILLSHOP_SCHEMA_VERSION = 1;
9
+
10
+ function readJson(file) {
11
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
12
+ catch (error) { throw new Error(`Cannot read ${file}: ${error.message}`); }
13
+ }
14
+
15
+ function packageFile(root) { return path.join(path.resolve(root), 'package.json'); }
16
+ function libraryFile(root) { return path.join(path.resolve(root), SKILLSHOP_LIBRARY_FILE); }
17
+
18
+ function normalizeLocalSource(source) {
19
+ if (typeof source !== 'string' || !source.trim()) throw new Error('skill source must be a nonempty relative path');
20
+ const raw = source.trim().replaceAll('\\', '/');
21
+ if (path.posix.isAbsolute(raw)) throw new Error('skill source must be relative to the library package');
22
+ const normalized = path.posix.normalize(raw.replace(/^\.\//, ''));
23
+ if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../')) throw new Error('skill source must stay inside the library package');
24
+ if (/[*?\[\]{}()!]/.test(normalized)) {
25
+ throw new Error('skill source must not contain npm glob metacharacters');
26
+ }
27
+ const segments = normalized.split('/');
28
+ const npmExcludedDirectories = new Set(['node_modules', '.git', '.svn', 'CVS']);
29
+ if (segments.some((segment) => npmExcludedDirectories.has(segment))) {
30
+ throw new Error('skill source must not live under an npm-excluded directory');
31
+ }
32
+ return `./${normalized}`;
33
+ }
34
+
35
+ function normalizeTarget(target, exportedSpecifier) {
36
+ if (typeof target === 'string') return normalizeLocalSource(target);
37
+ if (!target || typeof target !== 'object' || Array.isArray(target)) throw new Error(`Invalid export target for ${exportedSpecifier}`);
38
+ if (typeof target.from !== 'string' || !target.from.trim()) throw new Error(`Imported export ${exportedSpecifier} needs a package in "from"`);
39
+ const importedSkill = normalizeSkillSpecifier(target.skill || exportedSpecifier);
40
+ return { from: target.from.trim(), skill: importedSkill };
41
+ }
42
+
43
+ function inside(root, candidate) {
44
+ const relative = path.relative(root, candidate);
45
+ return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
46
+ }
47
+
48
+ function assertNoSymlinks(directory, specifier) {
49
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
50
+ if (entry.name === '.npmignore' || entry.name === '.gitignore') {
51
+ throw new Error(`Skill ${specifier} must not contain nested npm/git ignore files that can exclude packaged assets`);
52
+ }
53
+ const entryPath = path.join(directory, entry.name);
54
+ const stat = fs.lstatSync(entryPath);
55
+ if (stat.isSymbolicLink()) {
56
+ throw new Error(`Skill ${specifier} must not contain symlinks because npm packages omit symbolic links`);
57
+ }
58
+ if (stat.isDirectory()) assertNoSymlinks(entryPath, specifier);
59
+ }
60
+ }
61
+
62
+ function assertNoParentIgnores(root, source, specifier) {
63
+ // Root ignore rules are overridden by package.json#files, but nested ignore
64
+ // rules can still omit SKILL.md even when its directory is explicitly packed.
65
+ for (let directory = path.dirname(source); directory !== root; directory = path.dirname(directory)) {
66
+ for (const name of ['.npmignore', '.gitignore']) {
67
+ if (fs.existsSync(path.join(directory, name))) {
68
+ throw new Error(`Skill ${specifier} has nested ignore rules in ${directory} that can exclude packaged assets`);
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ function legacyRootExport(packageRoot) {
75
+ const realRoot = fs.realpathSync(packageRoot);
76
+ const requireFromPackage = createRequire(path.join(realRoot, '__skillshop_resolve__.cjs'));
77
+ let resolved;
78
+ try {
79
+ resolved = requireFromPackage.resolve(realRoot);
80
+ } catch {
81
+ return null;
82
+ }
83
+ const realResolved = fs.realpathSync(resolved);
84
+ if (!inside(realRoot, realResolved)) {
85
+ throw new Error('package root entry resolves outside the package');
86
+ }
87
+ const relative = path.relative(realRoot, realResolved).split(path.sep).join('/');
88
+ return relative ? `./${relative}` : null;
89
+ }
90
+
91
+ export function resolveLocalSkill(root, specifier, library = readSkillLibrary(root, { required: true })) {
92
+ const packageRoot = fs.realpathSync(path.resolve(root));
93
+ const normalized = normalizeSkillSpecifier(specifier);
94
+ const target = library.exports[normalized];
95
+ if (!target) throw new Error(`Skill ${normalized} is not exported by this library`);
96
+ if (typeof target !== 'string') throw new Error(`Skill ${normalized} is re-exported from ${target.from}, not stored locally`);
97
+ const source = normalizeLocalSource(target);
98
+ const sourceDir = path.resolve(packageRoot, source);
99
+ if (!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory()) throw new Error(`Skill ${normalized} source directory does not exist: ${source}`);
100
+ const realSource = fs.realpathSync(sourceDir);
101
+ if (!inside(packageRoot, realSource)) throw new Error(`Skill ${normalized} source escapes the library package`);
102
+ if (path.normalize(realSource) !== path.normalize(sourceDir)) {
103
+ throw new Error(`Skill ${normalized} source must not use symlinks because npm packages omit symbolic links`);
104
+ }
105
+ assertNoParentIgnores(packageRoot, realSource, normalized);
106
+ assertNoSymlinks(realSource, normalized);
107
+ const manifestPath = path.join(realSource, 'SKILL.md');
108
+ if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile()) {
109
+ throw new Error(`Skill ${normalized} is missing a regular SKILL.md file`);
110
+ }
111
+ return Object.freeze({ ...parseSkillSpecifier(normalized), kind: 'local', source, sourceDir: realSource, manifestPath });
112
+ }
113
+
114
+ export function readSkillLibrary(root = process.cwd(), { required = false } = {}) {
115
+ const file = libraryFile(root);
116
+ if (!fs.existsSync(file)) {
117
+ if (required) throw new Error(`No ${SKILLSHOP_LIBRARY_FILE} found at ${path.resolve(root)}`);
118
+ return { schemaVersion: SKILLSHOP_SCHEMA_VERSION, exports: {} };
119
+ }
120
+ const raw = readJson(file);
121
+ if (raw?.schemaVersion !== SKILLSHOP_SCHEMA_VERSION || !raw.exports || Array.isArray(raw.exports) || typeof raw.exports !== 'object') throw new Error(`${file} is not a supported Skillshop library`);
122
+ const exports = {};
123
+ for (const [specifier, target] of Object.entries(raw.exports)) {
124
+ const normalized = normalizeSkillSpecifier(specifier);
125
+ if (normalized !== specifier) throw new Error(`Skill export must use canonical specifier ${normalized}`);
126
+ exports[normalized] = normalizeTarget(target, normalized);
127
+ }
128
+ return { schemaVersion: SKILLSHOP_SCHEMA_VERSION, exports };
129
+ }
130
+
131
+ export function writeSkillLibrary(root, library) {
132
+ const file = libraryFile(root);
133
+ const stable = {
134
+ schemaVersion: SKILLSHOP_SCHEMA_VERSION,
135
+ exports: Object.fromEntries(Object.entries(library.exports || {}).sort(([a], [b]) => a.localeCompare(b))),
136
+ };
137
+ fs.writeFileSync(file, `${JSON.stringify(stable, null, 2)}\n`);
138
+ return file;
139
+ }
140
+
141
+ export function initSkillLibrary(root = process.cwd()) {
142
+ const packageRoot = path.resolve(root);
143
+ if (!fs.existsSync(packageFile(packageRoot))) throw new Error('Skillshop libraries require a package.json');
144
+ const file = libraryFile(packageRoot);
145
+ if (!fs.existsSync(file)) writeSkillLibrary(packageRoot, { exports: {} });
146
+ return readSkillLibrary(packageRoot, { required: true });
147
+ }
148
+
149
+ function dependencyNames(pkg) {
150
+ const names = new Set(Object.keys(pkg.dependencies || {}));
151
+ for (const name of Object.keys(pkg.peerDependencies || {})) {
152
+ if (pkg.peerDependenciesMeta?.[name]?.optional === true) continue;
153
+ names.add(name);
154
+ }
155
+ return names;
156
+ }
157
+
158
+ function validateExports(packageRoot, pkg, library) {
159
+ const dependencies = dependencyNames(pkg);
160
+ return Object.entries(library.exports).map(([specifier, target]) => {
161
+ if (typeof target === 'string') return resolveLocalSkill(packageRoot, specifier, library);
162
+ if (!dependencies.has(target.from)) throw new Error(`Re-export ${specifier} requires ${target.from} in dependencies or peerDependencies`);
163
+ return Object.freeze({ ...parseSkillSpecifier(specifier), kind: 'import', from: target.from, importedSpecifier: target.skill });
164
+ });
165
+ }
166
+
167
+ function relativeUrlPath(source) {
168
+ return source.split('/').map((segment) => encodeURIComponent(segment)).join('/');
169
+ }
170
+
171
+ function manifestSource(packageName, library) {
172
+ const importedPackages = [...new Set(
173
+ Object.values(library.exports)
174
+ .filter((target) => typeof target === 'object')
175
+ .map((target) => target.from),
176
+ )].sort();
177
+ const aliases = new Map(importedPackages.map((name, index) => [name, `library${index}`]));
178
+ const imports = importedPackages.map((name) => `import ${aliases.get(name)} from ${JSON.stringify(`${name}/skillshop`)};`).join('\n');
179
+ const skills = Object.entries(library.exports).sort(([a], [b]) => a.localeCompare(b)).map(([specifier, target]) => {
180
+ if (typeof target === 'string') {
181
+ const directory = target.endsWith('/') ? target : `${target}/`;
182
+ return ` ${JSON.stringify(specifier)}: new URL(${JSON.stringify(relativeUrlPath(directory))}, import.meta.url).href,`;
183
+ }
184
+ return ` ${JSON.stringify(specifier)}: ${aliases.get(target.from)}.skills[${JSON.stringify(target.skill)}],`;
185
+ }).join('\n');
186
+ return `// Generated by Skillshop. Do not edit by hand.\n${imports}${imports ? '\n\n' : ''}const skills = Object.freeze({\n${skills}\n});\n\nexport default Object.freeze({\n schemaVersion: ${SKILLSHOP_SCHEMA_VERSION},\n packageName: ${JSON.stringify(packageName)},\n skills,\n});\n`;
187
+ }
188
+
189
+ function mergePackageExports(pkg, packageRoot) {
190
+ const skillshopExport = `./${SKILLSHOP_MANIFEST_FILE}`;
191
+ if (pkg.exports == null) {
192
+ const rootExport = legacyRootExport(packageRoot);
193
+ return rootExport
194
+ ? { '.': rootExport, './skillshop': skillshopExport }
195
+ : { './skillshop': skillshopExport };
196
+ }
197
+ if (typeof pkg.exports !== 'object' || Array.isArray(pkg.exports)) {
198
+ return { '.': pkg.exports, './skillshop': skillshopExport };
199
+ }
200
+ const keys = Object.keys(pkg.exports);
201
+ const subpaths = keys.filter((key) => key.startsWith('.'));
202
+ if (subpaths.length && subpaths.length !== keys.length) {
203
+ throw new Error('package.json exports mixes subpaths and conditions and cannot be extended safely');
204
+ }
205
+ if (!subpaths.length) return { '.': pkg.exports, './skillshop': skillshopExport };
206
+ return { ...pkg.exports, './skillshop': skillshopExport };
207
+ }
208
+
209
+ function nextPackageManifest(packageRoot, pkg, skills) {
210
+ const next = structuredClone(pkg);
211
+ next.exports = mergePackageExports(next, packageRoot);
212
+ if (Array.isArray(next.files)) {
213
+ const previousManaged = new Set(Array.isArray(next.skillshop?.managedFiles) ? next.skillshop.managedFiles : []);
214
+ const userFiles = next.files.filter((file) => !previousManaged.has(file));
215
+ const desired = [
216
+ SKILLSHOP_MANIFEST_FILE,
217
+ ...skills.filter((skill) => skill.kind === 'local').map((skill) => skill.source.replace(/^\.\//, '').replace(/\/$/, '')),
218
+ ];
219
+ const managedFiles = desired.filter((file) => !userFiles.includes(file));
220
+ next.files = [...new Set([...userFiles, ...desired])];
221
+ next.skillshop = { ...(next.skillshop || {}), schemaVersion: SKILLSHOP_SCHEMA_VERSION, managedFiles };
222
+ }
223
+ return next;
224
+ }
225
+
226
+ export function validateSkillLibrary(root = process.cwd()) {
227
+ const packageRoot = path.resolve(root);
228
+ const pkgPath = packageFile(packageRoot);
229
+ if (!fs.existsSync(pkgPath)) throw new Error('Skillshop libraries require a package.json');
230
+ const pkg = readJson(pkgPath);
231
+ if (typeof pkg.name !== 'string' || !pkg.name.trim()) throw new Error('package.json must have a package name');
232
+ const library = readSkillLibrary(packageRoot, { required: true });
233
+ const skills = validateExports(packageRoot, pkg, library);
234
+ const userInstall = pkg.skillshop?.userInstall ?? [];
235
+ if (!Array.isArray(userInstall) || new Set(userInstall).size !== userInstall.length ||
236
+ userInstall.some((specifier) => {
237
+ try {
238
+ return normalizeSkillSpecifier(specifier) !== specifier || !Object.hasOwn(library.exports, specifier);
239
+ } catch { return true; }
240
+ })) {
241
+ throw new Error('package.json#skillshop.userInstall must list unique exported skill specifiers');
242
+ }
243
+ return { packageName: pkg.name, library, skills };
244
+ }
245
+
246
+ export function buildSkillLibrary(root = process.cwd()) {
247
+ const packageRoot = path.resolve(root);
248
+ const { packageName, library, skills } = validateSkillLibrary(packageRoot);
249
+ const manifestPath = path.join(packageRoot, SKILLSHOP_MANIFEST_FILE);
250
+ const pkgPath = packageFile(packageRoot);
251
+ const pkg = readJson(pkgPath);
252
+
253
+ const nextPkg = nextPackageManifest(packageRoot, pkg, skills);
254
+ const nextManifest = manifestSource(packageName, library);
255
+
256
+ fs.writeFileSync(manifestPath, nextManifest);
257
+ fs.writeFileSync(pkgPath, `${JSON.stringify(nextPkg, null, 2)}\n`);
258
+ return { packageName, manifestPath, skills };
259
+ }
@@ -0,0 +1,27 @@
1
+ const SKILL_SPECIFIER = /^skill:([a-z0-9][a-z0-9._-]*)$/;
2
+
3
+ export function parseSkillSpecifier(value) {
4
+ if (typeof value !== 'string') throw new TypeError('skill specifier must be a string');
5
+ const match = value.trim().match(SKILL_SPECIFIER);
6
+ if (!match) throw new Error(`Invalid skill specifier: ${value}. Expected skill:<name>`);
7
+ return Object.freeze({ specifier: `skill:${match[1]}`, name: match[1] });
8
+ }
9
+
10
+ export function normalizeSkillSpecifier(value) {
11
+ return parseSkillSpecifier(value).specifier;
12
+ }
13
+
14
+ export function skillName(value) {
15
+ return parseSkillSpecifier(value).name;
16
+ }
17
+
18
+ export function parseLibrarySkillReference(value) {
19
+ if (typeof value !== 'string') throw new TypeError('library skill reference must be a string');
20
+ const separator = value.lastIndexOf(':');
21
+ if (separator <= 0 || separator === value.length - 1) {
22
+ throw new Error(`Invalid library skill reference: ${value}. Expected <library>:<skill>`);
23
+ }
24
+ const library = value.slice(0, separator);
25
+ const skill = parseSkillSpecifier(`skill:${value.slice(separator + 1)}`);
26
+ return Object.freeze({ library, specifier: skill.specifier, name: skill.name });
27
+ }
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { importSkill } from './import.js';
5
+ import { normalizeSkillSpecifier, skillName } from './specifier.js';
6
+
7
+ function declaredUserSkills(packageRoot) {
8
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
9
+ const declared = pkg.skillshop?.userInstall ?? [];
10
+ if (!Array.isArray(declared) || new Set(declared).size !== declared.length ||
11
+ declared.some((specifier) => {
12
+ try { return normalizeSkillSpecifier(specifier) !== specifier; }
13
+ catch { return true; }
14
+ })) {
15
+ throw new Error('package.json#skillshop.userInstall must list unique skill specifiers');
16
+ }
17
+ return declared;
18
+ }
19
+
20
+ export async function installUserSkill({
21
+ library,
22
+ specifier,
23
+ cwd = process.cwd(),
24
+ userSkillsDir = path.join(os.homedir(), '.agents', 'skills'),
25
+ }) {
26
+ const normalized = normalizeSkillSpecifier(specifier);
27
+ return importSkill({
28
+ library,
29
+ specifier: normalized,
30
+ targetDir: path.resolve(userSkillsDir, skillName(normalized)),
31
+ cwd,
32
+ overwrite: true,
33
+ });
34
+ }
35
+
36
+ export async function installDeclaredUserSkills({
37
+ library = process.cwd(),
38
+ userSkillsDir,
39
+ cwd = process.cwd(),
40
+ } = {}) {
41
+ const packageRoot = path.resolve(cwd, library);
42
+ const declared = declaredUserSkills(packageRoot);
43
+ const results = [];
44
+ for (const specifier of declared) {
45
+ results.push(await installUserSkill({ library: packageRoot, specifier, cwd, userSkillsDir }));
46
+ }
47
+ return results;
48
+ }