skillshop 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -6
- package/docs/installation-and-dependency-state.md +76 -0
- package/docs/package-library-compatibility.md +3 -3
- package/docs/releases/4.1.0.md +12 -0
- package/package.json +4 -2
- package/skills-content/skillshop/SKILL.md +23 -9
- package/src/cli.js +27 -0
- package/src/dependencies.js +282 -0
- package/src/github-library.js +21 -9
- package/src/import.js +15 -0
- package/src/index.js +2 -0
- package/src/installation-state.js +113 -0
- package/src/legacy-cli.js +186 -87
- package/src/user-install.js +34 -1
- package/templates/skillshop-library/README.md +2 -0
package/src/legacy-cli.js
CHANGED
|
@@ -27,6 +27,10 @@ import {
|
|
|
27
27
|
validatePersonalSkillDirectory
|
|
28
28
|
} from './personal-library.js';
|
|
29
29
|
import { downloadPersonalSkill, GitHubLibraryError } from './github-library.js';
|
|
30
|
+
import {
|
|
31
|
+
assertRegularPath, readInstallationManifest, writeInstallationManifest,
|
|
32
|
+
readInstallationIndex, registerInstallationDirectory, sameOwner
|
|
33
|
+
} from './installation-state.js';
|
|
30
34
|
|
|
31
35
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
36
|
const skillsContentRoot = path.join(packageRoot, 'skills-content');
|
|
@@ -60,7 +64,82 @@ function lockPathFor(root) {
|
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
function hasLock(root) {
|
|
63
|
-
return fs.existsSync(path.join(root, lockFileName)) || fs.existsSync(path.join(root, legacyLockFileName))
|
|
67
|
+
return fs.existsSync(path.join(root, lockFileName)) || fs.existsSync(path.join(root, legacyLockFileName)) ||
|
|
68
|
+
installationDirectories(root).some((directory) => fs.existsSync(path.join(directory, lockFileName)));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function installationDirectories(root = null) {
|
|
72
|
+
const base = root ?? os.homedir();
|
|
73
|
+
const directories = agentOptions.map((agent) => path.join(base, root ? agent.local : agent.global));
|
|
74
|
+
for (const item of readInstallationIndex(skillshopHome())) {
|
|
75
|
+
if (root ? item.scope === 'project' && canonicalPath(item.root) === canonicalPath(root) : item.scope === 'global') {
|
|
76
|
+
assertLocalDestination(base, item.directory, 'indexed installation directory');
|
|
77
|
+
directories.push(item.directory);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return [...new Set(directories.map((directory) => path.resolve(directory)))];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function cliOwner(root = null) {
|
|
84
|
+
return { kind: 'skillshop', id: root ? 'project' : 'global' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function installationManifestExists(directory) {
|
|
88
|
+
try { fs.lstatSync(path.join(directory, lockFileName)); return true; }
|
|
89
|
+
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function directoryOwnedEntries(root = null) {
|
|
93
|
+
const result = [];
|
|
94
|
+
for (const directory of installationDirectories(root)) {
|
|
95
|
+
if (!installationManifestExists(directory)) continue;
|
|
96
|
+
for (const [name, entry] of Object.entries(readInstallationManifest(directory).skills)) {
|
|
97
|
+
if (sameOwner(entry.owner, cliOwner(root))) result.push({ name, entry, target: path.join(directory, name) });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function saveDirectoryOwnership(root, lock) {
|
|
104
|
+
const owner = cliOwner(root);
|
|
105
|
+
const directories = new Set(installationDirectories(root));
|
|
106
|
+
const wanted = new Map();
|
|
107
|
+
for (const [name, entry] of Object.entries(lock.skills)) {
|
|
108
|
+
const targets = root ? lockedTargets(root, name, entry) : globalTargets(name, entry);
|
|
109
|
+
for (const target of targets) {
|
|
110
|
+
assertRegularPath(path.dirname(target));
|
|
111
|
+
const directory = path.dirname(target);
|
|
112
|
+
directories.add(directory);
|
|
113
|
+
const metadata = root ? entry : { ...entry, ...entry.copies?.[target] };
|
|
114
|
+
const copy = { owner, ...(typeof metadata.version === 'string' ? { version: metadata.version } : {}),
|
|
115
|
+
...(typeof metadata.author === 'string' ? { author: metadata.author } : {}),
|
|
116
|
+
...(metadata.source ? { source: metadata.source } : {}) };
|
|
117
|
+
if (!wanted.has(directory)) wanted.set(directory, {});
|
|
118
|
+
wanted.get(directory)[name] = copy;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Validate every directory before changing ownership in any of them.
|
|
122
|
+
const planned = [];
|
|
123
|
+
for (const directory of directories) {
|
|
124
|
+
if (!wanted.has(directory) && !installationManifestExists(directory)) continue;
|
|
125
|
+
const manifest = readInstallationManifest(directory);
|
|
126
|
+
const skills = Object.fromEntries(Object.entries(manifest.skills).filter(([, entry]) => !sameOwner(entry.owner, owner)));
|
|
127
|
+
for (const [name, entry] of Object.entries(wanted.get(directory) ?? {})) {
|
|
128
|
+
if (skills[name]) throw new CliError(`${path.join(directory, name)} is owned by ${skills[name].owner.kind}:${skills[name].owner.id}`);
|
|
129
|
+
skills[name] = entry;
|
|
130
|
+
}
|
|
131
|
+
if (Object.keys(skills).length || fs.existsSync(path.join(directory, lockFileName))) planned.push({ directory, manifest: { manifestVersion: 1, skills } });
|
|
132
|
+
}
|
|
133
|
+
for (const { directory, manifest } of planned) {
|
|
134
|
+
writeInstallationManifest(directory, manifest);
|
|
135
|
+
registerInstallationDirectory(directory, { root, home: skillshopHome() });
|
|
136
|
+
}
|
|
137
|
+
const legacyFiles = root ? [path.join(root, lockFileName), path.join(root, legacyLockFileName)]
|
|
138
|
+
: [path.join(skillshopHome(), globalSkillsFileName)];
|
|
139
|
+
for (const file of legacyFiles) {
|
|
140
|
+
assertRegularPath(file);
|
|
141
|
+
fs.rmSync(file, { force: true });
|
|
142
|
+
}
|
|
64
143
|
}
|
|
65
144
|
const agentOptions = [
|
|
66
145
|
{ id: 'copilot', label: 'Copilot', local: '.github/skills', global: '.copilot/skills' },
|
|
@@ -353,20 +432,38 @@ function emptyLock() {
|
|
|
353
432
|
|
|
354
433
|
function readLock(root) {
|
|
355
434
|
const lockPath = lockPathFor(root);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
435
|
+
let lock = emptyLock();
|
|
436
|
+
if (fs.existsSync(lockPath)) {
|
|
437
|
+
assertRegularPath(lockPath);
|
|
438
|
+
const olderPath = path.join(root, legacyLockFileName);
|
|
439
|
+
if (lockPath !== olderPath && fs.existsSync(olderPath)) {
|
|
440
|
+
assertRegularPath(olderPath);
|
|
441
|
+
if (!fs.readFileSync(lockPath).equals(fs.readFileSync(olderPath))) {
|
|
442
|
+
throw new CliError(`${root} has conflicting .skillshop-lock and .skillz-lock records; reconcile them before migrating`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
try { lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); }
|
|
446
|
+
catch (error) { throw new CliError(`cannot parse ${lockPath}: ${error.message}`); }
|
|
447
|
+
if (![1, 2].includes(lock?.lockfileVersion) || lock.registry !== 'local' ||
|
|
448
|
+
!lock.skills || typeof lock.skills !== 'object' || Array.isArray(lock.skills)) {
|
|
449
|
+
throw new CliError(`${lockPath} is not a supported skillshop lockfile`);
|
|
450
|
+
}
|
|
451
|
+
for (const [name, entry] of Object.entries(lock.skills)) {
|
|
452
|
+
for (const target of lockedTargets(root, name, entry)) {
|
|
453
|
+
assertRegularPath(target);
|
|
454
|
+
const claim = readInstallationManifest(path.dirname(target)).skills[name];
|
|
455
|
+
if (claim && !sameOwner(claim.owner, cliOwner(root))) throw new CliError(`${target} is owned by ${claim.owner.kind}:${claim.owner.id}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
363
458
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
459
|
+
for (const { name, entry, target } of directoryOwnedEntries(root)) {
|
|
460
|
+
const { owner, ...metadata } = entry;
|
|
461
|
+
const previous = lock.skills[name];
|
|
462
|
+
if (previous && JSON.stringify(previous.source ?? null) !== JSON.stringify(metadata.source ?? null)) {
|
|
463
|
+
throw new CliError(`${name} has conflicting installation sources in ${root}`);
|
|
464
|
+
}
|
|
465
|
+
lock.skills[name] = { ...metadata, paths: [...new Set([...(previous?.paths ?? (previous?.path ? [previous.path] : [])), relativePath(root, target)])] };
|
|
368
466
|
}
|
|
369
|
-
|
|
370
467
|
return lock;
|
|
371
468
|
}
|
|
372
469
|
|
|
@@ -410,23 +507,8 @@ function localSkillDirectories(root, lock, cwd) {
|
|
|
410
507
|
}
|
|
411
508
|
|
|
412
509
|
function writeLock(root, lock) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
416
|
-
.map(([name, entry]) => [name, {
|
|
417
|
-
...(typeof entry.version === 'string' ? { version: entry.version } : {}),
|
|
418
|
-
...(typeof entry.author === 'string' ? { author: entry.author } : {}),
|
|
419
|
-
...(entry.source ? { source: entry.source } : {}),
|
|
420
|
-
paths: lockedTargets(root, name, entry).map((target) => relativePath(root, target)).sort()
|
|
421
|
-
}])
|
|
422
|
-
);
|
|
423
|
-
const stableLock = { lockfileVersion: 2, registry: 'local', skills };
|
|
424
|
-
const lockPath = path.join(root, lockFileName);
|
|
425
|
-
const contents = `${JSON.stringify(stableLock, null, 2)}\n`;
|
|
426
|
-
if (fs.existsSync(lockPath) && fs.readFileSync(lockPath, 'utf8') === contents) return;
|
|
427
|
-
const temporaryPath = `${lockPath}.tmp-${process.pid}`;
|
|
428
|
-
fs.writeFileSync(temporaryPath, contents);
|
|
429
|
-
fs.renameSync(temporaryPath, lockPath);
|
|
510
|
+
saveDirectoryOwnership(root, lock);
|
|
511
|
+
syncGitExclude(root, lock);
|
|
430
512
|
}
|
|
431
513
|
|
|
432
514
|
function registryPath() {
|
|
@@ -497,9 +579,14 @@ function staleTargets(previousTargets, targetDirs) {
|
|
|
497
579
|
return previousTargets.filter((target) => !destinations.has(canonicalDestination(target)));
|
|
498
580
|
}
|
|
499
581
|
|
|
500
|
-
function assertTargetsAvailable(name, targetDirs, previousTargets) {
|
|
582
|
+
function assertTargetsAvailable(name, targetDirs, previousTargets, root) {
|
|
501
583
|
const managedDestinations = new Set(previousTargets.map(canonicalDestination));
|
|
502
584
|
for (const targetDir of targetDirs) {
|
|
585
|
+
assertRegularPath(targetDir);
|
|
586
|
+
const claim = readInstallationManifest(path.dirname(targetDir)).skills[name];
|
|
587
|
+
if (claim && !sameOwner(claim.owner, cliOwner(root))) {
|
|
588
|
+
throw new CliError(`${targetDir} is owned by ${claim.owner.kind}:${claim.owner.id}`);
|
|
589
|
+
}
|
|
503
590
|
if (fs.existsSync(targetDir) && !managedDestinations.has(canonicalDestination(targetDir))) {
|
|
504
591
|
throw new CliError(`${targetDir} exists and is not managed as ${name} in ${lockFileName}`);
|
|
505
592
|
}
|
|
@@ -538,7 +625,11 @@ function managedGitPaths(root, lock) {
|
|
|
538
625
|
const gitRoot = canonicalPath(gitOutput(root, ['rev-parse', '--show-toplevel']));
|
|
539
626
|
const scope = relativePath(gitRoot, canonicalPath(root));
|
|
540
627
|
const scopedPath = (target) => relativePath(gitRoot, path.join(root, target));
|
|
541
|
-
const paths = [
|
|
628
|
+
const paths = [];
|
|
629
|
+
if (fs.existsSync(path.join(root, lockFileName))) paths.push(scopedPath(lockFileName));
|
|
630
|
+
for (const directory of installationDirectories(root)) {
|
|
631
|
+
if (fs.existsSync(path.join(directory, lockFileName))) paths.push(relativePath(gitRoot, path.join(directory, lockFileName)));
|
|
632
|
+
}
|
|
542
633
|
if (fs.existsSync(path.join(root, legacyLockFileName))) paths.push(scopedPath(legacyLockFileName));
|
|
543
634
|
|
|
544
635
|
for (const [name, entry] of Object.entries(lock.skills)) {
|
|
@@ -679,16 +770,34 @@ function repairInstalledManifest(name, entry, manifestPath) {
|
|
|
679
770
|
|
|
680
771
|
function readGlobalSkills() {
|
|
681
772
|
const filePath = path.join(skillshopHome(), globalSkillsFileName);
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
lock = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
686
|
-
|
|
687
|
-
|
|
773
|
+
let lock = emptyLock();
|
|
774
|
+
if (fs.existsSync(filePath)) {
|
|
775
|
+
assertRegularPath(filePath);
|
|
776
|
+
try { lock = JSON.parse(fs.readFileSync(filePath, 'utf8')); }
|
|
777
|
+
catch (error) { throw new CliError(`cannot parse ${filePath}: ${error.message}`); }
|
|
778
|
+
if (lock?.lockfileVersion !== 2 || lock.registry !== 'local' ||
|
|
779
|
+
!lock.skills || typeof lock.skills !== 'object' || Array.isArray(lock.skills)) {
|
|
780
|
+
throw new CliError(`${filePath} is not a supported skillshop global manifest`);
|
|
781
|
+
}
|
|
782
|
+
for (const [name, entry] of Object.entries(lock.skills)) {
|
|
783
|
+
for (const target of globalTargets(name, entry)) {
|
|
784
|
+
assertRegularPath(target);
|
|
785
|
+
const claim = readInstallationManifest(path.dirname(target)).skills[name];
|
|
786
|
+
if (claim && !sameOwner(claim.owner, cliOwner())) throw new CliError(`${target} is owned by ${claim.owner.kind}:${claim.owner.id}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
688
789
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
790
|
+
for (const { name, entry, target } of directoryOwnedEntries()) {
|
|
791
|
+
const { owner, ...metadata } = entry;
|
|
792
|
+
const previous = lock.skills[name] ?? { paths: [], copies: {} };
|
|
793
|
+
const oldCopy = previous.copies?.[target];
|
|
794
|
+
if (oldCopy && JSON.stringify(oldCopy.source ?? previous.source ?? null) !== JSON.stringify(metadata.source ?? null)) {
|
|
795
|
+
throw new CliError(`${name} has conflicting global installation sources`);
|
|
796
|
+
}
|
|
797
|
+
previous.paths = [...new Set([...previous.paths, target])];
|
|
798
|
+
previous.copies ??= {};
|
|
799
|
+
previous.copies[target] = metadata;
|
|
800
|
+
lock.skills[name] = previous;
|
|
692
801
|
}
|
|
693
802
|
return lock;
|
|
694
803
|
}
|
|
@@ -709,24 +818,7 @@ function globalTargets(name, entry) {
|
|
|
709
818
|
}
|
|
710
819
|
|
|
711
820
|
function writeGlobalSkills(lock) {
|
|
712
|
-
|
|
713
|
-
.map(([name, entry]) => {
|
|
714
|
-
const paths = globalTargets(name, entry).sort();
|
|
715
|
-
return [name, {
|
|
716
|
-
...(typeof entry.version === 'string' ? { version: entry.version } : {}),
|
|
717
|
-
...(typeof entry.author === 'string' ? { author: entry.author } : {}),
|
|
718
|
-
...(entry.source ? { source: entry.source } : {}),
|
|
719
|
-
paths,
|
|
720
|
-
copies: Object.fromEntries(paths.map((target) => [target, entry.copies?.[target] ?? {}]))
|
|
721
|
-
}];
|
|
722
|
-
}));
|
|
723
|
-
const filePath = path.join(skillshopHome(), globalSkillsFileName);
|
|
724
|
-
const contents = `${JSON.stringify({ ...emptyLock(), skills }, null, 2)}\n`;
|
|
725
|
-
if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === contents) return;
|
|
726
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
727
|
-
const temporaryPath = `${filePath}.tmp-${process.pid}`;
|
|
728
|
-
fs.writeFileSync(temporaryPath, contents);
|
|
729
|
-
fs.renameSync(temporaryPath, filePath);
|
|
821
|
+
saveDirectoryOwnership(null, lock);
|
|
730
822
|
}
|
|
731
823
|
|
|
732
824
|
function repairTrackedSkills(root, lock) {
|
|
@@ -771,7 +863,7 @@ function reconcileRepository(root, { register = true } = {}) {
|
|
|
771
863
|
}
|
|
772
864
|
|
|
773
865
|
function reconcileCurrentRepository() {
|
|
774
|
-
if (fs.existsSync(path.join(skillshopHome(), globalSkillsFileName))) {
|
|
866
|
+
if (fs.existsSync(path.join(skillshopHome(), globalSkillsFileName)) || installationDirectories().some((directory) => fs.existsSync(path.join(directory, lockFileName)))) {
|
|
775
867
|
const globalSkills = readGlobalSkills();
|
|
776
868
|
repairTrackedSkills(null, globalSkills);
|
|
777
869
|
writeGlobalSkills(globalSkills);
|
|
@@ -1171,13 +1263,13 @@ function syncPersonalCopies(cwd) {
|
|
|
1171
1263
|
}
|
|
1172
1264
|
|
|
1173
1265
|
for (const context of contexts) {
|
|
1174
|
-
if (context.kind === 'project' && JSON.stringify(context.lock) !== context.original) {
|
|
1266
|
+
if (context.kind === 'project' && (JSON.stringify(context.lock) !== context.original || fs.existsSync(lockPathFor(context.root)))) {
|
|
1175
1267
|
try { writeLock(context.root, context.lock); }
|
|
1176
1268
|
catch (error) { failures.push(`project ${context.root} state: ${error.message}`); }
|
|
1177
1269
|
}
|
|
1178
1270
|
}
|
|
1179
1271
|
|
|
1180
|
-
if (globalLock && JSON.stringify(globalLock) !== contexts.find(({ kind }) => kind === 'global')?.original) {
|
|
1272
|
+
if (globalLock && (JSON.stringify(globalLock) !== contexts.find(({ kind }) => kind === 'global')?.original || fs.existsSync(path.join(skillshopHome(), globalSkillsFileName)))) {
|
|
1181
1273
|
try { writeGlobalSkills(globalLock); }
|
|
1182
1274
|
catch (error) { failures.push(`global state: ${error.message}`); }
|
|
1183
1275
|
}
|
|
@@ -1283,9 +1375,11 @@ function installGlobal(name, requestedVersion) {
|
|
|
1283
1375
|
assertNoManagedSymlink(os.homedir(), targetDir, `global install destination for ${name}`);
|
|
1284
1376
|
assertInstallPathsDoNotOverlap(metadata.sourceDir, targetDir);
|
|
1285
1377
|
}
|
|
1286
|
-
if (
|
|
1378
|
+
if (fs.existsSync(targetDir) && !managedTargets.has(canonicalDestination(targetDir))) {
|
|
1287
1379
|
throw new CliError(`${targetDir} exists and is not a Skillshop-managed copy of ${name}`);
|
|
1288
1380
|
}
|
|
1381
|
+
const claim = readInstallationManifest(skillDirectory).skills[name];
|
|
1382
|
+
if (claim && !sameOwner(claim.owner, cliOwner())) throw new CliError(`${targetDir} is owned by ${claim.owner.kind}:${claim.owner.id}`);
|
|
1289
1383
|
return { skillDirectory, targetDir };
|
|
1290
1384
|
});
|
|
1291
1385
|
for (const { skillDirectory, targetDir } of targetPairs) {
|
|
@@ -1536,7 +1630,7 @@ function installLocal(name, requestedVersion, cwd) {
|
|
|
1536
1630
|
);
|
|
1537
1631
|
}
|
|
1538
1632
|
for (const { metadata, targetDirs, previousTargets } of installs) {
|
|
1539
|
-
assertTargetsAvailable(metadata.name, targetDirs, previousTargets);
|
|
1633
|
+
assertTargetsAvailable(metadata.name, targetDirs, previousTargets, root);
|
|
1540
1634
|
for (const targetDir of targetDirs) {
|
|
1541
1635
|
if (metadata.kind === 'personal') {
|
|
1542
1636
|
assertNoManagedSymlink(root, targetDir, `install destination for ${metadata.name}`);
|
|
@@ -1561,31 +1655,26 @@ function installLocal(name, requestedVersion, cwd) {
|
|
|
1561
1655
|
|
|
1562
1656
|
writeLock(root, lock);
|
|
1563
1657
|
maintainRepository(root, lock);
|
|
1564
|
-
console.log(`Wrote ${
|
|
1658
|
+
console.log(`Wrote installation manifests for ${root}`);
|
|
1565
1659
|
}
|
|
1566
1660
|
|
|
1567
1661
|
function uninstallGlobal(name) {
|
|
1568
|
-
const config = readConfig();
|
|
1569
|
-
const skillDirectories = config
|
|
1570
|
-
? configuredSkillDirectories(config, 'global')
|
|
1571
|
-
: [path.join(os.homedir(), '.agents', 'skills')];
|
|
1572
|
-
const targets = skillDirectories.map((directory) => path.join(directory, name));
|
|
1573
|
-
const installedTargets = targets.filter((target) => fs.existsSync(target));
|
|
1574
|
-
if (installedTargets.length === 0) {
|
|
1575
|
-
throw new CliError(`${name} is not installed globally in the configured skill directories`);
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
for (const targetDir of installedTargets) {
|
|
1579
|
-
fs.rmSync(targetDir, { recursive: true });
|
|
1580
|
-
console.log(`Uninstalled ${name} from ${targetDir}`);
|
|
1581
|
-
}
|
|
1582
1662
|
const globalSkills = readGlobalSkills();
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1663
|
+
const entry = globalSkills.skills[name];
|
|
1664
|
+
if (!entry) throw new CliError(`${name} is not a Skillshop-managed global installation`);
|
|
1665
|
+
const config = readConfig();
|
|
1666
|
+
const directories = config ? configuredSkillDirectories(config, 'global') : [path.join(os.homedir(), '.agents', 'skills')];
|
|
1667
|
+
const targets = globalTargets(name, entry).filter((target) => directories.includes(path.dirname(target)));
|
|
1668
|
+
if (!targets.length) throw new CliError(`${name} is not installed globally in the configured skill directories`);
|
|
1669
|
+
for (const target of targets) assertRegularPath(target);
|
|
1670
|
+
for (const target of targets) {
|
|
1671
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
1672
|
+
console.log(`Uninstalled ${name} from ${target}`);
|
|
1588
1673
|
}
|
|
1674
|
+
const remaining = globalTargets(name, entry).filter((target) => !targets.includes(target));
|
|
1675
|
+
if (remaining.length) entry.paths = remaining;
|
|
1676
|
+
else delete globalSkills.skills[name];
|
|
1677
|
+
writeGlobalSkills(globalSkills);
|
|
1589
1678
|
}
|
|
1590
1679
|
|
|
1591
1680
|
function uninstallLocal(name, cwd) {
|
|
@@ -1604,7 +1693,7 @@ function uninstallLocal(name, cwd) {
|
|
|
1604
1693
|
maintainRepository(root, lock);
|
|
1605
1694
|
|
|
1606
1695
|
for (const targetDir of targetDirs) console.log(`Uninstalled ${name} from ${targetDir}`);
|
|
1607
|
-
console.log(`Updated ${
|
|
1696
|
+
console.log(`Updated installation manifests for ${root}`);
|
|
1608
1697
|
}
|
|
1609
1698
|
|
|
1610
1699
|
function updateLocal(requestedName, cwd, options = {}) {
|
|
@@ -1632,7 +1721,7 @@ function updateLocal(requestedName, cwd, options = {}) {
|
|
|
1632
1721
|
return { metadata, targetDirs, previousTargets };
|
|
1633
1722
|
});
|
|
1634
1723
|
for (const { metadata, targetDirs, previousTargets } of updates) {
|
|
1635
|
-
assertTargetsAvailable(metadata.name, targetDirs, previousTargets);
|
|
1724
|
+
assertTargetsAvailable(metadata.name, targetDirs, previousTargets, root);
|
|
1636
1725
|
for (const targetDir of targetDirs) {
|
|
1637
1726
|
if (metadata.kind === 'personal') {
|
|
1638
1727
|
assertNoManagedSymlink(root, targetDir, `update destination for ${metadata.name}`);
|
|
@@ -1654,7 +1743,7 @@ function updateLocal(requestedName, cwd, options = {}) {
|
|
|
1654
1743
|
|
|
1655
1744
|
writeLock(root, lock);
|
|
1656
1745
|
maintainRepository(root, lock, options);
|
|
1657
|
-
console.log(`Updated ${
|
|
1746
|
+
console.log(`Updated installation manifests for ${root}`);
|
|
1658
1747
|
}
|
|
1659
1748
|
|
|
1660
1749
|
function updateAllRepositories() {
|
|
@@ -1861,6 +1950,7 @@ function refreshManagedCopies(scope) {
|
|
|
1861
1950
|
} else {
|
|
1862
1951
|
try {
|
|
1863
1952
|
const lock = readLock(owner.root);
|
|
1953
|
+
if (fs.existsSync(lockPathFor(owner.root))) writeLock(owner.root, lock);
|
|
1864
1954
|
refreshManagedLock(owner.root, lock, { summary });
|
|
1865
1955
|
} catch (error) {
|
|
1866
1956
|
summary.failed.push(`${owner.root}: ${error.message}`);
|
|
@@ -1869,6 +1959,7 @@ function refreshManagedCopies(scope) {
|
|
|
1869
1959
|
} else if (scope === 'global') {
|
|
1870
1960
|
try {
|
|
1871
1961
|
const globalLock = readGlobalSkills();
|
|
1962
|
+
if (fs.existsSync(path.join(skillshopHome(), globalSkillsFileName))) writeGlobalSkills(globalLock);
|
|
1872
1963
|
refreshManagedLock(null, globalLock, { global: true, summary });
|
|
1873
1964
|
} catch (error) {
|
|
1874
1965
|
summary.failed.push(`global managed copies: ${error.message}`);
|
|
@@ -1880,6 +1971,7 @@ function refreshManagedCopies(scope) {
|
|
|
1880
1971
|
if (!hasLock(root)) continue;
|
|
1881
1972
|
try {
|
|
1882
1973
|
const lock = readLock(root);
|
|
1974
|
+
if (fs.existsSync(lockPathFor(root))) writeLock(root, lock);
|
|
1883
1975
|
refreshManagedLock(root, lock, { summary });
|
|
1884
1976
|
} catch (error) {
|
|
1885
1977
|
summary.failed.push(`${root}: ${error.message}`);
|
|
@@ -1922,11 +2014,18 @@ function discoverLockRoots(startPath, roots) {
|
|
|
1922
2014
|
}
|
|
1923
2015
|
|
|
1924
2016
|
if (stat.isFile()) {
|
|
2017
|
+
if (path.basename(startPath) === lockFileName) {
|
|
2018
|
+
try { if (JSON.parse(fs.readFileSync(startPath, 'utf8')).manifestVersion === 1) return; } catch {}
|
|
2019
|
+
}
|
|
1925
2020
|
if ([lockFileName, legacyLockFileName].includes(path.basename(startPath))) roots.add(canonicalPath(path.dirname(startPath)));
|
|
1926
2021
|
return;
|
|
1927
2022
|
}
|
|
1928
2023
|
if (!stat.isDirectory()) return;
|
|
1929
2024
|
|
|
2025
|
+
// An installation manifest is not a nested project ownership file.
|
|
2026
|
+
if (path.basename(startPath) === 'skills' && fs.existsSync(path.join(startPath, lockFileName))) {
|
|
2027
|
+
try { if (JSON.parse(fs.readFileSync(path.join(startPath, lockFileName), 'utf8')).manifestVersion === 1) return; } catch {}
|
|
2028
|
+
}
|
|
1930
2029
|
if (hasLock(startPath)) roots.add(canonicalPath(startPath));
|
|
1931
2030
|
|
|
1932
2031
|
let entries;
|
package/src/user-install.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { importSkill } from './import.js';
|
|
4
|
+
import { importSkill, resolveSkill } from './import.js';
|
|
5
5
|
import { normalizeSkillSpecifier, skillName } from './specifier.js';
|
|
6
|
+
import { assertInstallationOwner, assertRegularPath, readInstallationManifest, writeInstallationManifest, sameOwner } from './installation-state.js';
|
|
6
7
|
|
|
7
8
|
function declaredUserSkills(packageRoot) {
|
|
8
9
|
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
@@ -30,6 +31,7 @@ export async function installUserSkill({
|
|
|
30
31
|
targetDir: path.resolve(userSkillsDir, skillName(normalized)),
|
|
31
32
|
cwd,
|
|
32
33
|
overwrite: true,
|
|
34
|
+
ownerKind: 'package',
|
|
33
35
|
});
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -40,9 +42,40 @@ export async function installDeclaredUserSkills({
|
|
|
40
42
|
} = {}) {
|
|
41
43
|
const packageRoot = path.resolve(cwd, library);
|
|
42
44
|
const declared = declaredUserSkills(packageRoot);
|
|
45
|
+
const directory = path.resolve(userSkillsDir ?? path.join(os.homedir(), '.agents', 'skills'));
|
|
46
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
47
|
+
const owner = { kind: 'package', id: pkg.name || packageRoot };
|
|
48
|
+
// Check the complete declaration before copying its first skill.
|
|
49
|
+
for (const specifier of declared) {
|
|
50
|
+
const skill = await resolveSkill({ library: packageRoot, specifier, cwd });
|
|
51
|
+
assertInstallationOwner(directory, skill.name, owner);
|
|
52
|
+
}
|
|
53
|
+
const keep = new Set(declared.map(skillName));
|
|
54
|
+
const obsolete = Object.entries(readInstallationManifest(directory).skills)
|
|
55
|
+
.filter(([name, entry]) => sameOwner(entry.owner, owner) && !keep.has(name)).map(([name]) => name);
|
|
56
|
+
for (const name of obsolete) assertRegularPath(path.join(directory, name));
|
|
43
57
|
const results = [];
|
|
44
58
|
for (const specifier of declared) {
|
|
45
59
|
results.push(await installUserSkill({ library: packageRoot, specifier, cwd, userSkillsDir }));
|
|
46
60
|
}
|
|
61
|
+
if (obsolete.length) {
|
|
62
|
+
const manifest = readInstallationManifest(directory);
|
|
63
|
+
for (const name of obsolete) {
|
|
64
|
+
fs.rmSync(path.join(directory, name), { recursive: true, force: true });
|
|
65
|
+
delete manifest.skills[name];
|
|
66
|
+
}
|
|
67
|
+
writeInstallationManifest(directory, manifest);
|
|
68
|
+
}
|
|
47
69
|
return results;
|
|
48
70
|
}
|
|
71
|
+
|
|
72
|
+
export function uninstallUserSkill({ library, specifier, cwd = process.cwd(), userSkillsDir = path.join(os.homedir(), '.agents', 'skills') }) {
|
|
73
|
+
const packageRoot = path.resolve(cwd, library);
|
|
74
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
75
|
+
const name = skillName(normalizeSkillSpecifier(specifier));
|
|
76
|
+
const directory = path.resolve(userSkillsDir);
|
|
77
|
+
const manifest = assertInstallationOwner(directory, name, { kind: 'package', id: pkg.name || packageRoot }, { requireOwned: true });
|
|
78
|
+
fs.rmSync(path.join(directory, name), { recursive: true, force: true });
|
|
79
|
+
delete manifest.skills[name];
|
|
80
|
+
writeInstallationManifest(directory, manifest);
|
|
81
|
+
}
|
|
@@ -36,3 +36,5 @@ skillshop sync # update recorded local and global copies
|
|
|
36
36
|
`sync` is also the explicit migration step for tracked copies made by older bundled Skillshop installs; `library init` never migrates. A source already in the selected library wins and updates the tracked copies. If the source is missing, Skillshop imports it only when every surviving tracked project and global copy agrees in paths, file bytes, and modes. Conflicts, unknown source kinds, malformed ownership state, unmanaged directories, and package-import snapshots are preserved and reported. Missing recorded destinations are pruned, never recreated.
|
|
37
37
|
|
|
38
38
|
The `skillsDirectory` field can name another safe relative directory inside this library. Set it to `.` to use skill folders directly in the library root.
|
|
39
|
+
|
|
40
|
+
Installation ownership is recorded in `.skillshop-lock` inside each destination skills directory, not at the library or project root. Project dependency declarations (`skillshop.json`) and their reproducibility lock (`skillshop-lock.json`) are a separate opt-in workflow.
|