skills-package-manager 0.9.0 → 0.10.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 +9 -6
- package/dist/index.d.ts +5 -2
- package/dist/index.js +155 -42
- package/package.json +1 -1
- package/skills/skills-package-manager-cli/SKILL.md +2 -1
package/README.md
CHANGED
|
@@ -50,12 +50,13 @@ npx skills-package-manager add https://github.com/owner/repo/tree/main/skills/my
|
|
|
50
50
|
# Direct specifier — skip discovery
|
|
51
51
|
npx skills-package-manager add https://github.com/owner/repo.git#path:/skills/my-skill
|
|
52
52
|
npx skills-package-manager add link:./local-source/skills/my-skill
|
|
53
|
+
npx skills-package-manager add local:./.agents/skills/my-skill
|
|
53
54
|
npx skills-package-manager add ./local-source
|
|
54
55
|
npx skills-package-manager add file:./skills-package.tgz#path:/skills/my-skill
|
|
55
56
|
npx skills-package-manager add npm:@scope/skills-package#path:/skills/my-skill
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
After `npx skills-package-manager add`, the newly added skills are resolved,
|
|
59
|
+
After `npx skills-package-manager add`, the newly added skills are resolved, installed or registered according to their protocol, and linked to each configured `linkTarget` immediately.
|
|
59
60
|
|
|
60
61
|
#### How it works
|
|
61
62
|
|
|
@@ -104,9 +105,9 @@ Install all skills declared in `skills.json`:
|
|
|
104
105
|
npx skills-package-manager install
|
|
105
106
|
```
|
|
106
107
|
|
|
107
|
-
This resolves each skill from its specifier,
|
|
108
|
+
This resolves each skill from its specifier, installs managed skills into `installDir` (default `.agents/skills/`), registers `local:` skills in place, and creates symlinks for each `linkTarget`.
|
|
108
109
|
When `selfSkill` is `true`, `npx skills-package-manager install` also installs the bundled `skills-package-manager-cli` skill so users get guidance for `skills.json`, `skills-lock.yaml`, and `npx skills-package-manager` commands. This helper skill is not written to `skills-lock.yaml`.
|
|
109
|
-
If `patchedSkills` contains an entry for a skill, the corresponding patch file is applied after the skill is materialized.
|
|
110
|
+
If `patchedSkills` contains an entry for a managed skill, the corresponding patch file is applied after the skill is materialized. `local:` skills cannot be patched because their source directories are user-owned.
|
|
110
111
|
|
|
111
112
|
### `npx skills-package-manager patch`
|
|
112
113
|
|
|
@@ -154,7 +155,7 @@ Behavior:
|
|
|
154
155
|
|
|
155
156
|
- Uses `skills.json` as the source of truth
|
|
156
157
|
- Re-resolves git refs and npm package targets
|
|
157
|
-
- Skips `link:` skills, including the bundled self skill
|
|
158
|
+
- Skips local `link:` and `local:` skills, including the bundled self skill
|
|
158
159
|
- Fails immediately for unknown skill names
|
|
159
160
|
- Writes `skills-lock.yaml` only after fetch and link succeed
|
|
160
161
|
|
|
@@ -183,11 +184,12 @@ const skills = await listRepoSkills('vercel-labs', 'skills')
|
|
|
183
184
|
```text
|
|
184
185
|
git/file/npm: <source>#[ref&]path:<skill-path>
|
|
185
186
|
link: link:<path-to-skill-dir>
|
|
187
|
+
local: local:<path-to-existing-skill-dir>
|
|
186
188
|
```
|
|
187
189
|
|
|
188
190
|
| Part | Description | Example |
|
|
189
191
|
|------|-------------|---------|
|
|
190
|
-
| `source` | Git URL, direct `link:` skill path, `file:` tarball, or `npm:` package name | `https://github.com/o/r.git`, `link:./local/skills/my-skill`, `file:./skills.tgz`, `npm:@scope/pkg` |
|
|
192
|
+
| `source` | Git URL, direct `link:` or `local:` skill path, `file:` tarball, or `npm:` package name | `https://github.com/o/r.git`, `link:./local/skills/my-skill`, `local:./.agents/skills/my-skill`, `file:./skills.tgz`, `npm:@scope/pkg` |
|
|
191
193
|
| `ref` | Optional git ref | `main`, `v1.0.0`, `HEAD`, `6cb0992`, `6cb0992a176f2ca142e19f64dca8ac12025b035e` |
|
|
192
194
|
| `path` | Path to skill directory within source | `/skills/my-skill` |
|
|
193
195
|
|
|
@@ -196,7 +198,8 @@ link: link:<path-to-skill-dir>
|
|
|
196
198
|
### Resolution Types
|
|
197
199
|
|
|
198
200
|
- **`git`** — Clones the repo, resolves commit hash, copies skill files
|
|
199
|
-
- **`link`** —
|
|
201
|
+
- **`link`** — Symlinks a local skill directory into `installDir`
|
|
202
|
+
- **`local`** — Uses an existing user-owned skill directory in place
|
|
200
203
|
- **`file`** — Extracts a local `tgz` package and copies the selected skill
|
|
201
204
|
- **`npm`** — Resolves a package from the configured npm registry, locks the tarball URL/version/integrity, and installs from the downloaded tarball
|
|
202
205
|
|
package/dist/index.d.ts
CHANGED
|
@@ -273,7 +273,7 @@ declare type NormalizedSkillsManifest = {
|
|
|
273
273
|
};
|
|
274
274
|
|
|
275
275
|
export declare type NormalizedSpecifier = {
|
|
276
|
-
type: 'git' | 'link' | 'file' | 'npm';
|
|
276
|
+
type: 'git' | 'link' | 'local' | 'file' | 'npm';
|
|
277
277
|
source: string;
|
|
278
278
|
ref: string | null;
|
|
279
279
|
path: string;
|
|
@@ -396,6 +396,9 @@ export declare type SkillsLockEntry = {
|
|
|
396
396
|
resolution: {
|
|
397
397
|
type: 'link';
|
|
398
398
|
path: string;
|
|
399
|
+
} | {
|
|
400
|
+
type: 'local';
|
|
401
|
+
path: string;
|
|
399
402
|
} | {
|
|
400
403
|
type: 'file';
|
|
401
404
|
tarball: string;
|
|
@@ -471,7 +474,7 @@ export declare type UpdateCommandResult = {
|
|
|
471
474
|
unchanged: string[];
|
|
472
475
|
skipped: Array<{
|
|
473
476
|
name: string;
|
|
474
|
-
reason: 'link-specifier';
|
|
477
|
+
reason: 'link-specifier' | 'local-specifier';
|
|
475
478
|
}>;
|
|
476
479
|
failed: Array<{
|
|
477
480
|
name: string;
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { co } from "./npm-tar.js";
|
|
|
16
16
|
import { cac } from "./npm-cac.js";
|
|
17
17
|
import { Ct } from "./npm-clack_core.js";
|
|
18
18
|
var package_namespaceObject = {
|
|
19
|
-
rE: "0.
|
|
19
|
+
rE: "0.10.0"
|
|
20
20
|
};
|
|
21
21
|
function getHomeDir() {
|
|
22
22
|
return homedir();
|
|
@@ -660,6 +660,7 @@ function formatErrorForDisplay(error) {
|
|
|
660
660
|
output += `\n - owner/repo (GitHub shorthand)`;
|
|
661
661
|
output += `\n - https://github.com/owner/repo.git`;
|
|
662
662
|
output += `\n - link:./path/to/skill-dir`;
|
|
663
|
+
output += `\n - local:./path/to/existing-skill-dir`;
|
|
663
664
|
output += `\n - file:./path/to/skill-package.tgz#path:/skills/my-skill`;
|
|
664
665
|
output += `\n - npm:@scope/skill-package#path:/skills/my-skill`;
|
|
665
666
|
}
|
|
@@ -899,6 +900,20 @@ async function resolveLinkEntry(cwd, source, skillName, specifier) {
|
|
|
899
900
|
}
|
|
900
901
|
};
|
|
901
902
|
}
|
|
903
|
+
async function resolveLocalEntry(cwd, source, skillName, specifier) {
|
|
904
|
+
const sourceRoot = node_path.resolve(cwd, source.slice(6));
|
|
905
|
+
return {
|
|
906
|
+
skillName,
|
|
907
|
+
entry: {
|
|
908
|
+
specifier,
|
|
909
|
+
resolution: {
|
|
910
|
+
type: 'local',
|
|
911
|
+
path: toPortableRelativePath(cwd, sourceRoot)
|
|
912
|
+
},
|
|
913
|
+
digest: ''
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
902
917
|
const node_modules_semver = __webpack_require__("../../node_modules/.pnpm/semver@7.7.4/node_modules/semver/index.js");
|
|
903
918
|
var node_modules_semver_default = /*#__PURE__*/ __webpack_require__.n(node_modules_semver);
|
|
904
919
|
const resolvedNpmPackageCache = new Map();
|
|
@@ -1159,6 +1174,8 @@ async function resolveEntry(cwd, normalized, skillName) {
|
|
|
1159
1174
|
switch(normalized.type){
|
|
1160
1175
|
case 'link':
|
|
1161
1176
|
return resolveLinkEntry(cwd, normalized.source, finalSkillName, normalized.normalized);
|
|
1177
|
+
case 'local':
|
|
1178
|
+
return resolveLocalEntry(cwd, normalized.source, finalSkillName, normalized.normalized);
|
|
1162
1179
|
case 'file':
|
|
1163
1180
|
return resolveFileEntry(cwd, normalized.source, normalized.path, finalSkillName, normalized.normalized);
|
|
1164
1181
|
case 'git':
|
|
@@ -1172,9 +1189,16 @@ async function resolveEntry(cwd, normalized, skillName) {
|
|
|
1172
1189
|
}
|
|
1173
1190
|
}
|
|
1174
1191
|
}
|
|
1192
|
+
function normalizeProtocolPathSource(sourcePart, protocol) {
|
|
1193
|
+
const prefix = `${protocol}:`;
|
|
1194
|
+
const sourcePath = sourcePart.slice(prefix.length).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
1195
|
+
return `${prefix}${sourcePath}`;
|
|
1196
|
+
}
|
|
1175
1197
|
function normalizeLinkSource(sourcePart) {
|
|
1176
|
-
|
|
1177
|
-
|
|
1198
|
+
return normalizeProtocolPathSource(sourcePart, 'link');
|
|
1199
|
+
}
|
|
1200
|
+
function normalizeLocalSource(sourcePart) {
|
|
1201
|
+
return normalizeProtocolPathSource(sourcePart, 'local');
|
|
1178
1202
|
}
|
|
1179
1203
|
function parseSpecifier(specifier) {
|
|
1180
1204
|
const firstHashIndex = specifier.indexOf('#');
|
|
@@ -1214,11 +1238,14 @@ function parseSpecifier(specifier) {
|
|
|
1214
1238
|
};
|
|
1215
1239
|
}
|
|
1216
1240
|
function normalizeSpecifier(specifier) {
|
|
1217
|
-
if (specifier.startsWith('link:') && specifier.includes('#'))
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1241
|
+
if ((specifier.startsWith('link:') || specifier.startsWith('local:')) && specifier.includes('#')) {
|
|
1242
|
+
const protocol = specifier.startsWith('link:') ? 'link' : 'local';
|
|
1243
|
+
throw new ParseError({
|
|
1244
|
+
code: codes_ErrorCode.INVALID_SPECIFIER,
|
|
1245
|
+
message: `Invalid ${protocol} specifier: ${protocol}: must point directly to a skill directory`,
|
|
1246
|
+
content: specifier
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1222
1249
|
let parsed;
|
|
1223
1250
|
try {
|
|
1224
1251
|
parsed = parseSpecifier(specifier);
|
|
@@ -1231,17 +1258,17 @@ function normalizeSpecifier(specifier) {
|
|
|
1231
1258
|
cause: error
|
|
1232
1259
|
});
|
|
1233
1260
|
}
|
|
1234
|
-
const type = parsed.sourcePart.startsWith('link:') ? 'link' : parsed.sourcePart.startsWith('file:') ? 'file' : parsed.sourcePart.startsWith('npm:') ? 'npm' : 'git';
|
|
1235
|
-
if ('link' === type) {
|
|
1236
|
-
const
|
|
1237
|
-
const
|
|
1238
|
-
const skillName = node_path.posix.basename(
|
|
1261
|
+
const type = parsed.sourcePart.startsWith('link:') ? 'link' : parsed.sourcePart.startsWith('local:') ? 'local' : parsed.sourcePart.startsWith('file:') ? 'file' : parsed.sourcePart.startsWith('npm:') ? 'npm' : 'git';
|
|
1262
|
+
if ('link' === type || 'local' === type) {
|
|
1263
|
+
const localSource = 'link' === type ? normalizeLinkSource(parsed.sourcePart) : normalizeLocalSource(parsed.sourcePart);
|
|
1264
|
+
const localPath = localSource.slice(`${type}:`.length);
|
|
1265
|
+
const skillName = node_path.posix.basename(localPath);
|
|
1239
1266
|
return {
|
|
1240
1267
|
type,
|
|
1241
|
-
source:
|
|
1268
|
+
source: localSource,
|
|
1242
1269
|
ref: null,
|
|
1243
1270
|
path: '/',
|
|
1244
|
-
normalized:
|
|
1271
|
+
normalized: localSource,
|
|
1245
1272
|
skillName
|
|
1246
1273
|
};
|
|
1247
1274
|
}
|
|
@@ -1260,10 +1287,11 @@ function normalizeSpecifier(specifier) {
|
|
|
1260
1287
|
function parseForComparison(specifier) {
|
|
1261
1288
|
const parsed = parseSpecifier(specifier);
|
|
1262
1289
|
const isLink = parsed.sourcePart.startsWith('link:');
|
|
1290
|
+
const isLocal = parsed.sourcePart.startsWith('local:');
|
|
1263
1291
|
return {
|
|
1264
|
-
sourcePart: isLink ? normalizeLinkSource(parsed.sourcePart) : parsed.sourcePart,
|
|
1265
|
-
ref: isLink ? null : parsed.ref,
|
|
1266
|
-
path: isLink ? '/' : parsed.path || '/'
|
|
1292
|
+
sourcePart: isLink ? normalizeLinkSource(parsed.sourcePart) : isLocal ? normalizeLocalSource(parsed.sourcePart) : parsed.sourcePart,
|
|
1293
|
+
ref: isLink || isLocal ? null : parsed.ref,
|
|
1294
|
+
path: isLink || isLocal ? '/' : parsed.path || '/'
|
|
1267
1295
|
};
|
|
1268
1296
|
}
|
|
1269
1297
|
function isSpecifierCompatible(manifestSpecifier, lockSpecifier) {
|
|
@@ -1344,6 +1372,7 @@ async function resolveLockEntry(cwd, specifier, skillName) {
|
|
|
1344
1372
|
async function attachManifestPatchToEntry(cwd, manifest, skillName, entry) {
|
|
1345
1373
|
const patchPath = manifest.patchedSkills?.[skillName];
|
|
1346
1374
|
if (!patchPath) return entry;
|
|
1375
|
+
if ('local' === entry.resolution.type) throw new Error(`local: skill ${skillName} cannot be patched because its source is user-owned`);
|
|
1347
1376
|
const absolutePatchPath = node_path.resolve(cwd, patchPath);
|
|
1348
1377
|
return {
|
|
1349
1378
|
...entry,
|
|
@@ -1675,6 +1704,49 @@ async function writeInstallState(rootDir, installDir, value) {
|
|
|
1675
1704
|
const filePath = node_path.join(dirPath, INSTALL_STATE_FILE);
|
|
1676
1705
|
await writeJson(filePath, value);
|
|
1677
1706
|
}
|
|
1707
|
+
function getLocalSkillDirs(rootDir, lockfiles) {
|
|
1708
|
+
const dirs = [];
|
|
1709
|
+
for (const lockfile of lockfiles)if (lockfile) {
|
|
1710
|
+
for (const entry of Object.values(lockfile.skills))if ('local' === entry.resolution.type) dirs.push(node_path.resolve(rootDir, entry.resolution.path));
|
|
1711
|
+
}
|
|
1712
|
+
return Array.from(new Set(dirs));
|
|
1713
|
+
}
|
|
1714
|
+
function getSkillInstallPath(rootDir, installDir, skillName, entry) {
|
|
1715
|
+
return 'local' === entry.resolution.type ? node_path.resolve(rootDir, entry.resolution.path) : node_path.join(rootDir, installDir, skillName);
|
|
1716
|
+
}
|
|
1717
|
+
function toRepoRelativePath(rootDir, absolutePath) {
|
|
1718
|
+
const relativePath = node_path.relative(rootDir, absolutePath);
|
|
1719
|
+
if (!relativePath || '..' === relativePath || relativePath.startsWith(`..${node_path.sep}`) || node_path.isAbsolute(relativePath)) return null;
|
|
1720
|
+
return relativePath.split(node_path.sep).join('/');
|
|
1721
|
+
}
|
|
1722
|
+
function createUnignoreRules(relativePath) {
|
|
1723
|
+
const parts = relativePath.split('/').filter(Boolean);
|
|
1724
|
+
const rules = [];
|
|
1725
|
+
for(let index = 0; index < parts.length; index += 1)rules.push(`!${parts.slice(0, index + 1).join('/')}/`);
|
|
1726
|
+
rules.push(`!${relativePath}/**`);
|
|
1727
|
+
return rules;
|
|
1728
|
+
}
|
|
1729
|
+
async function ensureLocalSkillGitignoreRules(rootDir, lockfile) {
|
|
1730
|
+
const desiredRules = new Set();
|
|
1731
|
+
for (const dir of getLocalSkillDirs(rootDir, [
|
|
1732
|
+
lockfile
|
|
1733
|
+
])){
|
|
1734
|
+
const relativePath = toRepoRelativePath(rootDir, dir);
|
|
1735
|
+
if (relativePath) for (const rule of createUnignoreRules(relativePath))desiredRules.add(rule);
|
|
1736
|
+
}
|
|
1737
|
+
if (0 === desiredRules.size) return;
|
|
1738
|
+
const gitignorePath = node_path.join(rootDir, '.gitignore');
|
|
1739
|
+
let existing = '';
|
|
1740
|
+
try {
|
|
1741
|
+
existing = await promises_readFile(gitignorePath, 'utf8');
|
|
1742
|
+
} catch {}
|
|
1743
|
+
const existingRules = new Set(existing.split(/\r?\n/).map((line)=>line.trim()));
|
|
1744
|
+
const missingRules = Array.from(desiredRules).filter((rule)=>!existingRules.has(rule));
|
|
1745
|
+
if (0 === missingRules.length) return;
|
|
1746
|
+
const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
1747
|
+
const separator = existing.trim() ? '\n' : '';
|
|
1748
|
+
await writeFile(gitignorePath, `${existing}${prefix}${separator}# Keep local skills tracked\n${missingRules.join('\n')}\n`, 'utf8');
|
|
1749
|
+
}
|
|
1678
1750
|
function resolveTargetPath(rootDir, targetPath) {
|
|
1679
1751
|
return node_path.isAbsolute(targetPath) ? targetPath : node_path.join(rootDir, targetPath);
|
|
1680
1752
|
}
|
|
@@ -1693,29 +1765,32 @@ async function isManagedSkillDir(dirPath) {
|
|
|
1693
1765
|
return false;
|
|
1694
1766
|
}
|
|
1695
1767
|
}
|
|
1696
|
-
async function pruneManagedSkills(rootDir, installDir, linkTargets, wantedSkillNames) {
|
|
1768
|
+
async function pruneManagedSkills(rootDir, installDir, linkTargets, wantedSkillNames, protectedSkillDirs = []) {
|
|
1697
1769
|
const wanted = new Set(wantedSkillNames);
|
|
1698
1770
|
const absoluteInstallDir = node_path.join(rootDir, installDir);
|
|
1771
|
+
const protectedDirs = new Set(protectedSkillDirs.map((dir)=>node_path.resolve(dir)));
|
|
1699
1772
|
try {
|
|
1700
1773
|
const entries = await promises_readdir(absoluteInstallDir);
|
|
1701
1774
|
for (const entry of entries){
|
|
1702
1775
|
if (entry.startsWith('.')) continue;
|
|
1703
1776
|
const skillDir = node_path.join(absoluteInstallDir, entry);
|
|
1704
|
-
if (
|
|
1705
|
-
if (
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1777
|
+
if (!protectedDirs.has(node_path.resolve(skillDir))) {
|
|
1778
|
+
if (await isManagedSkillDir(skillDir)) {
|
|
1779
|
+
if (!wanted.has(entry)) {
|
|
1780
|
+
await rm(skillDir, {
|
|
1781
|
+
recursive: true,
|
|
1782
|
+
force: true
|
|
1783
|
+
});
|
|
1784
|
+
for (const linkTarget of linkTargets){
|
|
1785
|
+
const linkPath = node_path.join(resolveTargetPath(rootDir, linkTarget), entry);
|
|
1786
|
+
try {
|
|
1787
|
+
const stat = await promises_lstat(linkPath);
|
|
1788
|
+
if (stat.isSymbolicLink() || stat.isDirectory() || stat.isFile()) await rm(linkPath, {
|
|
1789
|
+
recursive: true,
|
|
1790
|
+
force: true
|
|
1791
|
+
});
|
|
1792
|
+
} catch {}
|
|
1793
|
+
}
|
|
1719
1794
|
}
|
|
1720
1795
|
}
|
|
1721
1796
|
}
|
|
@@ -1946,6 +2021,16 @@ async function fetchLinkSkill(rootDir, skillName, entry, installDir) {
|
|
|
1946
2021
|
await symlink(sourceRoot, targetDir);
|
|
1947
2022
|
return targetDir;
|
|
1948
2023
|
}
|
|
2024
|
+
async function fetchLocalSkill(rootDir, entry) {
|
|
2025
|
+
if ('local' !== entry.resolution.type) throw new Error('Expected local resolution');
|
|
2026
|
+
const sourceRoot = node_path.resolve(rootDir, entry.resolution.path);
|
|
2027
|
+
try {
|
|
2028
|
+
await access(node_path.join(sourceRoot, 'SKILL.md'));
|
|
2029
|
+
} catch {
|
|
2030
|
+
throw new Error(`Invalid local skill at ${sourceRoot}: missing SKILL.md`);
|
|
2031
|
+
}
|
|
2032
|
+
return sourceRoot;
|
|
2033
|
+
}
|
|
1949
2034
|
const inFlightDownloads = new Map();
|
|
1950
2035
|
async function fetchNpmSkill(rootDir, skillName, entry, installDir, _cache) {
|
|
1951
2036
|
if ('npm' !== entry.resolution.type) throw new Error('Expected npm resolution');
|
|
@@ -1979,6 +2064,10 @@ async function fetchSkill(rootDir, skillName, entry, installDir, cache) {
|
|
|
1979
2064
|
return {
|
|
1980
2065
|
installPath: await fetchLinkSkill(rootDir, skillName, entry, installDir)
|
|
1981
2066
|
};
|
|
2067
|
+
case 'local':
|
|
2068
|
+
return {
|
|
2069
|
+
installPath: await fetchLocalSkill(rootDir, entry)
|
|
2070
|
+
};
|
|
1982
2071
|
case 'file':
|
|
1983
2072
|
return {
|
|
1984
2073
|
installPath: await fetchFileSkill(rootDir, skillName, entry, installDir)
|
|
@@ -2257,6 +2346,10 @@ function createTaskQueue(processor, options) {
|
|
|
2257
2346
|
async function isSkillUpToDate(rootDir, installDir, skillName, entry) {
|
|
2258
2347
|
const skillDir = node_path.join(rootDir, installDir, skillName);
|
|
2259
2348
|
try {
|
|
2349
|
+
if ('local' === entry.resolution.type) {
|
|
2350
|
+
await access(node_path.join(node_path.resolve(rootDir, entry.resolution.path), 'SKILL.md'));
|
|
2351
|
+
return true;
|
|
2352
|
+
}
|
|
2260
2353
|
const stats = await promises_lstat(skillDir);
|
|
2261
2354
|
if ('link' === entry.resolution.type) {
|
|
2262
2355
|
if (!stats.isSymbolicLink()) return false;
|
|
@@ -2285,7 +2378,7 @@ function createFetchTaskQueue(ctx, bus, options) {
|
|
|
2285
2378
|
const result = {
|
|
2286
2379
|
skillName: task.skillName,
|
|
2287
2380
|
entry: task.entry,
|
|
2288
|
-
installPath:
|
|
2381
|
+
installPath: getSkillInstallPath(ctx.cwd, installDir, task.skillName, task.entry),
|
|
2289
2382
|
skipped: true
|
|
2290
2383
|
};
|
|
2291
2384
|
bus.emitFetched(result);
|
|
@@ -2311,8 +2404,8 @@ function createFetchTaskQueue(ctx, bus, options) {
|
|
|
2311
2404
|
function links_resolveTargetPath(rootDir, targetPath) {
|
|
2312
2405
|
return node_path.isAbsolute(targetPath) ? targetPath : node_path.join(rootDir, targetPath);
|
|
2313
2406
|
}
|
|
2314
|
-
async function linkSkill(rootDir, installDir, linkTarget, skillName) {
|
|
2315
|
-
const absoluteTarget = node_path.join(rootDir, installDir, skillName);
|
|
2407
|
+
async function linkSkill(rootDir, installDir, linkTarget, skillName, sourcePath) {
|
|
2408
|
+
const absoluteTarget = sourcePath ?? node_path.join(rootDir, installDir, skillName);
|
|
2316
2409
|
const absoluteLink = node_path.join(links_resolveTargetPath(rootDir, linkTarget), skillName);
|
|
2317
2410
|
await ensureDir(node_path.dirname(absoluteLink));
|
|
2318
2411
|
await replaceSymlink(absoluteTarget, absoluteLink);
|
|
@@ -2322,7 +2415,7 @@ function createLinkTaskQueue(ctx, bus, options) {
|
|
|
2322
2415
|
const linkTargets = ctx.lockfile?.linkTargets ?? ctx.manifest.linkTargets ?? [];
|
|
2323
2416
|
async function processor(task) {
|
|
2324
2417
|
try {
|
|
2325
|
-
for (const linkTarget of linkTargets)await linkSkill(ctx.cwd, installDir, linkTarget, task.skillName);
|
|
2418
|
+
for (const linkTarget of linkTargets)await linkSkill(ctx.cwd, installDir, linkTarget, task.skillName, task.installPath);
|
|
2326
2419
|
const result = {
|
|
2327
2420
|
skillName: task.skillName
|
|
2328
2421
|
};
|
|
@@ -2451,7 +2544,17 @@ async function runPipeline(input) {
|
|
|
2451
2544
|
linkTargets,
|
|
2452
2545
|
skills: entries
|
|
2453
2546
|
});
|
|
2454
|
-
|
|
2547
|
+
const runtimeLockfile = {
|
|
2548
|
+
lockfileVersion: '0.1',
|
|
2549
|
+
installDir,
|
|
2550
|
+
linkTargets,
|
|
2551
|
+
skills: entries
|
|
2552
|
+
};
|
|
2553
|
+
await ensureLocalSkillGitignoreRules(ctx.cwd, runtimeLockfile);
|
|
2554
|
+
await pruneManagedSkills(ctx.cwd, installDir, linkTargets, skillNames, getLocalSkillDirs(ctx.cwd, [
|
|
2555
|
+
runtimeLockfile,
|
|
2556
|
+
ctx.lockfile
|
|
2557
|
+
]));
|
|
2455
2558
|
if (skipResolve) for (const [skillName, entry] of Object.entries(entries))bus.emitResolved({
|
|
2456
2559
|
skillName,
|
|
2457
2560
|
entry
|
|
@@ -2608,7 +2711,7 @@ function buildLinkSpecifier(sourceRoot, skillPath) {
|
|
|
2608
2711
|
return normalizeLinkSource(`link:${absoluteSkillPath}`);
|
|
2609
2712
|
}
|
|
2610
2713
|
function isDirectSkillSpecifier(specifier) {
|
|
2611
|
-
return specifier.startsWith('link:') || specifier.startsWith('file:') || specifier.startsWith('npm:') || specifier.includes('#path:') || specifier.includes('&path:');
|
|
2714
|
+
return specifier.startsWith('link:') || specifier.startsWith('local:') || specifier.startsWith('file:') || specifier.startsWith('npm:') || specifier.includes('#path:') || specifier.includes('&path:');
|
|
2612
2715
|
}
|
|
2613
2716
|
function isLocalPathSpecifier(specifier) {
|
|
2614
2717
|
return node_path.isAbsolute(specifier) || specifier.startsWith('./') || specifier.startsWith('../') || '.' === specifier || '..' === specifier || /^[a-zA-Z]:[/\\]/.test(specifier);
|
|
@@ -3505,6 +3608,13 @@ async function updateCommand(options) {
|
|
|
3505
3608
|
});
|
|
3506
3609
|
continue;
|
|
3507
3610
|
}
|
|
3611
|
+
if ('local' === normalized.type) {
|
|
3612
|
+
result.skipped.push({
|
|
3613
|
+
name: skillName,
|
|
3614
|
+
reason: 'local-specifier'
|
|
3615
|
+
});
|
|
3616
|
+
continue;
|
|
3617
|
+
}
|
|
3508
3618
|
const { entry } = await resolveLockEntry(options.cwd, specifier);
|
|
3509
3619
|
const nextEntry = await attachManifestPatchToEntry(options.cwd, ctx.manifest, skillName, entry);
|
|
3510
3620
|
const previous = ctx.lockfile?.skills[skillName];
|
|
@@ -3644,7 +3754,10 @@ async function installSkills_withBundledSelfSkillLock(rootDir, manifest, lockfil
|
|
|
3644
3754
|
async function fetchSkillsFromLock(rootDir, manifest, lockfile, options) {
|
|
3645
3755
|
const installDir = manifest.installDir ?? '.agents/skills';
|
|
3646
3756
|
const linkTargets = manifest.linkTargets ?? [];
|
|
3647
|
-
await
|
|
3757
|
+
await ensureLocalSkillGitignoreRules(rootDir, lockfile);
|
|
3758
|
+
await pruneManagedSkills(rootDir, installDir, linkTargets, Object.keys(lockfile.skills), getLocalSkillDirs(rootDir, [
|
|
3759
|
+
lockfile
|
|
3760
|
+
]));
|
|
3648
3761
|
const lockDigest = sha256(JSON.stringify(lockfile));
|
|
3649
3762
|
const state = await readInstallState(rootDir, installDir);
|
|
3650
3763
|
if (state?.lockDigest === lockDigest && await installSkills_areManagedSkillsInstalled(rootDir, installDir, Object.keys(lockfile.skills))) return {
|
|
@@ -3680,7 +3793,7 @@ async function linkSkillsFromLock(rootDir, manifest, lockfile, options) {
|
|
|
3680
3793
|
const installDir = manifest.installDir ?? '.agents/skills';
|
|
3681
3794
|
const linkTargets = manifest.linkTargets ?? [];
|
|
3682
3795
|
for (const skillName of Object.keys(lockfile.skills)){
|
|
3683
|
-
for (const linkTarget of linkTargets)await linkSkill(rootDir, installDir, linkTarget, skillName);
|
|
3796
|
+
for (const linkTarget of linkTargets)await linkSkill(rootDir, installDir, linkTarget, skillName, getSkillInstallPath(rootDir, installDir, skillName, lockfile.skills[skillName]));
|
|
3684
3797
|
options?.onProgress?.({
|
|
3685
3798
|
type: 'installed',
|
|
3686
3799
|
skillName
|
package/package.json
CHANGED
|
@@ -40,7 +40,7 @@ Use this skill for repositories that already use `skills-package-manager`, or wh
|
|
|
40
40
|
|
|
41
41
|
4. `npx skills-package-manager update [skill...]`
|
|
42
42
|
- Refreshes resolvable entries in `skills-lock.yaml`.
|
|
43
|
-
- Skips `link:` skills, including the bundled `skills-package-manager-cli` self skill.
|
|
43
|
+
- Skips local `link:` and `local:` skills, including the bundled `skills-package-manager-cli` self skill.
|
|
44
44
|
|
|
45
45
|
## How To Triage User Questions
|
|
46
46
|
|
|
@@ -59,6 +59,7 @@ Use this skill for repositories that already use `skills-package-manager`, or wh
|
|
|
59
59
|
## Specifier Reminders
|
|
60
60
|
|
|
61
61
|
- `link:./path/to/skill-dir` points to a local skill directory.
|
|
62
|
+
- `local:./path/to/existing-skill-dir` keeps an existing user-owned skill directory in place.
|
|
62
63
|
- `file:./pkg.tgz#path:/skills/name` points to a packaged tarball plus skill path.
|
|
63
64
|
- `npm:@scope/pkg#path:/skills/name` resolves a package from the configured registry.
|
|
64
65
|
- GitHub shorthand or Git URLs resolve remote repositories and may need `--skill` when multiple skills are available.
|