skillshop 2.1.0 → 4.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 +95 -178
- package/docs/package-library-compatibility.md +194 -0
- package/package.json +4 -2
- package/skills-content/skillshop/SKILL.md +83 -9
- package/src/agents-instructions.js +41 -4
- package/src/cli.js +13 -16
- package/src/github-library.js +310 -0
- package/src/legacy-cli.js +1293 -49
- package/src/personal-library.js +591 -0
- package/templates/skillshop-library/README.md +38 -0
- package/templates/skillshop-library/skills/example/SKILL.md +8 -0
- package/templates/skillshop-library/skills/example/references/example.md +3 -0
- package/templates/skillshop-library/skills/example/scripts/run.sh +2 -0
- package/templates/skillshop-library/skillshop-library.json +4 -0
package/src/legacy-cli.js
CHANGED
|
@@ -3,10 +3,30 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
7
7
|
import { createInterface } from 'node:readline/promises';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
AgentsInstructionsError,
|
|
11
|
+
applyInstructions,
|
|
12
|
+
applyLocalFragments,
|
|
13
|
+
applyProjectInstructions,
|
|
14
|
+
composeAgentsInstructions,
|
|
15
|
+
hasProjectInstructions
|
|
16
|
+
} from './agents-instructions.js';
|
|
17
|
+
import {
|
|
18
|
+
PersonalLibraryError,
|
|
19
|
+
importTrackedPersonalSkill,
|
|
20
|
+
inspectPersonalLibrary,
|
|
21
|
+
initPersonalLibrary,
|
|
22
|
+
librarySelectionFileName,
|
|
23
|
+
listPersonalSkills,
|
|
24
|
+
resolvePersonalSkill,
|
|
25
|
+
resolveRecordedPersonalSkill,
|
|
26
|
+
selectedLibraryRoot,
|
|
27
|
+
validatePersonalSkillDirectory
|
|
28
|
+
} from './personal-library.js';
|
|
29
|
+
import { downloadPersonalSkill, GitHubLibraryError } from './github-library.js';
|
|
10
30
|
|
|
11
31
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
32
|
const skillsContentRoot = path.join(packageRoot, 'skills-content');
|
|
@@ -21,6 +41,12 @@ const excludeBlockStart = '# >>> skillshop managed dependencies >>>';
|
|
|
21
41
|
const excludeBlockEnd = '# <<< skillshop managed dependencies <<<';
|
|
22
42
|
const legacyExcludeBlockStart = '# >>> skillz managed dependencies >>>';
|
|
23
43
|
const legacyExcludeBlockEnd = '# <<< skillz managed dependencies <<<';
|
|
44
|
+
const projectTemplateDirectory = path.join('.agents', 'instructions');
|
|
45
|
+
const projectTemplateName = 'AGENTS.template.md';
|
|
46
|
+
const defaultProjectTemplate = '# Project instructions\n\n{{ blocks }}\n';
|
|
47
|
+
const internalUpgradeRefreshCommand = '__skillshop_upgrade_refresh';
|
|
48
|
+
const internalUpgradeRefreshToken = 'skillshop-managed-upgrade-v1';
|
|
49
|
+
const personalLibraryTemplateRoot = path.join(packageRoot, 'templates', 'skillshop-library');
|
|
24
50
|
const globalAgentsTargets = [
|
|
25
51
|
path.join('.codex', 'AGENTS.md'),
|
|
26
52
|
path.join('.agents', 'AGENTS.md'),
|
|
@@ -107,7 +133,9 @@ function printHelp() {
|
|
|
107
133
|
console.log(`Usage:
|
|
108
134
|
skillshop init
|
|
109
135
|
skillshop config agents
|
|
110
|
-
skillshop agents [--
|
|
136
|
+
skillshop agents init [--project <directory>]
|
|
137
|
+
skillshop agents [--project <directory>] [--check]
|
|
138
|
+
skillshop agents --global
|
|
111
139
|
skillshop <name> [--version <version>] [--global]
|
|
112
140
|
skillshop install <name> [--version <version>] [--global]
|
|
113
141
|
skillshop uninstall <name> [--global]
|
|
@@ -115,12 +143,32 @@ function printHelp() {
|
|
|
115
143
|
skillshop update [name]
|
|
116
144
|
skillshop update --push
|
|
117
145
|
skillshop migrate [root ...]
|
|
146
|
+
skillshop library init [directory]
|
|
147
|
+
skillshop library list
|
|
148
|
+
skillshop download @<owner>/<skill> [--force]
|
|
149
|
+
skillshop install @<owner>/<skill> [--global]
|
|
150
|
+
skillshop sync
|
|
118
151
|
skillshop list
|
|
119
152
|
|
|
120
153
|
Install, uninstall, and update skills from the local registry.
|
|
154
|
+
Personal folder libraries are the source of truth for user-authored skills. Initialize or select one with skillshop library init [directory], list it with skillshop library list, download a source with skillshop download @owner/skill, and install it with skillshop install @owner/skill. Bare names install from the selected library.
|
|
155
|
+
skillshop sync propagates personal source edits and removals without fetching upstream. It can also adopt tracked legacy bundled copies into the selected library when all surviving copies agree; init itself never migrates.
|
|
156
|
+
|
|
157
|
+
Agent instructions:
|
|
158
|
+
skillshop agents init creates a neutral .agents/instructions/AGENTS.template.md and renders it into a separate managed region in the project-root AGENTS.md. Existing text and prior managed guidance are preserved. Edit the template and sibling *.block.md sources, not the generated region.
|
|
159
|
+
Plain skillshop agents keeps the bounded bundled behavior until project sources exist; afterward it renders project sources. Missing sources in an initialized project are errors.
|
|
160
|
+
skillshop agents --check reports whether generated output is current and never writes. skillshop agents --global retains user-level instruction updates.
|
|
161
|
+
The default boundary is the nearest Git/worktree root, or the current directory outside Git. --project selects another directory relative to the current working directory. Nested projects need an explicit selection.
|
|
162
|
+
Source, block, template, and output paths containing symlink components, including dangling links, are rejected.
|
|
163
|
+
Templates accept {{ include "name.block.md" }}, {{ include 'name.block.md' }}, legacy {{ fragments["name.block.md"] }}, and {{ blocks }} for unreferenced blocks. Inserted block text is literal.
|
|
164
|
+
|
|
165
|
+
Upgrade:
|
|
166
|
+
skillshop upgrade updates the npm installation that supplied the CLI, then refreshes recorded Skillshop-managed copies with the newly installed CLI. skillshop init is optional and upgrade does not generate AGENTS.md files.
|
|
121
167
|
|
|
122
168
|
Options:
|
|
123
169
|
-g, --global Use configured system skill directories without a project lockfile
|
|
170
|
+
--project <directory> Select the agents boundary relative to the current directory
|
|
171
|
+
--check Check project-generated instructions without writing
|
|
124
172
|
--push Update every registered repository
|
|
125
173
|
-v, --version <version> Require an exact skill version
|
|
126
174
|
-h, --help Show this help
|
|
@@ -314,7 +362,8 @@ function readLock(root) {
|
|
|
314
362
|
throw new CliError(`cannot parse ${lockPath}: ${error.message}`);
|
|
315
363
|
}
|
|
316
364
|
|
|
317
|
-
if (![1, 2].includes(lock?.lockfileVersion) || lock.registry !== 'local' ||
|
|
365
|
+
if (![1, 2].includes(lock?.lockfileVersion) || lock.registry !== 'local' ||
|
|
366
|
+
!lock.skills || typeof lock.skills !== 'object' || Array.isArray(lock.skills)) {
|
|
318
367
|
throw new CliError(`${lockPath} is not a supported skillshop lockfile`);
|
|
319
368
|
}
|
|
320
369
|
|
|
@@ -365,8 +414,9 @@ function writeLock(root, lock) {
|
|
|
365
414
|
Object.entries(lock.skills)
|
|
366
415
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
367
416
|
.map(([name, entry]) => [name, {
|
|
368
|
-
version: entry.version,
|
|
369
|
-
author: entry.author,
|
|
417
|
+
...(typeof entry.version === 'string' ? { version: entry.version } : {}),
|
|
418
|
+
...(typeof entry.author === 'string' ? { author: entry.author } : {}),
|
|
419
|
+
...(entry.source ? { source: entry.source } : {}),
|
|
370
420
|
paths: lockedTargets(root, name, entry).map((target) => relativePath(root, target)).sort()
|
|
371
421
|
}])
|
|
372
422
|
);
|
|
@@ -636,7 +686,8 @@ function readGlobalSkills() {
|
|
|
636
686
|
} catch (error) {
|
|
637
687
|
throw new CliError(`cannot parse ${filePath}: ${error.message}`);
|
|
638
688
|
}
|
|
639
|
-
if (lock?.lockfileVersion !== 2 || lock.registry !== 'local' ||
|
|
689
|
+
if (lock?.lockfileVersion !== 2 || lock.registry !== 'local' ||
|
|
690
|
+
!lock.skills || typeof lock.skills !== 'object' || Array.isArray(lock.skills)) {
|
|
640
691
|
throw new CliError(`${filePath} is not a supported skillshop global manifest`);
|
|
641
692
|
}
|
|
642
693
|
return lock;
|
|
@@ -661,7 +712,13 @@ function writeGlobalSkills(lock) {
|
|
|
661
712
|
const skills = Object.fromEntries(Object.entries(lock.skills).sort(([left], [right]) => left.localeCompare(right))
|
|
662
713
|
.map(([name, entry]) => {
|
|
663
714
|
const paths = globalTargets(name, entry).sort();
|
|
664
|
-
return [name, {
|
|
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
|
+
}];
|
|
665
722
|
}));
|
|
666
723
|
const filePath = path.join(skillshopHome(), globalSkillsFileName);
|
|
667
724
|
const contents = `${JSON.stringify({ ...emptyLock(), skills }, null, 2)}\n`;
|
|
@@ -682,7 +739,12 @@ function repairTrackedSkills(root, lock) {
|
|
|
682
739
|
delete lock.skills[name];
|
|
683
740
|
continue;
|
|
684
741
|
}
|
|
742
|
+
if (root && entry.source?.kind === 'personal-library') {
|
|
743
|
+
entry.paths = targets.map((target) => relativePath(root, target));
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
685
746
|
for (const target of targets) {
|
|
747
|
+
if (!root && entry.copies?.[target]?.source?.kind === 'personal-library') continue;
|
|
686
748
|
const manifestPath = path.join(target, 'SKILL.md');
|
|
687
749
|
assertLocalDestination(root ?? os.homedir(), manifestPath, `manifest for ${name}`);
|
|
688
750
|
const installation = root ? entry : entry.copies?.[target] ?? {};
|
|
@@ -726,35 +788,480 @@ function reconcileCurrentRepository() {
|
|
|
726
788
|
|
|
727
789
|
function replaceSkillDirectory(sourceDir, targetDir) {
|
|
728
790
|
const temporaryDir = `${targetDir}.skillshop-tmp-${process.pid}`;
|
|
791
|
+
const backupDir = `${targetDir}.skillshop-backup-${process.pid}`;
|
|
729
792
|
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
730
793
|
fs.rmSync(temporaryDir, { recursive: true, force: true });
|
|
794
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
731
795
|
fs.cpSync(sourceDir, temporaryDir, { recursive: true, force: true });
|
|
732
|
-
fs.
|
|
733
|
-
|
|
796
|
+
const hadTarget = fs.existsSync(targetDir);
|
|
797
|
+
try {
|
|
798
|
+
if (hadTarget) fs.renameSync(targetDir, backupDir);
|
|
799
|
+
fs.renameSync(temporaryDir, targetDir);
|
|
800
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
801
|
+
} catch (error) {
|
|
802
|
+
if (hadTarget && !fs.existsSync(targetDir) && fs.existsSync(backupDir)) fs.renameSync(backupDir, targetDir);
|
|
803
|
+
throw error;
|
|
804
|
+
} finally {
|
|
805
|
+
fs.rmSync(temporaryDir, { recursive: true, force: true });
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function personalTreeSnapshot(root, label) {
|
|
810
|
+
const entries = [];
|
|
811
|
+
const visit = (directory, relative = '') => {
|
|
812
|
+
const stat = fs.lstatSync(directory);
|
|
813
|
+
if (stat.isSymbolicLink()) throw new CliError(`${label} contains a symlink: ${directory}`);
|
|
814
|
+
if (stat.isDirectory()) {
|
|
815
|
+
entries.push({ path: relative, kind: 'directory', mode: stat.mode & 0o777 });
|
|
816
|
+
for (const name of fs.readdirSync(directory).sort()) {
|
|
817
|
+
visit(path.join(directory, name), relative ? `${relative}/${name}` : name);
|
|
818
|
+
}
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (!stat.isFile()) throw new CliError(`${label} contains a non-regular entry: ${directory}`);
|
|
822
|
+
entries.push({
|
|
823
|
+
path: relative,
|
|
824
|
+
kind: 'file',
|
|
825
|
+
mode: stat.mode & 0o777,
|
|
826
|
+
contents: fs.readFileSync(directory).toString('base64')
|
|
827
|
+
});
|
|
828
|
+
};
|
|
829
|
+
visit(root);
|
|
830
|
+
return entries;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function replaceSyncedSkillDirectory(sourceDir, targetDir) {
|
|
834
|
+
const parent = path.dirname(targetDir);
|
|
835
|
+
const transaction = fs.mkdtempSync(path.join(parent, `.skillshop-sync-${process.pid}-`));
|
|
836
|
+
const staged = path.join(transaction, 'skill');
|
|
837
|
+
const backup = path.join(transaction, 'previous-skill');
|
|
838
|
+
let backedUp = false;
|
|
839
|
+
let installed = false;
|
|
840
|
+
try {
|
|
841
|
+
fs.cpSync(sourceDir, staged, { recursive: true, errorOnExist: true, force: false });
|
|
842
|
+
personalTreeSnapshot(staged, 'staged personal skill');
|
|
843
|
+
fs.renameSync(targetDir, backup);
|
|
844
|
+
backedUp = true;
|
|
845
|
+
fs.renameSync(staged, targetDir);
|
|
846
|
+
installed = true;
|
|
847
|
+
} catch (error) {
|
|
848
|
+
if (installed) fs.rmSync(targetDir, { recursive: true, force: true });
|
|
849
|
+
if (backedUp && fs.existsSync(backup)) fs.renameSync(backup, targetDir);
|
|
850
|
+
throw error;
|
|
851
|
+
} finally {
|
|
852
|
+
// Once the new directory is installed, backup cleanup cannot invalidate
|
|
853
|
+
// that successful replacement or remove its only surviving content.
|
|
854
|
+
try { fs.rmSync(transaction, { recursive: true, force: true }); } catch {}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function recordedPersonalSkill(name, source) {
|
|
859
|
+
try {
|
|
860
|
+
const resolved = resolveRecordedPersonalSkill(source);
|
|
861
|
+
if (!resolved) throw new CliError(`recorded personal source for ${name} is missing from ${source.root}`);
|
|
862
|
+
return resolved;
|
|
863
|
+
} catch (error) {
|
|
864
|
+
if (/personal skill .* does not exist|\/SKILL\.md is missing/.test(error.message)) {
|
|
865
|
+
throw new CliError(`recorded personal source for ${name} is missing from ${source.root}`);
|
|
866
|
+
}
|
|
867
|
+
throw error;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function isLegacyBundleSource(source) {
|
|
872
|
+
return source == null || ['bundle', 'bundled'].includes(source.kind);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function syncPersonalCopy(name, source, target, boundary) {
|
|
876
|
+
assertNoManagedSymlink(boundary, target, `sync destination for ${name}`);
|
|
877
|
+
assertInstallPathsDoNotOverlap(source.sourceDir, target);
|
|
878
|
+
let targetStat;
|
|
879
|
+
try {
|
|
880
|
+
targetStat = fs.lstatSync(target);
|
|
881
|
+
} catch (error) {
|
|
882
|
+
if (error.code === 'ENOENT') return 'missing';
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
if (!targetStat.isDirectory() || targetStat.isSymbolicLink()) {
|
|
886
|
+
throw new CliError(`sync destination for ${name} is not a regular skill directory: ${target}`);
|
|
887
|
+
}
|
|
888
|
+
const sourceTree = personalTreeSnapshot(source.sourceDir, `personal source for ${name}`);
|
|
889
|
+
const targetTree = personalTreeSnapshot(target, `sync destination for ${name}`);
|
|
890
|
+
if (JSON.stringify(sourceTree) === JSON.stringify(targetTree)) return 'current';
|
|
891
|
+
replaceSyncedSkillDirectory(source.sourceDir, target);
|
|
892
|
+
return 'updated';
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function syncPersonalCopies(cwd) {
|
|
896
|
+
const failures = [];
|
|
897
|
+
const roots = new Map();
|
|
898
|
+
const currentRoot = findLockRoot(cwd, { required: false });
|
|
899
|
+
if (currentRoot) roots.set(canonicalPath(currentRoot), canonicalPath(currentRoot));
|
|
900
|
+
let ownershipComplete = true;
|
|
901
|
+
try {
|
|
902
|
+
for (const registeredRoot of readRegistry().repositories) {
|
|
903
|
+
const canonical = canonicalPath(registeredRoot);
|
|
904
|
+
roots.set(canonical, canonical);
|
|
905
|
+
}
|
|
906
|
+
} catch (error) {
|
|
907
|
+
failures.push(`project registry: ${error.message}`);
|
|
908
|
+
ownershipComplete = false;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const contexts = [];
|
|
912
|
+
for (const root of roots.values()) {
|
|
913
|
+
if (!hasLock(root)) continue;
|
|
914
|
+
try {
|
|
915
|
+
const lock = readLock(root);
|
|
916
|
+
contexts.push({ kind: 'project', root, lock, original: JSON.stringify(lock) });
|
|
917
|
+
} catch (error) {
|
|
918
|
+
failures.push(`project ${root}: ${error.message}`);
|
|
919
|
+
ownershipComplete = false;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
let globalLock = null;
|
|
923
|
+
try {
|
|
924
|
+
globalLock = readGlobalSkills();
|
|
925
|
+
} catch (error) {
|
|
926
|
+
failures.push(`global manifest: ${error.message}`);
|
|
927
|
+
ownershipComplete = false;
|
|
928
|
+
}
|
|
929
|
+
if (globalLock) contexts.push({ kind: 'global', lock: globalLock, original: JSON.stringify(globalLock) });
|
|
930
|
+
|
|
931
|
+
let updated = 0;
|
|
932
|
+
let current = 0;
|
|
933
|
+
let pruned = 0;
|
|
934
|
+
let selectedRoot = null;
|
|
935
|
+
try {
|
|
936
|
+
selectedRoot = selectedLibraryRoot(skillshopHome());
|
|
937
|
+
} catch (error) {
|
|
938
|
+
failures.push(`selected personal library: ${error.message}`);
|
|
939
|
+
}
|
|
940
|
+
const libraryInfo = new Map();
|
|
941
|
+
const getLibrarySkill = (name) => {
|
|
942
|
+
if (!selectedRoot) return null;
|
|
943
|
+
if (!libraryInfo.has(name)) {
|
|
944
|
+
libraryInfo.set(name, resolvePersonalSkill(name, skillshopHome(), selectedRoot));
|
|
945
|
+
}
|
|
946
|
+
return libraryInfo.get(name);
|
|
947
|
+
};
|
|
948
|
+
const isUnknownSource = (source) => source != null && source.kind !== 'personal-library' && !isLegacyBundleSource(source);
|
|
949
|
+
const legacyGroups = new Map();
|
|
950
|
+
const personalItems = [];
|
|
951
|
+
const unknownSourceNames = new Set();
|
|
952
|
+
|
|
953
|
+
for (const context of contexts) {
|
|
954
|
+
for (const [name, entry] of Object.entries(context.lock.skills)) {
|
|
955
|
+
if (context.kind === 'project') {
|
|
956
|
+
let targets;
|
|
957
|
+
try { targets = lockedTargets(context.root, name, entry); }
|
|
958
|
+
catch (error) { failures.push(`project ${name}: ${error.message}`); ownershipComplete = false; continue; }
|
|
959
|
+
if (entry.source?.kind === 'personal-library') {
|
|
960
|
+
personalItems.push({ kind: 'project', context, name, entry, sourceRecord: entry.source, targets });
|
|
961
|
+
} else if (isLegacyBundleSource(entry.source)) {
|
|
962
|
+
legacyGroups.set(name, [...(legacyGroups.get(name) ?? []), { kind: 'project', context, name, entry, targets }]);
|
|
963
|
+
} else if (isUnknownSource(entry.source)) unknownSourceNames.add(name);
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
let targets;
|
|
968
|
+
try { targets = globalTargets(name, entry); }
|
|
969
|
+
catch (error) { failures.push(`global ${name}: ${error.message}`); ownershipComplete = false; continue; }
|
|
970
|
+
for (const target of targets) {
|
|
971
|
+
const copy = entry.copies?.[target] ?? {};
|
|
972
|
+
const sourceRecord = copy.source ?? entry.source;
|
|
973
|
+
if (sourceRecord?.kind === 'personal-library') {
|
|
974
|
+
personalItems.push({ kind: 'global', context, name, entry, copy, target, sourceRecord });
|
|
975
|
+
} else if (isLegacyBundleSource(sourceRecord)) {
|
|
976
|
+
legacyGroups.set(name, [...(legacyGroups.get(name) ?? []), { kind: 'global', context, name, entry, copy, target }]);
|
|
977
|
+
} else if (isUnknownSource(sourceRecord)) unknownSourceNames.add(name);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const updateLocalOwnership = (context, name, entry, source, targets) => {
|
|
983
|
+
const kept = targets.filter((target) => target.present).map(({ target }) => relativePath(context.root, target)).sort();
|
|
984
|
+
if (!kept.length) {
|
|
985
|
+
delete context.lock.skills[name];
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
entry.paths = kept;
|
|
989
|
+
entry.source = source.source;
|
|
990
|
+
if (typeof source.author === 'string') entry.author = source.author;
|
|
991
|
+
else delete entry.author;
|
|
992
|
+
if (typeof source.version === 'string') entry.version = source.version;
|
|
993
|
+
else delete entry.version;
|
|
994
|
+
};
|
|
995
|
+
const updateGlobalOwnership = (entry, copy, target, source) => {
|
|
996
|
+
copy.source = source.source;
|
|
997
|
+
if (typeof source.author === 'string') copy.author = source.author;
|
|
998
|
+
else delete copy.author;
|
|
999
|
+
if (typeof source.version === 'string') copy.version = source.version;
|
|
1000
|
+
else delete copy.version;
|
|
1001
|
+
entry.copies ??= {};
|
|
1002
|
+
entry.copies[target] = copy;
|
|
1003
|
+
};
|
|
1004
|
+
const pruneMissing = (item) => {
|
|
1005
|
+
pruned += 1;
|
|
1006
|
+
if (item.kind === 'project') {
|
|
1007
|
+
const targets = lockedTargets(item.context.root, item.name, item.entry).filter((target) => target !== item.target);
|
|
1008
|
+
if (targets.length) item.entry.paths = targets.map((target) => relativePath(item.context.root, target));
|
|
1009
|
+
else delete item.context.lock.skills[item.name];
|
|
1010
|
+
console.log(`Pruned missing project copy ${item.target}`);
|
|
1011
|
+
} else {
|
|
1012
|
+
delete item.entry.copies[item.target];
|
|
1013
|
+
const paths = item.entry.paths.filter((target) => target !== item.target);
|
|
1014
|
+
if (paths.length) item.entry.paths = paths;
|
|
1015
|
+
else delete item.context.lock.skills[item.name];
|
|
1016
|
+
console.log(`Pruned missing global copy ${item.target}`);
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
for (const item of personalItems) {
|
|
1021
|
+
const { name, sourceRecord } = item;
|
|
1022
|
+
let source;
|
|
1023
|
+
try { source = recordedPersonalSkill(name, sourceRecord); }
|
|
1024
|
+
catch (error) { failures.push(`${item.kind} ${name}: ${error.message}`); continue; }
|
|
1025
|
+
const targets = item.kind === 'project' ? item.targets : [item.target];
|
|
1026
|
+
const prepared = [];
|
|
1027
|
+
let failed = false;
|
|
1028
|
+
for (const target of targets) {
|
|
1029
|
+
try {
|
|
1030
|
+
const boundary = item.kind === 'project' ? item.context.root : os.homedir();
|
|
1031
|
+
assertNoManagedSymlink(boundary, target, `sync destination for ${name}`);
|
|
1032
|
+
assertInstallPathsDoNotOverlap(source.sourceDir, target);
|
|
1033
|
+
let stat;
|
|
1034
|
+
try { stat = fs.lstatSync(target); }
|
|
1035
|
+
catch (error) { if (error.code === 'ENOENT') { prepared.push({ target, present: false }); continue; } throw error; }
|
|
1036
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new CliError(`sync destination for ${name} is not a regular skill directory: ${target}`);
|
|
1037
|
+
personalTreeSnapshot(target, `sync destination for ${name}`);
|
|
1038
|
+
prepared.push({ target, present: true });
|
|
1039
|
+
} catch (error) { failures.push(`${item.kind} ${name} at ${target}: ${error.message}`); failed = true; }
|
|
1040
|
+
}
|
|
1041
|
+
if (failed) continue;
|
|
1042
|
+
for (const target of prepared) {
|
|
1043
|
+
if (!target.present) { pruneMissing({ ...item, target: target.target }); continue; }
|
|
1044
|
+
try {
|
|
1045
|
+
const result = syncPersonalCopy(name, source, target.target, item.kind === 'project' ? item.context.root : os.homedir());
|
|
1046
|
+
if (result === 'updated') { updated += 1; console.log(`Synced ${item.kind} copy ${target.target}`); }
|
|
1047
|
+
else current += 1;
|
|
1048
|
+
} catch (error) { failures.push(`${item.kind} ${name} at ${target.target}: ${error.message}`); failed = true; }
|
|
1049
|
+
}
|
|
1050
|
+
if (failed) continue;
|
|
1051
|
+
if (item.kind === 'project') updateLocalOwnership(item.context, name, item.entry, source, prepared);
|
|
1052
|
+
else updateGlobalOwnership(item.entry, item.copy, item.target, source);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
for (const [name, group] of legacyGroups) {
|
|
1056
|
+
const root = selectedRoot;
|
|
1057
|
+
if (!root) {
|
|
1058
|
+
failures.push(`${name}: select a personal library with "skillshop library init" before adopting legacy copies`);
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
let source;
|
|
1062
|
+
try { source = getLibrarySkill(name); }
|
|
1063
|
+
catch (error) { failures.push(`${name}: selected library source is unsafe or invalid: ${error.message}`); continue; }
|
|
1064
|
+
if (!source && unknownSourceNames.has(name)) {
|
|
1065
|
+
failures.push(`${name}: an ownership record has an unknown source kind; legacy copies were left unchanged`);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const prepared = [];
|
|
1070
|
+
let failed = false;
|
|
1071
|
+
for (const item of group) {
|
|
1072
|
+
const boundary = item.kind === 'project' ? item.context.root : os.homedir();
|
|
1073
|
+
const target = item.kind === 'project' ? item.targets : [item.target];
|
|
1074
|
+
for (const destination of target) {
|
|
1075
|
+
try {
|
|
1076
|
+
assertNoManagedSymlink(boundary, destination, `legacy copy for ${name}`);
|
|
1077
|
+
if (source) assertInstallPathsDoNotOverlap(source.sourceDir, destination);
|
|
1078
|
+
else assertInstallPathsDoNotOverlap(destination, path.join(root, 'skills', name));
|
|
1079
|
+
let stat;
|
|
1080
|
+
try { stat = fs.lstatSync(destination); }
|
|
1081
|
+
catch (error) { if (error.code === 'ENOENT') { prepared.push({ item, target: destination, present: false }); continue; } throw error; }
|
|
1082
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new CliError(`legacy copy for ${name} is not a regular skill directory: ${destination}`);
|
|
1083
|
+
validatePersonalSkillDirectory(boundary, destination, name);
|
|
1084
|
+
const snapshot = personalTreeSnapshot(destination, `legacy copy for ${name}`);
|
|
1085
|
+
prepared.push({ item, target: destination, present: true, snapshot });
|
|
1086
|
+
} catch (error) { failures.push(`${name} at ${destination}: ${error.message}`); failed = true; }
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (failed) continue;
|
|
1090
|
+
|
|
1091
|
+
const present = prepared.filter(({ present: exists }) => exists);
|
|
1092
|
+
if (!present.length) {
|
|
1093
|
+
for (const item of group) {
|
|
1094
|
+
if (item.kind === 'project') {
|
|
1095
|
+
const existing = item.targets.filter((target) => fs.existsSync(target));
|
|
1096
|
+
const missing = item.targets.filter((target) => !fs.existsSync(target));
|
|
1097
|
+
for (const target of missing) {
|
|
1098
|
+
pruned += 1;
|
|
1099
|
+
console.log(`Pruned missing project copy ${target}`);
|
|
1100
|
+
}
|
|
1101
|
+
if (missing.length) {
|
|
1102
|
+
item.entry.paths = existing.map((target) => relativePath(item.context.root, target));
|
|
1103
|
+
if (!existing.length) delete item.context.lock.skills[name];
|
|
1104
|
+
}
|
|
1105
|
+
} else if (!fs.existsSync(item.target)) pruneMissing(item);
|
|
1106
|
+
}
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (!source) {
|
|
1111
|
+
if (!ownershipComplete) {
|
|
1112
|
+
failures.push(`${name}: cannot import a missing source because one or more ownership manifests could not be read`);
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
const expected = JSON.stringify(present[0].snapshot);
|
|
1116
|
+
if (present.some(({ snapshot }) => JSON.stringify(snapshot) !== expected)) {
|
|
1117
|
+
failures.push(`${name}: tracked legacy copies diverge; resolve them before adoption`);
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
try {
|
|
1121
|
+
const skillDirectory = path.join(inspectPersonalLibrary(root).skillsDirectory, name);
|
|
1122
|
+
for (const copy of present) assertInstallPathsDoNotOverlap(copy.target, skillDirectory);
|
|
1123
|
+
importTrackedPersonalSkill(root, present[0].target, name);
|
|
1124
|
+
source = resolvePersonalSkill(name, skillshopHome(), root);
|
|
1125
|
+
} catch (error) { failures.push(`${name}: could not import agreeing legacy copies: ${error.message}`); continue; }
|
|
1126
|
+
for (const copy of present.slice(1)) {
|
|
1127
|
+
if (JSON.stringify(copy.snapshot) !== JSON.stringify(present[0].snapshot)) {
|
|
1128
|
+
failures.push(`${name}: tracked legacy copies changed during adoption`);
|
|
1129
|
+
failed = true;
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (failed) continue;
|
|
1134
|
+
console.log(`Adopted ${name} from ${present.length} agreeing tracked copy/copies into ${root}`);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const localEntryResults = new Map();
|
|
1138
|
+
for (const copy of prepared) {
|
|
1139
|
+
if (!copy.present) { pruneMissing({ ...copy.item, target: copy.target }); continue; }
|
|
1140
|
+
let result;
|
|
1141
|
+
try {
|
|
1142
|
+
result = source ? syncPersonalCopy(name, source, copy.target, copy.item.kind === 'project' ? copy.item.context.root : os.homedir()) : 'current';
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
failures.push(`${copy.item.kind} ${name} at ${copy.target}: ${error.message}`);
|
|
1145
|
+
failed = true;
|
|
1146
|
+
if (copy.item.kind === 'project') {
|
|
1147
|
+
const state = localEntryResults.get(copy.item) ?? { successes: [], failed: false };
|
|
1148
|
+
state.failed = true;
|
|
1149
|
+
localEntryResults.set(copy.item, state);
|
|
1150
|
+
}
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
if (result === 'updated') { updated += 1; console.log(`Synced adopted ${name} copy ${copy.target}`); }
|
|
1154
|
+
else current += 1;
|
|
1155
|
+
if (copy.item.kind === 'project') {
|
|
1156
|
+
const state = localEntryResults.get(copy.item) ?? { successes: [], failed: false };
|
|
1157
|
+
state.successes.push(copy.target);
|
|
1158
|
+
localEntryResults.set(copy.item, state);
|
|
1159
|
+
} else updateGlobalOwnership(copy.item.entry, copy.item.copy, copy.target, source);
|
|
1160
|
+
}
|
|
1161
|
+
for (const [item, state] of localEntryResults) {
|
|
1162
|
+
if (state.failed) continue;
|
|
1163
|
+
if (!state.successes.length) continue;
|
|
1164
|
+
item.entry.source = source.source;
|
|
1165
|
+
if (typeof source.author === 'string') item.entry.author = source.author;
|
|
1166
|
+
else delete item.entry.author;
|
|
1167
|
+
if (typeof source.version === 'string') item.entry.version = source.version;
|
|
1168
|
+
else delete item.entry.version;
|
|
1169
|
+
item.entry.paths = state.successes.map((target) => relativePath(item.context.root, target)).sort();
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
for (const context of contexts) {
|
|
1174
|
+
if (context.kind === 'project' && JSON.stringify(context.lock) !== context.original) {
|
|
1175
|
+
try { writeLock(context.root, context.lock); }
|
|
1176
|
+
catch (error) { failures.push(`project ${context.root} state: ${error.message}`); }
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (globalLock && JSON.stringify(globalLock) !== contexts.find(({ kind }) => kind === 'global')?.original) {
|
|
1181
|
+
try { writeGlobalSkills(globalLock); }
|
|
1182
|
+
catch (error) { failures.push(`global state: ${error.message}`); }
|
|
1183
|
+
}
|
|
1184
|
+
console.log(`Personal sync: ${updated} updated, ${current} current, ${pruned} missing destination(s) pruned.`);
|
|
1185
|
+
if (failures.length) {
|
|
1186
|
+
for (const failure of failures) console.error(`skillshop: ${failure}`);
|
|
1187
|
+
throw new CliError(`sync completed with ${failures.length} failure(s); ${updated} copy/copies updated, ${current} current, ${pruned} missing destination(s) pruned`);
|
|
1188
|
+
}
|
|
734
1189
|
}
|
|
735
1190
|
|
|
736
|
-
function installAt(name, requestedVersion, targetDir) {
|
|
737
|
-
const metadata = skillMetadata(name);
|
|
1191
|
+
function installAt(name, requestedVersion, targetDir, metadata = skillMetadata(name), boundary = path.dirname(targetDir)) {
|
|
738
1192
|
if (requestedVersion && requestedVersion !== metadata.version) {
|
|
739
|
-
throw new CliError(
|
|
740
|
-
`${name}@${requestedVersion} is unavailable
|
|
741
|
-
|
|
1193
|
+
throw new CliError(metadata.kind === 'personal'
|
|
1194
|
+
? `${name}@${requestedVersion} is unavailable in the personal library${metadata.version ? `; library version is ${metadata.version}` : ''}`
|
|
1195
|
+
: `${name}@${requestedVersion} is unavailable; local registry provides ${name}@${metadata.version}`);
|
|
742
1196
|
}
|
|
743
1197
|
|
|
1198
|
+
if (metadata.kind === 'personal') {
|
|
1199
|
+
assertNoManagedSymlink(boundary, targetDir, `install destination for ${name}`);
|
|
1200
|
+
assertInstallPathsDoNotOverlap(metadata.sourceDir, targetDir);
|
|
1201
|
+
}
|
|
744
1202
|
const updating = fs.existsSync(targetDir);
|
|
745
1203
|
replaceSkillDirectory(metadata.sourceDir, targetDir);
|
|
746
1204
|
return { ...metadata, targetDir, updating };
|
|
747
1205
|
}
|
|
748
1206
|
|
|
1207
|
+
function assertInstallPathsDoNotOverlap(sourceDir, targetDir) {
|
|
1208
|
+
const source = path.resolve(sourceDir);
|
|
1209
|
+
const target = path.resolve(targetDir);
|
|
1210
|
+
const within = (parent, child) => {
|
|
1211
|
+
const relative = path.relative(parent, child);
|
|
1212
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
1213
|
+
};
|
|
1214
|
+
if (within(source, target) || within(target, source)) {
|
|
1215
|
+
throw new CliError(`install destination overlaps personal source directory: ${source}`);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
749
1219
|
function lockEntry(root, results) {
|
|
750
1220
|
const [result] = results;
|
|
751
1221
|
return {
|
|
752
|
-
version: result.version,
|
|
753
|
-
author: result.author,
|
|
1222
|
+
...(typeof result.version === 'string' ? { version: result.version } : {}),
|
|
1223
|
+
...(typeof result.author === 'string' ? { author: result.author } : {}),
|
|
1224
|
+
...(result.source ? { source: result.source } : {}),
|
|
754
1225
|
paths: results.map(({ targetDir }) => relativePath(root, targetDir)).sort()
|
|
755
1226
|
};
|
|
756
1227
|
}
|
|
757
1228
|
|
|
1229
|
+
function resolvedInstallSource(name, stateHome, { existing = null, requested = false, selectedRoot = undefined, personalOnly = false } = {}) {
|
|
1230
|
+
const allRecordedSources = [existing?.source, ...Object.values(existing?.copies ?? {}).map((copy) => copy?.source)]
|
|
1231
|
+
.filter((source) => source != null);
|
|
1232
|
+
if (personalOnly && allRecordedSources.some((source) => source.kind !== 'personal-library' && !isLegacyBundleSource(source))) {
|
|
1233
|
+
throw new CliError(`${name} has an ownership record with an unknown source kind; refusing to replace it`);
|
|
1234
|
+
}
|
|
1235
|
+
const recordedSources = allRecordedSources
|
|
1236
|
+
.filter((source) => source?.kind === 'personal-library');
|
|
1237
|
+
if (requested) {
|
|
1238
|
+
const selected = resolvePersonalSkill(name, stateHome, selectedRoot ?? null);
|
|
1239
|
+
if (selected) {
|
|
1240
|
+
if (recordedSources.some((source) => path.resolve(source.root) !== path.resolve(selected.source.root) || source.skillDirectory !== selected.source.skillDirectory)) {
|
|
1241
|
+
throw new CliError(`${name} is already owned by a different personal library; uninstall it before changing sources`);
|
|
1242
|
+
}
|
|
1243
|
+
return selected;
|
|
1244
|
+
}
|
|
1245
|
+
if (personalOnly && selectedRoot && !recordedSources.length) {
|
|
1246
|
+
throw new CliError(`${name} is not present in the selected personal library`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (recordedSources.length) {
|
|
1250
|
+
const identities = new Set(recordedSources.map((source) => `${path.resolve(source.root)}\0${source.skillDirectory ?? ''}\0${source.skill ?? name}`));
|
|
1251
|
+
if (identities.size !== 1) throw new CliError(`${name} has multiple personal library sources; refusing to choose one implicitly`);
|
|
1252
|
+
const source = recordedSources[0];
|
|
1253
|
+
const recorded = recordedPersonalSkill(name, source);
|
|
1254
|
+
if (recorded.name !== name) throw new CliError(`recorded personal source for ${name} is unavailable`);
|
|
1255
|
+
return recorded;
|
|
1256
|
+
}
|
|
1257
|
+
if (requested && selectedRoot === undefined) {
|
|
1258
|
+
const selected = resolvePersonalSkill(name, stateHome);
|
|
1259
|
+
if (selected) return selected;
|
|
1260
|
+
}
|
|
1261
|
+
if (personalOnly) throw new CliError(`${name} is not present in the selected or recorded personal library`);
|
|
1262
|
+
return skillMetadata(name);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
758
1265
|
function installGlobal(name, requestedVersion) {
|
|
759
1266
|
const config = readConfig();
|
|
760
1267
|
const skillDirectories = config
|
|
@@ -762,41 +1269,120 @@ function installGlobal(name, requestedVersion) {
|
|
|
762
1269
|
: [path.join(os.homedir(), '.agents', 'skills')];
|
|
763
1270
|
|
|
764
1271
|
const globalSkills = readGlobalSkills();
|
|
765
|
-
|
|
1272
|
+
const selectedRoot = selectedLibraryRoot(skillshopHome());
|
|
1273
|
+
const existing = globalSkills.skills[name];
|
|
1274
|
+
const recordedPersonal = [existing?.source, ...Object.values(existing?.copies ?? {}).map((copy) => copy?.source)]
|
|
1275
|
+
.some((source) => source?.kind === 'personal-library');
|
|
1276
|
+
const personalOnly = Boolean(selectedRoot) || recordedPersonal;
|
|
1277
|
+
const metadata = resolvedInstallSource(name, skillshopHome(), { existing, requested: true, selectedRoot, personalOnly });
|
|
1278
|
+
const managedTargets = existing ? new Set(globalTargets(name, existing).map(canonicalDestination)) : new Set();
|
|
1279
|
+
const targetPairs = skillDirectories.map((skillDirectory) => {
|
|
766
1280
|
const targetDir = path.join(skillDirectory, name);
|
|
767
1281
|
assertLocalDestination(os.homedir(), targetDir, `global installation for ${name}`);
|
|
768
|
-
|
|
1282
|
+
if (metadata.kind === 'personal') {
|
|
1283
|
+
assertNoManagedSymlink(os.homedir(), targetDir, `global install destination for ${name}`);
|
|
1284
|
+
assertInstallPathsDoNotOverlap(metadata.sourceDir, targetDir);
|
|
1285
|
+
}
|
|
1286
|
+
if (metadata.kind === 'personal' && fs.existsSync(targetDir) && !managedTargets.has(canonicalDestination(targetDir))) {
|
|
1287
|
+
throw new CliError(`${targetDir} exists and is not a Skillshop-managed copy of ${name}`);
|
|
1288
|
+
}
|
|
1289
|
+
return { skillDirectory, targetDir };
|
|
1290
|
+
});
|
|
1291
|
+
for (const { skillDirectory, targetDir } of targetPairs) {
|
|
1292
|
+
const result = installAt(name, requestedVersion, targetDir, metadata, os.homedir());
|
|
769
1293
|
const entry = globalSkills.skills[name] ?? { paths: [], copies: {} };
|
|
770
1294
|
entry.paths = [...new Set([...entry.paths, targetDir])];
|
|
771
|
-
entry.copies[targetDir] = {
|
|
1295
|
+
entry.copies[targetDir] = {
|
|
1296
|
+
...(typeof result.author === 'string' ? { author: result.author } : {}),
|
|
1297
|
+
...(typeof result.version === 'string' ? { version: result.version } : {}),
|
|
1298
|
+
...(result.source ? { source: result.source } : {})
|
|
1299
|
+
};
|
|
772
1300
|
globalSkills.skills[name] = entry;
|
|
773
1301
|
writeGlobalSkills(globalSkills);
|
|
774
|
-
console.log(`${result.updating ? 'Updated' : 'Installed'} ${name}@${result.version} at ${targetDir}`);
|
|
1302
|
+
console.log(`${result.updating ? 'Updated' : 'Installed'} ${name}@${result.version ?? 'personal'} at ${targetDir}`);
|
|
775
1303
|
}
|
|
776
1304
|
}
|
|
777
1305
|
|
|
778
|
-
function
|
|
779
|
-
|
|
780
|
-
|
|
1306
|
+
function instructionProjectRoot(cwd, selectedProject) {
|
|
1307
|
+
if (selectedProject) {
|
|
1308
|
+
const selected = path.resolve(cwd, selectedProject);
|
|
1309
|
+
let stats;
|
|
1310
|
+
try {
|
|
1311
|
+
stats = fs.statSync(selected);
|
|
1312
|
+
} catch {
|
|
1313
|
+
throw new CliError(`project directory does not exist: ${selected}`);
|
|
1314
|
+
}
|
|
1315
|
+
if (!stats.isDirectory()) throw new CliError(`project path is not a directory: ${selected}`);
|
|
1316
|
+
return canonicalPath(selected);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
return canonicalPath(projectRoot(cwd, { required: false }) ?? cwd);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function isWithinDirectory(root, target) {
|
|
1323
|
+
const relative = path.relative(root, target);
|
|
1324
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function nearestAgentsFile(cwd, root) {
|
|
1328
|
+
const start = canonicalPath(cwd);
|
|
1329
|
+
let directory = isWithinDirectory(root, start) ? start : root;
|
|
1330
|
+
while (isWithinDirectory(root, directory)) {
|
|
781
1331
|
const candidate = path.join(directory, 'AGENTS.md');
|
|
782
1332
|
if (fs.existsSync(candidate)) return candidate;
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
directory = parent;
|
|
1333
|
+
if (directory === root) break;
|
|
1334
|
+
directory = path.dirname(directory);
|
|
786
1335
|
}
|
|
1336
|
+
return path.join(root, 'AGENTS.md');
|
|
787
1337
|
}
|
|
788
1338
|
|
|
789
|
-
function
|
|
1339
|
+
function assertLocalInstructionSources(root, contentDirectory) {
|
|
1340
|
+
assertNoInstructionSymlink(root, contentDirectory, 'local instruction source directory');
|
|
1341
|
+
if (!fs.existsSync(contentDirectory)) return;
|
|
1342
|
+
for (const entry of fs.readdirSync(contentDirectory, { withFileTypes: true })) {
|
|
1343
|
+
if (entry.name.endsWith('.block.md')) {
|
|
1344
|
+
const sourcePath = path.join(contentDirectory, entry.name);
|
|
1345
|
+
const label = `local instruction source ${entry.name}`;
|
|
1346
|
+
assertLocalDestination(root, sourcePath, label);
|
|
1347
|
+
assertNoInstructionSymlink(root, sourcePath, label);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function assertNoInstructionSymlink(root, target, label) {
|
|
1353
|
+
const boundary = path.resolve(root);
|
|
1354
|
+
const absoluteTarget = path.resolve(target);
|
|
1355
|
+
const relative = path.relative(boundary, absoluteTarget);
|
|
1356
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
1357
|
+
throw new CliError(`${label} resolves outside the project`);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
let current = boundary;
|
|
1361
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
1362
|
+
current = path.join(current, segment);
|
|
1363
|
+
try {
|
|
1364
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
1365
|
+
throw new CliError(`${label} contains a symlink path component`);
|
|
1366
|
+
}
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
if (error instanceof CliError) throw error;
|
|
1369
|
+
if (error.code === 'ENOENT') break;
|
|
1370
|
+
throw error;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function installAgents(cwd, globalInstall, selectedProject) {
|
|
790
1376
|
const composed = composeAgentsInstructions(agentsContentRoot);
|
|
791
1377
|
const targetPaths = [];
|
|
1378
|
+
const root = globalInstall ? null : instructionProjectRoot(cwd, selectedProject);
|
|
792
1379
|
if (globalInstall) {
|
|
793
1380
|
targetPaths.push(...globalAgentsTargets.map((target) => path.join(os.homedir(), target)));
|
|
794
1381
|
} else {
|
|
795
|
-
const targetPath = nearestAgentsFile(cwd);
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
1382
|
+
const targetPath = nearestAgentsFile(cwd, root);
|
|
1383
|
+
assertLocalDestination(root, targetPath, 'local agents instructions');
|
|
1384
|
+
assertNoInstructionSymlink(root, targetPath, 'local agents instructions');
|
|
1385
|
+
assertLocalInstructionSources(root, path.dirname(targetPath));
|
|
800
1386
|
targetPaths.push(targetPath);
|
|
801
1387
|
}
|
|
802
1388
|
|
|
@@ -824,14 +1410,117 @@ function installAgents(cwd, globalInstall) {
|
|
|
824
1410
|
}
|
|
825
1411
|
}
|
|
826
1412
|
|
|
1413
|
+
function projectInstructionPaths(cwd, selectedProject) {
|
|
1414
|
+
const root = instructionProjectRoot(cwd, selectedProject);
|
|
1415
|
+
const sourceDirectory = path.join(root, projectTemplateDirectory);
|
|
1416
|
+
const templatePath = path.join(sourceDirectory, projectTemplateName);
|
|
1417
|
+
const outputPath = path.join(root, 'AGENTS.md');
|
|
1418
|
+
const paths = [
|
|
1419
|
+
[sourceDirectory, 'project instruction source directory'],
|
|
1420
|
+
[templatePath, 'project instruction template'],
|
|
1421
|
+
[outputPath, 'project agents output']
|
|
1422
|
+
];
|
|
1423
|
+
for (const [target, label] of paths) {
|
|
1424
|
+
assertLocalDestination(root, target, label);
|
|
1425
|
+
assertNoInstructionSymlink(root, target, label);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
if (fs.existsSync(sourceDirectory) && !fs.statSync(sourceDirectory).isDirectory()) {
|
|
1429
|
+
throw new CliError(`project instruction source path is not a directory: ${sourceDirectory}`);
|
|
1430
|
+
}
|
|
1431
|
+
if (fs.existsSync(templatePath) && !fs.statSync(templatePath).isFile()) {
|
|
1432
|
+
throw new CliError(`project instruction template is not a file: ${templatePath}`);
|
|
1433
|
+
}
|
|
1434
|
+
if (fs.existsSync(outputPath) && !fs.statSync(outputPath).isFile()) {
|
|
1435
|
+
throw new CliError(`project agents output is not a file: ${outputPath}`);
|
|
1436
|
+
}
|
|
1437
|
+
if (fs.existsSync(sourceDirectory)) {
|
|
1438
|
+
for (const entry of fs.readdirSync(sourceDirectory, { withFileTypes: true })) {
|
|
1439
|
+
if (!entry.name.endsWith('.block.md')) continue;
|
|
1440
|
+
const sourcePath = path.join(sourceDirectory, entry.name);
|
|
1441
|
+
const label = `project instruction source ${entry.name}`;
|
|
1442
|
+
assertLocalDestination(root, sourcePath, label);
|
|
1443
|
+
assertNoInstructionSymlink(root, sourcePath, label);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
return { root, sourceDirectory, templatePath, outputPath };
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function renderProjectAgents(cwd, selectedProject, { initialize = false, check = false } = {}) {
|
|
1451
|
+
let paths = projectInstructionPaths(cwd, selectedProject);
|
|
1452
|
+
if (initialize) {
|
|
1453
|
+
fs.mkdirSync(paths.sourceDirectory, { recursive: true });
|
|
1454
|
+
if (!fs.existsSync(paths.templatePath)) {
|
|
1455
|
+
fs.writeFileSync(paths.templatePath, defaultProjectTemplate);
|
|
1456
|
+
}
|
|
1457
|
+
paths = projectInstructionPaths(cwd, selectedProject);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (!fs.existsSync(paths.templatePath)) {
|
|
1461
|
+
throw new CliError(`project instruction template is missing; run "skillshop agents init" first: ${paths.templatePath}`);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const composed = composeAgentsInstructions(paths.sourceDirectory, projectTemplateName).content;
|
|
1465
|
+
const existing = fs.existsSync(paths.outputPath) ? fs.readFileSync(paths.outputPath, 'utf8') : '';
|
|
1466
|
+
const planned = applyProjectInstructions(existing, composed, packageJson.version);
|
|
1467
|
+
|
|
1468
|
+
if (check) {
|
|
1469
|
+
if (fs.existsSync(paths.outputPath) && planned.text === existing) {
|
|
1470
|
+
console.log(`Project agents instructions are current at ${paths.outputPath}`);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
console.error(
|
|
1474
|
+
`Project agents instructions are ${fs.existsSync(paths.outputPath) ? 'stale' : 'missing'} at ${paths.outputPath}`
|
|
1475
|
+
);
|
|
1476
|
+
process.exitCode = 1;
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
if (planned.text === existing) {
|
|
1481
|
+
console.log(`Project agents instructions already current at ${paths.outputPath}`);
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
fs.writeFileSync(paths.outputPath, planned.text);
|
|
1485
|
+
console.log(`${planned.action === 'refreshed' ? 'Refreshed' : 'Rendered'} project agents instructions at ${paths.outputPath}`);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function projectAgentsConfigured(cwd, selectedProject) {
|
|
1489
|
+
const root = instructionProjectRoot(cwd, selectedProject);
|
|
1490
|
+
const sourceDirectory = path.join(root, projectTemplateDirectory);
|
|
1491
|
+
const outputPath = path.join(root, 'AGENTS.md');
|
|
1492
|
+
for (const candidate of [sourceDirectory, outputPath]) {
|
|
1493
|
+
try {
|
|
1494
|
+
const stat = fs.lstatSync(candidate);
|
|
1495
|
+
if (candidate === sourceDirectory) return true;
|
|
1496
|
+
if (stat.isFile() && hasProjectInstructions(fs.readFileSync(candidate, 'utf8'))) return true;
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
if (error.code !== 'ENOENT') throw error;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return false;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
827
1504
|
function installLocal(name, requestedVersion, cwd) {
|
|
828
1505
|
const root = findLockRoot(cwd) ?? projectRoot(cwd);
|
|
829
1506
|
const lock = readLock(root);
|
|
830
1507
|
const config = readConfig();
|
|
1508
|
+
const selectedRoot = selectedLibraryRoot(skillshopHome());
|
|
831
1509
|
const skillDirectories = localSkillDirectories(root, lock, cwd);
|
|
832
|
-
const
|
|
1510
|
+
const existing = lock.skills[name];
|
|
1511
|
+
const recordedPersonal = existing?.source?.kind === 'personal-library';
|
|
1512
|
+
const personalOnly = Boolean(selectedRoot) || recordedPersonal;
|
|
1513
|
+
if (personalOnly && existing?.source && existing.source.kind !== 'personal-library' && !isLegacyBundleSource(existing.source)) {
|
|
1514
|
+
throw new CliError(`${name} has an ownership record with an unknown source kind; refusing to replace it`);
|
|
1515
|
+
}
|
|
1516
|
+
const names = personalOnly ? [name] : [...new Set([...Object.keys(lock.skills), name, 'skillshop'])].sort();
|
|
833
1517
|
const installs = names.map((skillName) => {
|
|
834
|
-
const metadata =
|
|
1518
|
+
const metadata = resolvedInstallSource(skillName, skillshopHome(), {
|
|
1519
|
+
existing: lock.skills[skillName],
|
|
1520
|
+
requested: skillName === name,
|
|
1521
|
+
selectedRoot,
|
|
1522
|
+
personalOnly: skillName === name && personalOnly
|
|
1523
|
+
});
|
|
835
1524
|
const targetDirs = config || !lock.skills[skillName]
|
|
836
1525
|
? skillDirectories.map((directory) => path.join(directory, skillName))
|
|
837
1526
|
: lockedTargets(root, skillName, lock.skills[skillName]);
|
|
@@ -843,23 +1532,29 @@ function installLocal(name, requestedVersion, cwd) {
|
|
|
843
1532
|
const requestedMetadata = installs.find(({ metadata }) => metadata.name === name).metadata;
|
|
844
1533
|
if (requestedVersion && requestedVersion !== requestedMetadata.version) {
|
|
845
1534
|
throw new CliError(
|
|
846
|
-
`${name}@${requestedVersion} is unavailable;
|
|
1535
|
+
`${name}@${requestedVersion} is unavailable; personal skills must declare that exact version or use a bundled skill`
|
|
847
1536
|
);
|
|
848
1537
|
}
|
|
849
1538
|
for (const { metadata, targetDirs, previousTargets } of installs) {
|
|
850
1539
|
assertTargetsAvailable(metadata.name, targetDirs, previousTargets);
|
|
1540
|
+
for (const targetDir of targetDirs) {
|
|
1541
|
+
if (metadata.kind === 'personal') {
|
|
1542
|
+
assertNoManagedSymlink(root, targetDir, `install destination for ${metadata.name}`);
|
|
1543
|
+
assertInstallPathsDoNotOverlap(metadata.sourceDir, targetDir);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
851
1546
|
}
|
|
852
1547
|
|
|
853
1548
|
for (const { metadata, targetDirs, previousTargets } of installs) {
|
|
854
1549
|
const version = metadata.name === name ? requestedVersion : null;
|
|
855
|
-
const results = targetDirs.map((targetDir) => installAt(metadata.name, version, targetDir));
|
|
1550
|
+
const results = targetDirs.map((targetDir) => installAt(metadata.name, version, targetDir, metadata, root));
|
|
856
1551
|
for (const staleTarget of staleTargets(previousTargets, targetDirs)) {
|
|
857
1552
|
fs.rmSync(staleTarget, { recursive: true, force: true });
|
|
858
1553
|
}
|
|
859
1554
|
lock.skills[metadata.name] = lockEntry(root, results);
|
|
860
1555
|
for (const result of results) {
|
|
861
1556
|
console.log(
|
|
862
|
-
`${result.updating ? 'Updated' : 'Installed'} ${result.name}@${result.version} at ${result.targetDir}`
|
|
1557
|
+
`${result.updating ? 'Updated' : 'Installed'} ${result.name}@${result.version ?? 'personal'} at ${result.targetDir}`
|
|
863
1558
|
);
|
|
864
1559
|
}
|
|
865
1560
|
}
|
|
@@ -926,7 +1621,10 @@ function updateLocal(requestedName, cwd, options = {}) {
|
|
|
926
1621
|
const skillDirectories = localSkillDirectories(root, lock, cwd);
|
|
927
1622
|
|
|
928
1623
|
const updates = [...new Set(names)].sort().map((name) => {
|
|
929
|
-
|
|
1624
|
+
let metadata;
|
|
1625
|
+
if (lock.skills[name]?.source?.kind === 'personal-library') {
|
|
1626
|
+
metadata = recordedPersonalSkill(name, lock.skills[name].source);
|
|
1627
|
+
} else metadata = skillMetadata(name);
|
|
930
1628
|
const targetDirs = config
|
|
931
1629
|
? skillDirectories.map((directory) => path.join(directory, name))
|
|
932
1630
|
: lockedTargets(root, name, lock.skills[name]);
|
|
@@ -935,16 +1633,22 @@ function updateLocal(requestedName, cwd, options = {}) {
|
|
|
935
1633
|
});
|
|
936
1634
|
for (const { metadata, targetDirs, previousTargets } of updates) {
|
|
937
1635
|
assertTargetsAvailable(metadata.name, targetDirs, previousTargets);
|
|
1636
|
+
for (const targetDir of targetDirs) {
|
|
1637
|
+
if (metadata.kind === 'personal') {
|
|
1638
|
+
assertNoManagedSymlink(root, targetDir, `update destination for ${metadata.name}`);
|
|
1639
|
+
assertInstallPathsDoNotOverlap(metadata.sourceDir, targetDir);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
938
1642
|
}
|
|
939
1643
|
|
|
940
1644
|
for (const { metadata, targetDirs, previousTargets } of updates) {
|
|
941
|
-
const results = targetDirs.map((targetDir) => installAt(metadata.name, null, targetDir));
|
|
1645
|
+
const results = targetDirs.map((targetDir) => installAt(metadata.name, null, targetDir, metadata, root));
|
|
942
1646
|
for (const staleTarget of staleTargets(previousTargets, targetDirs)) {
|
|
943
1647
|
fs.rmSync(staleTarget, { recursive: true, force: true });
|
|
944
1648
|
}
|
|
945
1649
|
lock.skills[metadata.name] = lockEntry(root, results);
|
|
946
1650
|
for (const result of results) {
|
|
947
|
-
console.log(`Updated ${result.name}@${result.version} at ${result.targetDir}`);
|
|
1651
|
+
console.log(`Updated ${result.name}@${result.version ?? 'personal'} at ${result.targetDir}`);
|
|
948
1652
|
}
|
|
949
1653
|
}
|
|
950
1654
|
|
|
@@ -983,6 +1687,223 @@ function updateAllRepositories() {
|
|
|
983
1687
|
console.log(`Updated ${retained.length} registered ${retained.length === 1 ? 'repository' : 'repositories'}`);
|
|
984
1688
|
}
|
|
985
1689
|
|
|
1690
|
+
function assertNoManagedSymlink(base, target, label) {
|
|
1691
|
+
const boundary = path.resolve(base);
|
|
1692
|
+
const absoluteTarget = path.resolve(target);
|
|
1693
|
+
const relative = path.relative(boundary, absoluteTarget);
|
|
1694
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
1695
|
+
throw new CliError(`${label} resolves outside its managed root`);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
let current = boundary;
|
|
1699
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
1700
|
+
current = path.join(current, segment);
|
|
1701
|
+
try {
|
|
1702
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
1703
|
+
throw new CliError(`${label} contains a symlink path component`);
|
|
1704
|
+
}
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
if (error instanceof CliError) throw error;
|
|
1707
|
+
if (error.code === 'ENOENT') break;
|
|
1708
|
+
throw error;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function replaceManagedSkillDirectory(sourceDir, targetDir) {
|
|
1714
|
+
const temporaryDir = `${targetDir}.skillshop-upgrade-tmp-${process.pid}`;
|
|
1715
|
+
const backupDir = `${targetDir}.skillshop-upgrade-backup-${process.pid}`;
|
|
1716
|
+
fs.rmSync(temporaryDir, { recursive: true, force: true });
|
|
1717
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
1718
|
+
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
1719
|
+
fs.cpSync(sourceDir, temporaryDir, { recursive: true, force: true });
|
|
1720
|
+
|
|
1721
|
+
const hadTarget = fs.existsSync(targetDir);
|
|
1722
|
+
if (hadTarget) fs.renameSync(targetDir, backupDir);
|
|
1723
|
+
try {
|
|
1724
|
+
fs.renameSync(temporaryDir, targetDir);
|
|
1725
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
if (hadTarget && !fs.existsSync(targetDir) && fs.existsSync(backupDir)) {
|
|
1728
|
+
fs.renameSync(backupDir, targetDir);
|
|
1729
|
+
}
|
|
1730
|
+
throw error;
|
|
1731
|
+
} finally {
|
|
1732
|
+
fs.rmSync(temporaryDir, { recursive: true, force: true });
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function upgradeSummary() {
|
|
1737
|
+
return { updated: 0, unchanged: 0, missing: 0, failed: [] };
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function validateManagedCopy(root, target, name) {
|
|
1741
|
+
assertNoManagedSymlink(root, target, `managed ${name} copy`);
|
|
1742
|
+
let targetStats;
|
|
1743
|
+
try {
|
|
1744
|
+
targetStats = fs.lstatSync(target);
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
if (error.code === 'ENOENT') return false;
|
|
1747
|
+
throw error;
|
|
1748
|
+
}
|
|
1749
|
+
if (!targetStats.isDirectory()) throw new CliError(`${target} is not a managed skill directory`);
|
|
1750
|
+
|
|
1751
|
+
const manifestPath = path.join(target, 'SKILL.md');
|
|
1752
|
+
assertNoManagedSymlink(root, manifestPath, `managed ${name} manifest`);
|
|
1753
|
+
let manifestStats;
|
|
1754
|
+
try {
|
|
1755
|
+
manifestStats = fs.lstatSync(manifestPath);
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
if (error.code === 'ENOENT') throw new CliError(`${manifestPath} is missing from a recorded skill copy`);
|
|
1758
|
+
throw error;
|
|
1759
|
+
}
|
|
1760
|
+
if (!manifestStats.isFile()) throw new CliError(`${manifestPath} is not a regular skill manifest`);
|
|
1761
|
+
return true;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function refreshManagedLock(root, lock, { global = false, summary }) {
|
|
1765
|
+
let changed = false;
|
|
1766
|
+
for (const name of Object.keys(lock.skills).sort()) {
|
|
1767
|
+
const entry = lock.skills[name];
|
|
1768
|
+
try {
|
|
1769
|
+
if (root && entry.source?.kind === 'personal-library') {
|
|
1770
|
+
summary.unchanged += (Array.isArray(entry.paths) ? entry.paths.length : 1);
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
if (global && entry.paths?.length && entry.paths.every((target) => entry.copies?.[target]?.source?.kind === 'personal-library')) {
|
|
1774
|
+
summary.unchanged += entry.paths.length;
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
const metadata = skillMetadata(name);
|
|
1778
|
+
const allTargets = global ? globalTargets(name, entry) : lockedTargets(root, name, entry);
|
|
1779
|
+
const personalTargets = global ? allTargets.filter((target) => entry.copies?.[target]?.source?.kind === 'personal-library') : [];
|
|
1780
|
+
const targets = global ? allTargets.filter((target) => !personalTargets.includes(target)) : allTargets;
|
|
1781
|
+
const base = global ? os.homedir() : root;
|
|
1782
|
+
const existingTargets = [];
|
|
1783
|
+
for (const target of targets) {
|
|
1784
|
+
if (validateManagedCopy(base, target, name)) existingTargets.push(target);
|
|
1785
|
+
else summary.missing += 1;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
if (!existingTargets.length) {
|
|
1789
|
+
if (personalTargets.length) {
|
|
1790
|
+
entry.paths = personalTargets;
|
|
1791
|
+
changed = true;
|
|
1792
|
+
} else {
|
|
1793
|
+
delete lock.skills[name];
|
|
1794
|
+
changed = true;
|
|
1795
|
+
}
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
const successfulTargets = [];
|
|
1800
|
+
let skillFailed = false;
|
|
1801
|
+
for (const target of existingTargets) {
|
|
1802
|
+
const recordedVersion = global ? entry.copies?.[target]?.version : entry.version;
|
|
1803
|
+
if (recordedVersion === metadata.version) {
|
|
1804
|
+
summary.unchanged += 1;
|
|
1805
|
+
successfulTargets.push(target);
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
try {
|
|
1809
|
+
replaceManagedSkillDirectory(metadata.sourceDir, target);
|
|
1810
|
+
summary.updated += 1;
|
|
1811
|
+
successfulTargets.push(target);
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
summary.failed.push(`${name} at ${target}: ${error.message}`);
|
|
1814
|
+
skillFailed = true;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
if (global) {
|
|
1819
|
+
const keptTargets = [...personalTargets, ...(skillFailed ? targets : successfulTargets)];
|
|
1820
|
+
entry.paths = keptTargets;
|
|
1821
|
+
entry.copies ??= {};
|
|
1822
|
+
for (const target of successfulTargets) {
|
|
1823
|
+
const previous = entry.copies[target] ?? {};
|
|
1824
|
+
entry.copies[target] = { ...previous, author: metadata.author, version: metadata.version };
|
|
1825
|
+
}
|
|
1826
|
+
if (!skillFailed) changed = true;
|
|
1827
|
+
if (!entry.paths.length) {
|
|
1828
|
+
delete lock.skills[name];
|
|
1829
|
+
changed = true;
|
|
1830
|
+
}
|
|
1831
|
+
} else if (!skillFailed) {
|
|
1832
|
+
entry.paths = successfulTargets.map((target) => relativePath(root, target)).sort();
|
|
1833
|
+
entry.author = metadata.author;
|
|
1834
|
+
entry.version = metadata.version;
|
|
1835
|
+
changed = true;
|
|
1836
|
+
} else if (existingTargets.length !== targets.length) {
|
|
1837
|
+
entry.paths = existingTargets.map((target) => relativePath(root, target)).sort();
|
|
1838
|
+
changed = true;
|
|
1839
|
+
}
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
summary.failed.push(`${name}: ${error.message}`);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
if (changed) {
|
|
1846
|
+
try {
|
|
1847
|
+
if (global) writeGlobalSkills(lock);
|
|
1848
|
+
else writeLock(root, lock);
|
|
1849
|
+
} catch (error) {
|
|
1850
|
+
summary.failed.push(`could not save ${global ? 'global skill manifest' : path.join(root, lockFileName)}: ${error.message}`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
function refreshManagedCopies(scope) {
|
|
1856
|
+
const summary = upgradeSummary();
|
|
1857
|
+
if (scope === 'local') {
|
|
1858
|
+
const owner = localPackageOwner();
|
|
1859
|
+
if (!owner) {
|
|
1860
|
+
summary.failed.push('fresh Skillshop package is no longer attached to a local npm dependency');
|
|
1861
|
+
} else {
|
|
1862
|
+
try {
|
|
1863
|
+
const lock = readLock(owner.root);
|
|
1864
|
+
refreshManagedLock(owner.root, lock, { summary });
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
summary.failed.push(`${owner.root}: ${error.message}`);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
} else if (scope === 'global') {
|
|
1870
|
+
try {
|
|
1871
|
+
const globalLock = readGlobalSkills();
|
|
1872
|
+
refreshManagedLock(null, globalLock, { global: true, summary });
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
summary.failed.push(`global managed copies: ${error.message}`);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
try {
|
|
1878
|
+
const registry = readRegistry();
|
|
1879
|
+
for (const root of registry.repositories) {
|
|
1880
|
+
if (!hasLock(root)) continue;
|
|
1881
|
+
try {
|
|
1882
|
+
const lock = readLock(root);
|
|
1883
|
+
refreshManagedLock(root, lock, { summary });
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
summary.failed.push(`${root}: ${error.message}`);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
summary.failed.push(`registered project copies: ${error.message}`);
|
|
1890
|
+
}
|
|
1891
|
+
} else {
|
|
1892
|
+
summary.failed.push(`unsupported managed-copy scope: ${scope}`);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
console.log(
|
|
1896
|
+
`Managed-copy refresh summary: ${summary.updated} updated, ${summary.unchanged} unchanged, ${summary.missing} missing paths pruned, ${summary.failed.length} failed.`
|
|
1897
|
+
);
|
|
1898
|
+
if (summary.failed.length) {
|
|
1899
|
+
for (const failure of summary.failed) console.error(`skillshop: upgrade refresh: ${failure}`);
|
|
1900
|
+
console.error('skillshop: npm upgrade succeeded, but managed-copy refresh was incomplete');
|
|
1901
|
+
process.exitCode = 1;
|
|
1902
|
+
} else {
|
|
1903
|
+
console.log(`Refreshed Skillshop-managed copies for ${scope} scope.`);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
986
1907
|
const migrationSkipDirectories = new Set([
|
|
987
1908
|
'.git',
|
|
988
1909
|
'.skillshop',
|
|
@@ -1074,6 +1995,8 @@ function parseArgs(argv) {
|
|
|
1074
1995
|
let pushUpdate = false;
|
|
1075
1996
|
const agentsTargets = [];
|
|
1076
1997
|
let requestedVersion = null;
|
|
1998
|
+
let selectedProject = null;
|
|
1999
|
+
let checkProject = false;
|
|
1077
2000
|
|
|
1078
2001
|
for (let index = 0; index < argv.length; index += 1) {
|
|
1079
2002
|
const argument = argv[index];
|
|
@@ -1081,6 +2004,15 @@ function parseArgs(argv) {
|
|
|
1081
2004
|
globalInstall = true;
|
|
1082
2005
|
} else if (argument === '--push') {
|
|
1083
2006
|
pushUpdate = true;
|
|
2007
|
+
} else if (argument === '--check') {
|
|
2008
|
+
checkProject = true;
|
|
2009
|
+
} else if (argument === '--project') {
|
|
2010
|
+
selectedProject = argv[index + 1];
|
|
2011
|
+
if (!selectedProject) throw new CliError('--project requires a directory');
|
|
2012
|
+
index += 1;
|
|
2013
|
+
} else if (argument.startsWith('--project=')) {
|
|
2014
|
+
selectedProject = argument.slice('--project='.length);
|
|
2015
|
+
if (!selectedProject) throw new CliError('--project requires a directory');
|
|
1084
2016
|
} else if (argument === '--codex' || argument === '--copilot') {
|
|
1085
2017
|
throw new CliError(`${argument} is no longer supported; use agents --global`);
|
|
1086
2018
|
} else if (argument === '--version' || argument === '-v') {
|
|
@@ -1097,18 +2029,152 @@ function parseArgs(argv) {
|
|
|
1097
2029
|
}
|
|
1098
2030
|
}
|
|
1099
2031
|
|
|
1100
|
-
return {
|
|
2032
|
+
return {
|
|
2033
|
+
positional,
|
|
2034
|
+
globalInstall,
|
|
2035
|
+
pushUpdate,
|
|
2036
|
+
requestedVersion,
|
|
2037
|
+
selectedProject,
|
|
2038
|
+
checkProject,
|
|
2039
|
+
agentsTargets: [...new Set(agentsTargets)]
|
|
2040
|
+
};
|
|
1101
2041
|
}
|
|
1102
2042
|
|
|
1103
2043
|
function listSkills() {
|
|
2044
|
+
const personalRoot = selectedLibraryRoot(skillshopHome());
|
|
2045
|
+
if (personalRoot) {
|
|
2046
|
+
const library = inspectPersonalLibrary(personalRoot);
|
|
2047
|
+
for (const skill of library.skills) console.log(`${skill.name} — ${skill.description}`);
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
1104
2050
|
for (const name of availableSkills()) {
|
|
1105
2051
|
const metadata = skillMetadata(name);
|
|
1106
2052
|
console.log(`${name}@${metadata.version} — ${metadata.author}`);
|
|
1107
2053
|
}
|
|
1108
2054
|
}
|
|
1109
2055
|
|
|
2056
|
+
function packageDependencySection(manifest) {
|
|
2057
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
2058
|
+
if (Object.hasOwn(manifest?.[section] ?? {}, 'skillshop')) return section;
|
|
2059
|
+
}
|
|
2060
|
+
return null;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
function localPackageOwner() {
|
|
2064
|
+
const installedPackageRoot = canonicalPath(packageRoot);
|
|
2065
|
+
let current = path.dirname(installedPackageRoot);
|
|
2066
|
+
while (true) {
|
|
2067
|
+
const manifestPath = path.join(current, 'package.json');
|
|
2068
|
+
if (fs.existsSync(manifestPath)) {
|
|
2069
|
+
try {
|
|
2070
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
2071
|
+
const section = packageDependencySection(manifest);
|
|
2072
|
+
const declaredInstall = path.join(current, 'node_modules', 'skillshop');
|
|
2073
|
+
if (section && fs.existsSync(declaredInstall) && canonicalPath(declaredInstall) === installedPackageRoot) {
|
|
2074
|
+
return { root: current, section };
|
|
2075
|
+
}
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
if (error instanceof SyntaxError) throw new CliError(`cannot parse ${manifestPath}: ${error.message}`);
|
|
2078
|
+
throw error;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
const parent = path.dirname(current);
|
|
2083
|
+
if (parent === current) return null;
|
|
2084
|
+
current = parent;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function npmGlobalRoot() {
|
|
2089
|
+
try {
|
|
2090
|
+
return path.resolve(execFileSync('npm', ['root', '--global'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim());
|
|
2091
|
+
} catch (error) {
|
|
2092
|
+
const detail = error.stderr?.toString().trim();
|
|
2093
|
+
throw new CliError(`cannot determine the active npm global package directory${detail ? `: ${detail}` : ''}`);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function npmPrefixFromRoot(globalRoot) {
|
|
2098
|
+
const parent = path.dirname(globalRoot);
|
|
2099
|
+
return path.basename(parent) === 'lib' ? path.dirname(parent) : parent;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
function resolveUpgradeScope() {
|
|
2103
|
+
const localOwner = localPackageOwner();
|
|
2104
|
+
if (localOwner) return { type: 'local', root: localOwner.root, dependencySection: localOwner.section, packageRoot };
|
|
2105
|
+
|
|
2106
|
+
const insideNodeModules = canonicalPath(packageRoot).split(path.sep).includes('node_modules');
|
|
2107
|
+
const activeGlobalRoot = npmGlobalRoot();
|
|
2108
|
+
if (canonicalPath(path.join(activeGlobalRoot, 'skillshop')) === canonicalPath(packageRoot)) {
|
|
2109
|
+
return { type: 'global', prefix: npmPrefixFromRoot(activeGlobalRoot), packageRoot };
|
|
2110
|
+
}
|
|
2111
|
+
if (insideNodeModules) {
|
|
2112
|
+
throw new CliError(
|
|
2113
|
+
`cannot match this installed Skillshop package to the active npm global prefix (${activeGlobalRoot}); no upgrade was run`
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// Preserve the historical source-checkout behavior: install the package into
|
|
2118
|
+
// the active npm global prefix without editing the checkout's package files.
|
|
2119
|
+
return { type: 'global-checkout', packageRoot };
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
function runUpgradeInstall(scope) {
|
|
2123
|
+
const args = scope.type === 'local'
|
|
2124
|
+
? ['install', `--save-${scope.dependencySection === 'dependencies' ? 'prod' : scope.dependencySection === 'devDependencies' ? 'dev' : scope.dependencySection === 'optionalDependencies' ? 'optional' : 'peer'}`, 'skillshop@latest']
|
|
2125
|
+
: scope.type === 'global'
|
|
2126
|
+
? ['install', '--global', '--prefix', scope.prefix, 'skillshop@latest']
|
|
2127
|
+
: ['install', '--global', 'skillshop@latest'];
|
|
2128
|
+
try {
|
|
2129
|
+
execFileSync('npm', args, {
|
|
2130
|
+
cwd: scope.type === 'local' ? scope.root : process.cwd(),
|
|
2131
|
+
stdio: 'inherit'
|
|
2132
|
+
});
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
const status = Number.isInteger(error.status) ? ` (exit ${error.status})` : '';
|
|
2135
|
+
throw new CliError(`npm upgrade failed${status}; managed copies were not refreshed`);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
function freshUpgradePackageRoot(scope) {
|
|
2140
|
+
if (scope.type !== 'global-checkout') return scope.packageRoot;
|
|
2141
|
+
return path.join(npmGlobalRoot(), 'skillshop');
|
|
2142
|
+
}
|
|
2143
|
+
|
|
1110
2144
|
function upgrade() {
|
|
1111
|
-
|
|
2145
|
+
const scope = resolveUpgradeScope();
|
|
2146
|
+
const scopeLabel = scope.type === 'local'
|
|
2147
|
+
? `project dependency in ${scope.root}`
|
|
2148
|
+
: scope.type === 'global'
|
|
2149
|
+
? `global installation at ${scope.packageRoot}`
|
|
2150
|
+
: 'global installation (source-checkout compatibility mode)';
|
|
2151
|
+
console.log(`Upgrading Skillshop ${scopeLabel}...`);
|
|
2152
|
+
runUpgradeInstall(scope);
|
|
2153
|
+
|
|
2154
|
+
let freshCliPath;
|
|
2155
|
+
try {
|
|
2156
|
+
freshCliPath = path.join(freshUpgradePackageRoot(scope), 'src', 'cli.js');
|
|
2157
|
+
if (!fs.statSync(freshCliPath).isFile()) throw new Error('not a file');
|
|
2158
|
+
} catch {
|
|
2159
|
+
throw new CliError('npm upgrade succeeded, but the newly installed Skillshop CLI could not be found for managed-copy refresh');
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
const child = spawnSync(process.execPath, [freshCliPath, internalUpgradeRefreshCommand], {
|
|
2163
|
+
cwd: scope.type === 'local' ? scope.root : process.cwd(),
|
|
2164
|
+
env: {
|
|
2165
|
+
...process.env,
|
|
2166
|
+
SKILLSHOP_INTERNAL_UPGRADE_TOKEN: internalUpgradeRefreshToken,
|
|
2167
|
+
SKILLSHOP_INTERNAL_UPGRADE_SCOPE: scope.type === 'local' ? 'local' : 'global'
|
|
2168
|
+
},
|
|
2169
|
+
stdio: 'inherit'
|
|
2170
|
+
});
|
|
2171
|
+
if (child.error) {
|
|
2172
|
+
throw new CliError(`npm upgrade succeeded, but fresh Skillshop managed-copy refresh could not start: ${child.error.message}`);
|
|
2173
|
+
}
|
|
2174
|
+
if (child.status !== 0) {
|
|
2175
|
+
const status = child.status === null ? 'without an exit status' : `with exit ${child.status}`;
|
|
2176
|
+
throw new CliError(`npm upgrade succeeded, but fresh Skillshop managed-copy refresh failed ${status}`);
|
|
2177
|
+
}
|
|
1112
2178
|
}
|
|
1113
2179
|
|
|
1114
2180
|
async function main(argv) {
|
|
@@ -1121,8 +2187,29 @@ async function main(argv) {
|
|
|
1121
2187
|
return;
|
|
1122
2188
|
}
|
|
1123
2189
|
|
|
1124
|
-
const
|
|
2190
|
+
const forceCount = argv.filter((argument) => argument === '--force').length;
|
|
2191
|
+
if (forceCount > 1) throw new CliError('--force may only be specified once');
|
|
2192
|
+
const parsedArgv = forceCount ? argv.filter((argument) => argument !== '--force') : argv;
|
|
2193
|
+
const { positional, globalInstall, pushUpdate, requestedVersion, selectedProject, checkProject, agentsTargets } = parseArgs(parsedArgv);
|
|
1125
2194
|
const command = positional[0];
|
|
2195
|
+
const forceDownload = forceCount === 1;
|
|
2196
|
+
if (forceDownload && command !== 'download') throw new CliError('--force is only valid with download');
|
|
2197
|
+
|
|
2198
|
+
if ((selectedProject || checkProject) && command !== 'agents') {
|
|
2199
|
+
throw new CliError('--project and --check are only valid with agents');
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
if (command === internalUpgradeRefreshCommand) {
|
|
2203
|
+
if (
|
|
2204
|
+
process.env.SKILLSHOP_INTERNAL_UPGRADE_TOKEN !== internalUpgradeRefreshToken ||
|
|
2205
|
+
positional.length !== 1 ||
|
|
2206
|
+
requestedVersion || globalInstall || pushUpdate || selectedProject || checkProject || agentsTargets.length
|
|
2207
|
+
) {
|
|
2208
|
+
throw new CliError('invalid internal upgrade continuation');
|
|
2209
|
+
}
|
|
2210
|
+
refreshManagedCopies(process.env.SKILLSHOP_INTERNAL_UPGRADE_SCOPE);
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
1126
2213
|
|
|
1127
2214
|
if (command === 'init') {
|
|
1128
2215
|
if (positional.length !== 1 || requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
|
|
@@ -1148,6 +2235,53 @@ async function main(argv) {
|
|
|
1148
2235
|
return;
|
|
1149
2236
|
}
|
|
1150
2237
|
|
|
2238
|
+
if (command === 'library') {
|
|
2239
|
+
if (requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
|
|
2240
|
+
throw new CliError('library commands do not accept install or update options');
|
|
2241
|
+
}
|
|
2242
|
+
if (positional[1] === 'init' && positional.length <= 3) {
|
|
2243
|
+
const directory = positional[2] ? path.resolve(process.cwd(), positional[2]) : undefined;
|
|
2244
|
+
const result = initPersonalLibrary(directory, skillshopHome(), personalLibraryTemplateRoot);
|
|
2245
|
+
console.log(`Initialized personal skill library at ${result.root}`);
|
|
2246
|
+
console.log(`Skills directory: ${result.skillsDirectory}`);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
if (positional[1] === 'list' && positional.length === 2) {
|
|
2250
|
+
const result = listPersonalSkills(skillshopHome());
|
|
2251
|
+
console.log(`Personal skill library: ${result.root}`);
|
|
2252
|
+
if (!result.skills.length) console.log('No personal skills found.');
|
|
2253
|
+
for (const skill of result.skills) console.log(`${skill.name}: ${skill.description}`);
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
throw new CliError('use "skillshop library init [directory]" or "skillshop library list"');
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
if (command === 'download') {
|
|
2260
|
+
if (positional.length !== 2 || requestedVersion || globalInstall || pushUpdate || selectedProject || checkProject || agentsTargets.length) {
|
|
2261
|
+
throw new CliError('download expects @<owner>/<skill> [--force] and does not accept install or update options');
|
|
2262
|
+
}
|
|
2263
|
+
const result = downloadPersonalSkill(positional[1], {
|
|
2264
|
+
stateHome: skillshopHome(),
|
|
2265
|
+
templateRoot: personalLibraryTemplateRoot,
|
|
2266
|
+
force: forceDownload
|
|
2267
|
+
});
|
|
2268
|
+
if (result.alreadyPresent) {
|
|
2269
|
+
console.log(`${result.name} is already downloaded from ${result.origin.repository} at ${result.root}`);
|
|
2270
|
+
} else {
|
|
2271
|
+
console.log(`Downloaded ${positional[1]} from ${result.origin.repository} at ${result.origin.commit}`);
|
|
2272
|
+
console.log(`Personal source: ${result.target}`);
|
|
2273
|
+
}
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
if (command === 'sync') {
|
|
2278
|
+
if (positional.length !== 1 || requestedVersion || globalInstall || pushUpdate || selectedProject || checkProject || agentsTargets.length) {
|
|
2279
|
+
throw new CliError('sync does not accept a skill name or install options');
|
|
2280
|
+
}
|
|
2281
|
+
syncPersonalCopies(process.cwd());
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
1151
2285
|
if (command === 'upgrade') {
|
|
1152
2286
|
if (positional.length !== 1 || requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
|
|
1153
2287
|
throw new CliError('upgrade does not accept arguments or options');
|
|
@@ -1178,10 +2312,32 @@ async function main(argv) {
|
|
|
1178
2312
|
}
|
|
1179
2313
|
|
|
1180
2314
|
if (command === 'agents') {
|
|
1181
|
-
|
|
2315
|
+
const initializeProject = positional.length === 2 && positional[1] === 'init';
|
|
2316
|
+
if (
|
|
2317
|
+
positional.length > 2 ||
|
|
2318
|
+
(positional.length === 2 && !initializeProject) ||
|
|
2319
|
+
requestedVersion ||
|
|
2320
|
+
pushUpdate ||
|
|
2321
|
+
agentsTargets.length
|
|
2322
|
+
) {
|
|
1182
2323
|
throw new CliError('agents does not accept skill or update options');
|
|
1183
2324
|
}
|
|
1184
|
-
|
|
2325
|
+
if (globalInstall) {
|
|
2326
|
+
if (initializeProject || selectedProject || checkProject) {
|
|
2327
|
+
throw new CliError('--global cannot be combined with project agents options');
|
|
2328
|
+
}
|
|
2329
|
+
installAgents(process.cwd(), true);
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
if (initializeProject && checkProject) throw new CliError('agents init does not accept --check');
|
|
2333
|
+
if (initializeProject || checkProject || projectAgentsConfigured(process.cwd(), selectedProject)) {
|
|
2334
|
+
renderProjectAgents(process.cwd(), selectedProject, {
|
|
2335
|
+
initialize: initializeProject,
|
|
2336
|
+
check: checkProject
|
|
2337
|
+
});
|
|
2338
|
+
} else {
|
|
2339
|
+
installAgents(process.cwd(), false, selectedProject);
|
|
2340
|
+
}
|
|
1185
2341
|
return;
|
|
1186
2342
|
}
|
|
1187
2343
|
|
|
@@ -1208,22 +2364,110 @@ async function main(argv) {
|
|
|
1208
2364
|
}
|
|
1209
2365
|
if (pushUpdate) throw new CliError('--push is only valid with update');
|
|
1210
2366
|
|
|
2367
|
+
if (installAliases.has(command) && (spec.includes('/') || spec.startsWith('@'))) {
|
|
2368
|
+
if (requestedVersion) throw new CliError('qualified GitHub installs do not accept --version');
|
|
2369
|
+
const result = downloadPersonalSkill(spec, {
|
|
2370
|
+
stateHome: skillshopHome(),
|
|
2371
|
+
templateRoot: personalLibraryTemplateRoot
|
|
2372
|
+
});
|
|
2373
|
+
if (globalInstall) installGlobal(result.name, null);
|
|
2374
|
+
else installLocal(result.name, null, process.cwd());
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
|
|
1211
2378
|
const { name, version } = parseSkillSpec(spec, requestedVersion);
|
|
1212
2379
|
validateSkillName(name);
|
|
1213
2380
|
if (globalInstall) installGlobal(name, version);
|
|
1214
2381
|
else installLocal(name, version, process.cwd());
|
|
1215
2382
|
}
|
|
1216
2383
|
|
|
2384
|
+
function isIsolatedAgentsInvocation(argv) {
|
|
2385
|
+
// Preserve the top-level help/version routing before looking for a command.
|
|
2386
|
+
// main() handles help anywhere and the standalone version form without parsing.
|
|
2387
|
+
if (argv[0] === '--help' || argv[0] === '-h') return false;
|
|
2388
|
+
if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-v')) return false;
|
|
2389
|
+
|
|
2390
|
+
// parseArgs accepts project options on either side of the command. Ignore help
|
|
2391
|
+
// flags here because main() handles them before parsing, then classify using
|
|
2392
|
+
// the same positional-command rules as execution.
|
|
2393
|
+
try {
|
|
2394
|
+
const args = argv.filter((argument) => argument !== '--help' && argument !== '-h');
|
|
2395
|
+
return parseArgs(args).positional[0] === 'agents';
|
|
2396
|
+
} catch {
|
|
2397
|
+
return false;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
function isUpgradeFlowInvocation(argv) {
|
|
2402
|
+
if (argv.includes('--help') || argv.includes('-h')) return false;
|
|
2403
|
+
if (
|
|
2404
|
+
argv[0] === internalUpgradeRefreshCommand &&
|
|
2405
|
+
process.env.SKILLSHOP_INTERNAL_UPGRADE_TOKEN === internalUpgradeRefreshToken
|
|
2406
|
+
) return true;
|
|
2407
|
+
try {
|
|
2408
|
+
return parseArgs(argv).positional[0] === 'upgrade';
|
|
2409
|
+
} catch {
|
|
2410
|
+
return false;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
function isPersonalLibraryInvocation(argv) {
|
|
2415
|
+
if (argv[0] === '--help' || argv[0] === '-h') return false;
|
|
2416
|
+
try {
|
|
2417
|
+
const args = argv.filter((argument) => argument !== '--help' && argument !== '-h' && argument !== '--force');
|
|
2418
|
+
const positional = parseArgs(args).positional;
|
|
2419
|
+
const command = positional[0];
|
|
2420
|
+
if (command === 'library' || command === 'download' || command === 'sync') return true;
|
|
2421
|
+
const remoteInstall = ['install', 'i', 'add'].includes(command) && positional.length === 2 &&
|
|
2422
|
+
(positional[1].includes('/') || positional[1].startsWith('@'));
|
|
2423
|
+
// Remote-reference attempts may download before installing. Do not reconcile
|
|
2424
|
+
// unrelated project/global state around that operation: a failed fetch
|
|
2425
|
+
// or validation error must leave those managed copies untouched as well.
|
|
2426
|
+
if (remoteInstall) return true;
|
|
2427
|
+
const stateHome = skillshopHome();
|
|
2428
|
+
const selectionPath = path.join(stateHome, librarySelectionFileName);
|
|
2429
|
+
let selectionIntent = false;
|
|
2430
|
+
try {
|
|
2431
|
+
// A saved selection expresses personal-mode intent even when the file is
|
|
2432
|
+
// malformed, symlinked, or points at a library that has since vanished.
|
|
2433
|
+
// main() remains responsible for reporting the validation error.
|
|
2434
|
+
fs.lstatSync(selectionPath);
|
|
2435
|
+
selectionIntent = true;
|
|
2436
|
+
} catch (error) {
|
|
2437
|
+
if (error.code !== 'ENOENT') selectionIntent = true;
|
|
2438
|
+
}
|
|
2439
|
+
if (selectionIntent && (
|
|
2440
|
+
(['list', 'ls'].includes(command) && positional.length === 1) ||
|
|
2441
|
+
(['install', 'i', 'add'].includes(command) && positional.length === 2)
|
|
2442
|
+
)) return true;
|
|
2443
|
+
const activePersonalRoot = selectedLibraryRoot(stateHome);
|
|
2444
|
+
if (!activePersonalRoot && !selectionIntent) return false;
|
|
2445
|
+
if (['list', 'ls'].includes(command) && positional.length === 1) return true;
|
|
2446
|
+
if (['install', 'i', 'add'].includes(command) && positional.length === 2) return true;
|
|
2447
|
+
const legacyCommands = new Set([
|
|
2448
|
+
'init', 'config', 'list', 'ls', 'upgrade', 'update', 'up', 'uninstall', 'remove', 'rm',
|
|
2449
|
+
'agents', 'migrate', 'library', 'download', 'sync', 'help'
|
|
2450
|
+
]);
|
|
2451
|
+
return positional.length === 1 && !legacyCommands.has(command);
|
|
2452
|
+
} catch {
|
|
2453
|
+
return false;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
|
|
1217
2457
|
export async function runLegacyCli(argv) {
|
|
2458
|
+
const isolatedAgentsCommand = isIsolatedAgentsInvocation(argv);
|
|
2459
|
+
const upgradeFlow = isUpgradeFlowInvocation(argv);
|
|
2460
|
+
const isolatedLibraryCommand = isPersonalLibraryInvocation(argv);
|
|
2461
|
+
const skipLegacyReconciliation = isolatedAgentsCommand || upgradeFlow || isolatedLibraryCommand;
|
|
1218
2462
|
try {
|
|
1219
|
-
reconcileCurrentRepository();
|
|
2463
|
+
if (!skipLegacyReconciliation) reconcileCurrentRepository();
|
|
1220
2464
|
try {
|
|
1221
2465
|
await main(argv);
|
|
1222
2466
|
} finally {
|
|
1223
|
-
reconcileCurrentRepository();
|
|
2467
|
+
if (!skipLegacyReconciliation) reconcileCurrentRepository();
|
|
1224
2468
|
}
|
|
1225
2469
|
} catch (error) {
|
|
1226
|
-
if (!(error instanceof CliError) && !(error instanceof AgentsInstructionsError)) throw error;
|
|
2470
|
+
if (!(error instanceof CliError) && !(error instanceof AgentsInstructionsError) && !(error instanceof PersonalLibraryError) && !(error instanceof GitHubLibraryError)) throw error;
|
|
1227
2471
|
console.error(`skillshop: ${error.message}`);
|
|
1228
2472
|
process.exitCode = 1;
|
|
1229
2473
|
}
|