skillshop 0.0.0-stage → 2.0.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/README.md +167 -2
- package/package.json +25 -4
- package/src/cli.js +96 -0
- package/src/export.js +113 -0
- package/src/import.js +134 -0
- package/src/index.js +4 -0
- package/src/library.js +250 -0
- package/src/specifier.js +16 -0
package/README.md
CHANGED
|
@@ -1,3 +1,168 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Skillshop
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Skillshop is infrastructure for **building and consuming skill libraries**. It is not a bundled personal skill catalog.
|
|
4
|
+
|
|
5
|
+
A skill is addressed by a portable component specifier:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
skill:clips
|
|
9
|
+
skill:loopz
|
|
10
|
+
skill:design-review
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The library owns where its `SKILL.md` lives. Consumers only depend on the `skill:*` name.
|
|
14
|
+
|
|
15
|
+
## The model
|
|
16
|
+
|
|
17
|
+
A producer package keeps a source manifest:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"schemaVersion": 1,
|
|
22
|
+
"exports": {
|
|
23
|
+
"skill:clips": "./.agents/skills/clips",
|
|
24
|
+
"skill:loopz": "./.agents/skills/loopz"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`skillshop build` validates each directory and generates a package-facing `skillshop.manifest.mjs`. It also adds the standard `./skillshop` package export and, when `package.json#files` is present, ensures the exported skill directories are shipped.
|
|
30
|
+
|
|
31
|
+
This makes a skill library behave like a component package: producers export named components; applications resolve or materialize only the components they choose.
|
|
32
|
+
|
|
33
|
+
## Explicit export boundary
|
|
34
|
+
|
|
35
|
+
`skillshop.library.json` is the **only** skill export definition. The `exports` map is an allowlist, not a directory scan. In particular, `.agents/skills/` is just a possible source/install location: a repository can contain any number of valid `SKILL.md` files there without publishing any of them.
|
|
36
|
+
|
|
37
|
+
- `skillshop init` creates an empty `exports: {}` map; it does not discover or publish skills.
|
|
38
|
+
- `skillshop build` requires that source manifest and generates entries only for explicitly named local exports and re-exports. Missing manifests fail validation; empty manifests publish zero skills.
|
|
39
|
+
- `skillshop resolve` and `skillshop import` reject unlisted `skill:*` names, even when their files are present on disk.
|
|
40
|
+
- `skillshop export` and `skillshop unexport` explicitly change that public surface. Removing an export does not delete the repository's local skill files.
|
|
41
|
+
- Aggregators must also explicitly list every upstream skill they re-export; importing a dependency does not re-export its entire library.
|
|
42
|
+
|
|
43
|
+
Skillshop only adds **declared local skill directories** to its managed `package.json#files` entries. Its export allowlist governs the Skillshop API, not arbitrary physical file access: if your own package `files` patterns include other private skills, npm may still publish those files. Keep such paths out of broad package globs if they must not ship.
|
|
44
|
+
|
|
45
|
+
A library can also re-export another library's skill without copying its source:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"schemaVersion": 1,
|
|
50
|
+
"exports": {
|
|
51
|
+
"skill:clips": {
|
|
52
|
+
"from": "@dfosco/clips",
|
|
53
|
+
"skill": "skill:clips"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The upstream package must be a runtime dependency or a non-optional peer dependency so the generated manifest can safely load its standard `<package>/skillshop` export.
|
|
60
|
+
|
|
61
|
+
## CLI
|
|
62
|
+
|
|
63
|
+
Initialize a library:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
skillshop init
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Export skills:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
skillshop export skill:clips ./.agents/skills/clips
|
|
73
|
+
skillshop export skill:loopz ./.agents/skills/loopz
|
|
74
|
+
skillshop export skill:clips --from @dfosco/clips
|
|
75
|
+
skillshop list
|
|
76
|
+
skillshop validate
|
|
77
|
+
skillshop build
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Consume a skill from an installed package or a local package directory:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
skillshop resolve @dfosco/clips skill:clips
|
|
84
|
+
skillshop import @dfosco/clips skill:clips --to .agents/skills/clips
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`--force` replaces an existing import target. Without `--force`, the CLI does not overwrite an existing skill directory.
|
|
88
|
+
|
|
89
|
+
**Imports are snapshots:** `importSkill` copies the complete skill directory to the destination. Imported files do not depend on the source package afterward and are never changed or deleted by a later `unexport`. To apply an updated source, the consumer must import it again explicitly.
|
|
90
|
+
|
|
91
|
+
For applications that update only files owned by a skill while preserving local additions, the SDK also supports `merge: true`:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
const result = await importSkill({
|
|
95
|
+
library: '/path/to/a/skill-library',
|
|
96
|
+
specifier: 'skill:clips',
|
|
97
|
+
targetDir: '.agents/skills/clips',
|
|
98
|
+
merge: true,
|
|
99
|
+
});
|
|
100
|
+
// result.installed and result.updated indicate newly copied or changed files.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The merge mode overlays source files and leaves unrelated destination files untouched. It is opt-in; the normal import still refuses an existing destination unless `overwrite: true` is set.
|
|
104
|
+
|
|
105
|
+
## SDK
|
|
106
|
+
|
|
107
|
+
Skillshop exports the builder and consumer capabilities as normal application modules:
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
import { exportSkill, reexportSkill, buildSkillLibrary } from 'skillshop/export';
|
|
111
|
+
import { resolveSkill, importSkill } from 'skillshop/import';
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Build a library programmatically:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
exportSkill({
|
|
118
|
+
root: process.cwd(),
|
|
119
|
+
specifier: 'skill:clips',
|
|
120
|
+
source: './.agents/skills/clips',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
reexportSkill({
|
|
124
|
+
root: process.cwd(),
|
|
125
|
+
specifier: 'skill:loopz',
|
|
126
|
+
from: '@dfosco/clips',
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
buildSkillLibrary(process.cwd());
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Resolve without copying, which is useful for applications such as Hypercanvas:
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
const clips = await resolveSkill({
|
|
136
|
+
library: '@dfosco/clips',
|
|
137
|
+
specifier: 'skill:clips',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log(clips.manifestPath);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Or materialize the complete skill component:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
await importSkill({
|
|
147
|
+
library: '@dfosco/clips',
|
|
148
|
+
specifier: 'skill:loopz',
|
|
149
|
+
targetDir: '.agents/skills/loopz',
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Package contract
|
|
154
|
+
|
|
155
|
+
Published libraries expose one standard package subpath:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
<package>/skillshop
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Skillshop generates that manifest. Consumers therefore do not need package-specific JS exports such as `clipsSkill` or `loopzSkill`.
|
|
162
|
+
|
|
163
|
+
The producer package, a personal library, and an application are separate concerns:
|
|
164
|
+
|
|
165
|
+
- **Skillshop** defines the protocol and tooling.
|
|
166
|
+
- **Clips** can export `skill:clips` and `skill:loopz`.
|
|
167
|
+
- **A personal library** can aggregate local skills and re-export `skill:*` components from packages such as Clips without vendoring them.
|
|
168
|
+
- **Hypercanvas** can optionally resolve/import Clips workflow skills for Squads.
|
package/package.json
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillshop",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Build, export, resolve, and import portable agent skill libraries",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skillshop": "./src/cli.js",
|
|
8
|
+
"skillz": "./src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.js",
|
|
12
|
+
"./library": "./src/library.js",
|
|
13
|
+
"./export": "./src/export.js",
|
|
14
|
+
"./import": "./src/import.js",
|
|
15
|
+
"./specifier": "./src/specifier.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src/",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { buildSkillLibrary, initSkillLibrary, readSkillLibrary, validateSkillLibrary } from './library.js';
|
|
6
|
+
import { exportSkill, reexportSkill, unexportSkill } from './export.js';
|
|
7
|
+
import { importSkill, resolveSkill } from './import.js';
|
|
8
|
+
|
|
9
|
+
const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
10
|
+
|
|
11
|
+
function help() {
|
|
12
|
+
console.log(`skillshop v${VERSION} — build and consume portable skill libraries
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
skillshop init
|
|
16
|
+
skillshop export skill:<name> <directory>
|
|
17
|
+
skillshop export skill:<name> --from <package> [--skill skill:<source-name>]
|
|
18
|
+
skillshop unexport skill:<name>
|
|
19
|
+
skillshop list
|
|
20
|
+
skillshop validate
|
|
21
|
+
skillshop build
|
|
22
|
+
skillshop resolve <library> skill:<name>
|
|
23
|
+
skillshop import <library> skill:<name> --to <directory> [--force]
|
|
24
|
+
|
|
25
|
+
A library publishes skills by portable specifier, for example skill:clips.
|
|
26
|
+
Consumers resolve the same specifier regardless of where the library stores SKILL.md.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function flag(args, name) {
|
|
30
|
+
const index = args.indexOf(name);
|
|
31
|
+
return index === -1 ? null : args[index + 1];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function main(argv) {
|
|
35
|
+
const [command, ...args] = argv;
|
|
36
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') return help();
|
|
37
|
+
if (command === '--version' || command === '-v') return console.log(`skillshop v${VERSION}`);
|
|
38
|
+
|
|
39
|
+
if (command === 'init') {
|
|
40
|
+
initSkillLibrary();
|
|
41
|
+
console.log(`Initialized ${path.resolve('skillshop.library.json')}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (command === 'export') {
|
|
45
|
+
if (args.length < 2) throw new Error('Usage: skillshop export skill:<name> <directory> | --from <package> [--skill skill:<source-name>]');
|
|
46
|
+
const from = flag(args, '--from');
|
|
47
|
+
const result = from
|
|
48
|
+
? reexportSkill({ specifier: args[0], from, importedSpecifier: flag(args, '--skill') || args[0] })
|
|
49
|
+
: exportSkill({ specifier: args[0], source: args[1] });
|
|
50
|
+
console.log(result.kind === 'import'
|
|
51
|
+
? `${result.specifier} -> ${result.from}#${result.importedSpecifier}`
|
|
52
|
+
: `${result.specifier} -> ${result.source}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (command === 'unexport') {
|
|
56
|
+
if (args.length !== 1) throw new Error('Usage: skillshop unexport skill:<name>');
|
|
57
|
+
console.log(unexportSkill({ specifier: args[0] }) ? `Removed ${args[0]}` : `${args[0]} was not exported`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (command === 'list') {
|
|
61
|
+
const library = readSkillLibrary(process.cwd(), { required: true });
|
|
62
|
+
for (const [specifier, source] of Object.entries(library.exports)) {
|
|
63
|
+
console.log(typeof source === 'string' ? `${specifier} -> ${source}` : `${specifier} -> ${source.from}#${source.skill}`);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (command === 'validate') {
|
|
68
|
+
const result = validateSkillLibrary();
|
|
69
|
+
console.log(`Valid ${result.packageName}: ${result.skills.length} skill(s)`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (command === 'build') {
|
|
73
|
+
const result = buildSkillLibrary();
|
|
74
|
+
console.log(`Built ${result.packageName}: ${result.skills.length} skill(s)`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (command === 'resolve') {
|
|
78
|
+
if (args.length !== 2) throw new Error('Usage: skillshop resolve <library> skill:<name>');
|
|
79
|
+
console.log(JSON.stringify(await resolveSkill({ library: args[0], specifier: args[1] }), null, 2));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (command === 'import') {
|
|
83
|
+
if (args.length < 2) throw new Error('Usage: skillshop import <library> skill:<name> --to <directory> [--force]');
|
|
84
|
+
const targetDir = flag(args, '--to');
|
|
85
|
+
if (!targetDir) throw new Error('--to <directory> is required');
|
|
86
|
+
const result = await importSkill({ library: args[0], specifier: args[1], targetDir, overwrite: args.includes('--force') });
|
|
87
|
+
console.log(`${result.specifier} -> ${result.targetDir}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`Unknown command: ${command}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
94
|
+
console.error(`skillshop: ${error.message}`);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
});
|
package/src/export.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
buildSkillLibrary,
|
|
5
|
+
initSkillLibrary,
|
|
6
|
+
readSkillLibrary,
|
|
7
|
+
resolveLocalSkill,
|
|
8
|
+
validateSkillLibrary,
|
|
9
|
+
writeSkillLibrary,
|
|
10
|
+
} from './library.js';
|
|
11
|
+
import { normalizeSkillSpecifier } from './specifier.js';
|
|
12
|
+
|
|
13
|
+
function packageDependencies(root) {
|
|
14
|
+
const pkg = JSON.parse(
|
|
15
|
+
fs.readFileSync(path.join(path.resolve(root), 'package.json'), 'utf8'),
|
|
16
|
+
);
|
|
17
|
+
const names = new Set(Object.keys(pkg.dependencies || {}));
|
|
18
|
+
for (const name of Object.keys(pkg.peerDependencies || {})) {
|
|
19
|
+
if (pkg.peerDependenciesMeta?.[name]?.optional === true) continue;
|
|
20
|
+
names.add(name);
|
|
21
|
+
}
|
|
22
|
+
return names;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function updateLibrary(root, mutate) {
|
|
26
|
+
initSkillLibrary(root);
|
|
27
|
+
const packageRoot = path.resolve(root);
|
|
28
|
+
const packagePath = path.join(packageRoot, 'package.json');
|
|
29
|
+
const manifestPath = path.join(packageRoot, 'skillshop.manifest.mjs');
|
|
30
|
+
const previousPackage = fs.readFileSync(packagePath);
|
|
31
|
+
const previousManifest = fs.existsSync(manifestPath)
|
|
32
|
+
? fs.readFileSync(manifestPath)
|
|
33
|
+
: null;
|
|
34
|
+
const previous = readSkillLibrary(root, { required: true });
|
|
35
|
+
const next = { ...previous, exports: { ...previous.exports } };
|
|
36
|
+
|
|
37
|
+
mutate(next);
|
|
38
|
+
writeSkillLibrary(root, next);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
return buildSkillLibrary(root);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
writeSkillLibrary(root, previous);
|
|
44
|
+
fs.writeFileSync(packagePath, previousPackage);
|
|
45
|
+
if (previousManifest === null) fs.rmSync(manifestPath, { force: true });
|
|
46
|
+
else fs.writeFileSync(manifestPath, previousManifest);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function exportSkill({
|
|
52
|
+
root = process.cwd(),
|
|
53
|
+
specifier,
|
|
54
|
+
source,
|
|
55
|
+
}) {
|
|
56
|
+
const normalized = normalizeSkillSpecifier(specifier);
|
|
57
|
+
resolveLocalSkill(root, normalized, {
|
|
58
|
+
schemaVersion: 1,
|
|
59
|
+
exports: { [normalized]: source },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result = updateLibrary(root, (library) => {
|
|
63
|
+
library.exports[normalized] = source;
|
|
64
|
+
});
|
|
65
|
+
return result.skills.find((skill) => skill.specifier === normalized);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function reexportSkill({
|
|
69
|
+
root = process.cwd(),
|
|
70
|
+
specifier,
|
|
71
|
+
from,
|
|
72
|
+
importedSpecifier = specifier,
|
|
73
|
+
}) {
|
|
74
|
+
const normalized = normalizeSkillSpecifier(specifier);
|
|
75
|
+
const imported = normalizeSkillSpecifier(importedSpecifier);
|
|
76
|
+
if (typeof from !== 'string' || !from.trim()) {
|
|
77
|
+
throw new Error('from package is required');
|
|
78
|
+
}
|
|
79
|
+
const packageName = from.trim();
|
|
80
|
+
if (!packageDependencies(root).has(packageName)) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Re-export ${normalized} requires ${packageName} in dependencies or peerDependencies`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = updateLibrary(root, (library) => {
|
|
87
|
+
library.exports[normalized] = {
|
|
88
|
+
from: packageName,
|
|
89
|
+
skill: imported,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
return result.skills.find((skill) => skill.specifier === normalized);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function unexportSkill({
|
|
96
|
+
root = process.cwd(),
|
|
97
|
+
specifier,
|
|
98
|
+
}) {
|
|
99
|
+
const normalized = normalizeSkillSpecifier(specifier);
|
|
100
|
+
const library = readSkillLibrary(root, { required: true });
|
|
101
|
+
if (!Object.hasOwn(library.exports, normalized)) return false;
|
|
102
|
+
|
|
103
|
+
updateLibrary(root, (next) => {
|
|
104
|
+
delete next.exports[normalized];
|
|
105
|
+
});
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
buildSkillLibrary,
|
|
111
|
+
initSkillLibrary,
|
|
112
|
+
validateSkillLibrary,
|
|
113
|
+
};
|
package/src/import.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
6
|
+
import { normalizeSkillSpecifier, parseSkillSpecifier } from './specifier.js';
|
|
7
|
+
import { SKILLSHOP_MANIFEST_FILE, SKILLSHOP_SCHEMA_VERSION } from './library.js';
|
|
8
|
+
|
|
9
|
+
async function importManifestFile(file) {
|
|
10
|
+
// Content-based cache keys also invalidate in long-lived consumers when two
|
|
11
|
+
// export changes land within the filesystem timestamp resolution.
|
|
12
|
+
const digest = createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
13
|
+
const url = `${pathToFileURL(file).href}?skillshop=${digest}`;
|
|
14
|
+
return (await import(url)).default;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function validatePublishedManifest(manifest, label) {
|
|
18
|
+
if (!manifest || manifest.schemaVersion !== SKILLSHOP_SCHEMA_VERSION || !manifest.skills || typeof manifest.skills !== 'object') {
|
|
19
|
+
throw new Error(`${label} is not a supported Skillshop library manifest`);
|
|
20
|
+
}
|
|
21
|
+
return manifest;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function loadSkillLibrary(library, { cwd = process.cwd() } = {}) {
|
|
25
|
+
if (library && typeof library === 'object') return validatePublishedManifest(library, 'library object');
|
|
26
|
+
if (typeof library !== 'string' || !library.trim()) throw new Error('library must be a package name, package directory, or manifest object');
|
|
27
|
+
const value = library.trim();
|
|
28
|
+
const local = path.resolve(cwd, value);
|
|
29
|
+
if (fs.existsSync(local) && fs.statSync(local).isDirectory()) {
|
|
30
|
+
const manifestFile = path.join(local, SKILLSHOP_MANIFEST_FILE);
|
|
31
|
+
if (!fs.existsSync(manifestFile)) throw new Error(`${value} has no ${SKILLSHOP_MANIFEST_FILE}; run skillshop build`);
|
|
32
|
+
return validatePublishedManifest(await importManifestFile(manifestFile), value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const requireFromConsumer = createRequire(path.join(path.resolve(cwd), 'package.json'));
|
|
36
|
+
let resolved;
|
|
37
|
+
try {
|
|
38
|
+
resolved = requireFromConsumer.resolve(`${value}/skillshop`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error(`Cannot resolve ${value}/skillshop from ${cwd}: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
return validatePublishedManifest(await importManifestFile(resolved), value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function resolveSkill({ library, specifier, cwd = process.cwd() }) {
|
|
46
|
+
const normalized = normalizeSkillSpecifier(specifier);
|
|
47
|
+
const manifest = await loadSkillLibrary(library, { cwd });
|
|
48
|
+
const href = manifest.skills[normalized];
|
|
49
|
+
if (typeof href !== 'string' || !href) throw new Error(`${normalized} is not exported by ${manifest.packageName || 'the library'}`);
|
|
50
|
+
const url = new URL(href);
|
|
51
|
+
if (url.protocol !== 'file:') throw new Error(`${normalized} does not resolve to a local packaged skill directory`);
|
|
52
|
+
const sourceDir = fileURLToPath(url);
|
|
53
|
+
const manifestPath = path.join(sourceDir, 'SKILL.md');
|
|
54
|
+
if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile()) {
|
|
55
|
+
throw new Error(`${normalized} is missing a regular SKILL.md file in the installed library`);
|
|
56
|
+
}
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
...parseSkillSpecifier(normalized),
|
|
59
|
+
packageName: manifest.packageName || null,
|
|
60
|
+
sourceDir,
|
|
61
|
+
manifestPath,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function listSkillFiles(sourceDir, relative = '') {
|
|
66
|
+
return fs.readdirSync(path.join(sourceDir, relative), { withFileTypes: true })
|
|
67
|
+
.flatMap((entry) => {
|
|
68
|
+
const nested = path.join(relative, entry.name);
|
|
69
|
+
if (entry.isSymbolicLink()) throw new Error(`Skill source contains an unsafe symlink: ${nested}`);
|
|
70
|
+
if (entry.isDirectory()) return listSkillFiles(sourceDir, nested);
|
|
71
|
+
if (!entry.isFile()) throw new Error(`Skill source contains an unsupported file: ${nested}`);
|
|
72
|
+
return [nested];
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertSafeMergeTarget(target, relativeFile) {
|
|
77
|
+
let current = target;
|
|
78
|
+
for (const segment of relativeFile.split(path.sep)) {
|
|
79
|
+
current = path.join(current, segment);
|
|
80
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
|
|
81
|
+
throw new Error(`Cannot merge through a symbolic link: ${current}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function importSkill({
|
|
87
|
+
library,
|
|
88
|
+
specifier,
|
|
89
|
+
targetDir,
|
|
90
|
+
cwd = process.cwd(),
|
|
91
|
+
overwrite = false,
|
|
92
|
+
merge = false,
|
|
93
|
+
}) {
|
|
94
|
+
if (typeof targetDir !== 'string' || !targetDir.trim()) throw new Error('targetDir is required');
|
|
95
|
+
if (merge && overwrite) throw new Error('merge and overwrite cannot both be enabled');
|
|
96
|
+
const skill = await resolveSkill({ library, specifier, cwd });
|
|
97
|
+
const target = path.resolve(cwd, targetDir);
|
|
98
|
+
const exists = fs.existsSync(target);
|
|
99
|
+
|
|
100
|
+
if (exists && merge) {
|
|
101
|
+
if (!fs.lstatSync(target).isDirectory()) throw new Error(`Merge target is not a directory: ${target}`);
|
|
102
|
+
let addedFiles = 0;
|
|
103
|
+
let updatedFiles = 0;
|
|
104
|
+
for (const relativeFile of listSkillFiles(skill.sourceDir)) {
|
|
105
|
+
assertSafeMergeTarget(target, relativeFile);
|
|
106
|
+
const sourceFile = path.join(skill.sourceDir, relativeFile);
|
|
107
|
+
const destinationFile = path.join(target, relativeFile);
|
|
108
|
+
if (fs.existsSync(destinationFile)) {
|
|
109
|
+
if (!fs.lstatSync(destinationFile).isFile()) throw new Error(`Merge target is not a file: ${destinationFile}`);
|
|
110
|
+
if (fs.readFileSync(sourceFile).equals(fs.readFileSync(destinationFile))) continue;
|
|
111
|
+
fs.copyFileSync(sourceFile, destinationFile);
|
|
112
|
+
updatedFiles += 1;
|
|
113
|
+
} else {
|
|
114
|
+
fs.mkdirSync(path.dirname(destinationFile), { recursive: true });
|
|
115
|
+
fs.copyFileSync(sourceFile, destinationFile);
|
|
116
|
+
addedFiles += 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { ...skill, targetDir: target, installed: addedFiles > 0, updated: updatedFiles > 0, addedFiles, updatedFiles };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (exists && !overwrite) throw new Error(`Target already exists: ${target}`);
|
|
123
|
+
const temporary = `${target}.skillshop-tmp-${process.pid}`;
|
|
124
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
125
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
126
|
+
try {
|
|
127
|
+
fs.cpSync(skill.sourceDir, temporary, { recursive: true, force: true });
|
|
128
|
+
if (overwrite) fs.rmSync(target, { recursive: true, force: true });
|
|
129
|
+
fs.renameSync(temporary, target);
|
|
130
|
+
} finally {
|
|
131
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
return { ...skill, targetDir: target, installed: !exists, updated: exists };
|
|
134
|
+
}
|
package/src/index.js
ADDED
package/src/library.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
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
|
+
return { packageName: pkg.name, library, skills };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function buildSkillLibrary(root = process.cwd()) {
|
|
238
|
+
const packageRoot = path.resolve(root);
|
|
239
|
+
const { packageName, library, skills } = validateSkillLibrary(packageRoot);
|
|
240
|
+
const manifestPath = path.join(packageRoot, SKILLSHOP_MANIFEST_FILE);
|
|
241
|
+
const pkgPath = packageFile(packageRoot);
|
|
242
|
+
const pkg = readJson(pkgPath);
|
|
243
|
+
|
|
244
|
+
const nextPkg = nextPackageManifest(packageRoot, pkg, skills);
|
|
245
|
+
const nextManifest = manifestSource(packageName, library);
|
|
246
|
+
|
|
247
|
+
fs.writeFileSync(manifestPath, nextManifest);
|
|
248
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(nextPkg, null, 2)}\n`);
|
|
249
|
+
return { packageName, manifestPath, skills };
|
|
250
|
+
}
|
package/src/specifier.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
}
|