@claude-collective/cli 0.21.0 → 0.26.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/CHANGELOG.md +192 -0
- package/README.md +41 -27
- package/config/skills-matrix.yaml +38 -37
- package/config/stacks.yaml +8 -14
- package/dist/{chunk-ZNIDWLL5.js → chunk-3X5D7RM5.js} +4 -3
- package/dist/chunk-3X5D7RM5.js.map +1 -0
- package/dist/chunk-4357L7VK.js +251 -0
- package/dist/chunk-4357L7VK.js.map +1 -0
- package/dist/chunk-4S4FCAA2.js +100 -0
- package/dist/chunk-4S4FCAA2.js.map +1 -0
- package/dist/chunk-7UPXT32F.js +197 -0
- package/dist/chunk-7UPXT32F.js.map +1 -0
- package/dist/{chunk-DHET7RCE.js → chunk-AWKZ5BDL.js} +9 -2
- package/dist/{chunk-DHET7RCE.js.map → chunk-AWKZ5BDL.js.map} +1 -1
- package/dist/{chunk-6Q3Y7KVB.js → chunk-DBRUQQUF.js} +8 -2
- package/dist/chunk-DBRUQQUF.js.map +1 -0
- package/dist/{chunk-OQYYMQJR.js → chunk-ETCVEV3S.js} +8 -11
- package/dist/chunk-ETCVEV3S.js.map +1 -0
- package/dist/chunk-ETQ3BPGU.js +4204 -0
- package/dist/chunk-ETQ3BPGU.js.map +1 -0
- package/dist/{chunk-ZSVMS677.js → chunk-F4RD5FYM.js} +2 -2
- package/dist/chunk-F4RD5FYM.js.map +1 -0
- package/dist/{chunk-5KXUDHAB.js → chunk-GGFOD5PK.js} +6 -9
- package/dist/chunk-GGFOD5PK.js.map +1 -0
- package/dist/{chunk-UMORK7OK.js → chunk-H7SSBSPR.js} +2 -2
- package/dist/chunk-H7SSBSPR.js.map +1 -0
- package/dist/chunk-HWD32NP7.js +19 -0
- package/dist/chunk-HWD32NP7.js.map +1 -0
- package/dist/{chunk-HGCBZUH5.js → chunk-I3YYG5IO.js} +9 -10
- package/dist/chunk-I3YYG5IO.js.map +1 -0
- package/dist/{chunk-RTE64SJA.js → chunk-IXBCRT3F.js} +2 -2
- package/dist/chunk-IXBCRT3F.js.map +1 -0
- package/dist/{chunk-WFEFICFM.js → chunk-KWYO3M5Q.js} +5 -5
- package/dist/chunk-KWYO3M5Q.js.map +1 -0
- package/dist/{chunk-HEOHU5EZ.js → chunk-MCTSHLAF.js} +22 -11
- package/dist/chunk-MCTSHLAF.js.map +1 -0
- package/dist/chunk-NQJ47R4N.js +1092 -0
- package/dist/chunk-NQJ47R4N.js.map +1 -0
- package/dist/{chunk-FJFEKPXF.js → chunk-O6ZTD7ZI.js} +14 -3
- package/dist/chunk-O6ZTD7ZI.js.map +1 -0
- package/dist/chunk-OBXAY23Y.js +56 -0
- package/dist/chunk-OBXAY23Y.js.map +1 -0
- package/dist/{chunk-XY3XDVMI.js → chunk-QR2EBWL2.js} +3 -3
- package/dist/chunk-R5KJVI54.js +311 -0
- package/dist/chunk-R5KJVI54.js.map +1 -0
- package/dist/{chunk-2LSGX6R4.js → chunk-R7B63JAP.js} +83 -25
- package/dist/chunk-R7B63JAP.js.map +1 -0
- package/dist/{chunk-66UDJBF6.js → chunk-REJGRCVQ.js} +2 -2
- package/dist/chunk-TDZE4TDG.js +220 -0
- package/dist/chunk-TDZE4TDG.js.map +1 -0
- package/dist/{chunk-CBLPAMZO.js → chunk-TMED5DQ2.js} +68 -42
- package/dist/chunk-TMED5DQ2.js.map +1 -0
- package/dist/{chunk-Q3J43SF3.js → chunk-U7HFKR74.js} +2 -2
- package/dist/chunk-U7HFKR74.js.map +1 -0
- package/dist/{chunk-3EHUF54X.js → chunk-UEMRJI2K.js} +17 -4
- package/dist/chunk-UEMRJI2K.js.map +1 -0
- package/dist/{chunk-Z2CWURZ6.js → chunk-UNN7523L.js} +2 -2
- package/dist/chunk-V2ZIH7HV.js +29 -0
- package/dist/chunk-V2ZIH7HV.js.map +1 -0
- package/dist/{chunk-ZEI3ZUDU.js → chunk-VVYNZZUX.js} +7 -15
- package/dist/chunk-VVYNZZUX.js.map +1 -0
- package/dist/{chunk-A46TPNBJ.js → chunk-XENOESJZ.js} +7 -16
- package/dist/chunk-XENOESJZ.js.map +1 -0
- package/dist/chunk-ZDREFYD2.js +696 -0
- package/dist/chunk-ZDREFYD2.js.map +1 -0
- package/dist/chunk-ZW2PELOH.js +197 -0
- package/dist/chunk-ZW2PELOH.js.map +1 -0
- package/dist/cli/defaults/agent-mappings.yaml +0 -1
- package/dist/commands/build/marketplace.js +22 -21
- package/dist/commands/build/marketplace.js.map +1 -1
- package/dist/commands/build/plugins.js +35 -231
- package/dist/commands/build/plugins.js.map +1 -1
- package/dist/commands/build/stack.js +11 -18
- package/dist/commands/build/stack.js.map +1 -1
- package/dist/commands/compile.js +36 -58
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/config/get.js +8 -8
- package/dist/commands/config/get.js.map +1 -1
- package/dist/commands/config/index.js +7 -7
- package/dist/commands/config/index.js.map +1 -1
- package/dist/commands/config/path.js +6 -6
- package/dist/commands/config/path.js.map +1 -1
- package/dist/commands/config/set-project.js +8 -8
- package/dist/commands/config/set-project.js.map +1 -1
- package/dist/commands/config/show.js +7 -7
- package/dist/commands/config/unset-project.js +8 -8
- package/dist/commands/config/unset-project.js.map +1 -1
- package/dist/commands/diff.js +10 -16
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/doctor.js +18 -51
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/edit.js +112 -57
- package/dist/commands/edit.js.map +1 -1
- package/dist/commands/eject.js +17 -49
- package/dist/commands/eject.js.map +1 -1
- package/dist/commands/import/skill.js +26 -26
- package/dist/commands/import/skill.js.map +1 -1
- package/dist/commands/info.js +15 -17
- package/dist/commands/info.js.map +1 -1
- package/dist/commands/init.js +103 -727
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/list.js +8 -87
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/new/agent.js +8 -12
- package/dist/commands/new/agent.js.map +1 -1
- package/dist/commands/new/skill.js +6 -6
- package/dist/commands/new/skill.js.map +1 -1
- package/dist/commands/outdated.js +15 -19
- package/dist/commands/outdated.js.map +1 -1
- package/dist/commands/search.js +21 -34
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/uninstall.js +15 -14
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.js +13 -24
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/validate.js +44 -487
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/version/bump.js +11 -11
- package/dist/commands/version/bump.js.map +1 -1
- package/dist/commands/version/index.js +9 -10
- package/dist/commands/version/index.js.map +1 -1
- package/dist/commands/version/set.js +10 -8
- package/dist/commands/version/set.js.map +1 -1
- package/dist/commands/version/show.js +9 -10
- package/dist/commands/version/show.js.map +1 -1
- package/dist/components/common/confirm.js +2 -2
- package/dist/components/common/confirm.test.js +203 -0
- package/dist/components/common/confirm.test.js.map +1 -0
- package/dist/components/common/message.js +1 -1
- package/dist/components/common/spinner.js +1 -1
- package/dist/components/common/spinner.js.map +1 -1
- package/dist/components/skill-search/skill-search.js +3 -3
- package/dist/components/wizard/category-grid.js +2 -2
- package/dist/components/wizard/category-grid.test.js +132 -78
- package/dist/components/wizard/category-grid.test.js.map +1 -1
- package/dist/components/wizard/menu-item.js +2 -2
- package/dist/components/wizard/search-modal.js +9 -0
- package/dist/components/wizard/search-modal.js.map +1 -0
- package/dist/components/wizard/search-modal.test.js +216 -0
- package/dist/components/wizard/search-modal.test.js.map +1 -0
- package/dist/components/wizard/section-progress.js +2 -2
- package/dist/components/wizard/section-progress.test.js +4 -4
- package/dist/components/wizard/section-progress.test.js.map +1 -1
- package/dist/components/wizard/source-grid.js +10 -0
- package/dist/components/wizard/source-grid.js.map +1 -0
- package/dist/components/wizard/source-grid.test.js +500 -0
- package/dist/components/wizard/source-grid.test.js.map +1 -0
- package/dist/components/wizard/step-approach.js +7 -6
- package/dist/components/wizard/step-approach.test.js +115 -0
- package/dist/components/wizard/step-approach.test.js.map +1 -0
- package/dist/components/wizard/step-build.js +9 -4
- package/dist/components/wizard/step-build.test.js +103 -122
- package/dist/components/wizard/step-build.test.js.map +1 -1
- package/dist/components/wizard/step-confirm.js +3 -2
- package/dist/components/wizard/step-confirm.test.js +364 -0
- package/dist/components/wizard/step-confirm.test.js.map +1 -0
- package/dist/components/wizard/step-refine.js +2 -2
- package/dist/components/wizard/step-refine.test.js +19 -13
- package/dist/components/wizard/step-refine.test.js.map +1 -1
- package/dist/components/wizard/step-settings.js +14 -0
- package/dist/components/wizard/step-settings.js.map +1 -0
- package/dist/components/wizard/step-settings.test.js +240 -0
- package/dist/components/wizard/step-settings.test.js.map +1 -0
- package/dist/components/wizard/step-sources.js +17 -0
- package/dist/components/wizard/step-sources.js.map +1 -0
- package/dist/components/wizard/step-sources.test.js +290 -0
- package/dist/components/wizard/step-sources.test.js.map +1 -0
- package/dist/components/wizard/step-stack.js +7 -6
- package/dist/components/wizard/step-stack.test.js +344 -0
- package/dist/components/wizard/step-stack.test.js.map +1 -0
- package/dist/components/wizard/view-title.js +2 -2
- package/dist/components/wizard/wizard-layout.js +6 -5
- package/dist/components/wizard/wizard-tabs.js +2 -2
- package/dist/components/wizard/wizard-tabs.test.js +292 -0
- package/dist/components/wizard/wizard-tabs.test.js.map +1 -0
- package/dist/components/wizard/wizard.js +22 -14
- package/dist/config/skills-matrix.yaml +38 -37
- package/dist/config/stacks.yaml +8 -14
- package/dist/hooks/init.js +5 -5
- package/dist/hooks/init.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/{magic-string.es-RGXYGAW3.js → magic-string.es-PAH2SOTR.js} +2 -2
- package/dist/source-manager-EYO3F2DV.js +16 -0
- package/dist/source-manager-EYO3F2DV.js.map +1 -0
- package/dist/src/agents/_templates/agent.liquid +1 -1
- package/dist/src/agents/developer/api-developer/agent.yaml +1 -1
- package/dist/src/agents/developer/cli-developer/agent.yaml +1 -1
- package/dist/src/agents/developer/web-architecture/agent.yaml +1 -1
- package/dist/src/agents/developer/web-developer/agent.yaml +1 -1
- package/dist/src/agents/meta/agent-summoner/agent.yaml +1 -1
- package/dist/src/agents/meta/documentor/agent.yaml +1 -1
- package/dist/src/agents/meta/skill-summoner/agent.yaml +1 -1
- package/dist/src/agents/migration/cli-migrator/agent.yaml +1 -1
- package/dist/src/agents/pattern/pattern-scout/agent.yaml +1 -1
- package/dist/src/agents/pattern/web-pattern-critique/agent.yaml +1 -1
- package/dist/src/agents/planning/web-pm/agent.yaml +1 -1
- package/dist/src/agents/researcher/api-researcher/agent.yaml +1 -1
- package/dist/src/agents/researcher/web-researcher/agent.yaml +1 -1
- package/dist/src/agents/reviewer/api-reviewer/agent.yaml +1 -1
- package/dist/src/agents/reviewer/cli-reviewer/agent.yaml +1 -1
- package/dist/src/agents/reviewer/web-reviewer/agent.yaml +1 -1
- package/dist/src/agents/tester/cli-tester/agent.yaml +1 -1
- package/dist/src/agents/tester/web-tester/agent.yaml +1 -1
- package/dist/stores/wizard-store.js +4 -3
- package/dist/stores/wizard-store.test.js +51 -82
- package/dist/stores/wizard-store.test.js.map +1 -1
- package/package.json +5 -3
- package/src/agents/_templates/agent.liquid +1 -1
- package/src/agents/developer/api-developer/agent.yaml +1 -1
- package/src/agents/developer/cli-developer/agent.yaml +1 -1
- package/src/agents/developer/web-architecture/agent.yaml +1 -1
- package/src/agents/developer/web-developer/agent.yaml +1 -1
- package/src/agents/meta/agent-summoner/agent.yaml +1 -1
- package/src/agents/meta/documentor/agent.yaml +1 -1
- package/src/agents/meta/skill-summoner/agent.yaml +1 -1
- package/src/agents/migration/cli-migrator/agent.yaml +1 -1
- package/src/agents/pattern/pattern-scout/agent.yaml +1 -1
- package/src/agents/pattern/web-pattern-critique/agent.yaml +1 -1
- package/src/agents/planning/web-pm/agent.yaml +1 -1
- package/src/agents/researcher/api-researcher/agent.yaml +1 -1
- package/src/agents/researcher/web-researcher/agent.yaml +1 -1
- package/src/agents/reviewer/api-reviewer/agent.yaml +1 -1
- package/src/agents/reviewer/cli-reviewer/agent.yaml +1 -1
- package/src/agents/reviewer/web-reviewer/agent.yaml +1 -1
- package/src/agents/tester/cli-tester/agent.yaml +1 -1
- package/src/agents/tester/web-tester/agent.yaml +1 -1
- package/dist/chunk-2LSGX6R4.js.map +0 -1
- package/dist/chunk-2OKUEELH.js +0 -32
- package/dist/chunk-2OKUEELH.js.map +0 -1
- package/dist/chunk-374JNMR6.js +0 -212
- package/dist/chunk-374JNMR6.js.map +0 -1
- package/dist/chunk-3EHUF54X.js.map +0 -1
- package/dist/chunk-3XR4PALU.js +0 -529
- package/dist/chunk-3XR4PALU.js.map +0 -1
- package/dist/chunk-5K2ZLUO5.js +0 -57
- package/dist/chunk-5K2ZLUO5.js.map +0 -1
- package/dist/chunk-5KXUDHAB.js.map +0 -1
- package/dist/chunk-6Q3Y7KVB.js.map +0 -1
- package/dist/chunk-7SLV7CMF.js +0 -615
- package/dist/chunk-7SLV7CMF.js.map +0 -1
- package/dist/chunk-A46TPNBJ.js.map +0 -1
- package/dist/chunk-AL74GBW4.js +0 -69
- package/dist/chunk-AL74GBW4.js.map +0 -1
- package/dist/chunk-BQX23RBV.js +0 -191
- package/dist/chunk-BQX23RBV.js.map +0 -1
- package/dist/chunk-CA4LH4LI.js +0 -132
- package/dist/chunk-CA4LH4LI.js.map +0 -1
- package/dist/chunk-CBLPAMZO.js.map +0 -1
- package/dist/chunk-CKPQHGXR.js +0 -417
- package/dist/chunk-CKPQHGXR.js.map +0 -1
- package/dist/chunk-CXOFOJCN.js +0 -80
- package/dist/chunk-CXOFOJCN.js.map +0 -1
- package/dist/chunk-EHGD7HIE.js +0 -104
- package/dist/chunk-EHGD7HIE.js.map +0 -1
- package/dist/chunk-EHS3TWWP.js +0 -95
- package/dist/chunk-EHS3TWWP.js.map +0 -1
- package/dist/chunk-FJFEKPXF.js.map +0 -1
- package/dist/chunk-HEOHU5EZ.js.map +0 -1
- package/dist/chunk-HGCBZUH5.js.map +0 -1
- package/dist/chunk-HPGFY5ZN.js +0 -114
- package/dist/chunk-HPGFY5ZN.js.map +0 -1
- package/dist/chunk-INJ2EFRW.js +0 -127
- package/dist/chunk-INJ2EFRW.js.map +0 -1
- package/dist/chunk-IOBFMF6X.js +0 -61
- package/dist/chunk-IOBFMF6X.js.map +0 -1
- package/dist/chunk-KH3HA7J7.js +0 -116
- package/dist/chunk-KH3HA7J7.js.map +0 -1
- package/dist/chunk-N6JNE326.js +0 -261
- package/dist/chunk-N6JNE326.js.map +0 -1
- package/dist/chunk-NAGU7TVZ.js +0 -36
- package/dist/chunk-NAGU7TVZ.js.map +0 -1
- package/dist/chunk-OQYYMQJR.js.map +0 -1
- package/dist/chunk-PLZOUVDD.js +0 -419
- package/dist/chunk-PLZOUVDD.js.map +0 -1
- package/dist/chunk-Q3J43SF3.js.map +0 -1
- package/dist/chunk-RTE64SJA.js.map +0 -1
- package/dist/chunk-T25OEQFI.js +0 -26
- package/dist/chunk-T25OEQFI.js.map +0 -1
- package/dist/chunk-UMORK7OK.js.map +0 -1
- package/dist/chunk-VFHWU7JU.js +0 -287
- package/dist/chunk-VFHWU7JU.js.map +0 -1
- package/dist/chunk-VS4GVTZE.js +0 -91
- package/dist/chunk-VS4GVTZE.js.map +0 -1
- package/dist/chunk-WFEFICFM.js.map +0 -1
- package/dist/chunk-WG6KIAPK.js +0 -54
- package/dist/chunk-WG6KIAPK.js.map +0 -1
- package/dist/chunk-ZEI3ZUDU.js.map +0 -1
- package/dist/chunk-ZNIDWLL5.js.map +0 -1
- package/dist/chunk-ZSVMS677.js.map +0 -1
- /package/dist/{chunk-XY3XDVMI.js.map → chunk-QR2EBWL2.js.map} +0 -0
- /package/dist/{chunk-66UDJBF6.js.map → chunk-REJGRCVQ.js.map} +0 -0
- /package/dist/{chunk-Z2CWURZ6.js.map → chunk-UNN7523L.js.map} +0 -0
- /package/dist/{magic-string.es-RGXYGAW3.js.map → magic-string.es-PAH2SOTR.js.map} +0 -0
|
@@ -0,0 +1,4204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
agentFrontmatterValidationSchema,
|
|
4
|
+
agentYamlConfigSchema,
|
|
5
|
+
categoryPathSchema,
|
|
6
|
+
copy,
|
|
7
|
+
directoryExists,
|
|
8
|
+
ensureDir,
|
|
9
|
+
fileExists,
|
|
10
|
+
glob,
|
|
11
|
+
hooksRecordSchema,
|
|
12
|
+
listDirectories,
|
|
13
|
+
localRawMetadataSchema,
|
|
14
|
+
localSkillMetadataSchema,
|
|
15
|
+
marketplaceSchema,
|
|
16
|
+
pluginAuthorSchema,
|
|
17
|
+
pluginManifestSchema,
|
|
18
|
+
projectConfigLoaderSchema,
|
|
19
|
+
projectSourceConfigSchema,
|
|
20
|
+
readFile,
|
|
21
|
+
readFileOptional,
|
|
22
|
+
remove,
|
|
23
|
+
skillDisplayNameSchema,
|
|
24
|
+
skillFrontmatterLoaderSchema,
|
|
25
|
+
skillFrontmatterValidationSchema,
|
|
26
|
+
skillIdSchema,
|
|
27
|
+
skillMetadataConfigSchema,
|
|
28
|
+
skillsMatrixConfigSchema,
|
|
29
|
+
stacksConfigSchema,
|
|
30
|
+
verbose,
|
|
31
|
+
warn,
|
|
32
|
+
writeFile
|
|
33
|
+
} from "./chunk-ZDREFYD2.js";
|
|
34
|
+
import {
|
|
35
|
+
typedEntries,
|
|
36
|
+
typedKeys
|
|
37
|
+
} from "./chunk-HWD32NP7.js";
|
|
38
|
+
import {
|
|
39
|
+
ARCHIVED_SKILLS_DIR_NAME,
|
|
40
|
+
CACHE_DIR,
|
|
41
|
+
CLAUDE_DIR,
|
|
42
|
+
CLAUDE_SRC_DIR,
|
|
43
|
+
DEFAULT_DISPLAY_VERSION,
|
|
44
|
+
DEFAULT_VERSION,
|
|
45
|
+
DIRS,
|
|
46
|
+
KEY_SUBCATEGORIES,
|
|
47
|
+
LOCAL_SKILLS_PATH,
|
|
48
|
+
PLUGINS_SUBDIR,
|
|
49
|
+
PLUGIN_MANIFEST_DIR,
|
|
50
|
+
PLUGIN_MANIFEST_FILE,
|
|
51
|
+
PROJECT_ROOT,
|
|
52
|
+
SKILLS_DIR_PATH,
|
|
53
|
+
SKILLS_MATRIX_PATH
|
|
54
|
+
} from "./chunk-O6ZTD7ZI.js";
|
|
55
|
+
import {
|
|
56
|
+
init_esm_shims
|
|
57
|
+
} from "./chunk-AWKZ5BDL.js";
|
|
58
|
+
|
|
59
|
+
// src/cli/lib/configuration/source-manager.ts
|
|
60
|
+
init_esm_shims();
|
|
61
|
+
|
|
62
|
+
// src/cli/lib/configuration/config.ts
|
|
63
|
+
init_esm_shims();
|
|
64
|
+
import path from "path";
|
|
65
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
66
|
+
var DEFAULT_SOURCE = "github:claude-collective/skills";
|
|
67
|
+
var SOURCE_ENV_VAR = "CC_SOURCE";
|
|
68
|
+
var PROJECT_CONFIG_FILE = "config.yaml";
|
|
69
|
+
function getProjectConfigPath(projectDir) {
|
|
70
|
+
return path.join(projectDir, CLAUDE_SRC_DIR, PROJECT_CONFIG_FILE);
|
|
71
|
+
}
|
|
72
|
+
async function loadProjectSourceConfig(projectDir) {
|
|
73
|
+
const srcConfigPath = getProjectConfigPath(projectDir);
|
|
74
|
+
const legacyConfigPath = path.join(projectDir, CLAUDE_DIR, "config.yaml");
|
|
75
|
+
let configPath = srcConfigPath;
|
|
76
|
+
if (!await fileExists(srcConfigPath)) {
|
|
77
|
+
if (await fileExists(legacyConfigPath)) {
|
|
78
|
+
configPath = legacyConfigPath;
|
|
79
|
+
verbose(`Using legacy config location: ${legacyConfigPath}`);
|
|
80
|
+
} else {
|
|
81
|
+
verbose(`Project config not found at ${srcConfigPath} or ${legacyConfigPath}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const content = await readFile(configPath);
|
|
87
|
+
const parsed = parseYaml(content);
|
|
88
|
+
const result = projectSourceConfigSchema.safeParse(parsed);
|
|
89
|
+
if (!result.success) {
|
|
90
|
+
warn(`Invalid project config at ${configPath}: ${result.error.message}`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
verbose(`Loaded project config from ${configPath}`);
|
|
94
|
+
return result.data;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
warn(`Failed to parse project config at ${configPath}: ${error}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function saveProjectConfig(projectDir, config) {
|
|
101
|
+
const configPath = getProjectConfigPath(projectDir);
|
|
102
|
+
await ensureDir(path.join(projectDir, CLAUDE_SRC_DIR));
|
|
103
|
+
const content = stringifyYaml(config, { lineWidth: 0 });
|
|
104
|
+
await writeFile(configPath, content);
|
|
105
|
+
verbose(`Saved project config to ${configPath}`);
|
|
106
|
+
}
|
|
107
|
+
async function resolveSource(flagValue, projectDir) {
|
|
108
|
+
const projectConfig = projectDir ? await loadProjectSourceConfig(projectDir) : null;
|
|
109
|
+
const marketplace = projectConfig?.marketplace;
|
|
110
|
+
if (flagValue !== void 0) {
|
|
111
|
+
if (flagValue === "" || flagValue.trim() === "") {
|
|
112
|
+
throw new Error("--source flag cannot be empty");
|
|
113
|
+
}
|
|
114
|
+
verbose(`Source from --source flag: ${flagValue}`);
|
|
115
|
+
return { source: flagValue, sourceOrigin: "flag", marketplace };
|
|
116
|
+
}
|
|
117
|
+
const envValue = process.env[SOURCE_ENV_VAR];
|
|
118
|
+
if (envValue) {
|
|
119
|
+
verbose(`Source from ${SOURCE_ENV_VAR} env var: ${envValue}`);
|
|
120
|
+
return { source: envValue, sourceOrigin: "env", marketplace };
|
|
121
|
+
}
|
|
122
|
+
if (projectConfig?.source) {
|
|
123
|
+
verbose(`Source from project config: ${projectConfig.source}`);
|
|
124
|
+
return {
|
|
125
|
+
source: projectConfig.source,
|
|
126
|
+
sourceOrigin: "project",
|
|
127
|
+
marketplace
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
verbose(`Using default source: ${DEFAULT_SOURCE}`);
|
|
131
|
+
return { source: DEFAULT_SOURCE, sourceOrigin: "default", marketplace };
|
|
132
|
+
}
|
|
133
|
+
async function resolveAgentsSource(flagValue, projectDir) {
|
|
134
|
+
if (flagValue !== void 0) {
|
|
135
|
+
if (flagValue === "" || flagValue.trim() === "") {
|
|
136
|
+
throw new Error("--agent-source flag cannot be empty");
|
|
137
|
+
}
|
|
138
|
+
verbose(`Agents source from --agent-source flag: ${flagValue}`);
|
|
139
|
+
return { agentsSource: flagValue, agentsSourceOrigin: "flag" };
|
|
140
|
+
}
|
|
141
|
+
const projectConfig = projectDir ? await loadProjectSourceConfig(projectDir) : null;
|
|
142
|
+
if (projectConfig?.agents_source) {
|
|
143
|
+
verbose(`Agents source from project config: ${projectConfig.agents_source}`);
|
|
144
|
+
return {
|
|
145
|
+
agentsSource: projectConfig.agents_source,
|
|
146
|
+
agentsSourceOrigin: "project"
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
verbose("Using default agents source (local CLI)");
|
|
150
|
+
return { agentsSource: void 0, agentsSourceOrigin: "default" };
|
|
151
|
+
}
|
|
152
|
+
var PROJECT_ORIGIN_LABEL = "project config (.claude-src/config.yaml)";
|
|
153
|
+
function formatOrigin(type, origin) {
|
|
154
|
+
if (origin === "project") return PROJECT_ORIGIN_LABEL;
|
|
155
|
+
if (type === "source") {
|
|
156
|
+
switch (origin) {
|
|
157
|
+
case "flag":
|
|
158
|
+
return "--source flag";
|
|
159
|
+
case "env":
|
|
160
|
+
return `${SOURCE_ENV_VAR} environment variable`;
|
|
161
|
+
case "default":
|
|
162
|
+
return "default";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
switch (origin) {
|
|
166
|
+
case "flag":
|
|
167
|
+
return "--agent-source flag";
|
|
168
|
+
case "default":
|
|
169
|
+
return "default (local CLI)";
|
|
170
|
+
}
|
|
171
|
+
return origin;
|
|
172
|
+
}
|
|
173
|
+
async function resolveAuthor(projectDir) {
|
|
174
|
+
const projectConfig = projectDir ? await loadProjectSourceConfig(projectDir) : null;
|
|
175
|
+
return projectConfig?.author;
|
|
176
|
+
}
|
|
177
|
+
async function resolveAllSources(projectDir) {
|
|
178
|
+
const projectConfig = projectDir ? await loadProjectSourceConfig(projectDir) : null;
|
|
179
|
+
const resolvedConfig = await resolveSource(void 0, projectDir);
|
|
180
|
+
const primary = {
|
|
181
|
+
name: "marketplace",
|
|
182
|
+
url: resolvedConfig.source,
|
|
183
|
+
description: "Primary skills marketplace"
|
|
184
|
+
};
|
|
185
|
+
const extras = [];
|
|
186
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
187
|
+
if (projectConfig?.sources) {
|
|
188
|
+
for (const source of projectConfig.sources) {
|
|
189
|
+
if (!seenNames.has(source.name)) {
|
|
190
|
+
seenNames.add(source.name);
|
|
191
|
+
extras.push(source);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { primary, extras };
|
|
196
|
+
}
|
|
197
|
+
function isLocalSource(source) {
|
|
198
|
+
if (source.startsWith("/") || source.startsWith(".")) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
const remoteProtocols = [
|
|
202
|
+
"github:",
|
|
203
|
+
"gh:",
|
|
204
|
+
"gitlab:",
|
|
205
|
+
"bitbucket:",
|
|
206
|
+
"sourcehut:",
|
|
207
|
+
"https://",
|
|
208
|
+
"http://"
|
|
209
|
+
];
|
|
210
|
+
const hasRemoteProtocol = remoteProtocols.some((prefix) => source.startsWith(prefix));
|
|
211
|
+
if (!hasRemoteProtocol) {
|
|
212
|
+
if (source.includes("..") || source.includes("~")) {
|
|
213
|
+
throw new Error(`Invalid source path: ${source}. Path traversal patterns are not allowed.`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return !hasRemoteProtocol;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/cli/lib/loading/source-fetcher.ts
|
|
220
|
+
init_esm_shims();
|
|
221
|
+
import path25 from "path";
|
|
222
|
+
import { downloadTemplate } from "giget";
|
|
223
|
+
|
|
224
|
+
// src/cli/lib/configuration/index.ts
|
|
225
|
+
init_esm_shims();
|
|
226
|
+
|
|
227
|
+
// src/cli/lib/configuration/config-generator.ts
|
|
228
|
+
init_esm_shims();
|
|
229
|
+
|
|
230
|
+
// src/cli/lib/skills/index.ts
|
|
231
|
+
init_esm_shims();
|
|
232
|
+
|
|
233
|
+
// src/cli/lib/skills/skill-metadata.ts
|
|
234
|
+
init_esm_shims();
|
|
235
|
+
import path3 from "path";
|
|
236
|
+
import { parse as parseYaml3, stringify as stringifyYaml3 } from "yaml";
|
|
237
|
+
import { sortBy } from "remeda";
|
|
238
|
+
|
|
239
|
+
// src/cli/lib/versioning.ts
|
|
240
|
+
init_esm_shims();
|
|
241
|
+
import { createHash } from "crypto";
|
|
242
|
+
import path2 from "path";
|
|
243
|
+
import { stringify as stringifyYaml2, parse as parseYaml2 } from "yaml";
|
|
244
|
+
var HASH_PREFIX_LENGTH = 7;
|
|
245
|
+
var HASHABLE_FILES = ["SKILL.md", "reference.md"];
|
|
246
|
+
var HASHABLE_DIRS = ["examples", "scripts"];
|
|
247
|
+
function getCurrentDate() {
|
|
248
|
+
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
249
|
+
}
|
|
250
|
+
function hashString(content) {
|
|
251
|
+
const hash = createHash("sha256");
|
|
252
|
+
hash.update(content);
|
|
253
|
+
return hash.digest("hex").slice(0, HASH_PREFIX_LENGTH);
|
|
254
|
+
}
|
|
255
|
+
async function hashFile(filePath) {
|
|
256
|
+
const content = await readFile(filePath);
|
|
257
|
+
return hashString(content);
|
|
258
|
+
}
|
|
259
|
+
async function hashSkillFolder(skillPath) {
|
|
260
|
+
const contents = [];
|
|
261
|
+
for (const fileName of HASHABLE_FILES) {
|
|
262
|
+
const filePath = path2.join(skillPath, fileName);
|
|
263
|
+
if (await fileExists(filePath)) {
|
|
264
|
+
const content = await readFile(filePath);
|
|
265
|
+
contents.push(`${fileName}:${content}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const dirName of HASHABLE_DIRS) {
|
|
269
|
+
const dirPath = path2.join(skillPath, dirName);
|
|
270
|
+
if (await fileExists(dirPath)) {
|
|
271
|
+
const files = await glob("**/*", dirPath);
|
|
272
|
+
for (const file of files.sort()) {
|
|
273
|
+
const filePath = path2.join(dirPath, file);
|
|
274
|
+
const content = await readFile(filePath);
|
|
275
|
+
contents.push(`${dirName}/${file}:${content}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const combined = contents.join("\n---\n");
|
|
280
|
+
return hashString(combined);
|
|
281
|
+
}
|
|
282
|
+
var CONTENT_HASH_FILE = ".content-hash";
|
|
283
|
+
function parseMajorVersion(version) {
|
|
284
|
+
const match = version.match(/^(\d+)\./);
|
|
285
|
+
return match ? parseInt(match[1], 10) : 1;
|
|
286
|
+
}
|
|
287
|
+
function bumpMajorVersion(version) {
|
|
288
|
+
const major = parseMajorVersion(version);
|
|
289
|
+
return `${major + 1}.0.0`;
|
|
290
|
+
}
|
|
291
|
+
async function readExistingPluginManifest(pluginDir, getManifestPath) {
|
|
292
|
+
const manifestPath = getManifestPath(pluginDir);
|
|
293
|
+
if (!await fileExists(manifestPath)) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const content = await readFile(manifestPath);
|
|
298
|
+
const manifest = pluginManifestSchema.parse(JSON.parse(content));
|
|
299
|
+
const hashFilePath = manifestPath.replace("plugin.json", CONTENT_HASH_FILE);
|
|
300
|
+
let contentHash;
|
|
301
|
+
if (await fileExists(hashFilePath)) {
|
|
302
|
+
contentHash = (await readFile(hashFilePath)).trim();
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
version: manifest.version ?? DEFAULT_VERSION,
|
|
306
|
+
contentHash
|
|
307
|
+
};
|
|
308
|
+
} catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function determinePluginVersion(newHash, pluginDir, getManifestPath) {
|
|
313
|
+
const existing = await readExistingPluginManifest(pluginDir, getManifestPath);
|
|
314
|
+
if (!existing) {
|
|
315
|
+
return {
|
|
316
|
+
version: DEFAULT_VERSION,
|
|
317
|
+
contentHash: newHash
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
if (existing.contentHash !== newHash) {
|
|
321
|
+
return {
|
|
322
|
+
version: bumpMajorVersion(existing.version),
|
|
323
|
+
contentHash: newHash
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
version: existing.version,
|
|
328
|
+
contentHash: newHash
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
async function writeContentHash(pluginDir, contentHash, getManifestPath) {
|
|
332
|
+
const hashFilePath = getManifestPath(pluginDir).replace("plugin.json", CONTENT_HASH_FILE);
|
|
333
|
+
await writeFile(hashFilePath, contentHash);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/cli/lib/skills/skill-metadata.ts
|
|
337
|
+
async function readForkedFromMetadata(skillDir) {
|
|
338
|
+
const metadataPath = path3.join(skillDir, "metadata.yaml");
|
|
339
|
+
if (!await fileExists(metadataPath)) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
const content = await readFile(metadataPath);
|
|
343
|
+
const result = localSkillMetadataSchema.safeParse(parseYaml3(content));
|
|
344
|
+
if (!result.success) {
|
|
345
|
+
warn(
|
|
346
|
+
`Invalid metadata.yaml at ${metadataPath}: ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
347
|
+
);
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
return result.data.forked_from ?? null;
|
|
351
|
+
}
|
|
352
|
+
async function getLocalSkillsWithMetadata(projectDir) {
|
|
353
|
+
const localSkillsPath = path3.join(projectDir, LOCAL_SKILLS_PATH);
|
|
354
|
+
const result = /* @__PURE__ */ new Map();
|
|
355
|
+
if (!await fileExists(localSkillsPath)) {
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
const skillDirs = await listDirectories(localSkillsPath);
|
|
359
|
+
for (const dirName of skillDirs) {
|
|
360
|
+
const skillDir = path3.join(localSkillsPath, dirName);
|
|
361
|
+
const forkedFrom = await readForkedFromMetadata(skillDir);
|
|
362
|
+
const skillId = forkedFrom?.skill_id ?? dirName;
|
|
363
|
+
result.set(skillId, { dirName, forkedFrom });
|
|
364
|
+
}
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
async function computeSourceHash(sourcePath, skillPath) {
|
|
368
|
+
const skillMdPath = path3.join(sourcePath, "src", skillPath, "SKILL.md");
|
|
369
|
+
if (!await fileExists(skillMdPath)) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
return hashFile(skillMdPath);
|
|
373
|
+
}
|
|
374
|
+
async function compareSkills(projectDir, sourcePath, sourceSkills) {
|
|
375
|
+
const results = [];
|
|
376
|
+
const localSkills = await getLocalSkillsWithMetadata(projectDir);
|
|
377
|
+
for (const [skillId, { dirName, forkedFrom }] of localSkills) {
|
|
378
|
+
if (!forkedFrom) {
|
|
379
|
+
results.push({
|
|
380
|
+
id: skillId,
|
|
381
|
+
localHash: null,
|
|
382
|
+
sourceHash: null,
|
|
383
|
+
status: "local-only",
|
|
384
|
+
dirName
|
|
385
|
+
});
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const localHash = forkedFrom.content_hash;
|
|
389
|
+
const sourceSkill = sourceSkills[forkedFrom.skill_id];
|
|
390
|
+
if (!sourceSkill) {
|
|
391
|
+
results.push({
|
|
392
|
+
id: forkedFrom.skill_id,
|
|
393
|
+
localHash,
|
|
394
|
+
sourceHash: null,
|
|
395
|
+
status: "local-only",
|
|
396
|
+
dirName
|
|
397
|
+
});
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const sourceHash = await computeSourceHash(sourcePath, sourceSkill.path);
|
|
401
|
+
if (sourceHash === null) {
|
|
402
|
+
results.push({
|
|
403
|
+
id: forkedFrom.skill_id,
|
|
404
|
+
localHash,
|
|
405
|
+
sourceHash: null,
|
|
406
|
+
status: "local-only",
|
|
407
|
+
dirName
|
|
408
|
+
});
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const status = localHash === sourceHash ? "current" : "outdated";
|
|
412
|
+
results.push({
|
|
413
|
+
id: forkedFrom.skill_id,
|
|
414
|
+
localHash,
|
|
415
|
+
sourceHash,
|
|
416
|
+
status,
|
|
417
|
+
dirName,
|
|
418
|
+
sourcePath: sourceSkill.path
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
return sortBy(results, (r) => r.id);
|
|
422
|
+
}
|
|
423
|
+
async function injectForkedFromMetadata(destPath, skillId, contentHash) {
|
|
424
|
+
const metadataPath = path3.join(destPath, "metadata.yaml");
|
|
425
|
+
const rawContent = await readFile(metadataPath);
|
|
426
|
+
const lines = rawContent.split("\n");
|
|
427
|
+
let yamlContent = rawContent;
|
|
428
|
+
if (lines[0]?.startsWith("# yaml-language-server:")) {
|
|
429
|
+
yamlContent = lines.slice(1).join("\n");
|
|
430
|
+
}
|
|
431
|
+
const parseResult = localSkillMetadataSchema.safeParse(parseYaml3(yamlContent));
|
|
432
|
+
if (!parseResult.success) {
|
|
433
|
+
warn(`Malformed metadata.yaml at ${metadataPath} \u2014 existing fields may be lost`);
|
|
434
|
+
}
|
|
435
|
+
const metadata = parseResult.success ? parseResult.data : { forked_from: void 0 };
|
|
436
|
+
metadata.forked_from = {
|
|
437
|
+
skill_id: skillId,
|
|
438
|
+
content_hash: contentHash,
|
|
439
|
+
date: getCurrentDate()
|
|
440
|
+
};
|
|
441
|
+
const newYamlContent = stringifyYaml3(metadata, { lineWidth: 0 });
|
|
442
|
+
await writeFile(metadataPath, newYamlContent);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/cli/lib/skills/skill-copier.ts
|
|
446
|
+
init_esm_shims();
|
|
447
|
+
import path4 from "path";
|
|
448
|
+
function getSkillDestPath(skill, stackDir) {
|
|
449
|
+
const skillRelativePath = skill.path.replace(/^skills\//, "");
|
|
450
|
+
return path4.join(stackDir, "skills", skillRelativePath);
|
|
451
|
+
}
|
|
452
|
+
async function generateSkillHash(skillSourcePath) {
|
|
453
|
+
const skillMdPath = path4.join(skillSourcePath, "SKILL.md");
|
|
454
|
+
return hashFile(skillMdPath);
|
|
455
|
+
}
|
|
456
|
+
function getSkillSourcePathFromSource(skill, sourceResult) {
|
|
457
|
+
return path4.join(sourceResult.sourcePath, "src", skill.path);
|
|
458
|
+
}
|
|
459
|
+
async function copySkillFromSource(skill, stackDir, sourceResult) {
|
|
460
|
+
const sourcePath = getSkillSourcePathFromSource(skill, sourceResult);
|
|
461
|
+
const destPath = getSkillDestPath(skill, stackDir);
|
|
462
|
+
const contentHash = await generateSkillHash(sourcePath);
|
|
463
|
+
await ensureDir(path4.dirname(destPath));
|
|
464
|
+
await copy(sourcePath, destPath);
|
|
465
|
+
await injectForkedFromMetadata(destPath, skill.id, contentHash);
|
|
466
|
+
return {
|
|
467
|
+
skillId: skill.id,
|
|
468
|
+
contentHash,
|
|
469
|
+
sourcePath,
|
|
470
|
+
destPath
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
async function copySkillsToPluginFromSource(selectedSkillIds, pluginDir, matrix, sourceResult, sourceSelections) {
|
|
474
|
+
const copiedSkills = [];
|
|
475
|
+
for (const skillId of selectedSkillIds) {
|
|
476
|
+
const skill = matrix.skills[skillId];
|
|
477
|
+
if (!skill) {
|
|
478
|
+
console.warn(`Warning: Skill not found in matrix: ${skillId}`);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const selectedSource = sourceSelections?.[skillId];
|
|
482
|
+
const userSelectedRemote = selectedSource && selectedSource !== "local";
|
|
483
|
+
if (skill.local && skill.localPath && !userSelectedRemote) {
|
|
484
|
+
const localSkillPath = path4.join(process.cwd(), skill.localPath);
|
|
485
|
+
const contentHash = await generateSkillHash(localSkillPath);
|
|
486
|
+
copiedSkills.push({
|
|
487
|
+
skillId: skill.id,
|
|
488
|
+
sourcePath: skill.localPath,
|
|
489
|
+
destPath: skill.localPath,
|
|
490
|
+
contentHash,
|
|
491
|
+
local: true
|
|
492
|
+
});
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const copied = await copySkillFromSource(skill, pluginDir, sourceResult);
|
|
496
|
+
copiedSkills.push(copied);
|
|
497
|
+
}
|
|
498
|
+
return copiedSkills;
|
|
499
|
+
}
|
|
500
|
+
function getFlattenedSkillDestPath(skill, localSkillsDir) {
|
|
501
|
+
return path4.join(localSkillsDir, skill.id);
|
|
502
|
+
}
|
|
503
|
+
async function copySkillToLocalFlattened(skill, localSkillsDir, sourceResult) {
|
|
504
|
+
const sourcePath = getSkillSourcePathFromSource(skill, sourceResult);
|
|
505
|
+
const destPath = getFlattenedSkillDestPath(skill, localSkillsDir);
|
|
506
|
+
const contentHash = await generateSkillHash(sourcePath);
|
|
507
|
+
await ensureDir(path4.dirname(destPath));
|
|
508
|
+
await copy(sourcePath, destPath);
|
|
509
|
+
await injectForkedFromMetadata(destPath, skill.id, contentHash);
|
|
510
|
+
return {
|
|
511
|
+
skillId: skill.id,
|
|
512
|
+
contentHash,
|
|
513
|
+
sourcePath,
|
|
514
|
+
destPath
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
async function copySkillsToLocalFlattened(selectedSkillIds, localSkillsDir, matrix, sourceResult, sourceSelections) {
|
|
518
|
+
const copiedSkills = [];
|
|
519
|
+
for (const skillId of selectedSkillIds) {
|
|
520
|
+
const skill = matrix.skills[skillId];
|
|
521
|
+
if (!skill) {
|
|
522
|
+
console.warn(`Warning: Skill not found in matrix: ${skillId}`);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const selectedSource = sourceSelections?.[skillId];
|
|
526
|
+
const userSelectedRemote = selectedSource && selectedSource !== "local";
|
|
527
|
+
if (skill.local && skill.localPath && !userSelectedRemote) {
|
|
528
|
+
const localSkillPath = path4.join(process.cwd(), skill.localPath);
|
|
529
|
+
const contentHash = await generateSkillHash(localSkillPath);
|
|
530
|
+
copiedSkills.push({
|
|
531
|
+
skillId: skill.id,
|
|
532
|
+
sourcePath: skill.localPath,
|
|
533
|
+
destPath: skill.localPath,
|
|
534
|
+
contentHash,
|
|
535
|
+
local: true
|
|
536
|
+
});
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const copied = await copySkillToLocalFlattened(skill, localSkillsDir, sourceResult);
|
|
540
|
+
copiedSkills.push(copied);
|
|
541
|
+
}
|
|
542
|
+
return copiedSkills;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/cli/lib/skills/skill-agent-mappings.ts
|
|
546
|
+
init_esm_shims();
|
|
547
|
+
|
|
548
|
+
// src/cli/lib/loading/index.ts
|
|
549
|
+
init_esm_shims();
|
|
550
|
+
|
|
551
|
+
// src/cli/lib/loading/loader.ts
|
|
552
|
+
init_esm_shims();
|
|
553
|
+
import { parse as parseYaml4 } from "yaml";
|
|
554
|
+
import path5 from "path";
|
|
555
|
+
import { unique } from "remeda";
|
|
556
|
+
var FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/;
|
|
557
|
+
function parseFrontmatter(content, filePath) {
|
|
558
|
+
const match = content.match(FRONTMATTER_REGEX);
|
|
559
|
+
if (!match) return null;
|
|
560
|
+
const yamlContent = match[1];
|
|
561
|
+
const parsed = skillFrontmatterLoaderSchema.safeParse(parseYaml4(yamlContent));
|
|
562
|
+
if (!parsed.success) {
|
|
563
|
+
const location = filePath ?? "unknown file";
|
|
564
|
+
warn(
|
|
565
|
+
`Invalid SKILL.md frontmatter in ${location}: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
566
|
+
);
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
return parsed.data;
|
|
570
|
+
}
|
|
571
|
+
async function loadAllAgents(projectRoot) {
|
|
572
|
+
const agents = {};
|
|
573
|
+
const agentSourcesDir = path5.join(projectRoot, DIRS.agents);
|
|
574
|
+
const files = await glob("**/agent.yaml", agentSourcesDir);
|
|
575
|
+
for (const file of files) {
|
|
576
|
+
const fullPath = path5.join(agentSourcesDir, file);
|
|
577
|
+
try {
|
|
578
|
+
const content = await readFile(fullPath);
|
|
579
|
+
const config = agentYamlConfigSchema.parse(parseYaml4(content));
|
|
580
|
+
const agentPath = path5.dirname(file);
|
|
581
|
+
agents[config.id] = {
|
|
582
|
+
title: config.title,
|
|
583
|
+
description: config.description,
|
|
584
|
+
model: config.model,
|
|
585
|
+
tools: config.tools,
|
|
586
|
+
path: agentPath,
|
|
587
|
+
sourceRoot: projectRoot
|
|
588
|
+
};
|
|
589
|
+
verbose(`Loaded agent: ${config.id} from ${file}`);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
warn(
|
|
592
|
+
`Skipping invalid agent.yaml at ${fullPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return agents;
|
|
597
|
+
}
|
|
598
|
+
async function loadProjectAgents(projectRoot) {
|
|
599
|
+
const agents = {};
|
|
600
|
+
const projectAgentsDir = path5.join(projectRoot, CLAUDE_SRC_DIR, "agents");
|
|
601
|
+
if (!await directoryExists(projectAgentsDir)) {
|
|
602
|
+
verbose(`No project agents directory at ${projectAgentsDir}`);
|
|
603
|
+
return agents;
|
|
604
|
+
}
|
|
605
|
+
const files = await glob("**/agent.yaml", projectAgentsDir);
|
|
606
|
+
for (const file of files) {
|
|
607
|
+
const fullPath = path5.join(projectAgentsDir, file);
|
|
608
|
+
try {
|
|
609
|
+
const content = await readFile(fullPath);
|
|
610
|
+
const config = agentYamlConfigSchema.parse(parseYaml4(content));
|
|
611
|
+
const agentPath = path5.dirname(file);
|
|
612
|
+
agents[config.id] = {
|
|
613
|
+
title: config.title,
|
|
614
|
+
description: config.description,
|
|
615
|
+
model: config.model,
|
|
616
|
+
tools: config.tools,
|
|
617
|
+
path: agentPath,
|
|
618
|
+
sourceRoot: projectRoot,
|
|
619
|
+
agentBaseDir: `${CLAUDE_SRC_DIR}/agents`
|
|
620
|
+
// Project agents are in .claude-src/agents/
|
|
621
|
+
};
|
|
622
|
+
verbose(`Loaded project agent: ${config.id} from ${file}`);
|
|
623
|
+
} catch (error) {
|
|
624
|
+
warn(
|
|
625
|
+
`Skipping invalid agent.yaml at ${fullPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return agents;
|
|
630
|
+
}
|
|
631
|
+
async function buildIdToDirectoryPathMap(skillsDir) {
|
|
632
|
+
const map = {};
|
|
633
|
+
const files = await glob("**/SKILL.md", skillsDir);
|
|
634
|
+
for (const file of files) {
|
|
635
|
+
const fullPath = path5.join(skillsDir, file);
|
|
636
|
+
const content = await readFile(fullPath);
|
|
637
|
+
const frontmatter = parseFrontmatter(content, fullPath);
|
|
638
|
+
if (frontmatter?.name) {
|
|
639
|
+
const directoryPath = file.replace("/SKILL.md", "");
|
|
640
|
+
map[frontmatter.name] = directoryPath;
|
|
641
|
+
map[directoryPath] = directoryPath;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return map;
|
|
645
|
+
}
|
|
646
|
+
async function loadSkillsByIds(skillIds, projectRoot) {
|
|
647
|
+
const skills = {};
|
|
648
|
+
const skillsDir = path5.join(projectRoot, DIRS.skills);
|
|
649
|
+
const idToDirectoryPath = await buildIdToDirectoryPathMap(skillsDir);
|
|
650
|
+
const allSkillIds = Object.keys(idToDirectoryPath);
|
|
651
|
+
const expandedSkillIds = [];
|
|
652
|
+
for (const { id: skillId } of skillIds) {
|
|
653
|
+
if (idToDirectoryPath[skillId]) {
|
|
654
|
+
expandedSkillIds.push(skillId);
|
|
655
|
+
} else {
|
|
656
|
+
const childSkills = allSkillIds.filter((id) => {
|
|
657
|
+
const dirPath = idToDirectoryPath[id];
|
|
658
|
+
return dirPath.startsWith(skillId + "/");
|
|
659
|
+
});
|
|
660
|
+
if (childSkills.length > 0) {
|
|
661
|
+
expandedSkillIds.push(...childSkills);
|
|
662
|
+
verbose(`Expanded directory '${skillId}' to ${childSkills.length} skills`);
|
|
663
|
+
} else {
|
|
664
|
+
console.warn(` Warning: Unknown skill reference '${skillId}'`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const uniqueSkillIds = unique(expandedSkillIds);
|
|
669
|
+
for (const skillId of uniqueSkillIds) {
|
|
670
|
+
const directoryPath = idToDirectoryPath[skillId];
|
|
671
|
+
if (!directoryPath) {
|
|
672
|
+
console.warn(` Warning: Could not find skill ${skillId}: No matching skill found`);
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const skillPath = path5.join(skillsDir, directoryPath);
|
|
676
|
+
const skillMdPath = path5.join(skillPath, "SKILL.md");
|
|
677
|
+
try {
|
|
678
|
+
const content = await readFile(skillMdPath);
|
|
679
|
+
const frontmatter = parseFrontmatter(content, skillMdPath);
|
|
680
|
+
if (!frontmatter) {
|
|
681
|
+
warn(`Skipping ${skillId}: Missing or invalid frontmatter`);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
const canonicalId = frontmatter.name;
|
|
685
|
+
const skillDef = {
|
|
686
|
+
id: canonicalId,
|
|
687
|
+
path: `${DIRS.skills}/${directoryPath}/`,
|
|
688
|
+
description: frontmatter.description
|
|
689
|
+
};
|
|
690
|
+
skills[canonicalId] = skillDef;
|
|
691
|
+
if (directoryPath !== canonicalId) {
|
|
692
|
+
skills[directoryPath] = skillDef;
|
|
693
|
+
}
|
|
694
|
+
verbose(`Loaded skill: ${canonicalId} (from ${directoryPath})`);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
console.warn(` Warning: Could not load skill ${skillId}: ${error}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return skills;
|
|
700
|
+
}
|
|
701
|
+
async function loadPluginSkills(pluginDir) {
|
|
702
|
+
const skills = {};
|
|
703
|
+
const pluginSkillsDir = path5.join(pluginDir, "skills");
|
|
704
|
+
if (!await directoryExists(pluginSkillsDir)) {
|
|
705
|
+
return skills;
|
|
706
|
+
}
|
|
707
|
+
const files = await glob("**/SKILL.md", pluginSkillsDir);
|
|
708
|
+
for (const file of files) {
|
|
709
|
+
const fullPath = path5.join(pluginSkillsDir, file);
|
|
710
|
+
const content = await readFile(fullPath);
|
|
711
|
+
const frontmatter = parseFrontmatter(content, fullPath);
|
|
712
|
+
if (!frontmatter) {
|
|
713
|
+
warn(`Skipping ${file}: Missing or invalid frontmatter`);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const folderPath = file.replace("/SKILL.md", "");
|
|
717
|
+
const skillPath = `skills/${folderPath}/`;
|
|
718
|
+
const skillId = frontmatter.name;
|
|
719
|
+
skills[skillId] = {
|
|
720
|
+
id: skillId,
|
|
721
|
+
path: skillPath,
|
|
722
|
+
description: frontmatter.description
|
|
723
|
+
};
|
|
724
|
+
verbose(`Loaded plugin skill: ${skillId} from ${file}`);
|
|
725
|
+
}
|
|
726
|
+
return skills;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/cli/lib/loading/source-loader.ts
|
|
730
|
+
init_esm_shims();
|
|
731
|
+
import path19 from "path";
|
|
732
|
+
|
|
733
|
+
// src/cli/lib/matrix/index.ts
|
|
734
|
+
init_esm_shims();
|
|
735
|
+
|
|
736
|
+
// src/cli/lib/matrix/matrix-loader.ts
|
|
737
|
+
init_esm_shims();
|
|
738
|
+
import { parse as parseYaml5 } from "yaml";
|
|
739
|
+
import path6 from "path";
|
|
740
|
+
import { z } from "zod";
|
|
741
|
+
var rawMetadataSchema = z.object({
|
|
742
|
+
category: categoryPathSchema,
|
|
743
|
+
category_exclusive: z.boolean().optional(),
|
|
744
|
+
author: z.string(),
|
|
745
|
+
version: z.coerce.string(),
|
|
746
|
+
cli_name: z.string().optional(),
|
|
747
|
+
cli_description: z.string().optional(),
|
|
748
|
+
usage_guidance: z.string().optional(),
|
|
749
|
+
tags: z.array(z.string()).optional(),
|
|
750
|
+
// Lenient: accepts display names and skill IDs from YAML, resolved to canonical IDs during matrix merge
|
|
751
|
+
compatible_with: z.array(z.string()).optional(),
|
|
752
|
+
conflicts_with: z.array(z.string()).optional(),
|
|
753
|
+
requires: z.array(z.string()).optional(),
|
|
754
|
+
requires_setup: z.array(z.string()).optional(),
|
|
755
|
+
provides_setup_for: z.array(z.string()).optional()
|
|
756
|
+
});
|
|
757
|
+
async function loadSkillsMatrix(configPath) {
|
|
758
|
+
const content = await readFile(configPath);
|
|
759
|
+
const raw = parseYaml5(content);
|
|
760
|
+
const result = skillsMatrixConfigSchema.safeParse(raw);
|
|
761
|
+
if (!result.success) {
|
|
762
|
+
throw new Error(
|
|
763
|
+
`Invalid skills matrix at ${configPath}: ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
verbose(`Loaded skills matrix: ${configPath}`);
|
|
767
|
+
return result.data;
|
|
768
|
+
}
|
|
769
|
+
async function extractAllSkills(skillsDir) {
|
|
770
|
+
const skills = [];
|
|
771
|
+
const metadataFiles = await glob("**/metadata.yaml", skillsDir);
|
|
772
|
+
for (const metadataFile of metadataFiles) {
|
|
773
|
+
const skillDir = path6.dirname(metadataFile);
|
|
774
|
+
const skillMdPath = path6.join(skillsDir, skillDir, "SKILL.md");
|
|
775
|
+
const metadataPath = path6.join(skillsDir, metadataFile);
|
|
776
|
+
if (!await fileExists(skillMdPath)) {
|
|
777
|
+
verbose(`Skipping ${metadataFile}: No SKILL.md found`);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const metadataContent = await readFile(metadataPath);
|
|
781
|
+
const rawMetadata = parseYaml5(metadataContent);
|
|
782
|
+
const metadataResult = rawMetadataSchema.safeParse(rawMetadata);
|
|
783
|
+
if (!metadataResult.success) {
|
|
784
|
+
warn(
|
|
785
|
+
`Skipping ${metadataFile}: Invalid metadata.yaml \u2014 ${metadataResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
786
|
+
);
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const metadata = metadataResult.data;
|
|
790
|
+
const skillMdContent = await readFile(skillMdPath);
|
|
791
|
+
const frontmatter = parseFrontmatter(skillMdContent, skillMdPath);
|
|
792
|
+
if (!frontmatter) {
|
|
793
|
+
verbose(`Skipping ${metadataFile}: Invalid SKILL.md frontmatter`);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (!metadata.cli_name) {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`Skill at ${metadataFile} is missing required 'cli_name' field in metadata.yaml`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
const skillId = frontmatter.name;
|
|
802
|
+
const extracted = {
|
|
803
|
+
id: skillId,
|
|
804
|
+
directoryPath: skillDir,
|
|
805
|
+
description: metadata.cli_description || frontmatter.description,
|
|
806
|
+
usageGuidance: metadata.usage_guidance,
|
|
807
|
+
category: metadata.category,
|
|
808
|
+
categoryExclusive: metadata.category_exclusive ?? true,
|
|
809
|
+
author: metadata.author,
|
|
810
|
+
tags: metadata.tags ?? [],
|
|
811
|
+
compatibleWith: metadata.compatible_with ?? [],
|
|
812
|
+
conflictsWith: metadata.conflicts_with ?? [],
|
|
813
|
+
requires: metadata.requires ?? [],
|
|
814
|
+
requiresSetup: metadata.requires_setup ?? [],
|
|
815
|
+
providesSetupFor: metadata.provides_setup_for ?? [],
|
|
816
|
+
path: `skills/${skillDir}/`
|
|
817
|
+
};
|
|
818
|
+
skills.push(extracted);
|
|
819
|
+
verbose(`Extracted skill: ${skillId}`);
|
|
820
|
+
}
|
|
821
|
+
return skills;
|
|
822
|
+
}
|
|
823
|
+
function buildReverseDisplayNames(displayNameToId) {
|
|
824
|
+
const reverse = {};
|
|
825
|
+
for (const [name, fullId] of Object.entries(displayNameToId)) {
|
|
826
|
+
const nameResult = skillDisplayNameSchema.safeParse(name);
|
|
827
|
+
const idResult = skillIdSchema.safeParse(fullId);
|
|
828
|
+
if (nameResult.success && idResult.success) {
|
|
829
|
+
reverse[idResult.data] = nameResult.data;
|
|
830
|
+
} else {
|
|
831
|
+
warn(
|
|
832
|
+
`Invalid skill alias mapping: '${name}' -> '${fullId}'${!nameResult.success ? ` (invalid display name: ${nameResult.error.message})` : ""}${!idResult.success ? ` (invalid skill ID: ${idResult.error.message})` : ""}`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return reverse;
|
|
837
|
+
}
|
|
838
|
+
function buildAliasTargetToSkillIdMap(displayNameToId, skills) {
|
|
839
|
+
const map = {};
|
|
840
|
+
for (const skill of skills) {
|
|
841
|
+
const parts = skill.id.split("/");
|
|
842
|
+
const shortForm = parts[parts.length - 1];
|
|
843
|
+
if (shortForm && shortForm !== skill.id) {
|
|
844
|
+
map[shortForm] = skill.id;
|
|
845
|
+
}
|
|
846
|
+
if (skill.directoryPath && skill.directoryPath !== skill.id) {
|
|
847
|
+
map[skill.directoryPath] = skill.id;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const aliasTargets = new Set(Object.values(displayNameToId));
|
|
851
|
+
for (const skill of skills) {
|
|
852
|
+
for (const aliasTarget of aliasTargets) {
|
|
853
|
+
if (aliasTarget !== skill.id && (skill.id.endsWith(`/${aliasTarget}`) || skill.id === aliasTarget)) {
|
|
854
|
+
map[aliasTarget] = skill.id;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return map;
|
|
859
|
+
}
|
|
860
|
+
function buildDirectoryPathToIdMap(skills) {
|
|
861
|
+
const map = {};
|
|
862
|
+
for (const skill of skills) {
|
|
863
|
+
if (skill.directoryPath && skill.directoryPath !== skill.id) {
|
|
864
|
+
map[skill.directoryPath] = skill.id;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return map;
|
|
868
|
+
}
|
|
869
|
+
function resolveToCanonicalId(nameOrId, displayNameToId, directoryPathToId = {}, aliasTargetToSkillId = {}, context) {
|
|
870
|
+
const displayNameResult = displayNameToId[nameOrId];
|
|
871
|
+
if (displayNameResult) {
|
|
872
|
+
return displayNameResult;
|
|
873
|
+
}
|
|
874
|
+
if (directoryPathToId[nameOrId]) {
|
|
875
|
+
return directoryPathToId[nameOrId];
|
|
876
|
+
}
|
|
877
|
+
if (aliasTargetToSkillId[nameOrId]) {
|
|
878
|
+
return aliasTargetToSkillId[nameOrId];
|
|
879
|
+
}
|
|
880
|
+
if (context) {
|
|
881
|
+
verbose(`Unresolved ID '${nameOrId}' in ${context} \u2014 passing through as-is`);
|
|
882
|
+
}
|
|
883
|
+
return nameOrId;
|
|
884
|
+
}
|
|
885
|
+
async function mergeMatrixWithSkills(matrix, skills) {
|
|
886
|
+
const displayNameToId = matrix.skill_aliases;
|
|
887
|
+
const displayNames = buildReverseDisplayNames(displayNameToId);
|
|
888
|
+
const directoryPathToId = buildDirectoryPathToIdMap(skills);
|
|
889
|
+
const aliasTargetToSkillId = buildAliasTargetToSkillIdMap(displayNameToId, skills);
|
|
890
|
+
const resolvedSkills = {};
|
|
891
|
+
for (const skill of skills) {
|
|
892
|
+
const resolved = buildResolvedSkill(
|
|
893
|
+
skill,
|
|
894
|
+
matrix,
|
|
895
|
+
displayNameToId,
|
|
896
|
+
displayNames,
|
|
897
|
+
directoryPathToId,
|
|
898
|
+
aliasTargetToSkillId
|
|
899
|
+
);
|
|
900
|
+
resolvedSkills[skill.id] = resolved;
|
|
901
|
+
}
|
|
902
|
+
const suggestedStacks = resolveSuggestedStacks();
|
|
903
|
+
const merged = {
|
|
904
|
+
version: matrix.version,
|
|
905
|
+
categories: matrix.categories,
|
|
906
|
+
skills: resolvedSkills,
|
|
907
|
+
suggestedStacks,
|
|
908
|
+
displayNameToId,
|
|
909
|
+
displayNames,
|
|
910
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
911
|
+
};
|
|
912
|
+
return merged;
|
|
913
|
+
}
|
|
914
|
+
function buildResolvedSkill(skill, matrix, displayNameToId, displayNames, directoryPathToId, aliasTargetToSkillId) {
|
|
915
|
+
const conflictsWith = [];
|
|
916
|
+
const recommends = [];
|
|
917
|
+
const requires = [];
|
|
918
|
+
const alternatives = [];
|
|
919
|
+
const discourages = [];
|
|
920
|
+
const resolve = (id, relationContext) => resolveToCanonicalId(
|
|
921
|
+
id,
|
|
922
|
+
displayNameToId,
|
|
923
|
+
directoryPathToId,
|
|
924
|
+
aliasTargetToSkillId,
|
|
925
|
+
relationContext ? `${skill.id} ${relationContext}` : void 0
|
|
926
|
+
);
|
|
927
|
+
for (const conflictRef of skill.conflictsWith) {
|
|
928
|
+
const canonicalId = resolve(conflictRef, "conflictsWith");
|
|
929
|
+
conflictsWith.push({
|
|
930
|
+
skillId: canonicalId,
|
|
931
|
+
reason: "Defined in skill metadata"
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
for (const conflictRule of matrix.relationships.conflicts) {
|
|
935
|
+
const resolvedSkills = conflictRule.skills.map((id) => resolve(id, "conflicts"));
|
|
936
|
+
if (resolvedSkills.includes(skill.id)) {
|
|
937
|
+
for (const otherSkill of resolvedSkills) {
|
|
938
|
+
if (otherSkill !== skill.id) {
|
|
939
|
+
if (!conflictsWith.some((c) => c.skillId === otherSkill)) {
|
|
940
|
+
conflictsWith.push({
|
|
941
|
+
skillId: otherSkill,
|
|
942
|
+
reason: conflictRule.reason
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
for (const compatRef of skill.compatibleWith) {
|
|
950
|
+
const canonicalId = resolve(compatRef, "compatibleWith");
|
|
951
|
+
recommends.push({
|
|
952
|
+
skillId: canonicalId,
|
|
953
|
+
reason: "Compatible with this skill"
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
for (const recommendRule of matrix.relationships.recommends) {
|
|
957
|
+
const whenCanonicalId = resolve(recommendRule.when, "recommends.when");
|
|
958
|
+
if (whenCanonicalId === skill.id) {
|
|
959
|
+
for (const suggested of recommendRule.suggest) {
|
|
960
|
+
const canonicalId = resolve(suggested, "recommends.suggest");
|
|
961
|
+
if (!recommends.some((r) => r.skillId === canonicalId)) {
|
|
962
|
+
recommends.push({
|
|
963
|
+
skillId: canonicalId,
|
|
964
|
+
reason: recommendRule.reason
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if (skill.requires.length > 0) {
|
|
971
|
+
requires.push({
|
|
972
|
+
skillIds: skill.requires.map((id) => resolve(id, "requires")),
|
|
973
|
+
needsAny: false,
|
|
974
|
+
reason: "Defined in skill metadata"
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
for (const requireRule of matrix.relationships.requires) {
|
|
978
|
+
const skillCanonicalId = resolve(requireRule.skill, "requires.skill");
|
|
979
|
+
if (skillCanonicalId === skill.id) {
|
|
980
|
+
requires.push({
|
|
981
|
+
skillIds: requireRule.needs.map((id) => resolve(id, "requires.needs")),
|
|
982
|
+
needsAny: requireRule.needs_any ?? false,
|
|
983
|
+
reason: requireRule.reason
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
for (const altGroup of matrix.relationships.alternatives) {
|
|
988
|
+
const resolvedAlts = altGroup.skills.map((id) => resolve(id, "alternatives"));
|
|
989
|
+
if (resolvedAlts.includes(skill.id)) {
|
|
990
|
+
for (const altSkill of resolvedAlts) {
|
|
991
|
+
if (altSkill !== skill.id) {
|
|
992
|
+
alternatives.push({
|
|
993
|
+
skillId: altSkill,
|
|
994
|
+
purpose: altGroup.purpose
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (matrix.relationships.discourages) {
|
|
1001
|
+
for (const discourageRule of matrix.relationships.discourages) {
|
|
1002
|
+
const resolvedSkills = discourageRule.skills.map((id) => resolve(id, "discourages"));
|
|
1003
|
+
if (resolvedSkills.includes(skill.id)) {
|
|
1004
|
+
for (const otherSkill of resolvedSkills) {
|
|
1005
|
+
if (otherSkill !== skill.id) {
|
|
1006
|
+
if (!discourages.some((d) => d.skillId === otherSkill)) {
|
|
1007
|
+
discourages.push({
|
|
1008
|
+
skillId: otherSkill,
|
|
1009
|
+
reason: discourageRule.reason
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const compatibleWith = skill.compatibleWith.map((id) => resolve(id, "compatibleWith"));
|
|
1018
|
+
return {
|
|
1019
|
+
id: skill.id,
|
|
1020
|
+
displayName: displayNames[skill.id],
|
|
1021
|
+
description: skill.description,
|
|
1022
|
+
usageGuidance: skill.usageGuidance,
|
|
1023
|
+
category: skill.category,
|
|
1024
|
+
categoryExclusive: skill.categoryExclusive,
|
|
1025
|
+
tags: skill.tags,
|
|
1026
|
+
author: skill.author,
|
|
1027
|
+
conflictsWith,
|
|
1028
|
+
recommends,
|
|
1029
|
+
requires,
|
|
1030
|
+
alternatives,
|
|
1031
|
+
discourages,
|
|
1032
|
+
compatibleWith,
|
|
1033
|
+
requiresSetup: skill.requiresSetup.map((id) => resolve(id, "requiresSetup")),
|
|
1034
|
+
providesSetupFor: skill.providesSetupFor.map((id) => resolve(id, "providesSetupFor")),
|
|
1035
|
+
path: skill.path
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function resolveSuggestedStacks() {
|
|
1039
|
+
return [];
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/cli/lib/matrix/matrix-resolver.ts
|
|
1043
|
+
init_esm_shims();
|
|
1044
|
+
import { groupBy } from "remeda";
|
|
1045
|
+
function getLabel(skill, fallback) {
|
|
1046
|
+
return skill?.displayName || skill?.id || fallback;
|
|
1047
|
+
}
|
|
1048
|
+
function resolveAlias(aliasOrId, matrix) {
|
|
1049
|
+
return matrix.displayNameToId[aliasOrId] || aliasOrId;
|
|
1050
|
+
}
|
|
1051
|
+
function isDisabled(skillId, currentSelections, matrix, options) {
|
|
1052
|
+
if (options?.expertMode) {
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
const fullId = resolveAlias(skillId, matrix);
|
|
1056
|
+
const skill = matrix.skills[fullId];
|
|
1057
|
+
if (!skill) {
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
for (const selectedId of currentSelections) {
|
|
1061
|
+
const selectedFullId = resolveAlias(selectedId, matrix);
|
|
1062
|
+
if (skill.conflictsWith.some((c) => c.skillId === selectedFullId)) {
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
1065
|
+
const selectedSkill = matrix.skills[selectedFullId];
|
|
1066
|
+
if (selectedSkill && selectedSkill.conflictsWith.some((c) => c.skillId === fullId)) {
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
|
|
1071
|
+
for (const requirement of skill.requires) {
|
|
1072
|
+
if (requirement.needsAny) {
|
|
1073
|
+
const hasAny = requirement.skillIds.some((reqId) => resolvedSelections.includes(reqId));
|
|
1074
|
+
if (!hasAny) {
|
|
1075
|
+
return true;
|
|
1076
|
+
}
|
|
1077
|
+
} else {
|
|
1078
|
+
const hasAll = requirement.skillIds.every((reqId) => resolvedSelections.includes(reqId));
|
|
1079
|
+
if (!hasAll) {
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
function getDisableReason(skillId, currentSelections, matrix) {
|
|
1087
|
+
const fullId = resolveAlias(skillId, matrix);
|
|
1088
|
+
const skill = matrix.skills[fullId];
|
|
1089
|
+
if (!skill) {
|
|
1090
|
+
return void 0;
|
|
1091
|
+
}
|
|
1092
|
+
const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
|
|
1093
|
+
for (const selectedId of resolvedSelections) {
|
|
1094
|
+
const conflict = skill.conflictsWith.find((c) => c.skillId === selectedId);
|
|
1095
|
+
if (conflict) {
|
|
1096
|
+
const selectedSkill2 = matrix.skills[selectedId];
|
|
1097
|
+
return `${conflict.reason} (conflicts with ${getLabel(selectedSkill2, selectedId)})`;
|
|
1098
|
+
}
|
|
1099
|
+
const selectedSkill = matrix.skills[selectedId];
|
|
1100
|
+
if (selectedSkill) {
|
|
1101
|
+
const reverseConflict = selectedSkill.conflictsWith.find((c) => c.skillId === fullId);
|
|
1102
|
+
if (reverseConflict) {
|
|
1103
|
+
return `${reverseConflict.reason} (conflicts with ${getLabel(selectedSkill, selectedId)})`;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
for (const requirement of skill.requires) {
|
|
1108
|
+
if (requirement.needsAny) {
|
|
1109
|
+
const hasAny = requirement.skillIds.some((reqId) => resolvedSelections.includes(reqId));
|
|
1110
|
+
if (!hasAny) {
|
|
1111
|
+
const requiredNames = requirement.skillIds.map((id) => getLabel(matrix.skills[id], id)).join(" or ");
|
|
1112
|
+
return `${requirement.reason} (requires ${requiredNames})`;
|
|
1113
|
+
}
|
|
1114
|
+
} else {
|
|
1115
|
+
const missingIds = requirement.skillIds.filter(
|
|
1116
|
+
(reqId) => !resolvedSelections.includes(reqId)
|
|
1117
|
+
);
|
|
1118
|
+
if (missingIds.length > 0) {
|
|
1119
|
+
const missingNames = missingIds.map((id) => getLabel(matrix.skills[id], id)).join(", ");
|
|
1120
|
+
return `${requirement.reason} (requires ${missingNames})`;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return void 0;
|
|
1125
|
+
}
|
|
1126
|
+
function isDiscouraged(skillId, currentSelections, matrix) {
|
|
1127
|
+
const fullId = resolveAlias(skillId, matrix);
|
|
1128
|
+
const skill = matrix.skills[fullId];
|
|
1129
|
+
if (!skill) {
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
|
|
1133
|
+
for (const selectedId of resolvedSelections) {
|
|
1134
|
+
const selectedSkill = matrix.skills[selectedId];
|
|
1135
|
+
if (selectedSkill && selectedSkill.discourages.some((d) => d.skillId === fullId)) {
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
if (skill.discourages.some((d) => d.skillId === selectedId)) {
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
function getDiscourageReason(skillId, currentSelections, matrix) {
|
|
1145
|
+
const fullId = resolveAlias(skillId, matrix);
|
|
1146
|
+
const skill = matrix.skills[fullId];
|
|
1147
|
+
if (!skill) {
|
|
1148
|
+
return void 0;
|
|
1149
|
+
}
|
|
1150
|
+
const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
|
|
1151
|
+
for (const selectedId of resolvedSelections) {
|
|
1152
|
+
const selectedSkill = matrix.skills[selectedId];
|
|
1153
|
+
if (selectedSkill) {
|
|
1154
|
+
const discourage = selectedSkill.discourages.find((d) => d.skillId === fullId);
|
|
1155
|
+
if (discourage) {
|
|
1156
|
+
return discourage.reason;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const reverseDiscourage = skill.discourages.find((d) => d.skillId === selectedId);
|
|
1160
|
+
if (reverseDiscourage) {
|
|
1161
|
+
return reverseDiscourage.reason;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return void 0;
|
|
1165
|
+
}
|
|
1166
|
+
function isRecommended(skillId, currentSelections, matrix) {
|
|
1167
|
+
const fullId = resolveAlias(skillId, matrix);
|
|
1168
|
+
const skill = matrix.skills[fullId];
|
|
1169
|
+
if (!skill) {
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
|
|
1173
|
+
for (const selectedId of resolvedSelections) {
|
|
1174
|
+
const selectedSkill = matrix.skills[selectedId];
|
|
1175
|
+
if (selectedSkill && selectedSkill.recommends.some((r) => r.skillId === fullId)) {
|
|
1176
|
+
return true;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
function getRecommendReason(skillId, currentSelections, matrix) {
|
|
1182
|
+
const fullId = resolveAlias(skillId, matrix);
|
|
1183
|
+
const skill = matrix.skills[fullId];
|
|
1184
|
+
if (!skill) {
|
|
1185
|
+
return void 0;
|
|
1186
|
+
}
|
|
1187
|
+
const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
|
|
1188
|
+
for (const selectedId of resolvedSelections) {
|
|
1189
|
+
const selectedSkill = matrix.skills[selectedId];
|
|
1190
|
+
if (selectedSkill) {
|
|
1191
|
+
const recommendation = selectedSkill.recommends.find((r) => r.skillId === fullId);
|
|
1192
|
+
if (recommendation) {
|
|
1193
|
+
return `${recommendation.reason} (recommended by ${getLabel(selectedSkill, selectedId)})`;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return void 0;
|
|
1198
|
+
}
|
|
1199
|
+
function validateSelection(selections, matrix) {
|
|
1200
|
+
const errors = [];
|
|
1201
|
+
const warnings = [];
|
|
1202
|
+
const resolvedSelections = selections.map((s) => resolveAlias(s, matrix));
|
|
1203
|
+
for (let i = 0; i < resolvedSelections.length; i++) {
|
|
1204
|
+
const skillA = matrix.skills[resolvedSelections[i]];
|
|
1205
|
+
if (!skillA) continue;
|
|
1206
|
+
for (let j = i + 1; j < resolvedSelections.length; j++) {
|
|
1207
|
+
const skillBId = resolvedSelections[j];
|
|
1208
|
+
const conflict = skillA.conflictsWith.find((c) => c.skillId === skillBId);
|
|
1209
|
+
if (conflict) {
|
|
1210
|
+
errors.push({
|
|
1211
|
+
type: "conflict",
|
|
1212
|
+
message: `${getLabel(skillA, skillA.id)} conflicts with ${getLabel(matrix.skills[skillBId], skillBId)}: ${conflict.reason}`,
|
|
1213
|
+
skills: [skillA.id, skillBId]
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
for (const skillId of resolvedSelections) {
|
|
1219
|
+
const skill = matrix.skills[skillId];
|
|
1220
|
+
if (!skill) continue;
|
|
1221
|
+
for (const requirement of skill.requires) {
|
|
1222
|
+
if (requirement.needsAny) {
|
|
1223
|
+
const hasAny = requirement.skillIds.some((reqId) => resolvedSelections.includes(reqId));
|
|
1224
|
+
if (!hasAny) {
|
|
1225
|
+
errors.push({
|
|
1226
|
+
type: "missing_requirement",
|
|
1227
|
+
message: `${getLabel(skill, skillId)} requires one of: ${requirement.skillIds.map((id) => getLabel(matrix.skills[id], id)).join(", ")}`,
|
|
1228
|
+
skills: [skillId, ...requirement.skillIds]
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
} else {
|
|
1232
|
+
const missingIds = requirement.skillIds.filter(
|
|
1233
|
+
(reqId) => !resolvedSelections.includes(reqId)
|
|
1234
|
+
);
|
|
1235
|
+
if (missingIds.length > 0) {
|
|
1236
|
+
errors.push({
|
|
1237
|
+
type: "missing_requirement",
|
|
1238
|
+
message: `${getLabel(skill, skillId)} requires: ${missingIds.map((id) => getLabel(matrix.skills[id], id)).join(", ")}`,
|
|
1239
|
+
skills: [skillId, ...missingIds]
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const validSkills = resolvedSelections.map((skillId) => ({ skillId, skill: matrix.skills[skillId] })).filter((entry) => entry.skill != null);
|
|
1246
|
+
const categorySelections = groupBy(validSkills, (entry) => entry.skill.category);
|
|
1247
|
+
for (const [categoryId, entries] of typedEntries(categorySelections)) {
|
|
1248
|
+
if (entries.length > 1) {
|
|
1249
|
+
const skillIds = entries.map((e) => e.skillId);
|
|
1250
|
+
const category = matrix.categories[categoryId];
|
|
1251
|
+
if (category?.exclusive) {
|
|
1252
|
+
errors.push({
|
|
1253
|
+
type: "category_exclusive",
|
|
1254
|
+
message: `Category "${category.displayName}" only allows one selection, but multiple selected: ${skillIds.map((id) => getLabel(matrix.skills[id], id)).join(", ")}`,
|
|
1255
|
+
skills: skillIds
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
for (const skillId of resolvedSelections) {
|
|
1261
|
+
const skill = matrix.skills[skillId];
|
|
1262
|
+
if (!skill) continue;
|
|
1263
|
+
for (const recommendation of skill.recommends) {
|
|
1264
|
+
if (!resolvedSelections.includes(recommendation.skillId)) {
|
|
1265
|
+
const recommendedSkill = matrix.skills[recommendation.skillId];
|
|
1266
|
+
if (recommendedSkill) {
|
|
1267
|
+
const hasConflict = recommendedSkill.conflictsWith.some(
|
|
1268
|
+
(c) => resolvedSelections.includes(c.skillId)
|
|
1269
|
+
);
|
|
1270
|
+
if (!hasConflict) {
|
|
1271
|
+
warnings.push({
|
|
1272
|
+
type: "missing_recommendation",
|
|
1273
|
+
message: `${getLabel(skill, skillId)} recommends ${getLabel(recommendedSkill, recommendation.skillId)}: ${recommendation.reason}`,
|
|
1274
|
+
skills: [skillId, recommendation.skillId]
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
for (const skillId of resolvedSelections) {
|
|
1282
|
+
const skill = matrix.skills[skillId];
|
|
1283
|
+
if (!skill || skill.providesSetupFor.length === 0) continue;
|
|
1284
|
+
const hasUsageSkill = skill.providesSetupFor.some(
|
|
1285
|
+
(usageId) => resolvedSelections.includes(usageId)
|
|
1286
|
+
);
|
|
1287
|
+
if (!hasUsageSkill) {
|
|
1288
|
+
warnings.push({
|
|
1289
|
+
type: "unused_setup",
|
|
1290
|
+
message: `Setup skill "${getLabel(skill, skillId)}" selected but no corresponding usage skills: ${skill.providesSetupFor.map((id) => getLabel(matrix.skills[id], id)).join(", ")}`,
|
|
1291
|
+
skills: [skillId, ...skill.providesSetupFor]
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
return {
|
|
1296
|
+
valid: errors.length === 0,
|
|
1297
|
+
errors,
|
|
1298
|
+
warnings
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
function getAvailableSkills(categoryId, currentSelections, matrix, options) {
|
|
1302
|
+
const skillOptions = [];
|
|
1303
|
+
const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
|
|
1304
|
+
for (const skill of Object.values(matrix.skills)) {
|
|
1305
|
+
if (!skill) continue;
|
|
1306
|
+
if (skill.category !== categoryId) {
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
const disabled = isDisabled(skill.id, currentSelections, matrix, options);
|
|
1310
|
+
const discouraged = !disabled && isDiscouraged(skill.id, currentSelections, matrix);
|
|
1311
|
+
const recommended = !disabled && !discouraged && isRecommended(skill.id, currentSelections, matrix);
|
|
1312
|
+
skillOptions.push({
|
|
1313
|
+
id: skill.id,
|
|
1314
|
+
displayName: skill.displayName,
|
|
1315
|
+
description: skill.description,
|
|
1316
|
+
disabled,
|
|
1317
|
+
disabledReason: disabled ? getDisableReason(skill.id, currentSelections, matrix) : void 0,
|
|
1318
|
+
discouraged,
|
|
1319
|
+
discouragedReason: discouraged ? getDiscourageReason(skill.id, currentSelections, matrix) : void 0,
|
|
1320
|
+
recommended,
|
|
1321
|
+
recommendedReason: recommended ? getRecommendReason(skill.id, currentSelections, matrix) : void 0,
|
|
1322
|
+
selected: resolvedSelections.includes(skill.id),
|
|
1323
|
+
alternatives: skill.alternatives.map((a) => a.skillId)
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
return skillOptions;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// src/cli/lib/matrix/matrix-health-check.ts
|
|
1330
|
+
init_esm_shims();
|
|
1331
|
+
function checkMatrixHealth(matrix) {
|
|
1332
|
+
const issues = [];
|
|
1333
|
+
const skillIds = new Set(typedKeys(matrix.skills));
|
|
1334
|
+
checkRelationshipTargets(matrix, skillIds, issues);
|
|
1335
|
+
checkSubcategoryDomains(matrix, issues);
|
|
1336
|
+
checkSkillCategories(matrix, issues);
|
|
1337
|
+
checkCompatibleWithTargets(matrix, skillIds, issues);
|
|
1338
|
+
checkStackSkillIds(matrix, skillIds, issues);
|
|
1339
|
+
for (const issue of issues) {
|
|
1340
|
+
warn(`[matrix] ${issue.details}`);
|
|
1341
|
+
}
|
|
1342
|
+
return issues;
|
|
1343
|
+
}
|
|
1344
|
+
function checkRelationshipTargets(matrix, skillIds, issues) {
|
|
1345
|
+
for (const [skillId, skill] of typedEntries(matrix.skills)) {
|
|
1346
|
+
if (!skill) continue;
|
|
1347
|
+
for (const conflict of skill.conflictsWith) {
|
|
1348
|
+
if (!skillIds.has(conflict.skillId)) {
|
|
1349
|
+
issues.push({
|
|
1350
|
+
severity: "warning",
|
|
1351
|
+
finding: "ghost-relationship-target",
|
|
1352
|
+
details: `Skill '${skillId}' conflicts with '${conflict.skillId}' which does not exist in the matrix`
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
for (const recommend of skill.recommends) {
|
|
1357
|
+
if (!skillIds.has(recommend.skillId)) {
|
|
1358
|
+
issues.push({
|
|
1359
|
+
severity: "warning",
|
|
1360
|
+
finding: "ghost-relationship-target",
|
|
1361
|
+
details: `Skill '${skillId}' recommends '${recommend.skillId}' which does not exist in the matrix`
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
for (const requirement of skill.requires) {
|
|
1366
|
+
for (const reqId of requirement.skillIds) {
|
|
1367
|
+
if (!skillIds.has(reqId)) {
|
|
1368
|
+
issues.push({
|
|
1369
|
+
severity: "error",
|
|
1370
|
+
finding: "ghost-requirement-target",
|
|
1371
|
+
details: `Skill '${skillId}' requires '${reqId}' which does not exist in the matrix`
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
for (const alt of skill.alternatives) {
|
|
1377
|
+
if (!skillIds.has(alt.skillId)) {
|
|
1378
|
+
issues.push({
|
|
1379
|
+
severity: "warning",
|
|
1380
|
+
finding: "ghost-alternative-target",
|
|
1381
|
+
details: `Skill '${skillId}' lists alternative '${alt.skillId}' which does not exist in the matrix`
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
for (const discourage of skill.discourages) {
|
|
1386
|
+
if (!skillIds.has(discourage.skillId)) {
|
|
1387
|
+
issues.push({
|
|
1388
|
+
severity: "warning",
|
|
1389
|
+
finding: "ghost-relationship-target",
|
|
1390
|
+
details: `Skill '${skillId}' discourages '${discourage.skillId}' which does not exist in the matrix`
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
for (const setupId of skill.requiresSetup) {
|
|
1395
|
+
if (!skillIds.has(setupId)) {
|
|
1396
|
+
issues.push({
|
|
1397
|
+
severity: "warning",
|
|
1398
|
+
finding: "ghost-setup-target",
|
|
1399
|
+
details: `Skill '${skillId}' requiresSetup '${setupId}' which does not exist in the matrix`
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
for (const providesId of skill.providesSetupFor) {
|
|
1404
|
+
if (!skillIds.has(providesId)) {
|
|
1405
|
+
issues.push({
|
|
1406
|
+
severity: "warning",
|
|
1407
|
+
finding: "ghost-setup-target",
|
|
1408
|
+
details: `Skill '${skillId}' providesSetupFor '${providesId}' which does not exist in the matrix`
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function checkSubcategoryDomains(matrix, issues) {
|
|
1415
|
+
for (const [catId, cat] of typedEntries(matrix.categories)) {
|
|
1416
|
+
if (!cat) continue;
|
|
1417
|
+
if (!cat.domain) {
|
|
1418
|
+
issues.push({
|
|
1419
|
+
severity: "warning",
|
|
1420
|
+
finding: "category-missing-domain",
|
|
1421
|
+
details: `Category '${catId}' has no domain \u2014 it won't appear in any wizard domain view`
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
function checkSkillCategories(matrix, issues) {
|
|
1427
|
+
for (const [skillId, skill] of typedEntries(matrix.skills)) {
|
|
1428
|
+
if (!skill) continue;
|
|
1429
|
+
if (!matrix.categories[skill.category]) {
|
|
1430
|
+
issues.push({
|
|
1431
|
+
severity: "warning",
|
|
1432
|
+
finding: "skill-unknown-category",
|
|
1433
|
+
details: `Skill '${skillId}' references category '${skill.category}' which does not exist in the matrix`
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
function checkCompatibleWithTargets(matrix, skillIds, issues) {
|
|
1439
|
+
for (const [skillId, skill] of typedEntries(matrix.skills)) {
|
|
1440
|
+
if (!skill) continue;
|
|
1441
|
+
for (const compatId of skill.compatibleWith) {
|
|
1442
|
+
if (!skillIds.has(compatId)) {
|
|
1443
|
+
issues.push({
|
|
1444
|
+
severity: "warning",
|
|
1445
|
+
finding: "ghost-compatible-with-target",
|
|
1446
|
+
details: `Skill '${skillId}' has compatibleWith '${compatId}' which does not exist in the matrix`
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
function checkStackSkillIds(matrix, skillIds, issues) {
|
|
1453
|
+
for (const stack of matrix.suggestedStacks) {
|
|
1454
|
+
for (const stackSkillId of stack.allSkillIds) {
|
|
1455
|
+
if (!skillIds.has(stackSkillId)) {
|
|
1456
|
+
issues.push({
|
|
1457
|
+
severity: "warning",
|
|
1458
|
+
finding: "stack-ghost-skill",
|
|
1459
|
+
details: `Stack '${stack.id}' references skill '${stackSkillId}' which does not exist in the matrix`
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// src/cli/lib/loading/multi-source-loader.ts
|
|
1467
|
+
init_esm_shims();
|
|
1468
|
+
import path18 from "path";
|
|
1469
|
+
|
|
1470
|
+
// src/cli/lib/plugins/index.ts
|
|
1471
|
+
init_esm_shims();
|
|
1472
|
+
|
|
1473
|
+
// src/cli/lib/plugins/plugin-manifest.ts
|
|
1474
|
+
init_esm_shims();
|
|
1475
|
+
import path7 from "path";
|
|
1476
|
+
var PLUGIN_DIR_NAME = ".claude-plugin";
|
|
1477
|
+
var PLUGIN_MANIFEST_FILE2 = "plugin.json";
|
|
1478
|
+
var SKILL_PLUGIN_PREFIX = "";
|
|
1479
|
+
var AGENT_PLUGIN_PREFIX = "agent-";
|
|
1480
|
+
function buildAuthor(name, email) {
|
|
1481
|
+
if (!name) {
|
|
1482
|
+
return void 0;
|
|
1483
|
+
}
|
|
1484
|
+
const author = { name };
|
|
1485
|
+
if (email) {
|
|
1486
|
+
author.email = email;
|
|
1487
|
+
}
|
|
1488
|
+
return author;
|
|
1489
|
+
}
|
|
1490
|
+
function generateSkillPluginManifest(options) {
|
|
1491
|
+
const manifest = {
|
|
1492
|
+
name: `${SKILL_PLUGIN_PREFIX}${options.skillName}`,
|
|
1493
|
+
version: options.version ?? DEFAULT_VERSION,
|
|
1494
|
+
skills: "./skills/"
|
|
1495
|
+
};
|
|
1496
|
+
if (options.description) {
|
|
1497
|
+
manifest.description = options.description;
|
|
1498
|
+
}
|
|
1499
|
+
const author = buildAuthor(options.author, options.authorEmail);
|
|
1500
|
+
if (author) {
|
|
1501
|
+
manifest.author = author;
|
|
1502
|
+
}
|
|
1503
|
+
if (options.keywords && options.keywords.length > 0) {
|
|
1504
|
+
manifest.keywords = options.keywords;
|
|
1505
|
+
}
|
|
1506
|
+
return manifest;
|
|
1507
|
+
}
|
|
1508
|
+
function generateAgentPluginManifest(options) {
|
|
1509
|
+
const manifest = {
|
|
1510
|
+
name: `${AGENT_PLUGIN_PREFIX}${options.agentName}`,
|
|
1511
|
+
version: options.version ?? DEFAULT_VERSION,
|
|
1512
|
+
agents: "./agents/"
|
|
1513
|
+
};
|
|
1514
|
+
if (options.description) {
|
|
1515
|
+
manifest.description = options.description;
|
|
1516
|
+
}
|
|
1517
|
+
return manifest;
|
|
1518
|
+
}
|
|
1519
|
+
function generateStackPluginManifest(options) {
|
|
1520
|
+
const manifest = {
|
|
1521
|
+
name: options.stackName,
|
|
1522
|
+
version: options.version ?? DEFAULT_VERSION
|
|
1523
|
+
};
|
|
1524
|
+
if (options.hasSkills) {
|
|
1525
|
+
manifest.skills = "./skills/";
|
|
1526
|
+
}
|
|
1527
|
+
if (options.description) {
|
|
1528
|
+
manifest.description = options.description;
|
|
1529
|
+
}
|
|
1530
|
+
const author = buildAuthor(options.author, options.authorEmail);
|
|
1531
|
+
if (author) {
|
|
1532
|
+
manifest.author = author;
|
|
1533
|
+
}
|
|
1534
|
+
if (options.keywords && options.keywords.length > 0) {
|
|
1535
|
+
manifest.keywords = options.keywords;
|
|
1536
|
+
}
|
|
1537
|
+
if (options.hasHooks) {
|
|
1538
|
+
manifest.hooks = "./hooks/hooks.json";
|
|
1539
|
+
}
|
|
1540
|
+
return manifest;
|
|
1541
|
+
}
|
|
1542
|
+
async function writePluginManifest(outputDir, manifest) {
|
|
1543
|
+
const pluginDir = path7.join(outputDir, PLUGIN_DIR_NAME);
|
|
1544
|
+
const manifestPath = path7.join(pluginDir, PLUGIN_MANIFEST_FILE2);
|
|
1545
|
+
await ensureDir(pluginDir);
|
|
1546
|
+
const content = JSON.stringify(manifest, null, 2);
|
|
1547
|
+
await writeFile(manifestPath, content);
|
|
1548
|
+
return manifestPath;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// src/cli/lib/plugins/plugin-manifest-finder.ts
|
|
1552
|
+
init_esm_shims();
|
|
1553
|
+
import path8 from "path";
|
|
1554
|
+
async function findPluginManifest(startDir) {
|
|
1555
|
+
let currentDir = startDir;
|
|
1556
|
+
const root = path8.parse(currentDir).root;
|
|
1557
|
+
while (currentDir !== root) {
|
|
1558
|
+
const manifestPath = path8.join(currentDir, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
|
|
1559
|
+
if (await fileExists(manifestPath)) {
|
|
1560
|
+
return manifestPath;
|
|
1561
|
+
}
|
|
1562
|
+
currentDir = path8.dirname(currentDir);
|
|
1563
|
+
}
|
|
1564
|
+
return null;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// src/cli/lib/plugins/plugin-finder.ts
|
|
1568
|
+
init_esm_shims();
|
|
1569
|
+
import path9 from "path";
|
|
1570
|
+
function getCollectivePluginDir(projectDir) {
|
|
1571
|
+
const dir = projectDir ?? process.cwd();
|
|
1572
|
+
return path9.join(dir, CLAUDE_DIR, PLUGINS_SUBDIR, "claude-collective");
|
|
1573
|
+
}
|
|
1574
|
+
function getProjectPluginsDir(projectDir) {
|
|
1575
|
+
const dir = projectDir ?? process.cwd();
|
|
1576
|
+
return path9.join(dir, CLAUDE_DIR, PLUGINS_SUBDIR);
|
|
1577
|
+
}
|
|
1578
|
+
function getPluginSkillsDir(pluginDir) {
|
|
1579
|
+
return path9.join(pluginDir, "skills");
|
|
1580
|
+
}
|
|
1581
|
+
function getPluginAgentsDir(pluginDir) {
|
|
1582
|
+
return path9.join(pluginDir, "agents");
|
|
1583
|
+
}
|
|
1584
|
+
function getPluginManifestPath(pluginDir) {
|
|
1585
|
+
return path9.join(pluginDir, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
|
|
1586
|
+
}
|
|
1587
|
+
async function readPluginManifest(pluginDir) {
|
|
1588
|
+
const manifestPath = getPluginManifestPath(pluginDir);
|
|
1589
|
+
if (!await fileExists(manifestPath)) {
|
|
1590
|
+
verbose(` No manifest at ${manifestPath}`);
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
try {
|
|
1594
|
+
const content = await readFile(manifestPath);
|
|
1595
|
+
const manifest = pluginManifestSchema.parse(JSON.parse(content));
|
|
1596
|
+
if (!manifest.name || typeof manifest.name !== "string") {
|
|
1597
|
+
verbose(` Invalid manifest at ${manifestPath}: missing name`);
|
|
1598
|
+
return null;
|
|
1599
|
+
}
|
|
1600
|
+
return manifest;
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
verbose(` Failed to parse manifest at ${manifestPath}: ${error}`);
|
|
1603
|
+
return null;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
async function getPluginSkillIds(pluginSkillsDir, matrix) {
|
|
1607
|
+
const skillFiles = await glob("**/SKILL.md", pluginSkillsDir);
|
|
1608
|
+
const skillIds = [];
|
|
1609
|
+
const aliasToId = /* @__PURE__ */ new Map();
|
|
1610
|
+
for (const [id, skill] of Object.entries(matrix.skills)) {
|
|
1611
|
+
if (!skill) continue;
|
|
1612
|
+
if (skill.displayName) {
|
|
1613
|
+
aliasToId.set(skill.displayName.toLowerCase(), id);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
const dirToId = /* @__PURE__ */ new Map();
|
|
1617
|
+
for (const [id] of Object.entries(matrix.skills)) {
|
|
1618
|
+
const idParts = id.split("/");
|
|
1619
|
+
const lastPart = idParts[idParts.length - 1];
|
|
1620
|
+
if (lastPart) {
|
|
1621
|
+
dirToId.set(lastPart.toLowerCase(), id);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
for (const skillFile of skillFiles) {
|
|
1625
|
+
const fullPath = path9.join(pluginSkillsDir, skillFile);
|
|
1626
|
+
const content = await readFile(fullPath);
|
|
1627
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1628
|
+
if (frontmatterMatch) {
|
|
1629
|
+
const frontmatter = frontmatterMatch[1];
|
|
1630
|
+
const nameMatch = frontmatter.match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
1631
|
+
if (nameMatch) {
|
|
1632
|
+
const skillName = nameMatch[1].trim();
|
|
1633
|
+
if (matrix.skills[skillName]) {
|
|
1634
|
+
skillIds.push(skillName);
|
|
1635
|
+
continue;
|
|
1636
|
+
}
|
|
1637
|
+
const skillId2 = aliasToId.get(skillName.toLowerCase());
|
|
1638
|
+
if (skillId2) {
|
|
1639
|
+
skillIds.push(skillId2);
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
const dirPath = path9.dirname(skillFile);
|
|
1645
|
+
const dirName = path9.basename(dirPath);
|
|
1646
|
+
const skillId = dirToId.get(dirName.toLowerCase());
|
|
1647
|
+
if (skillId) {
|
|
1648
|
+
skillIds.push(skillId);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return skillIds;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// src/cli/lib/plugins/plugin-info.ts
|
|
1655
|
+
init_esm_shims();
|
|
1656
|
+
import { readdir } from "fs/promises";
|
|
1657
|
+
import { countBy } from "remeda";
|
|
1658
|
+
|
|
1659
|
+
// src/cli/lib/installation/index.ts
|
|
1660
|
+
init_esm_shims();
|
|
1661
|
+
|
|
1662
|
+
// src/cli/lib/installation/installation.ts
|
|
1663
|
+
init_esm_shims();
|
|
1664
|
+
import path10 from "path";
|
|
1665
|
+
async function detectInstallation(projectDir = process.cwd()) {
|
|
1666
|
+
const srcConfigPath = path10.join(projectDir, CLAUDE_SRC_DIR, "config.yaml");
|
|
1667
|
+
const legacyConfigPath = path10.join(projectDir, CLAUDE_DIR, "config.yaml");
|
|
1668
|
+
const localConfigPath = await fileExists(srcConfigPath) ? srcConfigPath : await fileExists(legacyConfigPath) ? legacyConfigPath : null;
|
|
1669
|
+
if (localConfigPath) {
|
|
1670
|
+
const loaded = await loadProjectConfig(projectDir);
|
|
1671
|
+
const mode = loaded?.config?.installMode ?? "local";
|
|
1672
|
+
if (mode === "local") {
|
|
1673
|
+
return {
|
|
1674
|
+
mode: "local",
|
|
1675
|
+
configPath: localConfigPath,
|
|
1676
|
+
agentsDir: path10.join(projectDir, CLAUDE_DIR, "agents"),
|
|
1677
|
+
skillsDir: path10.join(projectDir, CLAUDE_DIR, "skills"),
|
|
1678
|
+
projectDir
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
const pluginDir = getCollectivePluginDir(projectDir);
|
|
1683
|
+
const pluginConfigPath = path10.join(pluginDir, "config.yaml");
|
|
1684
|
+
if (await directoryExists(pluginDir)) {
|
|
1685
|
+
return {
|
|
1686
|
+
mode: "plugin",
|
|
1687
|
+
configPath: pluginConfigPath,
|
|
1688
|
+
agentsDir: path10.join(pluginDir, "agents"),
|
|
1689
|
+
skillsDir: path10.join(pluginDir, "skills"),
|
|
1690
|
+
projectDir
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// src/cli/lib/installation/local-installer.ts
|
|
1697
|
+
init_esm_shims();
|
|
1698
|
+
import path15 from "path";
|
|
1699
|
+
import { stringify as stringifyYaml4 } from "yaml";
|
|
1700
|
+
|
|
1701
|
+
// src/cli/lib/stacks/index.ts
|
|
1702
|
+
init_esm_shims();
|
|
1703
|
+
|
|
1704
|
+
// src/cli/lib/stacks/stacks-loader.ts
|
|
1705
|
+
init_esm_shims();
|
|
1706
|
+
import { parse as parseYaml6 } from "yaml";
|
|
1707
|
+
import path11 from "path";
|
|
1708
|
+
import { mapValues } from "remeda";
|
|
1709
|
+
var STACKS_FILE = "config/stacks.yaml";
|
|
1710
|
+
var stacksCache = /* @__PURE__ */ new Map();
|
|
1711
|
+
async function loadStacks(configDir) {
|
|
1712
|
+
const cacheKey = configDir;
|
|
1713
|
+
const cached = stacksCache.get(cacheKey);
|
|
1714
|
+
if (cached) return cached;
|
|
1715
|
+
const stacksPath = path11.join(configDir, STACKS_FILE);
|
|
1716
|
+
if (!await fileExists(stacksPath)) {
|
|
1717
|
+
verbose(`No stacks file found at ${stacksPath}`);
|
|
1718
|
+
return [];
|
|
1719
|
+
}
|
|
1720
|
+
try {
|
|
1721
|
+
const content = await readFile(stacksPath);
|
|
1722
|
+
const result = stacksConfigSchema.safeParse(parseYaml6(content));
|
|
1723
|
+
if (!result.success) {
|
|
1724
|
+
throw new Error(
|
|
1725
|
+
`Invalid stacks.yaml at ${stacksPath}: ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
const config = result.data;
|
|
1729
|
+
stacksCache.set(cacheKey, config.stacks);
|
|
1730
|
+
verbose(`Loaded ${config.stacks.length} stacks from ${stacksPath}`);
|
|
1731
|
+
return config.stacks;
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1734
|
+
throw new Error(`Failed to load stacks from '${stacksPath}': ${errorMessage}`);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
async function loadStackById(stackId, configDir) {
|
|
1738
|
+
const stacks = await loadStacks(configDir);
|
|
1739
|
+
const stack = stacks.find((s) => s.id === stackId);
|
|
1740
|
+
if (!stack) {
|
|
1741
|
+
verbose(`Stack '${stackId}' not found`);
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
verbose(`Found stack: ${stack.name} (${stackId})`);
|
|
1745
|
+
return stack;
|
|
1746
|
+
}
|
|
1747
|
+
function resolveAgentConfigToSkills(agentConfig, displayNameToId) {
|
|
1748
|
+
const skillRefs = [];
|
|
1749
|
+
for (const [subcategory, technologyDisplayName] of Object.entries(agentConfig)) {
|
|
1750
|
+
const fullSkillId = displayNameToId[technologyDisplayName];
|
|
1751
|
+
if (!fullSkillId) {
|
|
1752
|
+
warn(
|
|
1753
|
+
`No skill found for display name '${technologyDisplayName}' (subcategory: ${subcategory}) in stack config. Skipping.`
|
|
1754
|
+
);
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
const isKeySkill = KEY_SUBCATEGORIES.has(subcategory);
|
|
1758
|
+
skillRefs.push({
|
|
1759
|
+
id: fullSkillId,
|
|
1760
|
+
usage: `when working with ${subcategory}`,
|
|
1761
|
+
preloaded: isKeySkill
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
return skillRefs;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// src/cli/lib/stacks/stack-installer.ts
|
|
1768
|
+
init_esm_shims();
|
|
1769
|
+
import os from "os";
|
|
1770
|
+
import path14 from "path";
|
|
1771
|
+
|
|
1772
|
+
// src/cli/lib/stacks/stack-plugin-compiler.ts
|
|
1773
|
+
init_esm_shims();
|
|
1774
|
+
import path13 from "path";
|
|
1775
|
+
|
|
1776
|
+
// src/cli/lib/compiler.ts
|
|
1777
|
+
init_esm_shims();
|
|
1778
|
+
import { Liquid } from "liquidjs";
|
|
1779
|
+
import path12 from "path";
|
|
1780
|
+
import { pipe, flatMap, filter, uniqueBy } from "remeda";
|
|
1781
|
+
|
|
1782
|
+
// src/cli/lib/resolver.ts
|
|
1783
|
+
init_esm_shims();
|
|
1784
|
+
function resolveSkillReference(ref, skills) {
|
|
1785
|
+
const definition = skills[ref.id];
|
|
1786
|
+
if (!definition) {
|
|
1787
|
+
verbose(`Skill '${ref.id}' not found in available skills, skipping`);
|
|
1788
|
+
return null;
|
|
1789
|
+
}
|
|
1790
|
+
return {
|
|
1791
|
+
...definition,
|
|
1792
|
+
usage: ref.usage,
|
|
1793
|
+
preloaded: ref.preloaded ?? false
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
function resolveSkillReferences(skillRefs, skills) {
|
|
1797
|
+
return skillRefs.map((ref) => resolveSkillReference(ref, skills)).filter((skill) => skill !== null);
|
|
1798
|
+
}
|
|
1799
|
+
function buildSkillRefsFromConfig(agentStack) {
|
|
1800
|
+
const skillRefs = [];
|
|
1801
|
+
for (const [subcategory, skillId] of typedEntries(agentStack)) {
|
|
1802
|
+
skillRefs.push({
|
|
1803
|
+
id: skillId,
|
|
1804
|
+
usage: `when working with ${subcategory}`,
|
|
1805
|
+
preloaded: KEY_SUBCATEGORIES.has(subcategory)
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
return skillRefs;
|
|
1809
|
+
}
|
|
1810
|
+
function resolveAgentSkillsFromStack(agentName, stack, displayNameToId) {
|
|
1811
|
+
const agentConfig = stack.agents[agentName];
|
|
1812
|
+
if (!agentConfig) {
|
|
1813
|
+
verbose(`Agent '${agentName}' not found in stack '${stack.id}'`);
|
|
1814
|
+
return [];
|
|
1815
|
+
}
|
|
1816
|
+
if (typedKeys(agentConfig).length === 0) {
|
|
1817
|
+
verbose(`Agent '${agentName}' has no technology config in stack '${stack.id}'`);
|
|
1818
|
+
return [];
|
|
1819
|
+
}
|
|
1820
|
+
const skillRefs = [];
|
|
1821
|
+
for (const [subcategory, technologyDisplayName] of typedEntries(
|
|
1822
|
+
agentConfig
|
|
1823
|
+
)) {
|
|
1824
|
+
const fullSkillId = displayNameToId[technologyDisplayName];
|
|
1825
|
+
if (!fullSkillId) {
|
|
1826
|
+
verbose(
|
|
1827
|
+
`Warning: No skill found for display name '${technologyDisplayName}' (agent: ${agentName}, subcategory: ${subcategory}). Skipping.`
|
|
1828
|
+
);
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
const isKeySkill = KEY_SUBCATEGORIES.has(subcategory);
|
|
1832
|
+
skillRefs.push({
|
|
1833
|
+
id: fullSkillId,
|
|
1834
|
+
usage: `when working with ${subcategory}`,
|
|
1835
|
+
preloaded: isKeySkill
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
verbose(`Resolved ${skillRefs.length} skills for agent '${agentName}' from stack '${stack.id}'`);
|
|
1839
|
+
return skillRefs;
|
|
1840
|
+
}
|
|
1841
|
+
async function getAgentSkills(agentName, agentConfig, stack, displayNameToId) {
|
|
1842
|
+
if (agentConfig.skills && agentConfig.skills.length > 0) {
|
|
1843
|
+
return agentConfig.skills;
|
|
1844
|
+
}
|
|
1845
|
+
if (stack && displayNameToId) {
|
|
1846
|
+
const stackSkills = resolveAgentSkillsFromStack(agentName, stack, displayNameToId);
|
|
1847
|
+
if (stackSkills.length > 0) {
|
|
1848
|
+
verbose(`Resolved ${stackSkills.length} skills from stack for ${agentName}`);
|
|
1849
|
+
return stackSkills;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return [];
|
|
1853
|
+
}
|
|
1854
|
+
async function resolveAgents(agents, skills, compileConfig, _projectRoot, stack, displayNameToId) {
|
|
1855
|
+
const resolved = {};
|
|
1856
|
+
const agentNames = typedKeys(compileConfig.agents);
|
|
1857
|
+
for (const agentName of agentNames) {
|
|
1858
|
+
const definition = agents[agentName];
|
|
1859
|
+
if (!definition) {
|
|
1860
|
+
const availableAgents = typedKeys(agents);
|
|
1861
|
+
const agentList = availableAgents.length > 0 ? `Available agents: ${availableAgents.slice(0, 5).join(", ")}${availableAgents.length > 5 ? ` (and ${availableAgents.length - 5} more)` : ""}` : "No agents found in scanned directories";
|
|
1862
|
+
throw new Error(
|
|
1863
|
+
`Agent '${agentName}' referenced in compile config but not found in scanned agents. ${agentList}. Check that src/agents/${agentName}/agent.yaml exists.`
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
const agentConfig = compileConfig.agents[agentName];
|
|
1867
|
+
const skillRefs = await getAgentSkills(agentName, agentConfig, stack, displayNameToId);
|
|
1868
|
+
const resolvedSkills = resolveSkillReferences(skillRefs, skills);
|
|
1869
|
+
resolved[agentName] = {
|
|
1870
|
+
name: agentName,
|
|
1871
|
+
title: definition.title,
|
|
1872
|
+
description: definition.description,
|
|
1873
|
+
model: definition.model,
|
|
1874
|
+
tools: definition.tools,
|
|
1875
|
+
skills: resolvedSkills,
|
|
1876
|
+
path: definition.path,
|
|
1877
|
+
sourceRoot: definition.sourceRoot,
|
|
1878
|
+
agentBaseDir: definition.agentBaseDir
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
return resolved;
|
|
1882
|
+
}
|
|
1883
|
+
function stackToCompileConfig(stackId, stack) {
|
|
1884
|
+
const agents = {};
|
|
1885
|
+
for (const agentId of stack.agents) {
|
|
1886
|
+
agents[agentId] = {};
|
|
1887
|
+
}
|
|
1888
|
+
return {
|
|
1889
|
+
name: stack.name,
|
|
1890
|
+
description: stack.description || "",
|
|
1891
|
+
stack: stackId,
|
|
1892
|
+
agents
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// src/cli/utils/frontmatter.ts
|
|
1897
|
+
init_esm_shims();
|
|
1898
|
+
import { parse as parseYaml7 } from "yaml";
|
|
1899
|
+
function extractFrontmatter(content) {
|
|
1900
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
1901
|
+
const match = content.match(frontmatterRegex);
|
|
1902
|
+
if (!match || !match[1]) {
|
|
1903
|
+
return null;
|
|
1904
|
+
}
|
|
1905
|
+
try {
|
|
1906
|
+
return parseYaml7(match[1]);
|
|
1907
|
+
} catch {
|
|
1908
|
+
return null;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// src/cli/lib/compiler.ts
|
|
1913
|
+
async function createLiquidEngine(projectDir) {
|
|
1914
|
+
const roots = [];
|
|
1915
|
+
if (projectDir) {
|
|
1916
|
+
const srcTemplatesDir = path12.join(projectDir, CLAUDE_SRC_DIR, "agents", "_templates");
|
|
1917
|
+
if (await directoryExists(srcTemplatesDir)) {
|
|
1918
|
+
roots.push(srcTemplatesDir);
|
|
1919
|
+
verbose(`Using local templates from: ${srcTemplatesDir}`);
|
|
1920
|
+
}
|
|
1921
|
+
const legacyTemplatesDir = path12.join(projectDir, CLAUDE_DIR, "templates");
|
|
1922
|
+
if (await directoryExists(legacyTemplatesDir)) {
|
|
1923
|
+
roots.push(legacyTemplatesDir);
|
|
1924
|
+
verbose(`Using legacy templates from: ${legacyTemplatesDir}`);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
roots.push(path12.join(PROJECT_ROOT, DIRS.templates));
|
|
1928
|
+
return new Liquid({
|
|
1929
|
+
root: roots,
|
|
1930
|
+
extname: ".liquid",
|
|
1931
|
+
strictVariables: false,
|
|
1932
|
+
strictFilters: true
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// src/cli/lib/stacks/stack-plugin-compiler.ts
|
|
1937
|
+
import { unique as unique2 } from "remeda";
|
|
1938
|
+
function hashStackConfig(stack) {
|
|
1939
|
+
const stackSkillIds = stack.stack ? [...new Set(Object.values(stack.stack).flatMap((a) => Object.values(a)))].sort() : [];
|
|
1940
|
+
const parts = [
|
|
1941
|
+
`name:${stack.name}`,
|
|
1942
|
+
`description:${stack.description ?? ""}`,
|
|
1943
|
+
`skills:${stackSkillIds.join(",")}`,
|
|
1944
|
+
`agents:${(stack.agents || []).sort().join(",")}`
|
|
1945
|
+
];
|
|
1946
|
+
return hashString(parts.join("\n"));
|
|
1947
|
+
}
|
|
1948
|
+
async function compileAgentForPlugin(name, agent, fallbackRoot, engine, installMode) {
|
|
1949
|
+
verbose(`Compiling agent: ${name}`);
|
|
1950
|
+
const agentSourceRoot = agent.sourceRoot || fallbackRoot;
|
|
1951
|
+
const agentBaseDir = agent.agentBaseDir || DIRS.agents;
|
|
1952
|
+
const agentDir = path13.join(agentSourceRoot, agentBaseDir, agent.path || name);
|
|
1953
|
+
const intro = await readFile(path13.join(agentDir, "intro.md"));
|
|
1954
|
+
const workflow = await readFile(path13.join(agentDir, "workflow.md"));
|
|
1955
|
+
const examples = await readFileOptional(
|
|
1956
|
+
path13.join(agentDir, "examples.md"),
|
|
1957
|
+
"## Examples\n\n_No examples defined._"
|
|
1958
|
+
);
|
|
1959
|
+
const criticalRequirementsTop = await readFileOptional(
|
|
1960
|
+
path13.join(agentDir, "critical-requirements.md"),
|
|
1961
|
+
""
|
|
1962
|
+
);
|
|
1963
|
+
const criticalReminders = await readFileOptional(
|
|
1964
|
+
path13.join(agentDir, "critical-reminders.md"),
|
|
1965
|
+
""
|
|
1966
|
+
);
|
|
1967
|
+
const agentPath = agent.path || name;
|
|
1968
|
+
const category = agentPath.split("/")[0];
|
|
1969
|
+
const categoryDir = path13.join(agentSourceRoot, agentBaseDir, category);
|
|
1970
|
+
let outputFormat = await readFileOptional(path13.join(agentDir, "output-format.md"), "");
|
|
1971
|
+
if (!outputFormat) {
|
|
1972
|
+
outputFormat = await readFileOptional(path13.join(categoryDir, "output-format.md"), "");
|
|
1973
|
+
}
|
|
1974
|
+
const skills = installMode === "plugin" ? agent.skills.map((s) => ({ ...s, pluginRef: `${s.id}:${s.id}` })) : agent.skills;
|
|
1975
|
+
const preloadedSkills = skills.filter((s) => s.preloaded);
|
|
1976
|
+
const dynamicSkills = skills.filter((s) => !s.preloaded);
|
|
1977
|
+
const preloadedSkillIds = preloadedSkills.map((s) => s.pluginRef ?? s.id);
|
|
1978
|
+
verbose(
|
|
1979
|
+
`Skills for ${name}: ${preloadedSkills.length} preloaded, ${dynamicSkills.length} dynamic`
|
|
1980
|
+
);
|
|
1981
|
+
const data = {
|
|
1982
|
+
agent,
|
|
1983
|
+
intro,
|
|
1984
|
+
workflow,
|
|
1985
|
+
examples,
|
|
1986
|
+
criticalRequirementsTop,
|
|
1987
|
+
criticalReminders,
|
|
1988
|
+
outputFormat,
|
|
1989
|
+
skills,
|
|
1990
|
+
preloadedSkills,
|
|
1991
|
+
dynamicSkills,
|
|
1992
|
+
preloadedSkillIds
|
|
1993
|
+
};
|
|
1994
|
+
return engine.renderFile("agent", data);
|
|
1995
|
+
}
|
|
1996
|
+
function generateStackReadme(stackId, stack, agents, skillPlugins) {
|
|
1997
|
+
const lines = [];
|
|
1998
|
+
lines.push(`# ${stack.name}`);
|
|
1999
|
+
lines.push("");
|
|
2000
|
+
lines.push(stack.description || "A Claude Code stack plugin.");
|
|
2001
|
+
lines.push("");
|
|
2002
|
+
lines.push("## Installation");
|
|
2003
|
+
lines.push("");
|
|
2004
|
+
lines.push("Add this plugin to your Claude Code configuration:");
|
|
2005
|
+
lines.push("");
|
|
2006
|
+
lines.push("```json");
|
|
2007
|
+
lines.push(`{`);
|
|
2008
|
+
lines.push(` "plugins": ["${stackId}"]`);
|
|
2009
|
+
lines.push(`}`);
|
|
2010
|
+
lines.push("```");
|
|
2011
|
+
lines.push("");
|
|
2012
|
+
lines.push("## Agents");
|
|
2013
|
+
lines.push("");
|
|
2014
|
+
lines.push("This stack includes the following agents:");
|
|
2015
|
+
lines.push("");
|
|
2016
|
+
for (const agent of agents) {
|
|
2017
|
+
lines.push(`- \`${agent}\``);
|
|
2018
|
+
}
|
|
2019
|
+
lines.push("");
|
|
2020
|
+
if (skillPlugins.length > 0) {
|
|
2021
|
+
lines.push("## Included Skills");
|
|
2022
|
+
lines.push("");
|
|
2023
|
+
lines.push("This stack includes the following skills:");
|
|
2024
|
+
lines.push("");
|
|
2025
|
+
const uniqueSkills = unique2(skillPlugins).sort();
|
|
2026
|
+
for (const skill of uniqueSkills) {
|
|
2027
|
+
lines.push(`- \`${skill}\``);
|
|
2028
|
+
}
|
|
2029
|
+
lines.push("");
|
|
2030
|
+
}
|
|
2031
|
+
lines.push("---");
|
|
2032
|
+
lines.push("");
|
|
2033
|
+
lines.push("*Generated by Claude Collective stack-plugin-compiler*");
|
|
2034
|
+
lines.push("");
|
|
2035
|
+
return lines.join("\n");
|
|
2036
|
+
}
|
|
2037
|
+
async function compileStackPlugin(options) {
|
|
2038
|
+
const { stackId, outputDir, projectRoot, agentSourcePath } = options;
|
|
2039
|
+
const localAgentRoot = agentSourcePath || projectRoot;
|
|
2040
|
+
verbose(`Compiling stack plugin: ${stackId}`);
|
|
2041
|
+
verbose(` Stack/skills source: ${projectRoot}`);
|
|
2042
|
+
verbose(` Local agent source: ${localAgentRoot}`);
|
|
2043
|
+
verbose(` CLI agent source: ${PROJECT_ROOT}`);
|
|
2044
|
+
const cliAgents = await loadAllAgents(PROJECT_ROOT);
|
|
2045
|
+
const localAgents = await loadAllAgents(localAgentRoot);
|
|
2046
|
+
const agents = { ...cliAgents, ...localAgents };
|
|
2047
|
+
verbose(
|
|
2048
|
+
` Loaded ${Object.keys(localAgents).length} local agents, ${Object.keys(cliAgents).length} CLI agents`
|
|
2049
|
+
);
|
|
2050
|
+
const newStack = options.stack || await loadStackById(stackId, PROJECT_ROOT);
|
|
2051
|
+
const matrixPath = path13.join(PROJECT_ROOT, SKILLS_MATRIX_PATH);
|
|
2052
|
+
const matrix = await loadSkillsMatrix(matrixPath);
|
|
2053
|
+
const skillAliases = matrix.skill_aliases || {};
|
|
2054
|
+
let stack;
|
|
2055
|
+
if (newStack) {
|
|
2056
|
+
verbose(` Found stack: ${newStack.name}`);
|
|
2057
|
+
const agentSkillIds = /* @__PURE__ */ new Set();
|
|
2058
|
+
for (const agentName of typedKeys(newStack.agents)) {
|
|
2059
|
+
const agentConfig = newStack.agents[agentName];
|
|
2060
|
+
if (!agentConfig) continue;
|
|
2061
|
+
const skillRefs = resolveAgentConfigToSkills(agentConfig, skillAliases);
|
|
2062
|
+
for (const ref of skillRefs) {
|
|
2063
|
+
agentSkillIds.add(ref.id);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
stack = {
|
|
2067
|
+
name: newStack.name,
|
|
2068
|
+
description: newStack.description,
|
|
2069
|
+
agents: typedKeys(newStack.agents),
|
|
2070
|
+
skills: [...agentSkillIds],
|
|
2071
|
+
stack: buildStackProperty(newStack, skillAliases)
|
|
2072
|
+
};
|
|
2073
|
+
} else {
|
|
2074
|
+
throw new Error(`Stack '${stackId}' not found in config/stacks.yaml`);
|
|
2075
|
+
}
|
|
2076
|
+
const stackSkillIds = stack.stack ? [...new Set(Object.values(stack.stack).flatMap((a) => Object.values(a)))] : [];
|
|
2077
|
+
const skills = await loadSkillsByIds(
|
|
2078
|
+
stackSkillIds.map((id) => ({ id })),
|
|
2079
|
+
projectRoot
|
|
2080
|
+
);
|
|
2081
|
+
const compileConfig = stackToCompileConfig(stackId, stack);
|
|
2082
|
+
const resolvedAgents = await resolveAgents(
|
|
2083
|
+
agents,
|
|
2084
|
+
skills,
|
|
2085
|
+
compileConfig,
|
|
2086
|
+
projectRoot,
|
|
2087
|
+
newStack,
|
|
2088
|
+
skillAliases
|
|
2089
|
+
);
|
|
2090
|
+
const pluginDir = path13.join(outputDir, stackId);
|
|
2091
|
+
const agentsDir = path13.join(pluginDir, "agents");
|
|
2092
|
+
await ensureDir(pluginDir);
|
|
2093
|
+
await ensureDir(agentsDir);
|
|
2094
|
+
const pluginSkillsDir = path13.join(pluginDir, "skills");
|
|
2095
|
+
await ensureDir(pluginSkillsDir);
|
|
2096
|
+
const copiedSourcePaths = /* @__PURE__ */ new Set();
|
|
2097
|
+
for (const resolvedSkill of Object.values(skills)) {
|
|
2098
|
+
const sourceSkillDir = path13.join(projectRoot, resolvedSkill.path);
|
|
2099
|
+
if (copiedSourcePaths.has(resolvedSkill.path)) {
|
|
2100
|
+
continue;
|
|
2101
|
+
}
|
|
2102
|
+
const destSkillDir = path13.join(pluginSkillsDir, resolvedSkill.id);
|
|
2103
|
+
if (await directoryExists(sourceSkillDir)) {
|
|
2104
|
+
await copy(sourceSkillDir, destSkillDir);
|
|
2105
|
+
copiedSourcePaths.add(resolvedSkill.path);
|
|
2106
|
+
verbose(` Copied skill: ${resolvedSkill.id}`);
|
|
2107
|
+
} else {
|
|
2108
|
+
verbose(` Warning: Skill directory not found: ${sourceSkillDir}`);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
const engine = await createLiquidEngine();
|
|
2112
|
+
const compiledAgentNames = [];
|
|
2113
|
+
const allSkillPlugins = [];
|
|
2114
|
+
for (const [name, agent] of typedEntries(resolvedAgents)) {
|
|
2115
|
+
const output = await compileAgentForPlugin(name, agent, PROJECT_ROOT, engine);
|
|
2116
|
+
await writeFile(path13.join(agentsDir, `${name}.md`), output);
|
|
2117
|
+
compiledAgentNames.push(name);
|
|
2118
|
+
for (const skill of agent.skills) {
|
|
2119
|
+
allSkillPlugins.push(skill.id);
|
|
2120
|
+
}
|
|
2121
|
+
verbose(` Compiled agent: ${name}`);
|
|
2122
|
+
}
|
|
2123
|
+
const stackDir = path13.join(projectRoot, DIRS.stacks, stackId);
|
|
2124
|
+
const claudeMdPath = path13.join(stackDir, "CLAUDE.md");
|
|
2125
|
+
if (await fileExists(claudeMdPath)) {
|
|
2126
|
+
const claudeContent = await readFile(claudeMdPath);
|
|
2127
|
+
await writeFile(path13.join(pluginDir, "CLAUDE.md"), claudeContent);
|
|
2128
|
+
verbose(` Copied CLAUDE.md`);
|
|
2129
|
+
}
|
|
2130
|
+
const newHash = hashStackConfig(stack);
|
|
2131
|
+
const { version, contentHash } = await determinePluginVersion(
|
|
2132
|
+
newHash,
|
|
2133
|
+
pluginDir,
|
|
2134
|
+
getPluginManifestPath
|
|
2135
|
+
);
|
|
2136
|
+
const uniqueSkillPlugins = unique2(allSkillPlugins);
|
|
2137
|
+
const manifest = generateStackPluginManifest({
|
|
2138
|
+
stackName: stackId,
|
|
2139
|
+
description: stack.description,
|
|
2140
|
+
author: stack.author,
|
|
2141
|
+
version,
|
|
2142
|
+
keywords: void 0,
|
|
2143
|
+
hasAgents: true,
|
|
2144
|
+
hasHooks: false,
|
|
2145
|
+
hasSkills: true
|
|
2146
|
+
});
|
|
2147
|
+
await writePluginManifest(pluginDir, manifest);
|
|
2148
|
+
await writeContentHash(pluginDir, contentHash, getPluginManifestPath);
|
|
2149
|
+
verbose(` Wrote plugin.json (v${version})`);
|
|
2150
|
+
const readme = generateStackReadme(stackId, stack, compiledAgentNames, uniqueSkillPlugins);
|
|
2151
|
+
await writeFile(path13.join(pluginDir, "README.md"), readme);
|
|
2152
|
+
verbose(` Generated README.md`);
|
|
2153
|
+
return {
|
|
2154
|
+
pluginPath: pluginDir,
|
|
2155
|
+
manifest,
|
|
2156
|
+
stackName: stack.name,
|
|
2157
|
+
agents: compiledAgentNames,
|
|
2158
|
+
skillPlugins: uniqueSkillPlugins,
|
|
2159
|
+
hasHooks: false
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
function printStackCompilationSummary(result) {
|
|
2163
|
+
console.log(`
|
|
2164
|
+
Stack plugin compiled: ${result.stackName}`);
|
|
2165
|
+
console.log(` Path: ${result.pluginPath}`);
|
|
2166
|
+
console.log(` Agents: ${result.agents.length}`);
|
|
2167
|
+
for (const agent of result.agents) {
|
|
2168
|
+
console.log(` - ${agent}`);
|
|
2169
|
+
}
|
|
2170
|
+
if (result.skillPlugins.length > 0) {
|
|
2171
|
+
console.log(` Skills included: ${result.skillPlugins.length}`);
|
|
2172
|
+
for (const skill of result.skillPlugins) {
|
|
2173
|
+
console.log(` - ${skill}`);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
if (result.hasHooks) {
|
|
2177
|
+
console.log(` Hooks: enabled`);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// src/cli/utils/exec.ts
|
|
2182
|
+
init_esm_shims();
|
|
2183
|
+
import { spawn } from "child_process";
|
|
2184
|
+
async function execCommand(command, args, options) {
|
|
2185
|
+
return new Promise((resolve, reject) => {
|
|
2186
|
+
const proc = spawn(command, args, {
|
|
2187
|
+
cwd: options?.cwd,
|
|
2188
|
+
env: { ...process.env, ...options?.env },
|
|
2189
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2190
|
+
});
|
|
2191
|
+
let stdout = "";
|
|
2192
|
+
let stderr = "";
|
|
2193
|
+
proc.stdout.on("data", (data) => {
|
|
2194
|
+
stdout += data.toString();
|
|
2195
|
+
});
|
|
2196
|
+
proc.stderr.on("data", (data) => {
|
|
2197
|
+
stderr += data.toString();
|
|
2198
|
+
});
|
|
2199
|
+
proc.on("close", (code) => {
|
|
2200
|
+
resolve({
|
|
2201
|
+
stdout,
|
|
2202
|
+
stderr,
|
|
2203
|
+
exitCode: code ?? 1
|
|
2204
|
+
});
|
|
2205
|
+
});
|
|
2206
|
+
proc.on("error", (err) => {
|
|
2207
|
+
reject(err);
|
|
2208
|
+
});
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
async function claudePluginInstall(pluginPath, scope, projectDir) {
|
|
2212
|
+
const args = ["plugin", "install", pluginPath, "--scope", scope];
|
|
2213
|
+
const result = await execCommand("claude", args, { cwd: projectDir });
|
|
2214
|
+
if (result.exitCode !== 0) {
|
|
2215
|
+
const errorMessage = result.stderr || result.stdout || "Unknown error";
|
|
2216
|
+
throw new Error(`Plugin installation failed: ${errorMessage.trim()}`);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
async function isClaudeCLIAvailable() {
|
|
2220
|
+
try {
|
|
2221
|
+
const result = await execCommand("claude", ["--version"], {});
|
|
2222
|
+
return result.exitCode === 0;
|
|
2223
|
+
} catch {
|
|
2224
|
+
return false;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
async function claudePluginMarketplaceList() {
|
|
2228
|
+
try {
|
|
2229
|
+
const result = await execCommand("claude", ["plugin", "marketplace", "list", "--json"], {});
|
|
2230
|
+
if (result.exitCode !== 0) {
|
|
2231
|
+
return [];
|
|
2232
|
+
}
|
|
2233
|
+
let parsed;
|
|
2234
|
+
try {
|
|
2235
|
+
parsed = JSON.parse(result.stdout);
|
|
2236
|
+
} catch {
|
|
2237
|
+
warn("Failed to parse marketplace list output as JSON");
|
|
2238
|
+
return [];
|
|
2239
|
+
}
|
|
2240
|
+
if (!Array.isArray(parsed)) {
|
|
2241
|
+
warn("Unexpected marketplace list format \u2014 expected an array");
|
|
2242
|
+
return [];
|
|
2243
|
+
}
|
|
2244
|
+
return parsed;
|
|
2245
|
+
} catch {
|
|
2246
|
+
return [];
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
async function claudePluginMarketplaceExists(name) {
|
|
2250
|
+
const marketplaces = await claudePluginMarketplaceList();
|
|
2251
|
+
return marketplaces.some((m) => m.name === name);
|
|
2252
|
+
}
|
|
2253
|
+
async function claudePluginMarketplaceAdd(githubRepo, name) {
|
|
2254
|
+
const args = ["plugin", "marketplace", "add", githubRepo, "--name", name];
|
|
2255
|
+
let result;
|
|
2256
|
+
try {
|
|
2257
|
+
result = await execCommand("claude", args, {});
|
|
2258
|
+
} catch (err) {
|
|
2259
|
+
throw new Error(
|
|
2260
|
+
`Failed to add marketplace: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
if (result.exitCode !== 0) {
|
|
2264
|
+
const errorMessage = result.stderr || result.stdout || "Unknown error";
|
|
2265
|
+
if (errorMessage.includes("already installed")) {
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
throw new Error(`Failed to add marketplace: ${errorMessage.trim()}`);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
async function claudePluginUninstall(pluginName, scope, projectDir) {
|
|
2272
|
+
const args = ["plugin", "uninstall", pluginName, "--scope", scope];
|
|
2273
|
+
const result = await execCommand("claude", args, { cwd: projectDir });
|
|
2274
|
+
if (result.exitCode !== 0) {
|
|
2275
|
+
const errorMessage = result.stderr || result.stdout || "Unknown error";
|
|
2276
|
+
if (errorMessage.includes("not installed") || errorMessage.includes("not found")) {
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
throw new Error(`Plugin uninstall failed: ${errorMessage.trim()}`);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// src/cli/lib/stacks/stack-installer.ts
|
|
2284
|
+
async function compileStackToTemp(options) {
|
|
2285
|
+
const tempDir = path14.join(os.tmpdir(), `cc-stack-${Date.now()}`);
|
|
2286
|
+
await ensureDir(tempDir);
|
|
2287
|
+
const result = await compileStackPlugin({
|
|
2288
|
+
stackId: options.stackId,
|
|
2289
|
+
outputDir: tempDir,
|
|
2290
|
+
projectRoot: options.projectRoot,
|
|
2291
|
+
agentSourcePath: options.agentSourcePath
|
|
2292
|
+
});
|
|
2293
|
+
return {
|
|
2294
|
+
result,
|
|
2295
|
+
cleanup: async () => {
|
|
2296
|
+
await remove(tempDir);
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
async function installStackAsPlugin(options) {
|
|
2301
|
+
const { stackId, projectDir, sourcePath, agentSourcePath, marketplace } = options;
|
|
2302
|
+
const claudeAvailable = await isClaudeCLIAvailable();
|
|
2303
|
+
if (!claudeAvailable) {
|
|
2304
|
+
throw new Error(
|
|
2305
|
+
"Claude CLI not found. Please install Claude Code first: https://claude.ai/code"
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
if (marketplace) {
|
|
2309
|
+
verbose(`Installing from marketplace: ${stackId}@${marketplace}`);
|
|
2310
|
+
const pluginRef = `${stackId}@${marketplace}`;
|
|
2311
|
+
await claudePluginInstall(pluginRef, "project", projectDir);
|
|
2312
|
+
return {
|
|
2313
|
+
pluginName: stackId,
|
|
2314
|
+
stackName: stackId,
|
|
2315
|
+
agents: [],
|
|
2316
|
+
skills: [],
|
|
2317
|
+
pluginPath: pluginRef,
|
|
2318
|
+
fromMarketplace: true
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
verbose(`Compiling stack locally: ${stackId}`);
|
|
2322
|
+
const { result, cleanup } = await compileStackToTemp({
|
|
2323
|
+
stackId,
|
|
2324
|
+
projectRoot: sourcePath,
|
|
2325
|
+
agentSourcePath
|
|
2326
|
+
});
|
|
2327
|
+
try {
|
|
2328
|
+
await claudePluginInstall(result.pluginPath, "project", projectDir);
|
|
2329
|
+
return {
|
|
2330
|
+
pluginName: `stack-${stackId}`,
|
|
2331
|
+
stackName: result.stackName,
|
|
2332
|
+
agents: result.agents,
|
|
2333
|
+
skills: result.skillPlugins,
|
|
2334
|
+
pluginPath: result.pluginPath,
|
|
2335
|
+
fromMarketplace: false
|
|
2336
|
+
};
|
|
2337
|
+
} finally {
|
|
2338
|
+
await cleanup();
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// src/cli/lib/installation/local-installer.ts
|
|
2343
|
+
var PLUGIN_NAME = "claude-collective";
|
|
2344
|
+
var YAML_INDENT = 2;
|
|
2345
|
+
var YAML_LINE_WIDTH = 120;
|
|
2346
|
+
function buildLocalSkillsMap(copiedSkills, matrix) {
|
|
2347
|
+
const localSkillsForResolution = {};
|
|
2348
|
+
for (const copiedSkill of copiedSkills) {
|
|
2349
|
+
const skill = matrix.skills[copiedSkill.skillId];
|
|
2350
|
+
if (skill) {
|
|
2351
|
+
localSkillsForResolution[copiedSkill.skillId] = {
|
|
2352
|
+
id: copiedSkill.skillId,
|
|
2353
|
+
description: skill.description || "",
|
|
2354
|
+
path: copiedSkill.destPath,
|
|
2355
|
+
content: ""
|
|
2356
|
+
// Content not needed for skill references
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
return localSkillsForResolution;
|
|
2361
|
+
}
|
|
2362
|
+
async function buildLocalConfig(wizardResult, sourceResult) {
|
|
2363
|
+
let loadedStack = null;
|
|
2364
|
+
if (wizardResult.selectedStackId) {
|
|
2365
|
+
loadedStack = await loadStackById(wizardResult.selectedStackId, sourceResult.sourcePath);
|
|
2366
|
+
if (!loadedStack) {
|
|
2367
|
+
loadedStack = await loadStackById(wizardResult.selectedStackId, PROJECT_ROOT);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
let localConfig;
|
|
2371
|
+
if (wizardResult.selectedStackId) {
|
|
2372
|
+
if (loadedStack) {
|
|
2373
|
+
localConfig = generateProjectConfigFromSkills(
|
|
2374
|
+
PLUGIN_NAME,
|
|
2375
|
+
wizardResult.selectedSkills,
|
|
2376
|
+
sourceResult.matrix
|
|
2377
|
+
);
|
|
2378
|
+
localConfig.description = loadedStack.description;
|
|
2379
|
+
const stackAgentIds = typedKeys(loadedStack.agents);
|
|
2380
|
+
for (const agentId of stackAgentIds) {
|
|
2381
|
+
if (!localConfig.agents.includes(agentId)) {
|
|
2382
|
+
localConfig.agents.push(agentId);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
localConfig.agents.sort();
|
|
2386
|
+
} else {
|
|
2387
|
+
throw new Error(
|
|
2388
|
+
`Stack '${wizardResult.selectedStackId}' not found in config/stacks.yaml. Available stacks are defined in the CLI's config/stacks.yaml file.`
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
} else {
|
|
2392
|
+
localConfig = generateProjectConfigFromSkills(
|
|
2393
|
+
PLUGIN_NAME,
|
|
2394
|
+
wizardResult.selectedSkills,
|
|
2395
|
+
sourceResult.matrix
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
return { config: localConfig, loadedStack };
|
|
2399
|
+
}
|
|
2400
|
+
function setConfigMetadata(config, wizardResult, sourceResult, sourceFlag) {
|
|
2401
|
+
config.installMode = wizardResult.installMode;
|
|
2402
|
+
if (sourceFlag) {
|
|
2403
|
+
config.source = sourceFlag;
|
|
2404
|
+
} else if (sourceResult.sourceConfig.source) {
|
|
2405
|
+
config.source = sourceResult.sourceConfig.source;
|
|
2406
|
+
}
|
|
2407
|
+
if (sourceResult.marketplace) {
|
|
2408
|
+
config.marketplace = sourceResult.marketplace;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
function buildCompileAgents(config, agents) {
|
|
2412
|
+
const compileAgents = {};
|
|
2413
|
+
for (const agentId of config.agents) {
|
|
2414
|
+
if (agents[agentId]) {
|
|
2415
|
+
const agentStack = config.stack?.[agentId];
|
|
2416
|
+
compileAgents[agentId] = agentStack ? { skills: buildSkillRefsFromConfig(agentStack) } : {};
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
return compileAgents;
|
|
2420
|
+
}
|
|
2421
|
+
async function compileAndWriteAgents(compileConfig, agents, localSkills, sourceResult, projectDir, agentsDir, installMode) {
|
|
2422
|
+
const engine = await createLiquidEngine(projectDir);
|
|
2423
|
+
const resolvedAgents = await resolveAgents(
|
|
2424
|
+
agents,
|
|
2425
|
+
localSkills,
|
|
2426
|
+
compileConfig,
|
|
2427
|
+
sourceResult.sourcePath
|
|
2428
|
+
);
|
|
2429
|
+
const compiledAgentNames = [];
|
|
2430
|
+
for (const [name, agent] of typedEntries(resolvedAgents)) {
|
|
2431
|
+
const output = await compileAgentForPlugin(
|
|
2432
|
+
name,
|
|
2433
|
+
agent,
|
|
2434
|
+
sourceResult.sourcePath,
|
|
2435
|
+
engine,
|
|
2436
|
+
installMode
|
|
2437
|
+
);
|
|
2438
|
+
await writeFile(path15.join(agentsDir, `${name}.md`), output);
|
|
2439
|
+
compiledAgentNames.push(name);
|
|
2440
|
+
}
|
|
2441
|
+
return compiledAgentNames;
|
|
2442
|
+
}
|
|
2443
|
+
async function installLocal(options) {
|
|
2444
|
+
const { wizardResult, sourceResult, projectDir, sourceFlag } = options;
|
|
2445
|
+
const matrix = sourceResult.matrix;
|
|
2446
|
+
const localSkillsDir = path15.join(projectDir, LOCAL_SKILLS_PATH);
|
|
2447
|
+
const localAgentsDir = path15.join(projectDir, CLAUDE_DIR, "agents");
|
|
2448
|
+
const localConfigPath = path15.join(projectDir, CLAUDE_SRC_DIR, "config.yaml");
|
|
2449
|
+
await ensureDir(localSkillsDir);
|
|
2450
|
+
await ensureDir(localAgentsDir);
|
|
2451
|
+
await ensureDir(path15.dirname(localConfigPath));
|
|
2452
|
+
for (const skillId of wizardResult.selectedSkills) {
|
|
2453
|
+
const selectedSource = wizardResult.sourceSelections?.[skillId];
|
|
2454
|
+
if (selectedSource && selectedSource !== "public") {
|
|
2455
|
+
verbose(`Using alternate source '${selectedSource}' for ${skillId}`);
|
|
2456
|
+
await archiveLocalSkill(projectDir, skillId);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
const copiedSkills = await copySkillsToLocalFlattened(
|
|
2460
|
+
wizardResult.selectedSkills,
|
|
2461
|
+
localSkillsDir,
|
|
2462
|
+
matrix,
|
|
2463
|
+
sourceResult
|
|
2464
|
+
);
|
|
2465
|
+
const localSkillsForResolution = buildLocalSkillsMap(copiedSkills, matrix);
|
|
2466
|
+
const cliAgents = await loadAllAgents(PROJECT_ROOT);
|
|
2467
|
+
const localAgents = await loadAllAgents(sourceResult.sourcePath);
|
|
2468
|
+
const agents = { ...cliAgents, ...localAgents };
|
|
2469
|
+
const { config: builtConfig } = await buildLocalConfig(wizardResult, sourceResult);
|
|
2470
|
+
setConfigMetadata(builtConfig, wizardResult, sourceResult, sourceFlag);
|
|
2471
|
+
const mergeResult = await mergeWithExistingConfig(builtConfig, { projectDir });
|
|
2472
|
+
const finalConfig = mergeResult.config;
|
|
2473
|
+
const configYaml = stringifyYaml4(finalConfig, {
|
|
2474
|
+
indent: YAML_INDENT,
|
|
2475
|
+
lineWidth: YAML_LINE_WIDTH
|
|
2476
|
+
});
|
|
2477
|
+
await writeFile(localConfigPath, configYaml);
|
|
2478
|
+
const compileAgentsConfig = buildCompileAgents(finalConfig, agents);
|
|
2479
|
+
const compileConfig = {
|
|
2480
|
+
name: PLUGIN_NAME,
|
|
2481
|
+
description: finalConfig.description || `Local setup with ${wizardResult.selectedSkills.length} skills`,
|
|
2482
|
+
agents: compileAgentsConfig
|
|
2483
|
+
};
|
|
2484
|
+
const compiledAgentNames = await compileAndWriteAgents(
|
|
2485
|
+
compileConfig,
|
|
2486
|
+
agents,
|
|
2487
|
+
localSkillsForResolution,
|
|
2488
|
+
sourceResult,
|
|
2489
|
+
projectDir,
|
|
2490
|
+
localAgentsDir,
|
|
2491
|
+
wizardResult.installMode
|
|
2492
|
+
);
|
|
2493
|
+
return {
|
|
2494
|
+
copiedSkills,
|
|
2495
|
+
config: finalConfig,
|
|
2496
|
+
configPath: localConfigPath,
|
|
2497
|
+
compiledAgents: compiledAgentNames,
|
|
2498
|
+
wasMerged: mergeResult.merged,
|
|
2499
|
+
mergedConfigPath: mergeResult.existingConfigPath,
|
|
2500
|
+
skillsDir: localSkillsDir,
|
|
2501
|
+
agentsDir: localAgentsDir
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// src/cli/lib/plugins/plugin-info.ts
|
|
2506
|
+
var DEFAULT_NAME = "claude-collective";
|
|
2507
|
+
async function getInstallationInfo() {
|
|
2508
|
+
const installation = await detectInstallation();
|
|
2509
|
+
if (!installation) {
|
|
2510
|
+
return null;
|
|
2511
|
+
}
|
|
2512
|
+
let skillCount = 0;
|
|
2513
|
+
let agentCount = 0;
|
|
2514
|
+
let name = DEFAULT_NAME;
|
|
2515
|
+
let version = DEFAULT_DISPLAY_VERSION;
|
|
2516
|
+
if (await directoryExists(installation.skillsDir)) {
|
|
2517
|
+
try {
|
|
2518
|
+
const skills = await readdir(installation.skillsDir, {
|
|
2519
|
+
withFileTypes: true
|
|
2520
|
+
});
|
|
2521
|
+
skillCount = skills.filter((s) => s.isDirectory()).length;
|
|
2522
|
+
} catch {
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
if (await directoryExists(installation.agentsDir)) {
|
|
2526
|
+
try {
|
|
2527
|
+
const agents = await readdir(installation.agentsDir, {
|
|
2528
|
+
withFileTypes: true
|
|
2529
|
+
});
|
|
2530
|
+
agentCount = agents.filter((a) => a.isFile() && a.name.endsWith(".md")).length;
|
|
2531
|
+
} catch {
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
if (installation.mode === "local") {
|
|
2535
|
+
const loaded = await loadProjectConfig(installation.projectDir);
|
|
2536
|
+
if (loaded?.config) {
|
|
2537
|
+
name = loaded.config.name || DEFAULT_NAME;
|
|
2538
|
+
version = "local";
|
|
2539
|
+
}
|
|
2540
|
+
} else {
|
|
2541
|
+
const pluginDir = getCollectivePluginDir(installation.projectDir);
|
|
2542
|
+
const manifest = await readPluginManifest(pluginDir);
|
|
2543
|
+
if (manifest) {
|
|
2544
|
+
name = manifest.name || DEFAULT_NAME;
|
|
2545
|
+
version = manifest.version || DEFAULT_DISPLAY_VERSION;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
return {
|
|
2549
|
+
mode: installation.mode,
|
|
2550
|
+
name,
|
|
2551
|
+
version,
|
|
2552
|
+
skillCount,
|
|
2553
|
+
agentCount,
|
|
2554
|
+
configPath: installation.configPath,
|
|
2555
|
+
agentsDir: installation.agentsDir,
|
|
2556
|
+
skillsDir: installation.skillsDir
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
function formatInstallationDisplay(info) {
|
|
2560
|
+
const modeLabel = info.mode === "local" ? "Local" : "Plugin";
|
|
2561
|
+
const versionDisplay = info.mode === "local" ? "(local mode)" : `v${info.version}`;
|
|
2562
|
+
return `Installation: ${info.name} ${versionDisplay}
|
|
2563
|
+
Mode: ${modeLabel}
|
|
2564
|
+
Skills: ${info.skillCount}
|
|
2565
|
+
Agents: ${info.agentCount}
|
|
2566
|
+
Config: ${info.configPath}
|
|
2567
|
+
Agents: ${info.agentsDir}`;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// src/cli/lib/plugins/plugin-version.ts
|
|
2571
|
+
init_esm_shims();
|
|
2572
|
+
import path16 from "path";
|
|
2573
|
+
function parseVersion(version) {
|
|
2574
|
+
const parts = version.split(".").map(Number);
|
|
2575
|
+
return [parts[0] || 1, parts[1] || 0, parts[2] || 0];
|
|
2576
|
+
}
|
|
2577
|
+
async function bumpPluginVersion(pluginDir, type) {
|
|
2578
|
+
const manifestPath = path16.join(pluginDir, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
|
|
2579
|
+
const content = await readFile(manifestPath);
|
|
2580
|
+
const manifest = pluginManifestSchema.parse(JSON.parse(content));
|
|
2581
|
+
const [major, minor, patch] = parseVersion(manifest.version || DEFAULT_VERSION);
|
|
2582
|
+
let newVersion;
|
|
2583
|
+
switch (type) {
|
|
2584
|
+
case "major":
|
|
2585
|
+
newVersion = `${major + 1}.0.0`;
|
|
2586
|
+
break;
|
|
2587
|
+
case "minor":
|
|
2588
|
+
newVersion = `${major}.${minor + 1}.0`;
|
|
2589
|
+
break;
|
|
2590
|
+
case "patch":
|
|
2591
|
+
newVersion = `${major}.${minor}.${patch + 1}`;
|
|
2592
|
+
break;
|
|
2593
|
+
}
|
|
2594
|
+
manifest.version = newVersion;
|
|
2595
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
2596
|
+
return newVersion;
|
|
2597
|
+
}
|
|
2598
|
+
async function getPluginVersion(pluginDir) {
|
|
2599
|
+
const manifestPath = path16.join(pluginDir, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
|
|
2600
|
+
const content = await readFile(manifestPath);
|
|
2601
|
+
const manifest = pluginManifestSchema.parse(JSON.parse(content));
|
|
2602
|
+
return manifest.version || DEFAULT_VERSION;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
// src/cli/lib/plugins/plugin-validator.ts
|
|
2606
|
+
init_esm_shims();
|
|
2607
|
+
import { z as z2 } from "zod";
|
|
2608
|
+
import path17 from "path";
|
|
2609
|
+
import fg from "fast-glob";
|
|
2610
|
+
import { countBy as countBy2 } from "remeda";
|
|
2611
|
+
var PLUGIN_DIR = ".claude-plugin";
|
|
2612
|
+
var PLUGIN_MANIFEST = "plugin.json";
|
|
2613
|
+
var KEBAB_CASE_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2614
|
+
var SEMVER_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
2615
|
+
var pluginManifestValidationSchema = z2.object({
|
|
2616
|
+
name: z2.string(),
|
|
2617
|
+
version: z2.string().optional(),
|
|
2618
|
+
description: z2.string().optional(),
|
|
2619
|
+
author: pluginAuthorSchema.optional(),
|
|
2620
|
+
keywords: z2.array(z2.string()).optional(),
|
|
2621
|
+
commands: z2.union([z2.string(), z2.array(z2.string())]).optional(),
|
|
2622
|
+
agents: z2.union([z2.string(), z2.array(z2.string())]).optional(),
|
|
2623
|
+
skills: z2.union([z2.string(), z2.array(z2.string())]).optional(),
|
|
2624
|
+
hooks: z2.union([z2.string(), hooksRecordSchema]).optional()
|
|
2625
|
+
}).strict();
|
|
2626
|
+
function formatZodErrors(error) {
|
|
2627
|
+
return error.issues.map((issue) => {
|
|
2628
|
+
const path26 = issue.path.join(".");
|
|
2629
|
+
if (issue.code === "unrecognized_keys") {
|
|
2630
|
+
return `Unrecognized key: "${issue.keys.join('", "')}"`;
|
|
2631
|
+
}
|
|
2632
|
+
return path26 ? `${path26}: ${issue.message}` : issue.message;
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
function isKebabCase(str) {
|
|
2636
|
+
return KEBAB_CASE_REGEX.test(str);
|
|
2637
|
+
}
|
|
2638
|
+
function isValidSemver(str) {
|
|
2639
|
+
return SEMVER_REGEX.test(str);
|
|
2640
|
+
}
|
|
2641
|
+
async function validatePluginStructure(pluginPath) {
|
|
2642
|
+
const errors = [];
|
|
2643
|
+
const warnings = [];
|
|
2644
|
+
if (!await directoryExists(pluginPath)) {
|
|
2645
|
+
return {
|
|
2646
|
+
valid: false,
|
|
2647
|
+
errors: [`Plugin directory does not exist: ${pluginPath}`],
|
|
2648
|
+
warnings: []
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
const pluginDir = path17.join(pluginPath, PLUGIN_DIR);
|
|
2652
|
+
if (!await directoryExists(pluginDir)) {
|
|
2653
|
+
errors.push(`Missing ${PLUGIN_DIR}/ directory`);
|
|
2654
|
+
}
|
|
2655
|
+
const manifestPath = path17.join(pluginDir, PLUGIN_MANIFEST);
|
|
2656
|
+
if (!await fileExists(manifestPath)) {
|
|
2657
|
+
errors.push(`Missing ${PLUGIN_DIR}/${PLUGIN_MANIFEST}`);
|
|
2658
|
+
}
|
|
2659
|
+
const readmePath = path17.join(pluginPath, "README.md");
|
|
2660
|
+
if (!await fileExists(readmePath)) {
|
|
2661
|
+
warnings.push("Missing README.md (recommended for documentation)");
|
|
2662
|
+
}
|
|
2663
|
+
return {
|
|
2664
|
+
valid: errors.length === 0,
|
|
2665
|
+
errors,
|
|
2666
|
+
warnings
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
async function validatePluginManifest(manifestPath) {
|
|
2670
|
+
const errors = [];
|
|
2671
|
+
const warnings = [];
|
|
2672
|
+
if (!await fileExists(manifestPath)) {
|
|
2673
|
+
return {
|
|
2674
|
+
valid: false,
|
|
2675
|
+
errors: [`Manifest file not found: ${manifestPath}`],
|
|
2676
|
+
warnings: []
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
let manifest;
|
|
2680
|
+
try {
|
|
2681
|
+
const content = await readFile(manifestPath);
|
|
2682
|
+
manifest = JSON.parse(content);
|
|
2683
|
+
} catch (err) {
|
|
2684
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2685
|
+
return {
|
|
2686
|
+
valid: false,
|
|
2687
|
+
errors: [`Invalid JSON in ${PLUGIN_MANIFEST}: ${message}`],
|
|
2688
|
+
warnings: []
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
const result = pluginManifestValidationSchema.safeParse(manifest);
|
|
2692
|
+
if (!result.success) {
|
|
2693
|
+
errors.push(...formatZodErrors(result.error));
|
|
2694
|
+
}
|
|
2695
|
+
if (manifest.name && typeof manifest.name === "string") {
|
|
2696
|
+
if (!isKebabCase(manifest.name)) {
|
|
2697
|
+
errors.push(`name must be kebab-case: "${manifest.name}"`);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
if (manifest.version && typeof manifest.version === "string") {
|
|
2701
|
+
if (!isValidSemver(manifest.version)) {
|
|
2702
|
+
errors.push(
|
|
2703
|
+
`version "${manifest.version}" is not valid semver (expected: major.minor.patch)`
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
if (!manifest.description) {
|
|
2708
|
+
warnings.push("Missing description field (recommended for discoverability)");
|
|
2709
|
+
}
|
|
2710
|
+
const pluginDir = path17.dirname(path17.dirname(manifestPath));
|
|
2711
|
+
if (manifest.skills && typeof manifest.skills === "string") {
|
|
2712
|
+
const skillsPath = path17.join(pluginDir, manifest.skills);
|
|
2713
|
+
if (!await directoryExists(skillsPath)) {
|
|
2714
|
+
errors.push(`Skills path does not exist: ${manifest.skills}`);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
if (manifest.agents && typeof manifest.agents === "string") {
|
|
2718
|
+
const agentsPath = path17.join(pluginDir, manifest.agents);
|
|
2719
|
+
if (!await directoryExists(agentsPath)) {
|
|
2720
|
+
errors.push(`Agents path does not exist: ${manifest.agents}`);
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
return {
|
|
2724
|
+
valid: errors.length === 0,
|
|
2725
|
+
errors,
|
|
2726
|
+
warnings
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
async function validateSkillFrontmatter(skillPath) {
|
|
2730
|
+
const errors = [];
|
|
2731
|
+
const warnings = [];
|
|
2732
|
+
if (!await fileExists(skillPath)) {
|
|
2733
|
+
return {
|
|
2734
|
+
valid: false,
|
|
2735
|
+
errors: [`Skill file not found: ${skillPath}`],
|
|
2736
|
+
warnings: []
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2739
|
+
const content = await readFile(skillPath);
|
|
2740
|
+
const frontmatter = extractFrontmatter(content);
|
|
2741
|
+
if (frontmatter === null) {
|
|
2742
|
+
return {
|
|
2743
|
+
valid: false,
|
|
2744
|
+
errors: ["Missing or invalid YAML frontmatter"],
|
|
2745
|
+
warnings: []
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
const result = skillFrontmatterValidationSchema.safeParse(frontmatter);
|
|
2749
|
+
if (!result.success) {
|
|
2750
|
+
errors.push(...formatZodErrors(result.error));
|
|
2751
|
+
}
|
|
2752
|
+
return {
|
|
2753
|
+
valid: errors.length === 0,
|
|
2754
|
+
errors,
|
|
2755
|
+
warnings
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
async function validateAgentFrontmatter(agentPath) {
|
|
2759
|
+
const errors = [];
|
|
2760
|
+
const warnings = [];
|
|
2761
|
+
if (!await fileExists(agentPath)) {
|
|
2762
|
+
return {
|
|
2763
|
+
valid: false,
|
|
2764
|
+
errors: [`Agent file not found: ${agentPath}`],
|
|
2765
|
+
warnings: []
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
const content = await readFile(agentPath);
|
|
2769
|
+
const frontmatter = extractFrontmatter(content);
|
|
2770
|
+
if (frontmatter === null) {
|
|
2771
|
+
return {
|
|
2772
|
+
valid: false,
|
|
2773
|
+
errors: ["Missing or invalid YAML frontmatter"],
|
|
2774
|
+
warnings: []
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
const result = agentFrontmatterValidationSchema.safeParse(frontmatter);
|
|
2778
|
+
if (!result.success) {
|
|
2779
|
+
errors.push(...formatZodErrors(result.error));
|
|
2780
|
+
}
|
|
2781
|
+
const fm = frontmatter;
|
|
2782
|
+
if (fm.name && typeof fm.name === "string") {
|
|
2783
|
+
if (!isKebabCase(fm.name)) {
|
|
2784
|
+
errors.push(`name must be kebab-case: "${fm.name}"`);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
return {
|
|
2788
|
+
valid: errors.length === 0,
|
|
2789
|
+
errors,
|
|
2790
|
+
warnings
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
async function validatePlugin(pluginPath) {
|
|
2794
|
+
const errors = [];
|
|
2795
|
+
const warnings = [];
|
|
2796
|
+
const structureResult = await validatePluginStructure(pluginPath);
|
|
2797
|
+
errors.push(...structureResult.errors);
|
|
2798
|
+
warnings.push(...structureResult.warnings);
|
|
2799
|
+
if (!structureResult.valid) {
|
|
2800
|
+
return { valid: false, errors, warnings };
|
|
2801
|
+
}
|
|
2802
|
+
const manifestPath = path17.join(pluginPath, PLUGIN_DIR, PLUGIN_MANIFEST);
|
|
2803
|
+
const manifestResult = await validatePluginManifest(manifestPath);
|
|
2804
|
+
errors.push(...manifestResult.errors);
|
|
2805
|
+
warnings.push(...manifestResult.warnings);
|
|
2806
|
+
let manifest = null;
|
|
2807
|
+
try {
|
|
2808
|
+
const content = await readFile(manifestPath);
|
|
2809
|
+
manifest = JSON.parse(content);
|
|
2810
|
+
} catch {
|
|
2811
|
+
}
|
|
2812
|
+
if (manifest) {
|
|
2813
|
+
if (manifest.skills && typeof manifest.skills === "string") {
|
|
2814
|
+
const skillsDir = path17.join(pluginPath, manifest.skills);
|
|
2815
|
+
if (await directoryExists(skillsDir)) {
|
|
2816
|
+
const skillFiles = await fg("**/SKILL.md", {
|
|
2817
|
+
cwd: skillsDir,
|
|
2818
|
+
absolute: true
|
|
2819
|
+
});
|
|
2820
|
+
if (skillFiles.length === 0) {
|
|
2821
|
+
warnings.push(
|
|
2822
|
+
`Skills directory exists but contains no SKILL.md files: ${manifest.skills}`
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
for (const skillFile of skillFiles) {
|
|
2826
|
+
const relativePath = path17.relative(pluginPath, skillFile);
|
|
2827
|
+
const skillResult = await validateSkillFrontmatter(skillFile);
|
|
2828
|
+
if (!skillResult.valid) {
|
|
2829
|
+
errors.push(...skillResult.errors.map((e) => `${relativePath}: ${e}`));
|
|
2830
|
+
}
|
|
2831
|
+
warnings.push(...skillResult.warnings.map((w) => `${relativePath}: ${w}`));
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
if (manifest.agents && typeof manifest.agents === "string") {
|
|
2836
|
+
const agentsDir = path17.join(pluginPath, manifest.agents);
|
|
2837
|
+
if (await directoryExists(agentsDir)) {
|
|
2838
|
+
const agentFiles = await fg("*.md", {
|
|
2839
|
+
cwd: agentsDir,
|
|
2840
|
+
absolute: true
|
|
2841
|
+
});
|
|
2842
|
+
if (agentFiles.length === 0) {
|
|
2843
|
+
warnings.push(`Agents directory exists but contains no .md files: ${manifest.agents}`);
|
|
2844
|
+
}
|
|
2845
|
+
for (const agentFile of agentFiles) {
|
|
2846
|
+
const relativePath = path17.relative(pluginPath, agentFile);
|
|
2847
|
+
const agentResult = await validateAgentFrontmatter(agentFile);
|
|
2848
|
+
if (!agentResult.valid) {
|
|
2849
|
+
errors.push(...agentResult.errors.map((e) => `${relativePath}: ${e}`));
|
|
2850
|
+
}
|
|
2851
|
+
warnings.push(...agentResult.warnings.map((w) => `${relativePath}: ${w}`));
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return {
|
|
2857
|
+
valid: errors.length === 0,
|
|
2858
|
+
errors,
|
|
2859
|
+
warnings
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
async function validateAllPlugins(pluginsDir) {
|
|
2863
|
+
const results = [];
|
|
2864
|
+
if (!await directoryExists(pluginsDir)) {
|
|
2865
|
+
return {
|
|
2866
|
+
valid: false,
|
|
2867
|
+
results: [
|
|
2868
|
+
{
|
|
2869
|
+
name: pluginsDir,
|
|
2870
|
+
result: {
|
|
2871
|
+
valid: false,
|
|
2872
|
+
errors: [`Directory does not exist: ${pluginsDir}`],
|
|
2873
|
+
warnings: []
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
],
|
|
2877
|
+
summary: { total: 0, valid: 0, invalid: 1, withWarnings: 0 }
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
const allDirs = await listDirectories(pluginsDir);
|
|
2881
|
+
const pluginDirs = [];
|
|
2882
|
+
for (const dirName of allDirs) {
|
|
2883
|
+
const potentialPluginDir = path17.join(pluginsDir, dirName, PLUGIN_DIR);
|
|
2884
|
+
if (await directoryExists(potentialPluginDir)) {
|
|
2885
|
+
pluginDirs.push(dirName);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
if (pluginDirs.length === 0) {
|
|
2889
|
+
return {
|
|
2890
|
+
valid: false,
|
|
2891
|
+
results: [
|
|
2892
|
+
{
|
|
2893
|
+
name: pluginsDir,
|
|
2894
|
+
result: {
|
|
2895
|
+
valid: false,
|
|
2896
|
+
errors: [
|
|
2897
|
+
`No plugins found in directory: ${pluginsDir}. Plugins must contain a ${PLUGIN_DIR}/ directory.`
|
|
2898
|
+
],
|
|
2899
|
+
warnings: []
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
],
|
|
2903
|
+
summary: { total: 0, valid: 0, invalid: 1, withWarnings: 0 }
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
for (const pluginName of pluginDirs) {
|
|
2907
|
+
const pluginPath = path17.join(pluginsDir, pluginName);
|
|
2908
|
+
const result = await validatePlugin(pluginPath);
|
|
2909
|
+
results.push({ name: pluginName, result });
|
|
2910
|
+
}
|
|
2911
|
+
const summary = {
|
|
2912
|
+
total: results.length,
|
|
2913
|
+
valid: countBy2(results, (r) => String(r.result.valid))["true"] ?? 0,
|
|
2914
|
+
invalid: countBy2(results, (r) => String(r.result.valid))["false"] ?? 0,
|
|
2915
|
+
withWarnings: countBy2(results, (r) => String(r.result.warnings.length > 0))["true"] ?? 0
|
|
2916
|
+
};
|
|
2917
|
+
return {
|
|
2918
|
+
valid: summary.invalid === 0,
|
|
2919
|
+
results,
|
|
2920
|
+
summary
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
function printPluginValidationResult(name, result, verbose2 = false) {
|
|
2924
|
+
const status = result.valid ? "\u2713" : "\u2717";
|
|
2925
|
+
if (result.valid && result.warnings.length === 0 && !verbose2) {
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
console.log(`
|
|
2929
|
+
${status} ${name}`);
|
|
2930
|
+
if (result.errors.length > 0) {
|
|
2931
|
+
console.log(" Errors:");
|
|
2932
|
+
result.errors.forEach((e) => console.log(` - ${e}`));
|
|
2933
|
+
}
|
|
2934
|
+
if (result.warnings.length > 0) {
|
|
2935
|
+
console.log(" Warnings:");
|
|
2936
|
+
result.warnings.forEach((w) => console.log(` - ${w}`));
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
// src/cli/lib/loading/multi-source-loader.ts
|
|
2941
|
+
var PUBLIC_SOURCE_NAME = "public";
|
|
2942
|
+
async function loadSkillsFromAllSources(primaryMatrix, sourceConfig, projectDir) {
|
|
2943
|
+
tagPrimarySourceSkills(primaryMatrix);
|
|
2944
|
+
tagLocalSkills(primaryMatrix);
|
|
2945
|
+
await tagPluginSkills(primaryMatrix, projectDir);
|
|
2946
|
+
await tagExtraSources(primaryMatrix, projectDir);
|
|
2947
|
+
setActiveSources(primaryMatrix);
|
|
2948
|
+
}
|
|
2949
|
+
function tagPrimarySourceSkills(matrix) {
|
|
2950
|
+
for (const [, skill] of typedEntries(
|
|
2951
|
+
matrix.skills
|
|
2952
|
+
)) {
|
|
2953
|
+
if (!skill) continue;
|
|
2954
|
+
const source = {
|
|
2955
|
+
name: PUBLIC_SOURCE_NAME,
|
|
2956
|
+
type: "public",
|
|
2957
|
+
version: skill.version,
|
|
2958
|
+
installed: false
|
|
2959
|
+
};
|
|
2960
|
+
skill.availableSources = skill.availableSources ?? [];
|
|
2961
|
+
skill.availableSources.push(source);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
function tagLocalSkills(matrix) {
|
|
2965
|
+
let count = 0;
|
|
2966
|
+
for (const [, skill] of typedEntries(
|
|
2967
|
+
matrix.skills
|
|
2968
|
+
)) {
|
|
2969
|
+
if (!skill) continue;
|
|
2970
|
+
if (!skill.local) continue;
|
|
2971
|
+
const source = {
|
|
2972
|
+
name: "local",
|
|
2973
|
+
type: "local",
|
|
2974
|
+
installed: true,
|
|
2975
|
+
installMode: "local"
|
|
2976
|
+
};
|
|
2977
|
+
skill.availableSources = skill.availableSources ?? [];
|
|
2978
|
+
skill.availableSources.push(source);
|
|
2979
|
+
count++;
|
|
2980
|
+
}
|
|
2981
|
+
verbose(`Tagged ${count} local skills with local source`);
|
|
2982
|
+
}
|
|
2983
|
+
async function tagPluginSkills(matrix, projectDir) {
|
|
2984
|
+
const pluginDir = getCollectivePluginDir(projectDir);
|
|
2985
|
+
if (!await directoryExists(pluginDir)) {
|
|
2986
|
+
verbose("No plugin directory found, skipping plugin skill tagging");
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
const pluginSkillsDir = getPluginSkillsDir(pluginDir);
|
|
2990
|
+
if (!await directoryExists(pluginSkillsDir)) {
|
|
2991
|
+
verbose("No plugin skills directory found, skipping plugin skill tagging");
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
try {
|
|
2995
|
+
const pluginSkillIds = await getPluginSkillIds(pluginSkillsDir, matrix);
|
|
2996
|
+
for (const skillId of pluginSkillIds) {
|
|
2997
|
+
const skill = matrix.skills[skillId];
|
|
2998
|
+
if (!skill) continue;
|
|
2999
|
+
skill.availableSources = skill.availableSources ?? [];
|
|
3000
|
+
const existingSource = skill.availableSources.find((s) => s.type === "public");
|
|
3001
|
+
if (existingSource && !existingSource.installMode) {
|
|
3002
|
+
existingSource.installed = true;
|
|
3003
|
+
existingSource.installMode = "plugin";
|
|
3004
|
+
} else if (!skill.availableSources.some((s) => s.installMode === "plugin")) {
|
|
3005
|
+
skill.availableSources.push({
|
|
3006
|
+
name: PUBLIC_SOURCE_NAME,
|
|
3007
|
+
type: "public",
|
|
3008
|
+
version: skill.version,
|
|
3009
|
+
installed: true,
|
|
3010
|
+
installMode: "plugin"
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
verbose(`Tagged ${pluginSkillIds.length} plugin-installed skills`);
|
|
3015
|
+
} catch (error) {
|
|
3016
|
+
verbose(`Failed to detect plugin skills: ${error}`);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
async function tagExtraSources(matrix, projectDir) {
|
|
3020
|
+
let allSources;
|
|
3021
|
+
try {
|
|
3022
|
+
allSources = await resolveAllSources(projectDir);
|
|
3023
|
+
} catch (error) {
|
|
3024
|
+
verbose(`Failed to resolve extra sources: ${error}`);
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
3027
|
+
if (allSources.extras.length === 0) {
|
|
3028
|
+
verbose("No extra sources configured");
|
|
3029
|
+
return;
|
|
3030
|
+
}
|
|
3031
|
+
for (const extraSource of allSources.extras) {
|
|
3032
|
+
verbose(`Loading extra source: ${extraSource.name} (${extraSource.url})`);
|
|
3033
|
+
try {
|
|
3034
|
+
const fetchResult = await fetchFromSource(extraSource.url, { forceRefresh: false });
|
|
3035
|
+
const skillsDir = path18.join(fetchResult.path, SKILLS_DIR_PATH);
|
|
3036
|
+
const skills = await extractAllSkills(skillsDir);
|
|
3037
|
+
let matchCount = 0;
|
|
3038
|
+
for (const extractedSkill of skills) {
|
|
3039
|
+
const matrixSkill = matrix.skills[extractedSkill.id];
|
|
3040
|
+
if (!matrixSkill) continue;
|
|
3041
|
+
const source = {
|
|
3042
|
+
name: extraSource.name,
|
|
3043
|
+
type: "private",
|
|
3044
|
+
url: extraSource.url,
|
|
3045
|
+
installed: false
|
|
3046
|
+
};
|
|
3047
|
+
matrixSkill.availableSources = matrixSkill.availableSources ?? [];
|
|
3048
|
+
matrixSkill.availableSources.push(source);
|
|
3049
|
+
matchCount++;
|
|
3050
|
+
}
|
|
3051
|
+
verbose(
|
|
3052
|
+
`Extra source '${extraSource.name}': ${skills.length} skills found, ${matchCount} matching`
|
|
3053
|
+
);
|
|
3054
|
+
} catch (error) {
|
|
3055
|
+
warn(`Failed to load extra source '${extraSource.name}' (${extraSource.url}): ${error}`);
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
function setActiveSources(matrix) {
|
|
3060
|
+
for (const [, skill] of typedEntries(
|
|
3061
|
+
matrix.skills
|
|
3062
|
+
)) {
|
|
3063
|
+
if (!skill) continue;
|
|
3064
|
+
if (!skill.availableSources || skill.availableSources.length === 0) continue;
|
|
3065
|
+
const installedSource = skill.availableSources.find((s) => s.installed);
|
|
3066
|
+
skill.activeSource = installedSource ?? skill.availableSources[0];
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
async function searchExtraSources(alias, configuredSources) {
|
|
3070
|
+
const candidates = [];
|
|
3071
|
+
if (configuredSources.length === 0) {
|
|
3072
|
+
return candidates;
|
|
3073
|
+
}
|
|
3074
|
+
const lowerAlias = alias.toLowerCase();
|
|
3075
|
+
for (const source of configuredSources) {
|
|
3076
|
+
try {
|
|
3077
|
+
const fetchResult = await fetchFromSource(source.url, { forceRefresh: false });
|
|
3078
|
+
const skillsDir = path18.join(fetchResult.path, SKILLS_DIR_PATH);
|
|
3079
|
+
const skills = await extractAllSkills(skillsDir);
|
|
3080
|
+
for (const skill of skills) {
|
|
3081
|
+
const segments = skill.directoryPath.split("/");
|
|
3082
|
+
const lastSegment = segments[segments.length - 1]?.toLowerCase();
|
|
3083
|
+
if (lastSegment === lowerAlias) {
|
|
3084
|
+
candidates.push({
|
|
3085
|
+
id: skill.id,
|
|
3086
|
+
sourceUrl: source.url,
|
|
3087
|
+
sourceName: source.name,
|
|
3088
|
+
alias,
|
|
3089
|
+
description: skill.description
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
} catch (error) {
|
|
3094
|
+
warn(`Failed to search extra source '${source.name}' (${source.url}): ${error}`);
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
return candidates;
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
// src/cli/lib/loading/source-loader.ts
|
|
3101
|
+
async function loadSkillsMatrixFromSource(options = {}) {
|
|
3102
|
+
const { sourceFlag, projectDir, forceRefresh = false, devMode = false } = options;
|
|
3103
|
+
const sourceConfig = await resolveSource(sourceFlag, projectDir);
|
|
3104
|
+
const { source } = sourceConfig;
|
|
3105
|
+
verbose(`Loading skills from source: ${source}`);
|
|
3106
|
+
const isLocal = isLocalSource(source) || devMode === true;
|
|
3107
|
+
let result;
|
|
3108
|
+
if (isLocal) {
|
|
3109
|
+
result = await loadFromLocal(source, sourceConfig);
|
|
3110
|
+
} else {
|
|
3111
|
+
result = await loadFromRemote(source, sourceConfig, forceRefresh);
|
|
3112
|
+
}
|
|
3113
|
+
const resolvedProjectDir = projectDir || process.cwd();
|
|
3114
|
+
const localSkillsResult = await discoverLocalSkills(resolvedProjectDir);
|
|
3115
|
+
if (localSkillsResult && localSkillsResult.skills.length > 0) {
|
|
3116
|
+
verbose(
|
|
3117
|
+
`Found ${localSkillsResult.skills.length} local skill(s) in ${localSkillsResult.localSkillsPath}`
|
|
3118
|
+
);
|
|
3119
|
+
result.matrix = mergeLocalSkillsIntoMatrix(result.matrix, localSkillsResult);
|
|
3120
|
+
}
|
|
3121
|
+
await loadSkillsFromAllSources(result.matrix, sourceConfig, resolvedProjectDir);
|
|
3122
|
+
checkMatrixHealth(result.matrix);
|
|
3123
|
+
return result;
|
|
3124
|
+
}
|
|
3125
|
+
async function loadFromLocal(source, sourceConfig) {
|
|
3126
|
+
let skillsPath;
|
|
3127
|
+
if (isLocalSource(source)) {
|
|
3128
|
+
skillsPath = path19.isAbsolute(source) ? source : path19.resolve(process.cwd(), source);
|
|
3129
|
+
} else {
|
|
3130
|
+
skillsPath = PROJECT_ROOT;
|
|
3131
|
+
}
|
|
3132
|
+
verbose(`Loading skills from local path: ${skillsPath}`);
|
|
3133
|
+
const sourceMatrixPath = path19.join(skillsPath, SKILLS_MATRIX_PATH);
|
|
3134
|
+
const cliMatrixPath = path19.join(PROJECT_ROOT, SKILLS_MATRIX_PATH);
|
|
3135
|
+
let matrixPath;
|
|
3136
|
+
if (await fileExists(sourceMatrixPath)) {
|
|
3137
|
+
matrixPath = sourceMatrixPath;
|
|
3138
|
+
verbose(`Matrix from source: ${matrixPath}`);
|
|
3139
|
+
} else {
|
|
3140
|
+
matrixPath = cliMatrixPath;
|
|
3141
|
+
verbose(`Matrix from CLI (source has no matrix): ${matrixPath}`);
|
|
3142
|
+
}
|
|
3143
|
+
const skillsDir = path19.join(skillsPath, SKILLS_DIR_PATH);
|
|
3144
|
+
verbose(`Skills from source: ${skillsDir}`);
|
|
3145
|
+
const matrix = await loadSkillsMatrix(matrixPath);
|
|
3146
|
+
const skills = await extractAllSkills(skillsDir);
|
|
3147
|
+
const mergedMatrix = await mergeMatrixWithSkills(matrix, skills);
|
|
3148
|
+
const sourceStacks = await loadStacks(skillsPath);
|
|
3149
|
+
const stacks = sourceStacks.length > 0 ? sourceStacks : await loadStacks(PROJECT_ROOT);
|
|
3150
|
+
if (stacks.length > 0) {
|
|
3151
|
+
mergedMatrix.suggestedStacks = stacks.map(
|
|
3152
|
+
(stack) => stackToResolvedStack(stack, mergedMatrix.displayNameToId)
|
|
3153
|
+
);
|
|
3154
|
+
const stackSource = sourceStacks.length > 0 ? "source" : "CLI";
|
|
3155
|
+
verbose(`Loaded ${stacks.length} stacks from ${stackSource}`);
|
|
3156
|
+
}
|
|
3157
|
+
return {
|
|
3158
|
+
matrix: mergedMatrix,
|
|
3159
|
+
sourceConfig,
|
|
3160
|
+
sourcePath: skillsPath,
|
|
3161
|
+
isLocal: true,
|
|
3162
|
+
marketplace: sourceConfig.marketplace
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
async function loadFromRemote(source, sourceConfig, forceRefresh) {
|
|
3166
|
+
verbose(`Fetching skills from remote source: ${source}`);
|
|
3167
|
+
const fetchResult = await fetchFromSource(source, { forceRefresh });
|
|
3168
|
+
verbose(`Fetched to: ${fetchResult.path}`);
|
|
3169
|
+
const sourceMatrixPath = path19.join(fetchResult.path, SKILLS_MATRIX_PATH);
|
|
3170
|
+
const cliMatrixPath = path19.join(PROJECT_ROOT, SKILLS_MATRIX_PATH);
|
|
3171
|
+
let matrixPath;
|
|
3172
|
+
if (await fileExists(sourceMatrixPath)) {
|
|
3173
|
+
matrixPath = sourceMatrixPath;
|
|
3174
|
+
verbose(`Matrix from source: ${matrixPath}`);
|
|
3175
|
+
} else {
|
|
3176
|
+
matrixPath = cliMatrixPath;
|
|
3177
|
+
verbose(`Matrix from CLI (source has no matrix): ${matrixPath}`);
|
|
3178
|
+
}
|
|
3179
|
+
const skillsDir = path19.join(fetchResult.path, SKILLS_DIR_PATH);
|
|
3180
|
+
verbose(`Skills from source: ${skillsDir}`);
|
|
3181
|
+
const matrix = await loadSkillsMatrix(matrixPath);
|
|
3182
|
+
const skills = await extractAllSkills(skillsDir);
|
|
3183
|
+
const mergedMatrix = await mergeMatrixWithSkills(matrix, skills);
|
|
3184
|
+
const sourceStacks = await loadStacks(fetchResult.path);
|
|
3185
|
+
const stacks = sourceStacks.length > 0 ? sourceStacks : await loadStacks(PROJECT_ROOT);
|
|
3186
|
+
if (stacks.length > 0) {
|
|
3187
|
+
mergedMatrix.suggestedStacks = stacks.map(
|
|
3188
|
+
(stack) => stackToResolvedStack(stack, mergedMatrix.displayNameToId)
|
|
3189
|
+
);
|
|
3190
|
+
const stackSource = sourceStacks.length > 0 ? "source" : "CLI";
|
|
3191
|
+
verbose(`Loaded ${stacks.length} stacks from ${stackSource}`);
|
|
3192
|
+
}
|
|
3193
|
+
return {
|
|
3194
|
+
matrix: mergedMatrix,
|
|
3195
|
+
sourceConfig,
|
|
3196
|
+
sourcePath: fetchResult.path,
|
|
3197
|
+
isLocal: false,
|
|
3198
|
+
marketplace: sourceConfig.marketplace
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
function stackToResolvedStack(stack, displayNameToId) {
|
|
3202
|
+
const allSkillIds = [];
|
|
3203
|
+
const seenSkillIds = /* @__PURE__ */ new Set();
|
|
3204
|
+
for (const agentId of typedKeys(stack.agents)) {
|
|
3205
|
+
const agentConfig = stack.agents[agentId];
|
|
3206
|
+
if (!agentConfig) continue;
|
|
3207
|
+
const skillRefs = resolveAgentConfigToSkills(agentConfig, displayNameToId);
|
|
3208
|
+
for (const ref of skillRefs) {
|
|
3209
|
+
if (!seenSkillIds.has(ref.id)) {
|
|
3210
|
+
seenSkillIds.add(ref.id);
|
|
3211
|
+
allSkillIds.push(ref.id);
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
const agentCount = typedKeys(stack.agents).length;
|
|
3216
|
+
verbose(`Stack '${stack.id}' has ${allSkillIds.length} skills from ${agentCount} agents`);
|
|
3217
|
+
return {
|
|
3218
|
+
id: stack.id,
|
|
3219
|
+
name: stack.name,
|
|
3220
|
+
description: stack.description,
|
|
3221
|
+
audience: [],
|
|
3222
|
+
// Not used in new format
|
|
3223
|
+
skills: {},
|
|
3224
|
+
// Skills come from stack agent configs, resolved at runtime
|
|
3225
|
+
allSkillIds,
|
|
3226
|
+
philosophy: stack.philosophy || ""
|
|
3227
|
+
};
|
|
3228
|
+
}
|
|
3229
|
+
function mergeLocalSkillsIntoMatrix(matrix, localResult) {
|
|
3230
|
+
for (const metadata of localResult.skills) {
|
|
3231
|
+
const existingSkill = matrix.skills[metadata.id];
|
|
3232
|
+
const category = existingSkill?.category ?? metadata.category;
|
|
3233
|
+
const displayName = existingSkill?.displayName ?? matrix.displayNames[metadata.id];
|
|
3234
|
+
const resolvedSkill = {
|
|
3235
|
+
id: metadata.id,
|
|
3236
|
+
displayName,
|
|
3237
|
+
description: metadata.description,
|
|
3238
|
+
usageGuidance: metadata.usageGuidance,
|
|
3239
|
+
category,
|
|
3240
|
+
categoryExclusive: metadata.categoryExclusive,
|
|
3241
|
+
tags: metadata.tags ?? [],
|
|
3242
|
+
author: "@local",
|
|
3243
|
+
conflictsWith: existingSkill?.conflictsWith ?? [],
|
|
3244
|
+
recommends: existingSkill?.recommends ?? [],
|
|
3245
|
+
requires: existingSkill?.requires ?? [],
|
|
3246
|
+
alternatives: existingSkill?.alternatives ?? [],
|
|
3247
|
+
discourages: existingSkill?.discourages ?? [],
|
|
3248
|
+
compatibleWith: existingSkill?.compatibleWith ?? [],
|
|
3249
|
+
requiresSetup: existingSkill?.requiresSetup ?? [],
|
|
3250
|
+
providesSetupFor: existingSkill?.providesSetupFor ?? [],
|
|
3251
|
+
path: metadata.path,
|
|
3252
|
+
local: true,
|
|
3253
|
+
localPath: metadata.localPath
|
|
3254
|
+
};
|
|
3255
|
+
matrix.skills[metadata.id] = resolvedSkill;
|
|
3256
|
+
verbose(`Added local skill: ${metadata.id} (category: ${category})`);
|
|
3257
|
+
}
|
|
3258
|
+
return matrix;
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
// src/cli/lib/loading/defaults-loader.ts
|
|
3262
|
+
init_esm_shims();
|
|
3263
|
+
import { parse as parseYaml8 } from "yaml";
|
|
3264
|
+
var cachedDefaults = null;
|
|
3265
|
+
function getCachedDefaults() {
|
|
3266
|
+
return cachedDefaults;
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
// src/cli/lib/skills/skill-agent-mappings.ts
|
|
3270
|
+
var SKILL_TO_AGENTS = {
|
|
3271
|
+
"web/*": [
|
|
3272
|
+
"web-developer",
|
|
3273
|
+
"web-reviewer",
|
|
3274
|
+
"web-researcher",
|
|
3275
|
+
"web-pm",
|
|
3276
|
+
"pattern-scout",
|
|
3277
|
+
"web-pattern-critique",
|
|
3278
|
+
"agent-summoner",
|
|
3279
|
+
"skill-summoner",
|
|
3280
|
+
"documentor"
|
|
3281
|
+
],
|
|
3282
|
+
"api/*": [
|
|
3283
|
+
"api-developer",
|
|
3284
|
+
"api-reviewer",
|
|
3285
|
+
"api-researcher",
|
|
3286
|
+
"web-architecture",
|
|
3287
|
+
"web-pm",
|
|
3288
|
+
"pattern-scout",
|
|
3289
|
+
"web-pattern-critique",
|
|
3290
|
+
"agent-summoner",
|
|
3291
|
+
"skill-summoner",
|
|
3292
|
+
"documentor"
|
|
3293
|
+
],
|
|
3294
|
+
"mobile/*": [
|
|
3295
|
+
"web-developer",
|
|
3296
|
+
"web-reviewer",
|
|
3297
|
+
"web-researcher",
|
|
3298
|
+
"web-pm",
|
|
3299
|
+
"agent-summoner",
|
|
3300
|
+
"skill-summoner",
|
|
3301
|
+
"documentor"
|
|
3302
|
+
],
|
|
3303
|
+
"infra/*": [
|
|
3304
|
+
"web-architecture",
|
|
3305
|
+
"web-developer",
|
|
3306
|
+
"api-developer",
|
|
3307
|
+
"agent-summoner",
|
|
3308
|
+
"skill-summoner",
|
|
3309
|
+
"documentor"
|
|
3310
|
+
],
|
|
3311
|
+
"security/*": [
|
|
3312
|
+
"web-developer",
|
|
3313
|
+
"api-developer",
|
|
3314
|
+
"web-reviewer",
|
|
3315
|
+
"api-reviewer",
|
|
3316
|
+
"web-architecture",
|
|
3317
|
+
"web-pm",
|
|
3318
|
+
"agent-summoner",
|
|
3319
|
+
"skill-summoner",
|
|
3320
|
+
"documentor"
|
|
3321
|
+
],
|
|
3322
|
+
"reviewing/*": [
|
|
3323
|
+
"web-reviewer",
|
|
3324
|
+
"api-reviewer",
|
|
3325
|
+
"cli-reviewer",
|
|
3326
|
+
"web-pattern-critique",
|
|
3327
|
+
"agent-summoner",
|
|
3328
|
+
"skill-summoner",
|
|
3329
|
+
"documentor"
|
|
3330
|
+
],
|
|
3331
|
+
"cli/*": [
|
|
3332
|
+
"cli-developer",
|
|
3333
|
+
"cli-reviewer",
|
|
3334
|
+
"api-developer",
|
|
3335
|
+
"api-reviewer",
|
|
3336
|
+
"api-researcher",
|
|
3337
|
+
"web-architecture",
|
|
3338
|
+
"web-pm",
|
|
3339
|
+
"agent-summoner",
|
|
3340
|
+
"skill-summoner",
|
|
3341
|
+
"documentor"
|
|
3342
|
+
],
|
|
3343
|
+
"research/*": [
|
|
3344
|
+
"web-researcher",
|
|
3345
|
+
"api-researcher",
|
|
3346
|
+
"web-pm",
|
|
3347
|
+
"pattern-scout",
|
|
3348
|
+
"web-pattern-critique",
|
|
3349
|
+
"documentor",
|
|
3350
|
+
"agent-summoner",
|
|
3351
|
+
"skill-summoner"
|
|
3352
|
+
],
|
|
3353
|
+
"methodology/*": [
|
|
3354
|
+
"web-developer",
|
|
3355
|
+
"api-developer",
|
|
3356
|
+
"web-reviewer",
|
|
3357
|
+
"api-reviewer",
|
|
3358
|
+
"web-researcher",
|
|
3359
|
+
"api-researcher",
|
|
3360
|
+
"web-tester",
|
|
3361
|
+
"web-pm",
|
|
3362
|
+
"web-architecture",
|
|
3363
|
+
"pattern-scout",
|
|
3364
|
+
"web-pattern-critique",
|
|
3365
|
+
"agent-summoner",
|
|
3366
|
+
"skill-summoner",
|
|
3367
|
+
"documentor"
|
|
3368
|
+
],
|
|
3369
|
+
"web/testing": ["web-tester", "web-developer", "web-reviewer"],
|
|
3370
|
+
"api/testing": ["web-tester", "api-developer", "api-reviewer"],
|
|
3371
|
+
"web/mocks": ["web-tester", "web-developer", "web-reviewer"]
|
|
3372
|
+
};
|
|
3373
|
+
var DEFAULT_AGENTS = ["agent-summoner", "skill-summoner", "documentor"];
|
|
3374
|
+
function getEffectiveSkillToAgents() {
|
|
3375
|
+
const defaults = getCachedDefaults();
|
|
3376
|
+
if (defaults?.skill_to_agents) {
|
|
3377
|
+
return defaults.skill_to_agents;
|
|
3378
|
+
}
|
|
3379
|
+
return SKILL_TO_AGENTS;
|
|
3380
|
+
}
|
|
3381
|
+
function getAgentsForSkill(skillPath, category, projectConfig) {
|
|
3382
|
+
const normalizedPath = skillPath.replace(/^skills\//, "").replace(/\/$/, "");
|
|
3383
|
+
const skillToAgents = getEffectiveSkillToAgents();
|
|
3384
|
+
if (skillToAgents[category]) {
|
|
3385
|
+
return skillToAgents[category];
|
|
3386
|
+
}
|
|
3387
|
+
for (const [pattern, agents] of Object.entries(skillToAgents)) {
|
|
3388
|
+
if (normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`)) {
|
|
3389
|
+
return agents;
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
for (const [pattern, agents] of Object.entries(skillToAgents)) {
|
|
3393
|
+
if (pattern.endsWith("/*")) {
|
|
3394
|
+
const prefix = pattern.slice(0, -2);
|
|
3395
|
+
if (normalizedPath.startsWith(prefix)) {
|
|
3396
|
+
return agents;
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
return DEFAULT_AGENTS;
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
// src/cli/lib/skills/skill-plugin-compiler.ts
|
|
3404
|
+
init_esm_shims();
|
|
3405
|
+
import path20 from "path";
|
|
3406
|
+
import { parse as parseYaml9 } from "yaml";
|
|
3407
|
+
var SKILL_FILES = ["SKILL.md", "reference.md"];
|
|
3408
|
+
var SKILL_DIRS = ["examples", "scripts"];
|
|
3409
|
+
function sanitizeSkillName(name) {
|
|
3410
|
+
return name.replace(/\+/g, "-");
|
|
3411
|
+
}
|
|
3412
|
+
async function readSkillMetadata(skillPath) {
|
|
3413
|
+
const metadataPath = path20.join(skillPath, "metadata.yaml");
|
|
3414
|
+
if (!await fileExists(metadataPath)) {
|
|
3415
|
+
return null;
|
|
3416
|
+
}
|
|
3417
|
+
try {
|
|
3418
|
+
const content = await readFile(metadataPath);
|
|
3419
|
+
const lines = content.split("\n");
|
|
3420
|
+
const yamlContent = lines[0]?.startsWith("# yaml-language-server:") ? lines.slice(1).join("\n") : content;
|
|
3421
|
+
const result = skillMetadataConfigSchema.safeParse(parseYaml9(yamlContent));
|
|
3422
|
+
if (!result.success) {
|
|
3423
|
+
warn(
|
|
3424
|
+
`Invalid metadata.yaml at ${skillPath}: ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
3425
|
+
);
|
|
3426
|
+
return null;
|
|
3427
|
+
}
|
|
3428
|
+
return result.data;
|
|
3429
|
+
} catch (error) {
|
|
3430
|
+
warn(`Failed to read metadata.yaml at ${skillPath}: ${error}`);
|
|
3431
|
+
return null;
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
function generateReadme(skillName, frontmatter, metadata) {
|
|
3435
|
+
const lines = [];
|
|
3436
|
+
lines.push(`# ${skillName}`);
|
|
3437
|
+
lines.push("");
|
|
3438
|
+
lines.push(frontmatter.description);
|
|
3439
|
+
lines.push("");
|
|
3440
|
+
if (metadata?.tags && metadata.tags.length > 0) {
|
|
3441
|
+
lines.push("## Tags");
|
|
3442
|
+
lines.push("");
|
|
3443
|
+
lines.push(metadata.tags.map((t) => `\`${t}\``).join(" "));
|
|
3444
|
+
lines.push("");
|
|
3445
|
+
}
|
|
3446
|
+
lines.push("## Installation");
|
|
3447
|
+
lines.push("");
|
|
3448
|
+
lines.push("Add this plugin to your Claude Code configuration:");
|
|
3449
|
+
lines.push("");
|
|
3450
|
+
lines.push("```json");
|
|
3451
|
+
lines.push(`{`);
|
|
3452
|
+
lines.push(` "plugins": ["${skillName}"]`);
|
|
3453
|
+
lines.push(`}`);
|
|
3454
|
+
lines.push("```");
|
|
3455
|
+
lines.push("");
|
|
3456
|
+
lines.push("## Usage");
|
|
3457
|
+
lines.push("");
|
|
3458
|
+
lines.push(`This skill is automatically available when installed.`);
|
|
3459
|
+
if (metadata?.requires && metadata.requires.length > 0) {
|
|
3460
|
+
lines.push("");
|
|
3461
|
+
lines.push("**Requires:** " + metadata.requires.join(", "));
|
|
3462
|
+
}
|
|
3463
|
+
lines.push("");
|
|
3464
|
+
lines.push("---");
|
|
3465
|
+
lines.push("");
|
|
3466
|
+
lines.push("*Generated by Claude Collective skill-plugin-compiler*");
|
|
3467
|
+
lines.push("");
|
|
3468
|
+
return lines.join("\n");
|
|
3469
|
+
}
|
|
3470
|
+
async function compileSkillPlugin(options) {
|
|
3471
|
+
const { skillPath, outputDir, skillName: overrideName } = options;
|
|
3472
|
+
const dirBasename = path20.basename(skillPath);
|
|
3473
|
+
const skillMdPath = path20.join(skillPath, "SKILL.md");
|
|
3474
|
+
if (!await fileExists(skillMdPath)) {
|
|
3475
|
+
throw new Error(
|
|
3476
|
+
`Skill '${dirBasename}' is missing required SKILL.md file. Expected at: ${skillMdPath}`
|
|
3477
|
+
);
|
|
3478
|
+
}
|
|
3479
|
+
const skillMdContent = await readFile(skillMdPath);
|
|
3480
|
+
const frontmatter = parseFrontmatter(skillMdContent, skillMdPath);
|
|
3481
|
+
if (!frontmatter) {
|
|
3482
|
+
throw new Error(
|
|
3483
|
+
`Skill '${dirBasename}' has invalid or missing YAML frontmatter in SKILL.md. Required fields: 'name' and 'description'. File: ${skillMdPath}`
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
3486
|
+
const skillName = overrideName ?? sanitizeSkillName(frontmatter.name);
|
|
3487
|
+
verbose(`Compiling skill plugin: ${skillName} from ${skillPath}`);
|
|
3488
|
+
const metadata = await readSkillMetadata(skillPath);
|
|
3489
|
+
const pluginDir = path20.join(outputDir, skillName);
|
|
3490
|
+
const skillsDir = path20.join(pluginDir, "skills", skillName);
|
|
3491
|
+
await ensureDir(pluginDir);
|
|
3492
|
+
await ensureDir(skillsDir);
|
|
3493
|
+
const newHash = await hashSkillFolder(skillPath);
|
|
3494
|
+
const { version, contentHash } = await determinePluginVersion(
|
|
3495
|
+
newHash,
|
|
3496
|
+
pluginDir,
|
|
3497
|
+
getPluginManifestPath
|
|
3498
|
+
);
|
|
3499
|
+
const manifest = generateSkillPluginManifest({
|
|
3500
|
+
skillName,
|
|
3501
|
+
description: frontmatter.description,
|
|
3502
|
+
author: metadata?.author,
|
|
3503
|
+
version,
|
|
3504
|
+
keywords: metadata?.tags
|
|
3505
|
+
});
|
|
3506
|
+
await writePluginManifest(pluginDir, manifest);
|
|
3507
|
+
await writeContentHash(pluginDir, contentHash, getPluginManifestPath);
|
|
3508
|
+
verbose(` Wrote plugin.json for ${skillName} (v${version})`);
|
|
3509
|
+
await writeFile(path20.join(skillsDir, "SKILL.md"), skillMdContent);
|
|
3510
|
+
verbose(` Copied SKILL.md`);
|
|
3511
|
+
for (const fileName of SKILL_FILES) {
|
|
3512
|
+
if (fileName === "SKILL.md") continue;
|
|
3513
|
+
const sourcePath = path20.join(skillPath, fileName);
|
|
3514
|
+
if (await fileExists(sourcePath)) {
|
|
3515
|
+
const content = await readFile(sourcePath);
|
|
3516
|
+
await writeFile(path20.join(skillsDir, fileName), content);
|
|
3517
|
+
verbose(` Copied ${fileName}`);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
for (const dirName of SKILL_DIRS) {
|
|
3521
|
+
const sourceDir = path20.join(skillPath, dirName);
|
|
3522
|
+
if (await fileExists(sourceDir)) {
|
|
3523
|
+
await copy(sourceDir, path20.join(skillsDir, dirName));
|
|
3524
|
+
verbose(` Copied ${dirName}/`);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
const readme = generateReadme(skillName, frontmatter, metadata);
|
|
3528
|
+
await writeFile(path20.join(pluginDir, "README.md"), readme);
|
|
3529
|
+
verbose(` Generated README.md`);
|
|
3530
|
+
return {
|
|
3531
|
+
pluginPath: pluginDir,
|
|
3532
|
+
manifest,
|
|
3533
|
+
skillName
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
async function compileAllSkillPlugins(skillsDir, outputDir) {
|
|
3537
|
+
const results = [];
|
|
3538
|
+
const skillMdFiles = await glob("**/SKILL.md", skillsDir);
|
|
3539
|
+
for (const skillMdFile of skillMdFiles) {
|
|
3540
|
+
const skillPath = path20.join(skillsDir, path20.dirname(skillMdFile));
|
|
3541
|
+
try {
|
|
3542
|
+
const result = await compileSkillPlugin({
|
|
3543
|
+
skillPath,
|
|
3544
|
+
outputDir
|
|
3545
|
+
});
|
|
3546
|
+
results.push(result);
|
|
3547
|
+
console.log(` [OK] ${result.skillName}`);
|
|
3548
|
+
} catch (error) {
|
|
3549
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3550
|
+
const dirBasename = path20.basename(skillPath);
|
|
3551
|
+
console.warn(` [WARN] Failed to compile skill from ${dirBasename}: ${errorMessage}`);
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
return results;
|
|
3555
|
+
}
|
|
3556
|
+
function printCompilationSummary(results) {
|
|
3557
|
+
console.log(`
|
|
3558
|
+
Compiled ${results.length} skill plugins:`);
|
|
3559
|
+
for (const result of results) {
|
|
3560
|
+
console.log(` - ${result.skillName} (v${result.manifest.version})`);
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
// src/cli/lib/skills/local-skill-loader.ts
|
|
3565
|
+
init_esm_shims();
|
|
3566
|
+
import { parse as parseYaml10 } from "yaml";
|
|
3567
|
+
import path21 from "path";
|
|
3568
|
+
var LOCAL_CATEGORY = "local";
|
|
3569
|
+
var LOCAL_AUTHOR = "@local";
|
|
3570
|
+
async function discoverLocalSkills(projectDir) {
|
|
3571
|
+
const localSkillsPath = path21.join(projectDir, LOCAL_SKILLS_PATH);
|
|
3572
|
+
if (!await directoryExists(localSkillsPath)) {
|
|
3573
|
+
verbose(`Local skills directory not found: ${localSkillsPath}`);
|
|
3574
|
+
return null;
|
|
3575
|
+
}
|
|
3576
|
+
const skills = [];
|
|
3577
|
+
const skillDirs = await listDirectories(localSkillsPath);
|
|
3578
|
+
for (const skillDirName of skillDirs) {
|
|
3579
|
+
const skill = await extractLocalSkill(localSkillsPath, skillDirName);
|
|
3580
|
+
if (skill) {
|
|
3581
|
+
skills.push(skill);
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
verbose(`Discovered ${skills.length} local skills from ${localSkillsPath}`);
|
|
3585
|
+
return {
|
|
3586
|
+
skills,
|
|
3587
|
+
localSkillsPath
|
|
3588
|
+
};
|
|
3589
|
+
}
|
|
3590
|
+
async function extractLocalSkill(localSkillsPath, skillDirName) {
|
|
3591
|
+
const skillDir = path21.join(localSkillsPath, skillDirName);
|
|
3592
|
+
const metadataPath = path21.join(skillDir, "metadata.yaml");
|
|
3593
|
+
const skillMdPath = path21.join(skillDir, "SKILL.md");
|
|
3594
|
+
if (!await fileExists(metadataPath)) {
|
|
3595
|
+
verbose(`Skipping local skill '${skillDirName}': No metadata.yaml found`);
|
|
3596
|
+
return null;
|
|
3597
|
+
}
|
|
3598
|
+
if (!await fileExists(skillMdPath)) {
|
|
3599
|
+
verbose(`Skipping local skill '${skillDirName}': No SKILL.md found`);
|
|
3600
|
+
return null;
|
|
3601
|
+
}
|
|
3602
|
+
const metadataContent = await readFile(metadataPath);
|
|
3603
|
+
const parsed = localRawMetadataSchema.safeParse(parseYaml10(metadataContent));
|
|
3604
|
+
if (!parsed.success) {
|
|
3605
|
+
warn(
|
|
3606
|
+
`Skipping local skill '${skillDirName}': Invalid metadata.yaml \u2014 ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
3607
|
+
);
|
|
3608
|
+
return null;
|
|
3609
|
+
}
|
|
3610
|
+
const metadata = parsed.data;
|
|
3611
|
+
if (!metadata.cli_name) {
|
|
3612
|
+
warn(`Skipping local skill '${skillDirName}': Missing required 'cli_name' in metadata.yaml`);
|
|
3613
|
+
return null;
|
|
3614
|
+
}
|
|
3615
|
+
const skillMdContent = await readFile(skillMdPath);
|
|
3616
|
+
const frontmatter = parseFrontmatter(skillMdContent, skillMdPath);
|
|
3617
|
+
if (!frontmatter) {
|
|
3618
|
+
warn(`Skipping local skill '${skillDirName}': Invalid SKILL.md frontmatter`);
|
|
3619
|
+
return null;
|
|
3620
|
+
}
|
|
3621
|
+
const relativePath = `${LOCAL_SKILLS_PATH}/${skillDirName}/`;
|
|
3622
|
+
const skillId = frontmatter.name;
|
|
3623
|
+
const category = metadata.category || LOCAL_CATEGORY;
|
|
3624
|
+
if (!metadata.category) {
|
|
3625
|
+
warn(
|
|
3626
|
+
`Local skill '${skillDirName}' has no category in metadata.yaml \u2014 defaulting to '${LOCAL_CATEGORY}' (will not appear in wizard domain views)`
|
|
3627
|
+
);
|
|
3628
|
+
}
|
|
3629
|
+
const extracted = {
|
|
3630
|
+
id: skillId,
|
|
3631
|
+
directoryPath: skillDirName,
|
|
3632
|
+
description: metadata.cli_description || frontmatter.description,
|
|
3633
|
+
usageGuidance: metadata.usage_guidance,
|
|
3634
|
+
category,
|
|
3635
|
+
categoryExclusive: metadata.category_exclusive ?? false,
|
|
3636
|
+
author: LOCAL_AUTHOR,
|
|
3637
|
+
tags: metadata.tags ?? [],
|
|
3638
|
+
compatibleWith: metadata.compatible_with ?? [],
|
|
3639
|
+
conflictsWith: metadata.conflicts_with ?? [],
|
|
3640
|
+
requires: metadata.requires ?? [],
|
|
3641
|
+
requiresSetup: metadata.requires_setup ?? [],
|
|
3642
|
+
providesSetupFor: metadata.provides_setup_for ?? [],
|
|
3643
|
+
path: relativePath,
|
|
3644
|
+
local: true,
|
|
3645
|
+
localPath: relativePath
|
|
3646
|
+
};
|
|
3647
|
+
verbose(`Extracted local skill: ${skillId}`);
|
|
3648
|
+
return extracted;
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
// src/cli/lib/skills/source-switcher.ts
|
|
3652
|
+
init_esm_shims();
|
|
3653
|
+
import path22 from "path";
|
|
3654
|
+
async function archiveLocalSkill(projectDir, skillId) {
|
|
3655
|
+
const skillPath = path22.join(projectDir, LOCAL_SKILLS_PATH, skillId);
|
|
3656
|
+
const archivedDir = path22.join(projectDir, LOCAL_SKILLS_PATH, ARCHIVED_SKILLS_DIR_NAME);
|
|
3657
|
+
const archivedSkillPath = path22.join(archivedDir, skillId);
|
|
3658
|
+
if (!await directoryExists(skillPath)) {
|
|
3659
|
+
warn(`Skill directory not found for archiving: ${skillPath}`);
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
3662
|
+
await ensureDir(archivedDir);
|
|
3663
|
+
await copy(skillPath, archivedSkillPath);
|
|
3664
|
+
await remove(skillPath);
|
|
3665
|
+
verbose(`Archived local skill '${skillId}' to ${ARCHIVED_SKILLS_DIR_NAME}/`);
|
|
3666
|
+
}
|
|
3667
|
+
async function restoreArchivedSkill(projectDir, skillId) {
|
|
3668
|
+
const skillPath = path22.join(projectDir, LOCAL_SKILLS_PATH, skillId);
|
|
3669
|
+
const archivedSkillPath = path22.join(
|
|
3670
|
+
projectDir,
|
|
3671
|
+
LOCAL_SKILLS_PATH,
|
|
3672
|
+
ARCHIVED_SKILLS_DIR_NAME,
|
|
3673
|
+
skillId
|
|
3674
|
+
);
|
|
3675
|
+
if (!await directoryExists(archivedSkillPath)) {
|
|
3676
|
+
return false;
|
|
3677
|
+
}
|
|
3678
|
+
await copy(archivedSkillPath, skillPath);
|
|
3679
|
+
await remove(archivedSkillPath);
|
|
3680
|
+
verbose(`Restored archived skill '${skillId}' from ${ARCHIVED_SKILLS_DIR_NAME}/`);
|
|
3681
|
+
return true;
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
// src/cli/lib/configuration/config-generator.ts
|
|
3685
|
+
function extractSubcategory(categoryPath) {
|
|
3686
|
+
if (categoryPath === "local") return void 0;
|
|
3687
|
+
const parts = categoryPath.split("/");
|
|
3688
|
+
return parts.length >= 2 ? parts[1] : parts[0];
|
|
3689
|
+
}
|
|
3690
|
+
function generateProjectConfigFromSkills(name, selectedSkillIds, matrix, options) {
|
|
3691
|
+
const neededAgents = /* @__PURE__ */ new Set();
|
|
3692
|
+
const stackProperty = {};
|
|
3693
|
+
for (const skillId of selectedSkillIds) {
|
|
3694
|
+
const skill = matrix.skills[skillId];
|
|
3695
|
+
if (!skill) {
|
|
3696
|
+
continue;
|
|
3697
|
+
}
|
|
3698
|
+
const skillPath = skill.path;
|
|
3699
|
+
const category = skill.category;
|
|
3700
|
+
const agents = getAgentsForSkill(skillPath, category);
|
|
3701
|
+
const subcategory = extractSubcategory(category);
|
|
3702
|
+
for (const agentId of agents) {
|
|
3703
|
+
neededAgents.add(agentId);
|
|
3704
|
+
if (subcategory) {
|
|
3705
|
+
if (!stackProperty[agentId]) {
|
|
3706
|
+
stackProperty[agentId] = {};
|
|
3707
|
+
}
|
|
3708
|
+
stackProperty[agentId][subcategory] = skillId;
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
const config = {
|
|
3713
|
+
name,
|
|
3714
|
+
agents: Array.from(neededAgents).sort(),
|
|
3715
|
+
skills: [...selectedSkillIds]
|
|
3716
|
+
};
|
|
3717
|
+
if (Object.keys(stackProperty).length > 0) {
|
|
3718
|
+
config.stack = stackProperty;
|
|
3719
|
+
}
|
|
3720
|
+
if (options?.description) {
|
|
3721
|
+
config.description = options.description;
|
|
3722
|
+
}
|
|
3723
|
+
if (options?.author) {
|
|
3724
|
+
config.author = options.author;
|
|
3725
|
+
}
|
|
3726
|
+
return config;
|
|
3727
|
+
}
|
|
3728
|
+
function buildStackProperty(stack, displayNameToId) {
|
|
3729
|
+
const result = {};
|
|
3730
|
+
for (const [agentId, agentConfig] of typedEntries(stack.agents)) {
|
|
3731
|
+
if (!agentConfig || Object.keys(agentConfig).length === 0) {
|
|
3732
|
+
continue;
|
|
3733
|
+
}
|
|
3734
|
+
const resolvedMappings = {};
|
|
3735
|
+
for (const [subcategoryId, displayName] of typedEntries(
|
|
3736
|
+
agentConfig
|
|
3737
|
+
)) {
|
|
3738
|
+
if (!displayName) continue;
|
|
3739
|
+
const skillId = displayNameToId[displayName];
|
|
3740
|
+
if (skillId) {
|
|
3741
|
+
resolvedMappings[subcategoryId] = skillId;
|
|
3742
|
+
} else {
|
|
3743
|
+
resolvedMappings[subcategoryId] = displayName;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
if (Object.keys(resolvedMappings).length > 0) {
|
|
3747
|
+
result[agentId] = resolvedMappings;
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
return result;
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
// src/cli/lib/configuration/config-merger.ts
|
|
3754
|
+
init_esm_shims();
|
|
3755
|
+
import { difference } from "remeda";
|
|
3756
|
+
|
|
3757
|
+
// src/cli/lib/configuration/project-config.ts
|
|
3758
|
+
init_esm_shims();
|
|
3759
|
+
import path23 from "path";
|
|
3760
|
+
import { parse as parseYaml11 } from "yaml";
|
|
3761
|
+
var CONFIG_PATH = `${CLAUDE_SRC_DIR}/config.yaml`;
|
|
3762
|
+
var LEGACY_CONFIG_PATH = `${CLAUDE_DIR}/config.yaml`;
|
|
3763
|
+
async function loadProjectConfig(projectDir) {
|
|
3764
|
+
const srcConfigPath = path23.join(projectDir, CONFIG_PATH);
|
|
3765
|
+
const legacyConfigPath = path23.join(projectDir, LEGACY_CONFIG_PATH);
|
|
3766
|
+
let configPath = srcConfigPath;
|
|
3767
|
+
if (!await fileExists(srcConfigPath)) {
|
|
3768
|
+
if (await fileExists(legacyConfigPath)) {
|
|
3769
|
+
configPath = legacyConfigPath;
|
|
3770
|
+
verbose(`Using legacy config location: ${legacyConfigPath}`);
|
|
3771
|
+
} else {
|
|
3772
|
+
verbose(`Project config not found at ${srcConfigPath} or ${legacyConfigPath}`);
|
|
3773
|
+
return null;
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
try {
|
|
3777
|
+
const content = await readFile(configPath);
|
|
3778
|
+
const parsed = parseYaml11(content);
|
|
3779
|
+
if (!parsed || typeof parsed !== "object") {
|
|
3780
|
+
warn(`Invalid project config structure at ${configPath}`);
|
|
3781
|
+
return null;
|
|
3782
|
+
}
|
|
3783
|
+
const result = projectConfigLoaderSchema.safeParse(parsed);
|
|
3784
|
+
if (!result.success) {
|
|
3785
|
+
warn(`Invalid project config at ${configPath}: ${result.error.message}`);
|
|
3786
|
+
return null;
|
|
3787
|
+
}
|
|
3788
|
+
const config = result.data;
|
|
3789
|
+
if (!config.name) {
|
|
3790
|
+
warn(
|
|
3791
|
+
`Project config at ${configPath} is missing required 'name' field \u2014 defaulting to directory name`
|
|
3792
|
+
);
|
|
3793
|
+
config.name = path23.basename(projectDir);
|
|
3794
|
+
}
|
|
3795
|
+
if (!config.skills) {
|
|
3796
|
+
warn(`Project config at ${configPath} is missing 'skills' array \u2014 defaulting to empty`);
|
|
3797
|
+
config.skills = [];
|
|
3798
|
+
}
|
|
3799
|
+
verbose(`Loaded project config from ${configPath}`);
|
|
3800
|
+
return {
|
|
3801
|
+
config,
|
|
3802
|
+
configPath
|
|
3803
|
+
};
|
|
3804
|
+
} catch (error) {
|
|
3805
|
+
warn(`Failed to parse project config at ${configPath}: ${error}`);
|
|
3806
|
+
return null;
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
function validateProjectConfig(config) {
|
|
3810
|
+
const errors = [];
|
|
3811
|
+
const warnings = [];
|
|
3812
|
+
if (!config || typeof config !== "object") {
|
|
3813
|
+
return { valid: false, errors: ["Config must be an object"], warnings: [] };
|
|
3814
|
+
}
|
|
3815
|
+
const c = config;
|
|
3816
|
+
if (!c.name || typeof c.name !== "string") {
|
|
3817
|
+
errors.push("name is required and must be a string");
|
|
3818
|
+
}
|
|
3819
|
+
if (!c.agents || !Array.isArray(c.agents)) {
|
|
3820
|
+
errors.push("agents is required and must be an array");
|
|
3821
|
+
} else {
|
|
3822
|
+
for (const agent of c.agents) {
|
|
3823
|
+
if (typeof agent !== "string") {
|
|
3824
|
+
errors.push(`agents must contain strings, found: ${typeof agent}`);
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
if (c.version !== void 0 && c.version !== "1") {
|
|
3829
|
+
errors.push('version must be "1" (or omitted for default)');
|
|
3830
|
+
}
|
|
3831
|
+
return {
|
|
3832
|
+
valid: errors.length === 0,
|
|
3833
|
+
errors,
|
|
3834
|
+
warnings
|
|
3835
|
+
};
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
// src/cli/lib/configuration/config-merger.ts
|
|
3839
|
+
async function mergeWithExistingConfig(newConfig, context) {
|
|
3840
|
+
const localConfig = { ...newConfig };
|
|
3841
|
+
const existingFullConfig = await loadProjectConfig(context.projectDir);
|
|
3842
|
+
if (existingFullConfig) {
|
|
3843
|
+
const existingConfig = existingFullConfig.config;
|
|
3844
|
+
if (existingConfig.name) {
|
|
3845
|
+
localConfig.name = existingConfig.name;
|
|
3846
|
+
}
|
|
3847
|
+
if (existingConfig.description) {
|
|
3848
|
+
localConfig.description = existingConfig.description;
|
|
3849
|
+
}
|
|
3850
|
+
if (existingConfig.source) {
|
|
3851
|
+
localConfig.source = existingConfig.source;
|
|
3852
|
+
}
|
|
3853
|
+
if (existingConfig.agents && existingConfig.agents.length > 0) {
|
|
3854
|
+
const newAgentIds = difference(localConfig.agents, existingConfig.agents);
|
|
3855
|
+
localConfig.agents = [...existingConfig.agents, ...newAgentIds];
|
|
3856
|
+
}
|
|
3857
|
+
if (existingConfig.stack) {
|
|
3858
|
+
const mergedStack = { ...localConfig.stack };
|
|
3859
|
+
for (const [agentId, agentConfig] of Object.entries(existingConfig.stack)) {
|
|
3860
|
+
mergedStack[agentId] = { ...mergedStack[agentId], ...agentConfig };
|
|
3861
|
+
}
|
|
3862
|
+
localConfig.stack = mergedStack;
|
|
3863
|
+
}
|
|
3864
|
+
if (existingConfig.author) {
|
|
3865
|
+
localConfig.author = existingConfig.author;
|
|
3866
|
+
}
|
|
3867
|
+
if (existingConfig.agents_source) {
|
|
3868
|
+
localConfig.agents_source = existingConfig.agents_source;
|
|
3869
|
+
}
|
|
3870
|
+
if (existingConfig.marketplace) {
|
|
3871
|
+
localConfig.marketplace = existingConfig.marketplace;
|
|
3872
|
+
}
|
|
3873
|
+
return {
|
|
3874
|
+
config: localConfig,
|
|
3875
|
+
merged: true,
|
|
3876
|
+
existingConfigPath: existingFullConfig.configPath
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
const existingProjectConfig = await loadProjectSourceConfig(context.projectDir);
|
|
3880
|
+
if (existingProjectConfig?.author) {
|
|
3881
|
+
localConfig.author = existingProjectConfig.author;
|
|
3882
|
+
}
|
|
3883
|
+
if (existingProjectConfig?.agents_source) {
|
|
3884
|
+
localConfig.agents_source = existingProjectConfig.agents_source;
|
|
3885
|
+
}
|
|
3886
|
+
return { config: localConfig, merged: false };
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
// src/cli/lib/configuration/config-saver.ts
|
|
3890
|
+
init_esm_shims();
|
|
3891
|
+
import path24 from "path";
|
|
3892
|
+
import { parse as parseYaml12, stringify as stringifyYaml5 } from "yaml";
|
|
3893
|
+
var YAML_INDENT2 = 2;
|
|
3894
|
+
async function saveSourceToProjectConfig(projectDir, source) {
|
|
3895
|
+
const configPath = path24.join(projectDir, CLAUDE_SRC_DIR, "config.yaml");
|
|
3896
|
+
let config = {};
|
|
3897
|
+
if (await fileExists(configPath)) {
|
|
3898
|
+
const content = await readFile(configPath);
|
|
3899
|
+
try {
|
|
3900
|
+
const parsed = parseYaml12(content);
|
|
3901
|
+
const result = projectSourceConfigSchema.safeParse(parsed);
|
|
3902
|
+
config = result.success ? result.data : {};
|
|
3903
|
+
if (!result.success) {
|
|
3904
|
+
warn(
|
|
3905
|
+
`Invalid config at ${configPath}: ${result.error.issues.map((i) => i.message).join(", ")}. Starting with empty config.`
|
|
3906
|
+
);
|
|
3907
|
+
}
|
|
3908
|
+
} catch (error) {
|
|
3909
|
+
warn(
|
|
3910
|
+
`Failed to parse existing config at ${configPath}: ${error instanceof Error ? error.message : String(error)}. Starting with empty config.`
|
|
3911
|
+
);
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
config.source = source;
|
|
3915
|
+
await ensureDir(path24.join(projectDir, CLAUDE_SRC_DIR));
|
|
3916
|
+
const configYaml = stringifyYaml5(config, { indent: YAML_INDENT2 });
|
|
3917
|
+
await writeFile(configPath, configYaml);
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
// src/cli/lib/loading/source-fetcher.ts
|
|
3921
|
+
function sanitizeSourceForCache(source) {
|
|
3922
|
+
return source.replace(/:/g, "-").replace(/[\/]/g, "-").replace(/--+/g, "-").replace(/^-|-$/g, "");
|
|
3923
|
+
}
|
|
3924
|
+
function getCacheDir(source) {
|
|
3925
|
+
const sanitized = sanitizeSourceForCache(source);
|
|
3926
|
+
return path25.join(CACHE_DIR, "sources", sanitized);
|
|
3927
|
+
}
|
|
3928
|
+
async function fetchFromSource(source, options = {}) {
|
|
3929
|
+
const { forceRefresh = false, subdir } = options;
|
|
3930
|
+
if (isLocalSource(source)) {
|
|
3931
|
+
return fetchFromLocalSource(source, subdir);
|
|
3932
|
+
}
|
|
3933
|
+
return fetchFromRemoteSource(source, { forceRefresh, subdir });
|
|
3934
|
+
}
|
|
3935
|
+
async function fetchFromLocalSource(source, subdir) {
|
|
3936
|
+
const fullPath = subdir ? path25.join(source, subdir) : source;
|
|
3937
|
+
const absolutePath = path25.isAbsolute(fullPath) ? fullPath : path25.resolve(process.cwd(), fullPath);
|
|
3938
|
+
if (!await directoryExists(absolutePath)) {
|
|
3939
|
+
throw new Error(`Local source not found: ${absolutePath}`);
|
|
3940
|
+
}
|
|
3941
|
+
verbose(`Using local source: ${absolutePath}`);
|
|
3942
|
+
return {
|
|
3943
|
+
path: absolutePath,
|
|
3944
|
+
fromCache: false,
|
|
3945
|
+
source
|
|
3946
|
+
};
|
|
3947
|
+
}
|
|
3948
|
+
async function fetchFromRemoteSource(source, options) {
|
|
3949
|
+
const { forceRefresh = false, subdir } = options;
|
|
3950
|
+
const cacheDir = getCacheDir(source);
|
|
3951
|
+
const fullSource = subdir ? `${source}/${subdir}` : source;
|
|
3952
|
+
verbose(`Fetching from remote: ${fullSource}`);
|
|
3953
|
+
verbose(`Cache directory: ${cacheDir}`);
|
|
3954
|
+
if (!forceRefresh && await directoryExists(cacheDir)) {
|
|
3955
|
+
verbose(`Using cached source: ${cacheDir}`);
|
|
3956
|
+
return {
|
|
3957
|
+
path: cacheDir,
|
|
3958
|
+
fromCache: true,
|
|
3959
|
+
source: fullSource
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
await ensureDir(path25.dirname(cacheDir));
|
|
3963
|
+
try {
|
|
3964
|
+
const result = await downloadTemplate(fullSource, {
|
|
3965
|
+
dir: cacheDir,
|
|
3966
|
+
force: true,
|
|
3967
|
+
// Always force when downloading to avoid "already exists" error
|
|
3968
|
+
offline: false
|
|
3969
|
+
});
|
|
3970
|
+
verbose(`Downloaded to: ${result.dir}`);
|
|
3971
|
+
return {
|
|
3972
|
+
path: result.dir,
|
|
3973
|
+
fromCache: false,
|
|
3974
|
+
source: fullSource
|
|
3975
|
+
};
|
|
3976
|
+
} catch (error) {
|
|
3977
|
+
throw wrapGigetError(error, source);
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
function wrapGigetError(error, source) {
|
|
3981
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3982
|
+
if (message.includes("404") || message.includes("Not Found")) {
|
|
3983
|
+
return new Error(
|
|
3984
|
+
`Repository not found: ${source}
|
|
3985
|
+
|
|
3986
|
+
This could mean:
|
|
3987
|
+
- The repository doesn't exist
|
|
3988
|
+
- The repository is private and you need to set authentication
|
|
3989
|
+
- There's a typo in the URL
|
|
3990
|
+
|
|
3991
|
+
For private repositories, set the GIGET_AUTH environment variable:
|
|
3992
|
+
export GIGET_AUTH=ghp_your_github_token`
|
|
3993
|
+
);
|
|
3994
|
+
}
|
|
3995
|
+
if (message.includes("401") || message.includes("Unauthorized")) {
|
|
3996
|
+
return new Error(
|
|
3997
|
+
`Authentication required for: ${source}
|
|
3998
|
+
|
|
3999
|
+
Set the GIGET_AUTH environment variable with a GitHub token:
|
|
4000
|
+
export GIGET_AUTH=ghp_your_github_token
|
|
4001
|
+
|
|
4002
|
+
Create a token at: https://github.com/settings/tokens
|
|
4003
|
+
Required scope: repo (for private repos) or public_repo (for public)`
|
|
4004
|
+
);
|
|
4005
|
+
}
|
|
4006
|
+
if (message.includes("403") || message.includes("Forbidden")) {
|
|
4007
|
+
return new Error(
|
|
4008
|
+
`Access denied to: ${source}
|
|
4009
|
+
|
|
4010
|
+
Your token may not have sufficient permissions.
|
|
4011
|
+
Ensure your GIGET_AUTH token has the 'repo' scope for private repositories.`
|
|
4012
|
+
);
|
|
4013
|
+
}
|
|
4014
|
+
if (message.includes("ENOTFOUND") || message.includes("ETIMEDOUT") || message.includes("network")) {
|
|
4015
|
+
return new Error(
|
|
4016
|
+
`Network error fetching: ${source}
|
|
4017
|
+
|
|
4018
|
+
Please check your internet connection.
|
|
4019
|
+
If you're behind a corporate proxy, you may need to set:
|
|
4020
|
+
export HTTPS_PROXY=http://your-proxy:port
|
|
4021
|
+
export FORCE_NODE_FETCH=true # Required for Node 20+`
|
|
4022
|
+
);
|
|
4023
|
+
}
|
|
4024
|
+
return new Error(`Failed to fetch ${source}: ${message}`);
|
|
4025
|
+
}
|
|
4026
|
+
async function fetchMarketplace(source, options = {}) {
|
|
4027
|
+
const result = await fetchFromSource(source, {
|
|
4028
|
+
forceRefresh: options.forceRefresh,
|
|
4029
|
+
subdir: ""
|
|
4030
|
+
// Root of repo
|
|
4031
|
+
});
|
|
4032
|
+
const marketplacePath = path25.join(result.path, ".claude-plugin", "marketplace.json");
|
|
4033
|
+
if (!await directoryExists(path25.dirname(marketplacePath))) {
|
|
4034
|
+
throw new Error(
|
|
4035
|
+
`Marketplace not found at: ${marketplacePath}
|
|
4036
|
+
|
|
4037
|
+
Expected .claude-plugin/marketplace.json in the source repository.`
|
|
4038
|
+
);
|
|
4039
|
+
}
|
|
4040
|
+
const content = await readFile(marketplacePath);
|
|
4041
|
+
const parsed = JSON.parse(content);
|
|
4042
|
+
const validation = marketplaceSchema.safeParse(parsed);
|
|
4043
|
+
if (!validation.success) {
|
|
4044
|
+
throw new Error(
|
|
4045
|
+
`Invalid marketplace.json at: ${marketplacePath}
|
|
4046
|
+
|
|
4047
|
+
Validation errors: ${validation.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
4048
|
+
);
|
|
4049
|
+
}
|
|
4050
|
+
const marketplace = validation.data;
|
|
4051
|
+
verbose(`Loaded marketplace: ${marketplace.name} v${marketplace.version}`);
|
|
4052
|
+
return {
|
|
4053
|
+
marketplace,
|
|
4054
|
+
sourcePath: result.path,
|
|
4055
|
+
fromCache: result.fromCache ?? false
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
// src/cli/lib/configuration/source-manager.ts
|
|
4060
|
+
var DEFAULT_SOURCE_NAME = "public";
|
|
4061
|
+
async function addSource(projectDir, url) {
|
|
4062
|
+
const result = await fetchMarketplace(url, { forceRefresh: true });
|
|
4063
|
+
const name = result.marketplace.name;
|
|
4064
|
+
const skillCount = result.marketplace.plugins.length;
|
|
4065
|
+
const config = await loadProjectSourceConfig(projectDir) ?? {};
|
|
4066
|
+
const sources = config.sources ?? [];
|
|
4067
|
+
const exists = sources.some((s) => s.name === name);
|
|
4068
|
+
if (exists) {
|
|
4069
|
+
throw new Error(`Source "${name}" already exists`);
|
|
4070
|
+
}
|
|
4071
|
+
sources.push({ name, url });
|
|
4072
|
+
config.sources = sources;
|
|
4073
|
+
await saveProjectConfig(projectDir, config);
|
|
4074
|
+
verbose(`Added source "${name}" with ${skillCount} skills`);
|
|
4075
|
+
return { name, skillCount };
|
|
4076
|
+
}
|
|
4077
|
+
async function removeSource(projectDir, name) {
|
|
4078
|
+
if (name === DEFAULT_SOURCE_NAME) {
|
|
4079
|
+
throw new Error(`Cannot remove the "${DEFAULT_SOURCE_NAME}" source`);
|
|
4080
|
+
}
|
|
4081
|
+
const config = await loadProjectSourceConfig(projectDir) ?? {};
|
|
4082
|
+
const sources = config.sources ?? [];
|
|
4083
|
+
const filtered = sources.filter((s) => s.name !== name);
|
|
4084
|
+
if (filtered.length === sources.length) {
|
|
4085
|
+
throw new Error(`Source "${name}" not found`);
|
|
4086
|
+
}
|
|
4087
|
+
config.sources = filtered;
|
|
4088
|
+
await saveProjectConfig(projectDir, config);
|
|
4089
|
+
verbose(`Removed source "${name}"`);
|
|
4090
|
+
}
|
|
4091
|
+
async function getSourceSummary(projectDir, matrix) {
|
|
4092
|
+
const config = await loadProjectSourceConfig(projectDir) ?? {};
|
|
4093
|
+
const sources = [
|
|
4094
|
+
{
|
|
4095
|
+
name: DEFAULT_SOURCE_NAME,
|
|
4096
|
+
url: config.source ?? DEFAULT_SOURCE,
|
|
4097
|
+
enabled: true
|
|
4098
|
+
}
|
|
4099
|
+
];
|
|
4100
|
+
if (config.sources) {
|
|
4101
|
+
for (const source of config.sources) {
|
|
4102
|
+
sources.push({ ...source, enabled: true });
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
let localSkillCount = 0;
|
|
4106
|
+
try {
|
|
4107
|
+
const localResult = await discoverLocalSkills(projectDir);
|
|
4108
|
+
if (localResult) {
|
|
4109
|
+
localSkillCount = localResult.skills.length;
|
|
4110
|
+
}
|
|
4111
|
+
} catch {
|
|
4112
|
+
verbose("Failed to discover local skills for source summary");
|
|
4113
|
+
}
|
|
4114
|
+
let pluginSkillCount = 0;
|
|
4115
|
+
if (matrix) {
|
|
4116
|
+
try {
|
|
4117
|
+
const pluginDir = getCollectivePluginDir(projectDir);
|
|
4118
|
+
const pluginSkillsDir = getPluginSkillsDir(pluginDir);
|
|
4119
|
+
const skillIds = await getPluginSkillIds(pluginSkillsDir, matrix);
|
|
4120
|
+
pluginSkillCount = skillIds.length;
|
|
4121
|
+
} catch {
|
|
4122
|
+
verbose("Failed to count plugin skills for source summary");
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
return { sources, localSkillCount, pluginSkillCount };
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
export {
|
|
4129
|
+
generateAgentPluginManifest,
|
|
4130
|
+
writePluginManifest,
|
|
4131
|
+
findPluginManifest,
|
|
4132
|
+
getCollectivePluginDir,
|
|
4133
|
+
getProjectPluginsDir,
|
|
4134
|
+
getPluginSkillsDir,
|
|
4135
|
+
getPluginAgentsDir,
|
|
4136
|
+
getPluginManifestPath,
|
|
4137
|
+
getPluginSkillIds,
|
|
4138
|
+
DEFAULT_SOURCE,
|
|
4139
|
+
SOURCE_ENV_VAR,
|
|
4140
|
+
getProjectConfigPath,
|
|
4141
|
+
loadProjectSourceConfig,
|
|
4142
|
+
saveProjectConfig,
|
|
4143
|
+
resolveSource,
|
|
4144
|
+
resolveAgentsSource,
|
|
4145
|
+
formatOrigin,
|
|
4146
|
+
resolveAuthor,
|
|
4147
|
+
resolveAllSources,
|
|
4148
|
+
getCurrentDate,
|
|
4149
|
+
hashString,
|
|
4150
|
+
hashFile,
|
|
4151
|
+
determinePluginVersion,
|
|
4152
|
+
writeContentHash,
|
|
4153
|
+
readForkedFromMetadata,
|
|
4154
|
+
compareSkills,
|
|
4155
|
+
injectForkedFromMetadata,
|
|
4156
|
+
copySkillsToPluginFromSource,
|
|
4157
|
+
copySkillsToLocalFlattened,
|
|
4158
|
+
parseFrontmatter,
|
|
4159
|
+
loadAllAgents,
|
|
4160
|
+
loadProjectAgents,
|
|
4161
|
+
loadPluginSkills,
|
|
4162
|
+
resolveAlias,
|
|
4163
|
+
validateSelection,
|
|
4164
|
+
getAvailableSkills,
|
|
4165
|
+
fetchFromSource,
|
|
4166
|
+
searchExtraSources,
|
|
4167
|
+
loadStacks,
|
|
4168
|
+
buildSkillRefsFromConfig,
|
|
4169
|
+
resolveAgents,
|
|
4170
|
+
extractFrontmatter,
|
|
4171
|
+
createLiquidEngine,
|
|
4172
|
+
compileAgentForPlugin,
|
|
4173
|
+
compileStackPlugin,
|
|
4174
|
+
printStackCompilationSummary,
|
|
4175
|
+
claudePluginInstall,
|
|
4176
|
+
isClaudeCLIAvailable,
|
|
4177
|
+
claudePluginMarketplaceExists,
|
|
4178
|
+
claudePluginMarketplaceAdd,
|
|
4179
|
+
claudePluginUninstall,
|
|
4180
|
+
installStackAsPlugin,
|
|
4181
|
+
loadSkillsMatrixFromSource,
|
|
4182
|
+
compileSkillPlugin,
|
|
4183
|
+
compileAllSkillPlugins,
|
|
4184
|
+
printCompilationSummary,
|
|
4185
|
+
discoverLocalSkills,
|
|
4186
|
+
archiveLocalSkill,
|
|
4187
|
+
restoreArchivedSkill,
|
|
4188
|
+
loadProjectConfig,
|
|
4189
|
+
validateProjectConfig,
|
|
4190
|
+
saveSourceToProjectConfig,
|
|
4191
|
+
addSource,
|
|
4192
|
+
removeSource,
|
|
4193
|
+
getSourceSummary,
|
|
4194
|
+
detectInstallation,
|
|
4195
|
+
installLocal,
|
|
4196
|
+
getInstallationInfo,
|
|
4197
|
+
formatInstallationDisplay,
|
|
4198
|
+
bumpPluginVersion,
|
|
4199
|
+
getPluginVersion,
|
|
4200
|
+
validatePlugin,
|
|
4201
|
+
validateAllPlugins,
|
|
4202
|
+
printPluginValidationResult
|
|
4203
|
+
};
|
|
4204
|
+
//# sourceMappingURL=chunk-ETQ3BPGU.js.map
|