skills-package-manager 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/dist/113.js +355 -0
- package/dist/bin/skills-pm.js +6 -0
- package/dist/bin/skills.js +6 -0
- package/dist/index.js +1 -0
- package/dist/src/bin/skills-pm.d.ts +2 -0
- package/dist/src/bin/skills.d.ts +2 -0
- package/dist/src/cli/runCli.d.ts +16 -0
- package/dist/src/commands/add.d.ts +5 -0
- package/dist/src/commands/install.d.ts +15 -0
- package/dist/src/config/readSkillsLock.d.ts +2 -0
- package/dist/src/config/readSkillsManifest.d.ts +2 -0
- package/dist/src/config/syncSkillsLock.d.ts +2 -0
- package/dist/src/config/types.d.ts +43 -0
- package/dist/src/config/writeSkillsLock.d.ts +2 -0
- package/dist/src/config/writeSkillsManifest.d.ts +2 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/install/installSkills.d.ts +13 -0
- package/dist/src/install/installState.d.ts +2 -0
- package/dist/src/install/links.d.ts +1 -0
- package/dist/src/install/materializeGitSkill.d.ts +1 -0
- package/dist/src/install/materializeLocalSkill.d.ts +1 -0
- package/dist/src/install/pruneManagedSkills.d.ts +1 -0
- package/dist/src/specifiers/normalizeSpecifier.d.ts +2 -0
- package/dist/src/specifiers/parseSpecifier.d.ts +5 -0
- package/dist/src/utils/fs.d.ts +4 -0
- package/dist/src/utils/hash.d.ts +1 -0
- package/dist/test/add.test.d.ts +1 -0
- package/dist/test/install.test.d.ts +1 -0
- package/dist/test/manifest.test.d.ts +1 -0
- package/dist/test/specifiers.test.d.ts +1 -0
- package/package.json +25 -0
- package/rslib.config.ts +21 -0
- package/src/bin/skills-pm.ts +7 -0
- package/src/bin/skills.ts +7 -0
- package/src/cli/prompt.ts +36 -0
- package/src/cli/runCli.ts +45 -0
- package/src/commands/add.ts +110 -0
- package/src/commands/install.ts +5 -0
- package/src/config/readSkillsLock.ts +18 -0
- package/src/config/readSkillsManifest.ts +22 -0
- package/src/config/syncSkillsLock.ts +75 -0
- package/src/config/types.ts +37 -0
- package/src/config/writeSkillsLock.ts +9 -0
- package/src/config/writeSkillsManifest.ts +14 -0
- package/src/github/listSkills.ts +170 -0
- package/src/github/types.ts +5 -0
- package/src/index.ts +5 -0
- package/src/install/installSkills.ts +78 -0
- package/src/install/installState.ts +20 -0
- package/src/install/links.ts +9 -0
- package/src/install/materializeGitSkill.ts +33 -0
- package/src/install/materializeLocalSkill.ts +35 -0
- package/src/install/pruneManagedSkills.ts +50 -0
- package/src/specifiers/normalizeSpecifier.ts +29 -0
- package/src/specifiers/parseSpecifier.ts +45 -0
- package/src/utils/fs.ts +19 -0
- package/src/utils/hash.ts +5 -0
- package/test/add.test.ts +75 -0
- package/test/fixtures/local-source/skills/hello-skill/SKILL.md +3 -0
- package/test/fixtures/local-source/skills/hello-skill/references/example.md +1 -0
- package/test/github.test.ts +120 -0
- package/test/install.test.ts +169 -0
- package/test/manifest.test.ts +19 -0
- package/test/specifiers.test.ts +43 -0
- package/tsconfig.json +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# skills-pm
|
|
2
|
+
|
|
3
|
+
Core library and CLI for managing agent skills.
|
|
4
|
+
|
|
5
|
+
## CLI Usage
|
|
6
|
+
|
|
7
|
+
### `skills add`
|
|
8
|
+
|
|
9
|
+
Add skills to your project.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Interactive — clone repo, discover skills, select via multiselect prompt
|
|
13
|
+
skills add owner/repo
|
|
14
|
+
skills add https://github.com/owner/repo
|
|
15
|
+
|
|
16
|
+
# Non-interactive — add a specific skill by name
|
|
17
|
+
skills add owner/repo --skill find-skills
|
|
18
|
+
|
|
19
|
+
# Direct specifier — skip discovery
|
|
20
|
+
skills add https://github.com/owner/repo.git#path:/skills/my-skill
|
|
21
|
+
skills add file:./local-source#path:/skills/my-skill
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
#### How it works
|
|
25
|
+
|
|
26
|
+
When given `owner/repo` or a GitHub URL:
|
|
27
|
+
|
|
28
|
+
1. Shallow-clones the repository into a temp directory
|
|
29
|
+
2. Scans for `SKILL.md` files (checks root, then `skills/`, `.agents/skills/`, etc.)
|
|
30
|
+
3. Presents an interactive multiselect prompt (powered by [@clack/prompts](https://github.com/bombshell-dev/clack))
|
|
31
|
+
4. Writes selected skills to `skills.json` and resolves `skills-lock.yaml`
|
|
32
|
+
5. Cleans up the temp directory
|
|
33
|
+
|
|
34
|
+
### `skills install`
|
|
35
|
+
|
|
36
|
+
Install all skills declared in `skills.json`:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
skills install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This resolves each skill from its specifier, materializes it into `installDir` (default `.agents/skills/`), and creates symlinks for each `linkTarget`.
|
|
43
|
+
|
|
44
|
+
## Programmatic API
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { addCommand, installCommand, listRepoSkills } from 'skills-pm'
|
|
48
|
+
|
|
49
|
+
// Add a skill
|
|
50
|
+
await addCommand({
|
|
51
|
+
cwd: process.cwd(),
|
|
52
|
+
specifier: 'vercel-labs/skills',
|
|
53
|
+
skill: 'find-skills',
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Install all skills from skills.json
|
|
57
|
+
await installCommand({ cwd: process.cwd() })
|
|
58
|
+
|
|
59
|
+
// List skills in a GitHub repo (clone + scan)
|
|
60
|
+
const skills = await listRepoSkills('vercel-labs', 'skills')
|
|
61
|
+
// => [{ name: 'find-skills', description: '...', path: '/skills/find-skills' }]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Specifier Format
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
<source>#[ref&]path:<skill-path>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| Part | Description | Example |
|
|
71
|
+
|------|-------------|---------|
|
|
72
|
+
| `source` | Git URL or `file:` path | `https://github.com/o/r.git`, `file:./local` |
|
|
73
|
+
| `ref` | Optional git ref | `main`, `v1.0.0`, `HEAD` |
|
|
74
|
+
| `path` | Path to skill directory within source | `/skills/my-skill` |
|
|
75
|
+
|
|
76
|
+
### Resolution Types
|
|
77
|
+
|
|
78
|
+
- **`git`** — Clones the repo, resolves commit hash, copies skill files
|
|
79
|
+
- **`file`** — Reads from local filesystem, computes content digest
|
|
80
|
+
|
|
81
|
+
## Architecture
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
src/
|
|
85
|
+
├── bin/ # CLI entry points (skills-pm, skills)
|
|
86
|
+
├── cli/ # CLI runner and interactive prompts
|
|
87
|
+
├── commands/ # add, install command implementations
|
|
88
|
+
├── config/ # skills.json / skills-lock.yaml read/write
|
|
89
|
+
├── github/ # Git clone + skill discovery (listSkills)
|
|
90
|
+
├── install/ # Skill materialization, linking, pruning
|
|
91
|
+
├── specifiers/ # Specifier parsing and normalization
|
|
92
|
+
└── utils/ # Hashing, filesystem helpers
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Build
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pnpm build # Builds with Rslib (ESM output + DTS)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Test
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pnpm test # Runs tests with Rstest
|
|
105
|
+
```
|
package/dist/113.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { cp as promises_cp, lstat, mkdir, mkdtemp, readFile, readdir, rm as promises_rm, symlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import node_path from "node:path";
|
|
3
|
+
import yaml from "yaml";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
async function readSkillsLock(rootDir) {
|
|
9
|
+
const filePath = node_path.join(rootDir, 'skills-lock.yaml');
|
|
10
|
+
try {
|
|
11
|
+
const raw = await readFile(filePath, 'utf8');
|
|
12
|
+
return yaml.parse(raw);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if ('ENOENT' === error.code) return null;
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function readSkillsManifest(rootDir) {
|
|
19
|
+
const filePath = node_path.join(rootDir, 'skills.json');
|
|
20
|
+
try {
|
|
21
|
+
const raw = await readFile(filePath, 'utf8');
|
|
22
|
+
const json = JSON.parse(raw);
|
|
23
|
+
return {
|
|
24
|
+
installDir: json.installDir ?? '.agents/skills',
|
|
25
|
+
linkTargets: json.linkTargets ?? [],
|
|
26
|
+
skills: json.skills ?? {}
|
|
27
|
+
};
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if ('ENOENT' === error.code) return null;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function parseSpecifier(specifier) {
|
|
34
|
+
const firstHashIndex = specifier.indexOf('#');
|
|
35
|
+
const secondHashIndex = firstHashIndex >= 0 ? specifier.indexOf('#', firstHashIndex + 1) : -1;
|
|
36
|
+
if (secondHashIndex >= 0) throw new Error('Invalid specifier: multiple # fragments are not supported');
|
|
37
|
+
const hashIndex = firstHashIndex;
|
|
38
|
+
const sourcePart = hashIndex >= 0 ? specifier.slice(0, hashIndex) : specifier;
|
|
39
|
+
const fragment = hashIndex >= 0 ? specifier.slice(hashIndex + 1) : '';
|
|
40
|
+
if (!sourcePart) throw new Error('Specifier source is required');
|
|
41
|
+
if (!fragment) return {
|
|
42
|
+
sourcePart,
|
|
43
|
+
ref: null,
|
|
44
|
+
path: ''
|
|
45
|
+
};
|
|
46
|
+
const parts = fragment.split('&').filter(Boolean);
|
|
47
|
+
let ref = null;
|
|
48
|
+
let parsedPath = '';
|
|
49
|
+
for (const part of parts){
|
|
50
|
+
if (part.startsWith('path:')) {
|
|
51
|
+
parsedPath = part.slice(5);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (null === ref) ref = part;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
sourcePart,
|
|
58
|
+
ref,
|
|
59
|
+
path: parsedPath
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function normalizeSpecifier(specifier) {
|
|
63
|
+
const parsed = parseSpecifier(specifier);
|
|
64
|
+
const type = parsed.sourcePart.startsWith('file:') ? 'file' : parsed.sourcePart.startsWith('npm:') ? 'npm' : 'git';
|
|
65
|
+
const skillPath = parsed.path || '/';
|
|
66
|
+
const skillName = node_path.posix.basename(skillPath);
|
|
67
|
+
const normalized = parsed.ref ? `${parsed.sourcePart}#${parsed.ref}&path:${skillPath}` : parsed.path ? `${parsed.sourcePart}#path:${skillPath}` : parsed.sourcePart;
|
|
68
|
+
return {
|
|
69
|
+
type,
|
|
70
|
+
source: parsed.sourcePart,
|
|
71
|
+
ref: parsed.ref,
|
|
72
|
+
path: skillPath,
|
|
73
|
+
normalized,
|
|
74
|
+
skillName
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function sha256(content) {
|
|
78
|
+
return `sha256-${createHash('sha256').update(content).digest('hex')}`;
|
|
79
|
+
}
|
|
80
|
+
const execFileAsync = promisify(execFile);
|
|
81
|
+
async function resolveGitCommit(url, ref) {
|
|
82
|
+
const target = ref ?? 'HEAD';
|
|
83
|
+
const { stdout } = await execFileAsync('git', [
|
|
84
|
+
'ls-remote',
|
|
85
|
+
url,
|
|
86
|
+
target
|
|
87
|
+
]);
|
|
88
|
+
const line = stdout.trim().split('\n')[0];
|
|
89
|
+
const commit = line?.split('\t')[0]?.trim();
|
|
90
|
+
if (!commit) throw new Error(`Unable to resolve git ref ${target} for ${url}`);
|
|
91
|
+
return commit;
|
|
92
|
+
}
|
|
93
|
+
async function createLockEntry(cwd, specifier) {
|
|
94
|
+
const normalized = normalizeSpecifier(specifier);
|
|
95
|
+
if ('file' === normalized.type) {
|
|
96
|
+
const sourceRoot = node_path.resolve(cwd, normalized.source.slice(5));
|
|
97
|
+
return {
|
|
98
|
+
skillName: normalized.skillName,
|
|
99
|
+
entry: {
|
|
100
|
+
specifier: normalized.normalized,
|
|
101
|
+
resolution: {
|
|
102
|
+
type: 'file',
|
|
103
|
+
path: sourceRoot
|
|
104
|
+
},
|
|
105
|
+
digest: sha256(`${sourceRoot}:${normalized.path}`)
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if ('git' === normalized.type) {
|
|
110
|
+
const commit = await resolveGitCommit(normalized.source, normalized.ref);
|
|
111
|
+
return {
|
|
112
|
+
skillName: normalized.skillName,
|
|
113
|
+
entry: {
|
|
114
|
+
specifier: normalized.normalized,
|
|
115
|
+
resolution: {
|
|
116
|
+
type: 'git',
|
|
117
|
+
url: normalized.source,
|
|
118
|
+
commit,
|
|
119
|
+
path: normalized.path
|
|
120
|
+
},
|
|
121
|
+
digest: sha256(`${normalized.source}:${commit}:${normalized.path}`)
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Unsupported specifier type in 0.1.0 core flow: ${normalized.type}`);
|
|
126
|
+
}
|
|
127
|
+
async function syncSkillsLock(cwd, manifest, existingLock) {
|
|
128
|
+
const nextSkills = {};
|
|
129
|
+
for (const specifier of Object.values(manifest.skills)){
|
|
130
|
+
const { skillName, entry } = await createLockEntry(cwd, specifier);
|
|
131
|
+
nextSkills[skillName] = entry;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
lockfileVersion: '0.1',
|
|
135
|
+
installDir: manifest.installDir ?? '.agents/skills',
|
|
136
|
+
linkTargets: manifest.linkTargets ?? [],
|
|
137
|
+
skills: nextSkills
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async function writeSkillsLock(rootDir, lockfile) {
|
|
141
|
+
const filePath = node_path.join(rootDir, 'skills-lock.yaml');
|
|
142
|
+
await writeFile(filePath, yaml.stringify(lockfile), 'utf8');
|
|
143
|
+
}
|
|
144
|
+
async function writeSkillsManifest(rootDir, manifest) {
|
|
145
|
+
const filePath = node_path.join(rootDir, 'skills.json');
|
|
146
|
+
const nextManifest = {
|
|
147
|
+
installDir: manifest.installDir ?? '.agents/skills',
|
|
148
|
+
linkTargets: manifest.linkTargets ?? [],
|
|
149
|
+
skills: manifest.skills
|
|
150
|
+
};
|
|
151
|
+
await writeFile(filePath, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
|
|
152
|
+
}
|
|
153
|
+
async function addCommand(options) {
|
|
154
|
+
const normalized = normalizeSpecifier(options.specifier);
|
|
155
|
+
const existingManifest = await readSkillsManifest(options.cwd) ?? {
|
|
156
|
+
installDir: '.agents/skills',
|
|
157
|
+
linkTargets: [],
|
|
158
|
+
skills: {}
|
|
159
|
+
};
|
|
160
|
+
const existing = existingManifest.skills[normalized.skillName];
|
|
161
|
+
if (existing && existing !== normalized.normalized) throw new Error(`Skill ${normalized.skillName} already exists with a different specifier`);
|
|
162
|
+
existingManifest.skills[normalized.skillName] = normalized.normalized;
|
|
163
|
+
await writeSkillsManifest(options.cwd, existingManifest);
|
|
164
|
+
const existingLock = await readSkillsLock(options.cwd);
|
|
165
|
+
const lockfile = await syncSkillsLock(options.cwd, existingManifest, existingLock);
|
|
166
|
+
await writeSkillsLock(options.cwd, lockfile);
|
|
167
|
+
return {
|
|
168
|
+
skillName: normalized.skillName,
|
|
169
|
+
specifier: normalized.normalized
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async function ensureDir(dirPath) {
|
|
173
|
+
await mkdir(dirPath, {
|
|
174
|
+
recursive: true
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async function replaceSymlink(target, linkPath) {
|
|
178
|
+
await promises_rm(linkPath, {
|
|
179
|
+
recursive: true,
|
|
180
|
+
force: true
|
|
181
|
+
});
|
|
182
|
+
await symlink(target, linkPath);
|
|
183
|
+
}
|
|
184
|
+
async function writeJson(filePath, value) {
|
|
185
|
+
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
186
|
+
}
|
|
187
|
+
async function linkSkill(rootDir, installDir, linkTarget, skillName) {
|
|
188
|
+
const absoluteTarget = node_path.join(rootDir, installDir, skillName);
|
|
189
|
+
const absoluteLink = node_path.join(rootDir, linkTarget, skillName);
|
|
190
|
+
await ensureDir(node_path.dirname(absoluteLink));
|
|
191
|
+
await replaceSymlink(absoluteTarget, absoluteLink);
|
|
192
|
+
}
|
|
193
|
+
async function readInstallState(rootDir) {
|
|
194
|
+
const filePath = node_path.join(rootDir, '.agents/skills/.skills-pm-install-state.json');
|
|
195
|
+
try {
|
|
196
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function writeInstallState(rootDir, value) {
|
|
202
|
+
const dirPath = node_path.join(rootDir, '.agents/skills');
|
|
203
|
+
await ensureDir(dirPath);
|
|
204
|
+
const filePath = node_path.join(dirPath, '.skills-pm-install-state.json');
|
|
205
|
+
await writeJson(filePath, value);
|
|
206
|
+
}
|
|
207
|
+
async function materializeLocalSkill(rootDir, skillName, sourceRoot, sourcePath, installDir) {
|
|
208
|
+
const relativeSkillPath = sourcePath.replace(/^\//, '');
|
|
209
|
+
const absoluteSkillPath = node_path.join(sourceRoot, relativeSkillPath);
|
|
210
|
+
const skillDocPath = node_path.join(absoluteSkillPath, 'SKILL.md');
|
|
211
|
+
let skillDoc = '';
|
|
212
|
+
try {
|
|
213
|
+
skillDoc = await readFile(skillDocPath, 'utf8');
|
|
214
|
+
} catch {
|
|
215
|
+
throw new Error(`Invalid skill at ${absoluteSkillPath}: missing SKILL.md`);
|
|
216
|
+
}
|
|
217
|
+
if (!skillDoc) throw new Error(`Invalid skill at ${absoluteSkillPath}: missing SKILL.md`);
|
|
218
|
+
const targetDir = node_path.join(rootDir, installDir, skillName);
|
|
219
|
+
await ensureDir(node_path.dirname(targetDir));
|
|
220
|
+
await promises_cp(absoluteSkillPath, targetDir, {
|
|
221
|
+
recursive: true,
|
|
222
|
+
force: true
|
|
223
|
+
});
|
|
224
|
+
await writeJson(node_path.join(targetDir, '.skills-pm.json'), {
|
|
225
|
+
name: skillName,
|
|
226
|
+
installedBy: 'skills-pm',
|
|
227
|
+
version: '0.1.0'
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const materializeGitSkill_execFileAsync = promisify(execFile);
|
|
231
|
+
async function materializeGitSkill(rootDir, skillName, repoUrl, commit, sourcePath, installDir) {
|
|
232
|
+
const checkoutRoot = await mkdtemp(node_path.join(tmpdir(), 'skills-pm-git-checkout-'));
|
|
233
|
+
try {
|
|
234
|
+
await materializeGitSkill_execFileAsync('git', [
|
|
235
|
+
'clone',
|
|
236
|
+
'--depth',
|
|
237
|
+
'1',
|
|
238
|
+
repoUrl,
|
|
239
|
+
checkoutRoot
|
|
240
|
+
]);
|
|
241
|
+
if (commit && 'HEAD' !== commit) await materializeGitSkill_execFileAsync('git', [
|
|
242
|
+
'checkout',
|
|
243
|
+
commit
|
|
244
|
+
], {
|
|
245
|
+
cwd: checkoutRoot
|
|
246
|
+
});
|
|
247
|
+
const skillDocPath = node_path.join(checkoutRoot, sourcePath.replace(/^\//, ''), 'SKILL.md');
|
|
248
|
+
await readFile(skillDocPath, 'utf8');
|
|
249
|
+
await materializeLocalSkill(rootDir, skillName, checkoutRoot, sourcePath, installDir);
|
|
250
|
+
} finally{
|
|
251
|
+
await promises_rm(checkoutRoot, {
|
|
252
|
+
recursive: true,
|
|
253
|
+
force: true
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function isManagedSkillDir(dirPath) {
|
|
258
|
+
try {
|
|
259
|
+
const marker = JSON.parse(await readFile(node_path.join(dirPath, '.skills-pm.json'), 'utf8'));
|
|
260
|
+
return marker?.installedBy === 'skills-pm';
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function pruneManagedSkills(rootDir, installDir, linkTargets, wantedSkillNames) {
|
|
266
|
+
const wanted = new Set(wantedSkillNames);
|
|
267
|
+
const absoluteInstallDir = node_path.join(rootDir, installDir);
|
|
268
|
+
try {
|
|
269
|
+
const entries = await readdir(absoluteInstallDir);
|
|
270
|
+
for (const entry of entries){
|
|
271
|
+
if (entry.startsWith('.')) continue;
|
|
272
|
+
const skillDir = node_path.join(absoluteInstallDir, entry);
|
|
273
|
+
if (await isManagedSkillDir(skillDir)) {
|
|
274
|
+
if (!wanted.has(entry)) {
|
|
275
|
+
await promises_rm(skillDir, {
|
|
276
|
+
recursive: true,
|
|
277
|
+
force: true
|
|
278
|
+
});
|
|
279
|
+
for (const linkTarget of linkTargets){
|
|
280
|
+
const linkPath = node_path.join(rootDir, linkTarget, entry);
|
|
281
|
+
try {
|
|
282
|
+
const stat = await lstat(linkPath);
|
|
283
|
+
if (stat.isSymbolicLink() || stat.isDirectory() || stat.isFile()) await promises_rm(linkPath, {
|
|
284
|
+
recursive: true,
|
|
285
|
+
force: true
|
|
286
|
+
});
|
|
287
|
+
} catch {}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch {}
|
|
293
|
+
}
|
|
294
|
+
function extractSkillPath(specifier, skillName) {
|
|
295
|
+
const marker = '#path:';
|
|
296
|
+
const index = specifier.indexOf(marker);
|
|
297
|
+
if (index >= 0) return specifier.slice(index + marker.length);
|
|
298
|
+
return `/${skillName}`;
|
|
299
|
+
}
|
|
300
|
+
async function installSkills(rootDir) {
|
|
301
|
+
const manifest = await readSkillsManifest(rootDir);
|
|
302
|
+
if (!manifest) return {
|
|
303
|
+
status: 'skipped',
|
|
304
|
+
reason: 'manifest-missing'
|
|
305
|
+
};
|
|
306
|
+
const currentLock = await readSkillsLock(rootDir);
|
|
307
|
+
const lockfile = await syncSkillsLock(rootDir, manifest, currentLock);
|
|
308
|
+
await writeSkillsLock(rootDir, lockfile);
|
|
309
|
+
const lockDigest = sha256(JSON.stringify(lockfile));
|
|
310
|
+
const state = await readInstallState(rootDir);
|
|
311
|
+
if (state?.lockDigest === lockDigest) return {
|
|
312
|
+
status: 'skipped',
|
|
313
|
+
reason: 'up-to-date'
|
|
314
|
+
};
|
|
315
|
+
const installDir = manifest.installDir ?? '.agents/skills';
|
|
316
|
+
const linkTargets = manifest.linkTargets ?? [];
|
|
317
|
+
await pruneManagedSkills(rootDir, installDir, linkTargets, Object.keys(lockfile.skills));
|
|
318
|
+
for (const [skillName, entry] of Object.entries(lockfile.skills)){
|
|
319
|
+
if ('file' === entry.resolution.type) await materializeLocalSkill(rootDir, skillName, entry.resolution.path, extractSkillPath(entry.specifier, skillName), installDir);
|
|
320
|
+
else if ('git' === entry.resolution.type) await materializeGitSkill(rootDir, skillName, entry.resolution.url, entry.resolution.commit, entry.resolution.path, installDir);
|
|
321
|
+
else throw new Error(`Unsupported resolution type in 0.1.0 core flow: ${entry.resolution.type}`);
|
|
322
|
+
for (const linkTarget of linkTargets)await linkSkill(rootDir, installDir, linkTarget, skillName);
|
|
323
|
+
}
|
|
324
|
+
await writeInstallState(rootDir, {
|
|
325
|
+
lockDigest,
|
|
326
|
+
installDir,
|
|
327
|
+
linkTargets,
|
|
328
|
+
installerVersion: '0.1.0',
|
|
329
|
+
installedAt: new Date().toISOString()
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
status: 'installed',
|
|
333
|
+
installed: Object.keys(lockfile.skills)
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
async function installCommand(options) {
|
|
337
|
+
return installSkills(options.cwd);
|
|
338
|
+
}
|
|
339
|
+
async function runCli(argv) {
|
|
340
|
+
const [, , command, ...rest] = argv;
|
|
341
|
+
const cwd = process.cwd();
|
|
342
|
+
if ('add' === command) {
|
|
343
|
+
const specifier = rest[0];
|
|
344
|
+
if (!specifier) throw new Error('Missing required specifier');
|
|
345
|
+
return addCommand({
|
|
346
|
+
cwd,
|
|
347
|
+
specifier
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if ('install' === command) return installCommand({
|
|
351
|
+
cwd
|
|
352
|
+
});
|
|
353
|
+
throw new Error(`Unknown command: ${command}`);
|
|
354
|
+
}
|
|
355
|
+
export { addCommand, installCommand, runCli };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { addCommand, installCommand, runCli } from "./113.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function runCli(argv: string[]): Promise<{
|
|
2
|
+
skillName: string;
|
|
3
|
+
specifier: string;
|
|
4
|
+
} | {
|
|
5
|
+
readonly status: "skipped";
|
|
6
|
+
readonly reason: "manifest-missing";
|
|
7
|
+
readonly installed?: undefined;
|
|
8
|
+
} | {
|
|
9
|
+
readonly status: "skipped";
|
|
10
|
+
readonly reason: "up-to-date";
|
|
11
|
+
readonly installed?: undefined;
|
|
12
|
+
} | {
|
|
13
|
+
readonly status: "installed";
|
|
14
|
+
readonly installed: string[];
|
|
15
|
+
readonly reason?: undefined;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function installCommand(options: {
|
|
2
|
+
cwd: string;
|
|
3
|
+
}): Promise<{
|
|
4
|
+
readonly status: "skipped";
|
|
5
|
+
readonly reason: "manifest-missing";
|
|
6
|
+
readonly installed?: undefined;
|
|
7
|
+
} | {
|
|
8
|
+
readonly status: "skipped";
|
|
9
|
+
readonly reason: "up-to-date";
|
|
10
|
+
readonly installed?: undefined;
|
|
11
|
+
} | {
|
|
12
|
+
readonly status: "installed";
|
|
13
|
+
readonly installed: string[];
|
|
14
|
+
readonly reason?: undefined;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type SkillsManifest = {
|
|
2
|
+
$schema?: string;
|
|
3
|
+
installDir?: string;
|
|
4
|
+
linkTargets?: string[];
|
|
5
|
+
skills: Record<string, string>;
|
|
6
|
+
};
|
|
7
|
+
export type NormalizedSpecifier = {
|
|
8
|
+
type: 'git' | 'file' | 'npm';
|
|
9
|
+
source: string;
|
|
10
|
+
ref: string | null;
|
|
11
|
+
path: string;
|
|
12
|
+
normalized: string;
|
|
13
|
+
skillName: string;
|
|
14
|
+
};
|
|
15
|
+
export type SkillsLockEntry = {
|
|
16
|
+
specifier: string;
|
|
17
|
+
resolution: {
|
|
18
|
+
type: 'file';
|
|
19
|
+
path: string;
|
|
20
|
+
} | {
|
|
21
|
+
type: 'git';
|
|
22
|
+
url: string;
|
|
23
|
+
commit: string;
|
|
24
|
+
path: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'npm';
|
|
27
|
+
packageName: string;
|
|
28
|
+
version: string;
|
|
29
|
+
path: string;
|
|
30
|
+
integrity?: string;
|
|
31
|
+
};
|
|
32
|
+
digest: string;
|
|
33
|
+
};
|
|
34
|
+
export type SkillsLock = {
|
|
35
|
+
lockfileVersion: '0.1';
|
|
36
|
+
installDir: string;
|
|
37
|
+
linkTargets: string[];
|
|
38
|
+
skills: Record<string, SkillsLockEntry>;
|
|
39
|
+
};
|
|
40
|
+
export type AddCommandOptions = {
|
|
41
|
+
cwd: string;
|
|
42
|
+
specifier: string;
|
|
43
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function installSkills(rootDir: string): Promise<{
|
|
2
|
+
readonly status: "skipped";
|
|
3
|
+
readonly reason: "manifest-missing";
|
|
4
|
+
readonly installed?: undefined;
|
|
5
|
+
} | {
|
|
6
|
+
readonly status: "skipped";
|
|
7
|
+
readonly reason: "up-to-date";
|
|
8
|
+
readonly installed?: undefined;
|
|
9
|
+
} | {
|
|
10
|
+
readonly status: "installed";
|
|
11
|
+
readonly installed: string[];
|
|
12
|
+
readonly reason?: undefined;
|
|
13
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function linkSkill(rootDir: string, installDir: string, linkTarget: string, skillName: string): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function materializeGitSkill(rootDir: string, skillName: string, repoUrl: string, commit: string, sourcePath: string, installDir: string): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function materializeLocalSkill(rootDir: string, skillName: string, sourceRoot: string, sourcePath: string, installDir: string): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function pruneManagedSkills(rootDir: string, installDir: string, linkTargets: string[], wantedSkillNames: string[]): Promise<void>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function ensureDir(dirPath: string): Promise<void>;
|
|
2
|
+
export declare function replaceDir(from: string, to: string): Promise<void>;
|
|
3
|
+
export declare function replaceSymlink(target: string, linkPath: string): Promise<void>;
|
|
4
|
+
export declare function writeJson(filePath: string, value: unknown): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sha256(content: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|