@claude-collective/cli 0.26.1 → 0.29.4
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 +117 -0
- package/README.md +47 -13
- package/config/stacks.yaml +330 -93
- package/dist/chunk-56ERY7H7.js +29 -0
- package/dist/chunk-56ERY7H7.js.map +1 -0
- package/dist/{chunk-OBXAY23Y.js → chunk-5I6VY2E7.js} +5 -5
- package/dist/chunk-5I6VY2E7.js.map +1 -0
- package/dist/{chunk-324DM2L6.js → chunk-5WIHSJRO.js} +230 -65
- package/dist/chunk-5WIHSJRO.js.map +1 -0
- package/dist/{chunk-GGFOD5PK.js → chunk-6F3ZKDVE.js} +122 -66
- package/dist/chunk-6F3ZKDVE.js.map +1 -0
- package/dist/{chunk-DBRUQQUF.js → chunk-7GHTQSWI.js} +5 -1
- package/dist/{chunk-DBRUQQUF.js.map → chunk-7GHTQSWI.js.map} +1 -1
- package/dist/chunk-7ICBJZV2.js +63 -0
- package/dist/chunk-7ICBJZV2.js.map +1 -0
- package/dist/{chunk-3X5D7RM5.js → chunk-7UKQZSWT.js} +15 -4
- package/dist/chunk-7UKQZSWT.js.map +1 -0
- package/dist/{chunk-F4RD5FYM.js → chunk-A4T4YSV4.js} +5 -2
- package/dist/chunk-A4T4YSV4.js.map +1 -0
- package/dist/{chunk-VVYNZZUX.js → chunk-AG5YGYJT.js} +9 -5
- package/dist/chunk-AG5YGYJT.js.map +1 -0
- package/dist/chunk-AJFSCLJ7.js +81 -0
- package/dist/chunk-AJFSCLJ7.js.map +1 -0
- package/dist/{chunk-NQJ47R4N.js → chunk-CQZAKMPJ.js} +66 -14
- package/dist/chunk-CQZAKMPJ.js.map +1 -0
- package/dist/chunk-DIRH4PDF.js +24 -0
- package/dist/chunk-DIRH4PDF.js.map +1 -0
- package/dist/{chunk-HIQGK5XJ.js → chunk-DUIYVKFK.js} +123 -86
- package/dist/chunk-DUIYVKFK.js.map +1 -0
- package/dist/chunk-EP6J44I4.js +142 -0
- package/dist/chunk-EP6J44I4.js.map +1 -0
- package/dist/{chunk-2YMMJP4Z.js → chunk-EUPMWSM3.js} +92 -29
- package/dist/chunk-EUPMWSM3.js.map +1 -0
- package/dist/chunk-FUPBGSRA.js +66 -0
- package/dist/chunk-FUPBGSRA.js.map +1 -0
- package/dist/{chunk-U7HFKR74.js → chunk-FY5D4KIC.js} +5 -2
- package/dist/chunk-FY5D4KIC.js.map +1 -0
- package/dist/chunk-G5WXKKQM.js +233 -0
- package/dist/chunk-G5WXKKQM.js.map +1 -0
- package/dist/{chunk-KWYO3M5Q.js → chunk-GVVEPVR7.js} +25 -24
- package/dist/chunk-GVVEPVR7.js.map +1 -0
- package/dist/chunk-IFODQTCX.js +162 -0
- package/dist/chunk-IFODQTCX.js.map +1 -0
- package/dist/chunk-IQUBOWWU.js +366 -0
- package/dist/chunk-IQUBOWWU.js.map +1 -0
- package/dist/{chunk-ETCVEV3S.js → chunk-IRG52AN5.js} +242 -155
- package/dist/chunk-IRG52AN5.js.map +1 -0
- package/dist/chunk-MQAYAISQ.js +88 -0
- package/dist/chunk-MQAYAISQ.js.map +1 -0
- package/dist/{chunk-G35SYE6Z.js → chunk-N73GQTCK.js} +37 -104
- package/dist/chunk-N73GQTCK.js.map +1 -0
- package/dist/{chunk-H7SSBSPR.js → chunk-OA5RCL2L.js} +8 -5
- package/dist/chunk-OA5RCL2L.js.map +1 -0
- package/dist/{chunk-CZBNDP5B.js → chunk-PNXFJPXF.js} +3 -3
- package/dist/{chunk-MCTSHLAF.js → chunk-RI5QEK5W.js} +41 -14
- package/dist/chunk-RI5QEK5W.js.map +1 -0
- package/dist/chunk-RXC7AF7N.js +31 -0
- package/dist/chunk-RXC7AF7N.js.map +1 -0
- package/dist/{chunk-CQ7657GA.js → chunk-SSHG7MEE.js} +1248 -735
- package/dist/chunk-SSHG7MEE.js.map +1 -0
- package/dist/{chunk-4S4FCAA2.js → chunk-VTUPUKFD.js} +26 -31
- package/dist/chunk-VTUPUKFD.js.map +1 -0
- package/dist/{chunk-XENOESJZ.js → chunk-WLQUQXWO.js} +10 -67
- package/dist/chunk-WLQUQXWO.js.map +1 -0
- package/dist/chunk-WPED6CL3.js +105 -0
- package/dist/chunk-WPED6CL3.js.map +1 -0
- package/dist/{chunk-NT4K647L.js → chunk-XPMEGGJK.js} +97 -76
- package/dist/chunk-XPMEGGJK.js.map +1 -0
- package/dist/chunk-XZKVOPCR.js +75 -0
- package/dist/chunk-XZKVOPCR.js.map +1 -0
- package/dist/{chunk-ZW2PELOH.js → chunk-ZX5DM4D5.js} +106 -69
- package/dist/chunk-ZX5DM4D5.js.map +1 -0
- package/dist/commands/build/marketplace.js +21 -20
- package/dist/commands/build/marketplace.js.map +1 -1
- package/dist/commands/build/plugins.js +7 -11
- package/dist/commands/build/plugins.js.map +1 -1
- package/dist/commands/build/stack.js +8 -13
- package/dist/commands/build/stack.js.map +1 -1
- package/dist/commands/compile.js +109 -135
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/config/get.js +4 -5
- package/dist/commands/config/get.js.map +1 -1
- package/dist/commands/config/index.js +5 -6
- package/dist/commands/config/index.js.map +1 -1
- package/dist/commands/config/path.js +4 -5
- package/dist/commands/config/path.js.map +1 -1
- package/dist/commands/config/set-project.js +4 -5
- package/dist/commands/config/set-project.js.map +1 -1
- package/dist/commands/config/show.js +5 -6
- package/dist/commands/config/unset-project.js +4 -5
- package/dist/commands/config/unset-project.js.map +1 -1
- package/dist/commands/diff.js +26 -11
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/doctor.js +13 -16
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/edit.js +71 -42
- package/dist/commands/edit.js.map +1 -1
- package/dist/commands/eject.js +34 -14
- package/dist/commands/eject.js.map +1 -1
- package/dist/commands/import/skill.js +93 -52
- package/dist/commands/import/skill.js.map +1 -1
- package/dist/commands/info.js +27 -9
- package/dist/commands/info.js.map +1 -1
- package/dist/commands/init.js +98 -48
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/list.js +10 -5
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/new/agent.js +8 -11
- package/dist/commands/new/agent.js.map +1 -1
- package/dist/commands/new/skill.js +17 -18
- package/dist/commands/new/skill.js.map +1 -1
- package/dist/commands/outdated.js +23 -9
- package/dist/commands/outdated.js.map +1 -1
- package/dist/commands/search.js +23 -20
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/uninstall.js +28 -21
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.js +30 -22
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/validate.js +103 -39
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/version/bump.js +4 -5
- package/dist/commands/version/bump.js.map +1 -1
- package/dist/commands/version/index.js +4 -5
- package/dist/commands/version/index.js.map +1 -1
- package/dist/commands/version/set.js +4 -5
- package/dist/commands/version/set.js.map +1 -1
- package/dist/commands/version/show.js +4 -5
- package/dist/commands/version/show.js.map +1 -1
- package/dist/components/common/confirm.test.js +2 -2
- package/dist/components/common/confirm.test.js.map +1 -1
- package/dist/components/skill-search/skill-search.js +4 -2
- package/dist/components/wizard/category-grid.js +3 -1
- package/dist/components/wizard/category-grid.test.js +63 -64
- package/dist/components/wizard/category-grid.test.js.map +1 -1
- package/dist/components/wizard/domain-selection.js +13 -0
- package/dist/components/wizard/help-modal.js +10 -0
- package/dist/components/wizard/help-modal.js.map +1 -0
- package/dist/components/wizard/menu-item.js +2 -1
- package/dist/components/wizard/search-modal.js +3 -1
- package/dist/components/wizard/search-modal.test.js +4 -2
- package/dist/components/wizard/search-modal.test.js.map +1 -1
- package/dist/components/wizard/section-progress.js +2 -1
- package/dist/components/wizard/section-progress.test.js +2 -1
- package/dist/components/wizard/section-progress.test.js.map +1 -1
- package/dist/components/wizard/source-grid.js +6 -2
- package/dist/components/wizard/source-grid.test.js +49 -45
- package/dist/components/wizard/source-grid.test.js.map +1 -1
- package/dist/components/wizard/stack-selection.js +15 -0
- package/dist/components/wizard/stack-selection.js.map +1 -0
- package/dist/components/wizard/step-approach.js +8 -6
- package/dist/components/wizard/step-approach.test.js +11 -9
- package/dist/components/wizard/step-approach.test.js.map +1 -1
- package/dist/components/wizard/step-build.js +9 -13
- package/dist/components/wizard/step-build.test.js +27 -45
- package/dist/components/wizard/step-build.test.js.map +1 -1
- package/dist/components/wizard/step-confirm.js +2 -1
- package/dist/components/wizard/step-confirm.test.js +6 -5
- package/dist/components/wizard/step-confirm.test.js.map +1 -1
- package/dist/components/wizard/step-refine.js +2 -1
- package/dist/components/wizard/step-refine.test.js +3 -2
- package/dist/components/wizard/step-refine.test.js.map +1 -1
- package/dist/components/wizard/step-settings.js +8 -6
- package/dist/components/wizard/step-settings.test.js +12 -10
- package/dist/components/wizard/step-settings.test.js.map +1 -1
- package/dist/components/wizard/step-sources.js +11 -9
- package/dist/components/wizard/step-sources.test.js +16 -15
- package/dist/components/wizard/step-sources.test.js.map +1 -1
- package/dist/components/wizard/step-stack.js +9 -6
- package/dist/components/wizard/step-stack.test.js +15 -12
- package/dist/components/wizard/step-stack.test.js.map +1 -1
- package/dist/components/wizard/view-title.js +2 -1
- package/dist/components/wizard/wizard-layout.js +7 -9
- package/dist/components/wizard/wizard-tabs.js +2 -1
- package/dist/components/wizard/wizard-tabs.test.js +2 -1
- package/dist/components/wizard/wizard-tabs.test.js.map +1 -1
- package/dist/components/wizard/wizard.js +26 -20
- package/dist/config/stacks.yaml +330 -93
- package/dist/hooks/init.js +3 -4
- package/dist/hooks/init.js.map +1 -1
- package/dist/source-manager-TV2YGPAN.js +15 -0
- package/dist/source-manager-TV2YGPAN.js.map +1 -0
- package/dist/stores/wizard-store.js +4 -3
- package/dist/stores/wizard-store.test.js +272 -25
- package/dist/stores/wizard-store.test.js.map +1 -1
- package/package.json +2 -1
- package/src/schemas/agent-frontmatter.schema.json +84 -0
- package/src/schemas/agent.schema.json +93 -0
- package/src/schemas/hooks.schema.json +47 -0
- package/src/schemas/marketplace.schema.json +119 -0
- package/src/schemas/metadata.schema.json +113 -0
- package/src/schemas/plugin.schema.json +130 -0
- package/src/schemas/project-config.schema.json +125 -0
- package/src/schemas/project-source-config.schema.json +81 -0
- package/src/schemas/skill-frontmatter.schema.json +42 -0
- package/src/schemas/skills-matrix.schema.json +467 -0
- package/src/schemas/stack.schema.json +191 -0
- package/src/schemas/stacks.schema.json +111 -0
- package/dist/chunk-2OW7FCIF.js +0 -197
- package/dist/chunk-2OW7FCIF.js.map +0 -1
- package/dist/chunk-2YMMJP4Z.js.map +0 -1
- package/dist/chunk-324DM2L6.js.map +0 -1
- package/dist/chunk-3X5D7RM5.js.map +0 -1
- package/dist/chunk-4S4FCAA2.js.map +0 -1
- package/dist/chunk-CQ7657GA.js.map +0 -1
- package/dist/chunk-ETCVEV3S.js.map +0 -1
- package/dist/chunk-F4RD5FYM.js.map +0 -1
- package/dist/chunk-G35SYE6Z.js.map +0 -1
- package/dist/chunk-GGFOD5PK.js.map +0 -1
- package/dist/chunk-H7SSBSPR.js.map +0 -1
- package/dist/chunk-HIQGK5XJ.js.map +0 -1
- package/dist/chunk-HWD32NP7.js +0 -19
- package/dist/chunk-HWD32NP7.js.map +0 -1
- package/dist/chunk-KWYO3M5Q.js.map +0 -1
- package/dist/chunk-MCTSHLAF.js.map +0 -1
- package/dist/chunk-NQJ47R4N.js.map +0 -1
- package/dist/chunk-NT4K647L.js.map +0 -1
- package/dist/chunk-O6ZTD7ZI.js +0 -70
- package/dist/chunk-O6ZTD7ZI.js.map +0 -1
- package/dist/chunk-OBXAY23Y.js.map +0 -1
- package/dist/chunk-TMED5DQ2.js +0 -210
- package/dist/chunk-TMED5DQ2.js.map +0 -1
- package/dist/chunk-U7HFKR74.js.map +0 -1
- package/dist/chunk-UEMRJI2K.js +0 -146
- package/dist/chunk-UEMRJI2K.js.map +0 -1
- package/dist/chunk-UNN7523L.js +0 -78
- package/dist/chunk-UNN7523L.js.map +0 -1
- package/dist/chunk-VVYNZZUX.js.map +0 -1
- package/dist/chunk-XENOESJZ.js.map +0 -1
- package/dist/chunk-ZW2PELOH.js.map +0 -1
- package/dist/source-manager-VWIIDTK5.js +0 -16
- /package/dist/{chunk-CZBNDP5B.js.map → chunk-PNXFJPXF.js.map} +0 -0
- /package/dist/{source-manager-VWIIDTK5.js.map → components/wizard/domain-selection.js.map} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
SKILL_ID_PATTERN,
|
|
3
4
|
agentFrontmatterValidationSchema,
|
|
4
5
|
agentYamlConfigSchema,
|
|
5
6
|
categoryPathSchema,
|
|
@@ -7,11 +8,14 @@ import {
|
|
|
7
8
|
directoryExists,
|
|
8
9
|
ensureDir,
|
|
9
10
|
fileExists,
|
|
11
|
+
formatZodErrors,
|
|
12
|
+
getErrorMessage,
|
|
10
13
|
glob,
|
|
11
14
|
hooksRecordSchema,
|
|
12
15
|
listDirectories,
|
|
13
16
|
localRawMetadataSchema,
|
|
14
17
|
localSkillMetadataSchema,
|
|
18
|
+
log,
|
|
15
19
|
marketplaceSchema,
|
|
16
20
|
pluginAuthorSchema,
|
|
17
21
|
pluginManifestSchema,
|
|
@@ -19,6 +23,7 @@ import {
|
|
|
19
23
|
projectSourceConfigSchema,
|
|
20
24
|
readFile,
|
|
21
25
|
readFileOptional,
|
|
26
|
+
readFileSafe,
|
|
22
27
|
remove,
|
|
23
28
|
skillDisplayNameSchema,
|
|
24
29
|
skillFrontmatterLoaderSchema,
|
|
@@ -27,31 +32,43 @@ import {
|
|
|
27
32
|
skillMetadataLoaderSchema,
|
|
28
33
|
skillsMatrixConfigSchema,
|
|
29
34
|
stacksConfigSchema,
|
|
35
|
+
validateNestingDepth,
|
|
30
36
|
verbose,
|
|
31
37
|
warn,
|
|
38
|
+
warnUnknownFields,
|
|
32
39
|
writeFile
|
|
33
|
-
} from "./chunk-
|
|
34
|
-
import {
|
|
35
|
-
typedEntries,
|
|
36
|
-
typedKeys
|
|
37
|
-
} from "./chunk-HWD32NP7.js";
|
|
40
|
+
} from "./chunk-5WIHSJRO.js";
|
|
38
41
|
import {
|
|
39
42
|
ARCHIVED_SKILLS_DIR_NAME,
|
|
40
43
|
CACHE_DIR,
|
|
44
|
+
CACHE_HASH_LENGTH,
|
|
45
|
+
CACHE_READABLE_PREFIX_LENGTH,
|
|
41
46
|
CLAUDE_DIR,
|
|
42
47
|
CLAUDE_SRC_DIR,
|
|
43
48
|
DEFAULT_DISPLAY_VERSION,
|
|
49
|
+
DEFAULT_PLUGIN_NAME,
|
|
44
50
|
DEFAULT_VERSION,
|
|
45
51
|
DIRS,
|
|
46
|
-
|
|
52
|
+
GITHUB_SOURCE,
|
|
53
|
+
HASH_PREFIX_LENGTH,
|
|
47
54
|
LOCAL_SKILLS_PATH,
|
|
55
|
+
MAX_CONFIG_FILE_SIZE,
|
|
56
|
+
MAX_JSON_NESTING_DEPTH,
|
|
57
|
+
MAX_MARKETPLACE_FILE_SIZE,
|
|
58
|
+
MAX_MARKETPLACE_PLUGINS,
|
|
59
|
+
MAX_PLUGIN_FILE_SIZE,
|
|
48
60
|
PLUGINS_SUBDIR,
|
|
49
61
|
PLUGIN_MANIFEST_DIR,
|
|
50
62
|
PLUGIN_MANIFEST_FILE,
|
|
51
63
|
PROJECT_ROOT,
|
|
64
|
+
SCHEMA_PATHS,
|
|
52
65
|
SKILLS_DIR_PATH,
|
|
53
|
-
SKILLS_MATRIX_PATH
|
|
54
|
-
|
|
66
|
+
SKILLS_MATRIX_PATH,
|
|
67
|
+
STANDARD_DIRS,
|
|
68
|
+
STANDARD_FILES,
|
|
69
|
+
YAML_FORMATTING,
|
|
70
|
+
yamlSchemaComment
|
|
71
|
+
} from "./chunk-IFODQTCX.js";
|
|
55
72
|
import {
|
|
56
73
|
init_esm_shims
|
|
57
74
|
} from "./chunk-AWKZ5BDL.js";
|
|
@@ -62,16 +79,37 @@ init_esm_shims();
|
|
|
62
79
|
// src/cli/lib/configuration/config.ts
|
|
63
80
|
init_esm_shims();
|
|
64
81
|
import path from "path";
|
|
65
|
-
import {
|
|
66
|
-
|
|
82
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
83
|
+
|
|
84
|
+
// src/cli/utils/yaml.ts
|
|
85
|
+
init_esm_shims();
|
|
86
|
+
import { parse as parseYaml } from "yaml";
|
|
87
|
+
async function safeLoadYamlFile(filePath, schema, maxSizeBytes = MAX_CONFIG_FILE_SIZE) {
|
|
88
|
+
try {
|
|
89
|
+
const content = await readFileSafe(filePath, maxSizeBytes);
|
|
90
|
+
const parsed = parseYaml(content);
|
|
91
|
+
const result = schema.safeParse(parsed);
|
|
92
|
+
if (!result.success) {
|
|
93
|
+
warn(`Invalid YAML at '${filePath}': ${result.error.message}`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return result.data;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
warn(`Failed to parse YAML at '${filePath}': ${error}`);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/cli/lib/configuration/config.ts
|
|
104
|
+
var DEFAULT_SOURCE = `${GITHUB_SOURCE.GITHUB_PREFIX}claude-collective/skills`;
|
|
67
105
|
var SOURCE_ENV_VAR = "CC_SOURCE";
|
|
68
|
-
var PROJECT_CONFIG_FILE =
|
|
106
|
+
var PROJECT_CONFIG_FILE = STANDARD_FILES.CONFIG_YAML;
|
|
69
107
|
function getProjectConfigPath(projectDir) {
|
|
70
108
|
return path.join(projectDir, CLAUDE_SRC_DIR, PROJECT_CONFIG_FILE);
|
|
71
109
|
}
|
|
72
110
|
async function loadProjectSourceConfig(projectDir) {
|
|
73
111
|
const srcConfigPath = getProjectConfigPath(projectDir);
|
|
74
|
-
const legacyConfigPath = path.join(projectDir, CLAUDE_DIR,
|
|
112
|
+
const legacyConfigPath = path.join(projectDir, CLAUDE_DIR, STANDARD_FILES.CONFIG_YAML);
|
|
75
113
|
let configPath = srcConfigPath;
|
|
76
114
|
if (!await fileExists(srcConfigPath)) {
|
|
77
115
|
if (await fileExists(legacyConfigPath)) {
|
|
@@ -82,26 +120,18 @@ async function loadProjectSourceConfig(projectDir) {
|
|
|
82
120
|
return null;
|
|
83
121
|
}
|
|
84
122
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
}
|
|
123
|
+
const data = await safeLoadYamlFile(configPath, projectSourceConfigSchema);
|
|
124
|
+
if (!data) return null;
|
|
125
|
+
verbose(`Loaded project config from ${configPath}`);
|
|
126
|
+
return data;
|
|
99
127
|
}
|
|
100
128
|
async function saveProjectConfig(projectDir, config) {
|
|
101
129
|
const configPath = getProjectConfigPath(projectDir);
|
|
102
130
|
await ensureDir(path.join(projectDir, CLAUDE_SRC_DIR));
|
|
103
|
-
const
|
|
104
|
-
|
|
131
|
+
const schemaComment = `${yamlSchemaComment(SCHEMA_PATHS.projectSourceConfig)}
|
|
132
|
+
`;
|
|
133
|
+
const content = stringifyYaml(config, { lineWidth: YAML_FORMATTING.LINE_WIDTH_NONE });
|
|
134
|
+
await writeFile(configPath, `${schemaComment}${content}`);
|
|
105
135
|
verbose(`Saved project config to ${configPath}`);
|
|
106
136
|
}
|
|
107
137
|
async function resolveSource(flagValue, projectDir) {
|
|
@@ -109,15 +139,32 @@ async function resolveSource(flagValue, projectDir) {
|
|
|
109
139
|
const marketplace = projectConfig?.marketplace;
|
|
110
140
|
if (flagValue !== void 0) {
|
|
111
141
|
if (flagValue === "" || flagValue.trim() === "") {
|
|
112
|
-
throw new Error(
|
|
142
|
+
throw new Error(
|
|
143
|
+
"--source flag cannot be empty. Provide a valid source: a local directory path or a git repository URL (e.g., './my-skills' or 'https://github.com/user/repo')"
|
|
144
|
+
);
|
|
113
145
|
}
|
|
146
|
+
validateSourceFormat(flagValue.trim(), "--source");
|
|
114
147
|
verbose(`Source from --source flag: ${flagValue}`);
|
|
115
148
|
return { source: flagValue, sourceOrigin: "flag", marketplace };
|
|
116
149
|
}
|
|
117
150
|
const envValue = process.env[SOURCE_ENV_VAR];
|
|
118
151
|
if (envValue) {
|
|
119
|
-
|
|
120
|
-
|
|
152
|
+
const trimmed = envValue.trim();
|
|
153
|
+
if (trimmed === "") {
|
|
154
|
+
warn(`${SOURCE_ENV_VAR} is set but empty \u2014 ignoring and falling back to next source.`);
|
|
155
|
+
} else {
|
|
156
|
+
try {
|
|
157
|
+
validateSourceFormat(trimmed, SOURCE_ENV_VAR);
|
|
158
|
+
verbose(`Source from ${SOURCE_ENV_VAR} env var: ${trimmed}`);
|
|
159
|
+
return { source: trimmed, sourceOrigin: "env", marketplace };
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
warn(
|
|
163
|
+
`${SOURCE_ENV_VAR} has an invalid value \u2014 ignoring and falling back to next source.
|
|
164
|
+
${message}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
121
168
|
}
|
|
122
169
|
if (projectConfig?.source) {
|
|
123
170
|
verbose(`Source from project config: ${projectConfig.source}`);
|
|
@@ -133,8 +180,11 @@ async function resolveSource(flagValue, projectDir) {
|
|
|
133
180
|
async function resolveAgentsSource(flagValue, projectDir) {
|
|
134
181
|
if (flagValue !== void 0) {
|
|
135
182
|
if (flagValue === "" || flagValue.trim() === "") {
|
|
136
|
-
throw new Error(
|
|
183
|
+
throw new Error(
|
|
184
|
+
"--agent-source flag cannot be empty. Provide a valid source: a local directory path or a git repository URL (e.g., './my-agents' or 'https://github.com/user/repo')"
|
|
185
|
+
);
|
|
137
186
|
}
|
|
187
|
+
validateSourceFormat(flagValue.trim(), "--agent-source");
|
|
138
188
|
verbose(`Agents source from --agent-source flag: ${flagValue}`);
|
|
139
189
|
return { agentsSource: flagValue, agentsSourceOrigin: "flag" };
|
|
140
190
|
}
|
|
@@ -160,6 +210,8 @@ function formatOrigin(type, origin) {
|
|
|
160
210
|
return `${SOURCE_ENV_VAR} environment variable`;
|
|
161
211
|
case "default":
|
|
162
212
|
return "default";
|
|
213
|
+
default:
|
|
214
|
+
break;
|
|
163
215
|
}
|
|
164
216
|
}
|
|
165
217
|
switch (origin) {
|
|
@@ -167,6 +219,8 @@ function formatOrigin(type, origin) {
|
|
|
167
219
|
return "--agent-source flag";
|
|
168
220
|
case "default":
|
|
169
221
|
return "default (local CLI)";
|
|
222
|
+
default:
|
|
223
|
+
break;
|
|
170
224
|
}
|
|
171
225
|
return origin;
|
|
172
226
|
}
|
|
@@ -194,23 +248,155 @@ async function resolveAllSources(projectDir) {
|
|
|
194
248
|
}
|
|
195
249
|
return { primary, extras };
|
|
196
250
|
}
|
|
251
|
+
var REMOTE_PROTOCOLS = [
|
|
252
|
+
GITHUB_SOURCE.GITHUB_PREFIX,
|
|
253
|
+
// "github:"
|
|
254
|
+
GITHUB_SOURCE.GH_PREFIX,
|
|
255
|
+
// "gh:"
|
|
256
|
+
"gitlab:",
|
|
257
|
+
"bitbucket:",
|
|
258
|
+
"sourcehut:",
|
|
259
|
+
"https://",
|
|
260
|
+
"http://"
|
|
261
|
+
];
|
|
262
|
+
var MIN_REMOTE_PATH_LENGTH = 3;
|
|
263
|
+
var MAX_SOURCE_LENGTH = 512;
|
|
264
|
+
var NULL_BYTE_PATTERN = /\0/;
|
|
265
|
+
var PATH_TRAVERSAL_PATTERN = /\.\./;
|
|
266
|
+
var UNC_PATH_PATTERN = /^(?:\/\/|\\\\)/;
|
|
267
|
+
var PRIVATE_IPV4_PATTERN = /^(?:127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|0\.0\.0\.0|169\.254\.\d+\.\d+)$/;
|
|
268
|
+
var PRIVATE_IPV6_PATTERN = /^\[(?:::1|::ffff:(?:127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+)|fd[0-9a-f]{2}:.*|fe80:.*)\]$/i;
|
|
269
|
+
function validateSourceFormat(source, flagName) {
|
|
270
|
+
if (NULL_BYTE_PATTERN.test(source)) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`${flagName} contains invalid characters.
|
|
273
|
+
|
|
274
|
+
Source values must not contain null bytes.
|
|
275
|
+
Examples:
|
|
276
|
+
${flagName} ./my-skills
|
|
277
|
+
${flagName} github:user/repo`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (source.length > MAX_SOURCE_LENGTH) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`${flagName} value is too long (${source.length} characters, max ${MAX_SOURCE_LENGTH}).
|
|
283
|
+
|
|
284
|
+
Provide a shorter source path or URL.
|
|
285
|
+
Examples:
|
|
286
|
+
${flagName} ./my-skills
|
|
287
|
+
${flagName} github:user/repo`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const matchedProtocol = REMOTE_PROTOCOLS.find((prefix) => source.startsWith(prefix));
|
|
291
|
+
if (matchedProtocol) {
|
|
292
|
+
validateRemoteSource(source, matchedProtocol, flagName);
|
|
293
|
+
} else {
|
|
294
|
+
validateLocalPath(source, flagName);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function validateRemoteSource(source, protocol, flagName) {
|
|
298
|
+
const pathAfterProtocol = source.slice(protocol.length).trim();
|
|
299
|
+
if (pathAfterProtocol.length < MIN_REMOTE_PATH_LENGTH) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`${flagName} has an incomplete URL: "${source}"
|
|
302
|
+
|
|
303
|
+
A repository path is required after the protocol prefix.
|
|
304
|
+
Examples:
|
|
305
|
+
${flagName} github:user/repo
|
|
306
|
+
${flagName} https://github.com/user/repo`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (PATH_TRAVERSAL_PATTERN.test(pathAfterProtocol)) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`${flagName} contains path traversal in URL: "${source}"
|
|
312
|
+
|
|
313
|
+
Remote source URLs must not contain '..' sequences.
|
|
314
|
+
Examples:
|
|
315
|
+
${flagName} github:user/repo
|
|
316
|
+
${flagName} https://github.com/user/repo`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if (protocol === "https://" || protocol === "http://") {
|
|
320
|
+
validateHttpUrl(source, flagName);
|
|
321
|
+
}
|
|
322
|
+
if (protocol !== "https://" && protocol !== "http://") {
|
|
323
|
+
validateGitShorthand(source, pathAfterProtocol, flagName);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function validateHttpUrl(source, flagName) {
|
|
327
|
+
const afterProtocol = source.replace(/^https?:\/\//, "");
|
|
328
|
+
const hostnameWithPort = afterProtocol.split("/")[0] ?? "";
|
|
329
|
+
const hostname = hostnameWithPort.split(":")[0] ?? "";
|
|
330
|
+
const isBracketedIPv6 = hostnameWithPort.startsWith("[") && hostnameWithPort.includes("]");
|
|
331
|
+
if (!hostname || !hostname.includes(".") && hostname !== "localhost" && !isBracketedIPv6) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`${flagName} has an invalid URL: "${source}"
|
|
334
|
+
|
|
335
|
+
The URL must include a valid hostname.
|
|
336
|
+
Examples:
|
|
337
|
+
${flagName} https://github.com/user/repo
|
|
338
|
+
${flagName} https://gitlab.company.com/team/skills`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (PRIVATE_IPV4_PATTERN.test(hostname) || PRIVATE_IPV6_PATTERN.test(hostnameWithPort)) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`${flagName} points to a private or reserved IP address: "${source}"
|
|
344
|
+
|
|
345
|
+
Source URLs must not target private network addresses.
|
|
346
|
+
Use a public hostname instead.
|
|
347
|
+
Examples:
|
|
348
|
+
${flagName} https://github.com/user/repo
|
|
349
|
+
${flagName} https://gitlab.company.com/team/skills`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function validateGitShorthand(source, repoPath, flagName) {
|
|
354
|
+
if (!repoPath.includes("/")) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`${flagName} has an invalid repository reference: "${source}"
|
|
357
|
+
|
|
358
|
+
Git shorthand sources require an owner/repo format.
|
|
359
|
+
Examples:
|
|
360
|
+
${flagName} github:user/repo
|
|
361
|
+
${flagName} gh:organization/skills`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function validateLocalPath(source, flagName) {
|
|
366
|
+
const CONTROL_CHAR_PATTERN2 = /[\x00-\x08\x0E-\x1F\x7F]/u;
|
|
367
|
+
if (CONTROL_CHAR_PATTERN2.test(source)) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
`${flagName} contains invalid characters: "${source}"
|
|
370
|
+
|
|
371
|
+
Source paths must not contain control characters.
|
|
372
|
+
Examples:
|
|
373
|
+
${flagName} ./my-skills
|
|
374
|
+
${flagName} /home/user/skills`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
if (UNC_PATH_PATTERN.test(source)) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
`${flagName} contains a UNC network path: "${source}"
|
|
380
|
+
|
|
381
|
+
Network paths (\\\\server\\share or //server/share) are not allowed for security reasons.
|
|
382
|
+
Use a local directory path or a remote URL instead.
|
|
383
|
+
Examples:
|
|
384
|
+
${flagName} ./my-skills
|
|
385
|
+
${flagName} /home/user/skills
|
|
386
|
+
${flagName} https://github.com/user/repo`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
197
390
|
function isLocalSource(source) {
|
|
198
391
|
if (source.startsWith("/") || source.startsWith(".")) {
|
|
199
392
|
return true;
|
|
200
393
|
}
|
|
201
|
-
const
|
|
202
|
-
"github:",
|
|
203
|
-
"gh:",
|
|
204
|
-
"gitlab:",
|
|
205
|
-
"bitbucket:",
|
|
206
|
-
"sourcehut:",
|
|
207
|
-
"https://",
|
|
208
|
-
"http://"
|
|
209
|
-
];
|
|
210
|
-
const hasRemoteProtocol = remoteProtocols.some((prefix) => source.startsWith(prefix));
|
|
394
|
+
const hasRemoteProtocol = REMOTE_PROTOCOLS.some((prefix) => source.startsWith(prefix));
|
|
211
395
|
if (!hasRemoteProtocol) {
|
|
212
396
|
if (source.includes("..") || source.includes("~")) {
|
|
213
|
-
throw new Error(
|
|
397
|
+
throw new Error(
|
|
398
|
+
`Invalid source path: ${source}. Path traversal patterns like '..' and '~' are not allowed for security reasons. Use absolute paths or remote URLs instead (e.g., '/home/user/skills' or 'https://github.com/user/repo').`
|
|
399
|
+
);
|
|
214
400
|
}
|
|
215
401
|
}
|
|
216
402
|
return !hasRemoteProtocol;
|
|
@@ -218,8 +404,9 @@ function isLocalSource(source) {
|
|
|
218
404
|
|
|
219
405
|
// src/cli/lib/loading/source-fetcher.ts
|
|
220
406
|
init_esm_shims();
|
|
221
|
-
import
|
|
407
|
+
import { createHash as createHash2 } from "crypto";
|
|
222
408
|
import { downloadTemplate } from "giget";
|
|
409
|
+
import path24 from "path";
|
|
223
410
|
|
|
224
411
|
// src/cli/lib/configuration/index.ts
|
|
225
412
|
init_esm_shims();
|
|
@@ -227,45 +414,73 @@ init_esm_shims();
|
|
|
227
414
|
// src/cli/lib/configuration/config-generator.ts
|
|
228
415
|
init_esm_shims();
|
|
229
416
|
|
|
417
|
+
// src/cli/utils/typed-object.ts
|
|
418
|
+
init_esm_shims();
|
|
419
|
+
function typedEntries(obj) {
|
|
420
|
+
return Object.entries(obj);
|
|
421
|
+
}
|
|
422
|
+
function typedKeys(obj) {
|
|
423
|
+
return Object.keys(obj);
|
|
424
|
+
}
|
|
425
|
+
|
|
230
426
|
// src/cli/lib/skills/index.ts
|
|
231
427
|
init_esm_shims();
|
|
232
428
|
|
|
233
429
|
// src/cli/lib/skills/skill-metadata.ts
|
|
234
430
|
init_esm_shims();
|
|
235
431
|
import path3 from "path";
|
|
236
|
-
import { parse as parseYaml3, stringify as stringifyYaml3 } from "yaml";
|
|
237
432
|
import { sortBy } from "remeda";
|
|
433
|
+
import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
|
|
238
434
|
|
|
239
435
|
// src/cli/lib/versioning.ts
|
|
240
436
|
init_esm_shims();
|
|
241
437
|
import { createHash } from "crypto";
|
|
242
438
|
import path2 from "path";
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
var
|
|
439
|
+
|
|
440
|
+
// src/cli/lib/metadata-keys.ts
|
|
441
|
+
init_esm_shims();
|
|
442
|
+
var METADATA_KEYS = {
|
|
443
|
+
CLI_NAME: "cli_name",
|
|
444
|
+
CLI_DESCRIPTION: "cli_description",
|
|
445
|
+
CATEGORY: "category",
|
|
446
|
+
FORKED_FROM: "forked_from",
|
|
447
|
+
CONTENT_HASH: "content_hash",
|
|
448
|
+
USAGE_GUIDANCE: "usage_guidance"
|
|
449
|
+
};
|
|
450
|
+
var IMPORT_DEFAULTS = {
|
|
451
|
+
CATEGORY: "imported",
|
|
452
|
+
AUTHOR: "@imported"
|
|
453
|
+
};
|
|
454
|
+
var LOCAL_DEFAULTS = {
|
|
455
|
+
CATEGORY: "local",
|
|
456
|
+
AUTHOR: "@local"
|
|
457
|
+
};
|
|
458
|
+
var SKILL_CONTENT_FILES = [STANDARD_FILES.SKILL_MD, STANDARD_FILES.REFERENCE_MD];
|
|
459
|
+
var SKILL_CONTENT_DIRS = [STANDARD_DIRS.EXAMPLES, STANDARD_DIRS.SCRIPTS];
|
|
460
|
+
|
|
461
|
+
// src/cli/lib/versioning.ts
|
|
247
462
|
function getCurrentDate() {
|
|
248
463
|
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
249
464
|
}
|
|
250
|
-
function
|
|
465
|
+
function computeStringHash(content) {
|
|
251
466
|
const hash = createHash("sha256");
|
|
252
467
|
hash.update(content);
|
|
253
468
|
return hash.digest("hex").slice(0, HASH_PREFIX_LENGTH);
|
|
254
469
|
}
|
|
255
|
-
async function
|
|
470
|
+
async function computeFileHash(filePath) {
|
|
256
471
|
const content = await readFile(filePath);
|
|
257
|
-
return
|
|
472
|
+
return computeStringHash(content);
|
|
258
473
|
}
|
|
259
|
-
async function
|
|
474
|
+
async function computeSkillFolderHash(skillPath) {
|
|
260
475
|
const contents = [];
|
|
261
|
-
for (const fileName of
|
|
476
|
+
for (const fileName of SKILL_CONTENT_FILES) {
|
|
262
477
|
const filePath = path2.join(skillPath, fileName);
|
|
263
478
|
if (await fileExists(filePath)) {
|
|
264
479
|
const content = await readFile(filePath);
|
|
265
480
|
contents.push(`${fileName}:${content}`);
|
|
266
481
|
}
|
|
267
482
|
}
|
|
268
|
-
for (const dirName of
|
|
483
|
+
for (const dirName of SKILL_CONTENT_DIRS) {
|
|
269
484
|
const dirPath = path2.join(skillPath, dirName);
|
|
270
485
|
if (await fileExists(dirPath)) {
|
|
271
486
|
const files = await glob("**/*", dirPath);
|
|
@@ -277,7 +492,7 @@ async function hashSkillFolder(skillPath) {
|
|
|
277
492
|
}
|
|
278
493
|
}
|
|
279
494
|
const combined = contents.join("\n---\n");
|
|
280
|
-
return
|
|
495
|
+
return computeStringHash(combined);
|
|
281
496
|
}
|
|
282
497
|
var CONTENT_HASH_FILE = ".content-hash";
|
|
283
498
|
function parseMajorVersion(version) {
|
|
@@ -294,9 +509,9 @@ async function readExistingPluginManifest(pluginDir, getManifestPath) {
|
|
|
294
509
|
return null;
|
|
295
510
|
}
|
|
296
511
|
try {
|
|
297
|
-
const content = await
|
|
512
|
+
const content = await readFileSafe(manifestPath, MAX_PLUGIN_FILE_SIZE);
|
|
298
513
|
const manifest = pluginManifestSchema.parse(JSON.parse(content));
|
|
299
|
-
const hashFilePath = manifestPath.replace(
|
|
514
|
+
const hashFilePath = manifestPath.replace(STANDARD_FILES.PLUGIN_JSON, CONTENT_HASH_FILE);
|
|
300
515
|
let contentHash;
|
|
301
516
|
if (await fileExists(hashFilePath)) {
|
|
302
517
|
contentHash = (await readFile(hashFilePath)).trim();
|
|
@@ -305,7 +520,8 @@ async function readExistingPluginManifest(pluginDir, getManifestPath) {
|
|
|
305
520
|
version: manifest.version ?? DEFAULT_VERSION,
|
|
306
521
|
contentHash
|
|
307
522
|
};
|
|
308
|
-
} catch {
|
|
523
|
+
} catch (error) {
|
|
524
|
+
warn(`Failed to read plugin manifest at '${manifestPath}': ${getErrorMessage(error)}`);
|
|
309
525
|
return null;
|
|
310
526
|
}
|
|
311
527
|
}
|
|
@@ -329,22 +545,23 @@ async function determinePluginVersion(newHash, pluginDir, getManifestPath) {
|
|
|
329
545
|
};
|
|
330
546
|
}
|
|
331
547
|
async function writeContentHash(pluginDir, contentHash, getManifestPath) {
|
|
332
|
-
const hashFilePath = getManifestPath(pluginDir).replace(
|
|
548
|
+
const hashFilePath = getManifestPath(pluginDir).replace(
|
|
549
|
+
STANDARD_FILES.PLUGIN_JSON,
|
|
550
|
+
CONTENT_HASH_FILE
|
|
551
|
+
);
|
|
333
552
|
await writeFile(hashFilePath, contentHash);
|
|
334
553
|
}
|
|
335
554
|
|
|
336
555
|
// src/cli/lib/skills/skill-metadata.ts
|
|
337
556
|
async function readForkedFromMetadata(skillDir) {
|
|
338
|
-
const metadataPath = path3.join(skillDir,
|
|
557
|
+
const metadataPath = path3.join(skillDir, STANDARD_FILES.METADATA_YAML);
|
|
339
558
|
if (!await fileExists(metadataPath)) {
|
|
340
559
|
return null;
|
|
341
560
|
}
|
|
342
561
|
const content = await readFile(metadataPath);
|
|
343
|
-
const result = localSkillMetadataSchema.safeParse(
|
|
562
|
+
const result = localSkillMetadataSchema.safeParse(parseYaml2(content));
|
|
344
563
|
if (!result.success) {
|
|
345
|
-
warn(
|
|
346
|
-
`Invalid metadata.yaml at ${metadataPath}: ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
347
|
-
);
|
|
564
|
+
warn(`Invalid metadata.yaml at ${metadataPath}: ${formatZodErrors(result.error.issues)}`);
|
|
348
565
|
return null;
|
|
349
566
|
}
|
|
350
567
|
return result.data.forked_from ?? null;
|
|
@@ -365,13 +582,13 @@ async function getLocalSkillsWithMetadata(projectDir) {
|
|
|
365
582
|
return result;
|
|
366
583
|
}
|
|
367
584
|
async function computeSourceHash(sourcePath, skillPath) {
|
|
368
|
-
const skillMdPath = path3.join(sourcePath, "src", skillPath,
|
|
585
|
+
const skillMdPath = path3.join(sourcePath, "src", skillPath, STANDARD_FILES.SKILL_MD);
|
|
369
586
|
if (!await fileExists(skillMdPath)) {
|
|
370
587
|
return null;
|
|
371
588
|
}
|
|
372
|
-
return
|
|
589
|
+
return computeFileHash(skillMdPath);
|
|
373
590
|
}
|
|
374
|
-
async function
|
|
591
|
+
async function compareLocalSkillsWithSource(projectDir, sourcePath, sourceSkills) {
|
|
375
592
|
const results = [];
|
|
376
593
|
const localSkills = await getLocalSkillsWithMetadata(projectDir);
|
|
377
594
|
for (const [skillId, { dirName, forkedFrom }] of localSkills) {
|
|
@@ -421,16 +638,16 @@ async function compareSkills(projectDir, sourcePath, sourceSkills) {
|
|
|
421
638
|
return sortBy(results, (r) => r.id);
|
|
422
639
|
}
|
|
423
640
|
async function injectForkedFromMetadata(destPath, skillId, contentHash) {
|
|
424
|
-
const metadataPath = path3.join(destPath,
|
|
641
|
+
const metadataPath = path3.join(destPath, STANDARD_FILES.METADATA_YAML);
|
|
425
642
|
const rawContent = await readFile(metadataPath);
|
|
426
643
|
const lines = rawContent.split("\n");
|
|
427
644
|
let yamlContent = rawContent;
|
|
428
645
|
if (lines[0]?.startsWith("# yaml-language-server:")) {
|
|
429
646
|
yamlContent = lines.slice(1).join("\n");
|
|
430
647
|
}
|
|
431
|
-
const parseResult = localSkillMetadataSchema.safeParse(
|
|
648
|
+
const parseResult = localSkillMetadataSchema.safeParse(parseYaml2(yamlContent));
|
|
432
649
|
if (!parseResult.success) {
|
|
433
|
-
warn(`Malformed metadata.yaml at ${metadataPath} \u2014 existing fields may be lost`);
|
|
650
|
+
warn(`Malformed metadata.yaml at '${metadataPath}' \u2014 existing fields may be lost`);
|
|
434
651
|
}
|
|
435
652
|
const metadata = parseResult.success ? parseResult.data : { forked_from: void 0 };
|
|
436
653
|
metadata.forked_from = {
|
|
@@ -438,23 +655,45 @@ async function injectForkedFromMetadata(destPath, skillId, contentHash) {
|
|
|
438
655
|
content_hash: contentHash,
|
|
439
656
|
date: getCurrentDate()
|
|
440
657
|
};
|
|
441
|
-
const
|
|
442
|
-
|
|
658
|
+
const schemaComment = `${yamlSchemaComment(SCHEMA_PATHS.metadata)}
|
|
659
|
+
`;
|
|
660
|
+
const newYamlContent = stringifyYaml2(metadata, { lineWidth: YAML_FORMATTING.LINE_WIDTH_NONE });
|
|
661
|
+
await writeFile(metadataPath, `${schemaComment}${newYamlContent}`);
|
|
443
662
|
}
|
|
444
663
|
|
|
445
664
|
// src/cli/lib/skills/skill-copier.ts
|
|
446
665
|
init_esm_shims();
|
|
447
666
|
import path4 from "path";
|
|
667
|
+
var NULL_BYTE_PATTERN2 = /\0/;
|
|
668
|
+
function validateSkillPath(resolvedPath, expectedParent, skillPath) {
|
|
669
|
+
if (NULL_BYTE_PATTERN2.test(skillPath)) {
|
|
670
|
+
throw new Error(`Invalid skill path: '${skillPath}' contains null bytes`);
|
|
671
|
+
}
|
|
672
|
+
const normalizedResolved = path4.resolve(resolvedPath);
|
|
673
|
+
const normalizedParent = path4.resolve(expectedParent);
|
|
674
|
+
if (!normalizedResolved.startsWith(normalizedParent + path4.sep) && normalizedResolved !== normalizedParent) {
|
|
675
|
+
throw new Error(
|
|
676
|
+
`Invalid skill path: '${skillPath}' escapes expected directory '${normalizedParent}'`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function resolveSkillPath(basePath, skillPath) {
|
|
681
|
+
const resolved = path4.join(basePath, skillPath);
|
|
682
|
+
validateSkillPath(resolved, basePath, skillPath);
|
|
683
|
+
return resolved;
|
|
684
|
+
}
|
|
448
685
|
function getSkillDestPath(skill, stackDir) {
|
|
449
686
|
const skillRelativePath = skill.path.replace(/^skills\//, "");
|
|
450
|
-
|
|
687
|
+
const skillsDir = path4.join(stackDir, "skills");
|
|
688
|
+
return resolveSkillPath(skillsDir, skillRelativePath);
|
|
451
689
|
}
|
|
452
690
|
async function generateSkillHash(skillSourcePath) {
|
|
453
|
-
const skillMdPath = path4.join(skillSourcePath,
|
|
454
|
-
return
|
|
691
|
+
const skillMdPath = path4.join(skillSourcePath, STANDARD_FILES.SKILL_MD);
|
|
692
|
+
return computeFileHash(skillMdPath);
|
|
455
693
|
}
|
|
456
694
|
function getSkillSourcePathFromSource(skill, sourceResult) {
|
|
457
|
-
|
|
695
|
+
const srcDir = path4.join(sourceResult.sourcePath, "src");
|
|
696
|
+
return resolveSkillPath(srcDir, skill.path);
|
|
458
697
|
}
|
|
459
698
|
async function copySkillFromSource(skill, stackDir, sourceResult) {
|
|
460
699
|
const sourcePath = getSkillSourcePathFromSource(skill, sourceResult);
|
|
@@ -470,35 +709,43 @@ async function copySkillFromSource(skill, stackDir, sourceResult) {
|
|
|
470
709
|
destPath
|
|
471
710
|
};
|
|
472
711
|
}
|
|
473
|
-
async function copySkillsToPluginFromSource(selectedSkillIds, pluginDir, matrix, sourceResult, sourceSelections) {
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
contentHash
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
712
|
+
async function copySkillsToPluginFromSource(selectedSkillIds, pluginDir, matrix, sourceResult, sourceSelections, onProgress) {
|
|
713
|
+
const total = selectedSkillIds.length;
|
|
714
|
+
let completed = 0;
|
|
715
|
+
const results = await Promise.all(
|
|
716
|
+
selectedSkillIds.map(async (skillId) => {
|
|
717
|
+
const skill = matrix.skills[skillId];
|
|
718
|
+
if (!skill) {
|
|
719
|
+
warn(`Skill not found in matrix: '${skillId}'`);
|
|
720
|
+
completed++;
|
|
721
|
+
onProgress?.(completed, total);
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
const selectedSource = sourceSelections?.[skillId];
|
|
725
|
+
const userSelectedRemote = selectedSource && selectedSource !== "local";
|
|
726
|
+
let result;
|
|
727
|
+
if (skill.local && skill.localPath && !userSelectedRemote) {
|
|
728
|
+
const localSkillPath = path4.join(process.cwd(), skill.localPath);
|
|
729
|
+
const contentHash = await generateSkillHash(localSkillPath);
|
|
730
|
+
result = {
|
|
731
|
+
skillId: skill.id,
|
|
732
|
+
sourcePath: skill.localPath,
|
|
733
|
+
destPath: skill.localPath,
|
|
734
|
+
contentHash,
|
|
735
|
+
local: true
|
|
736
|
+
};
|
|
737
|
+
} else {
|
|
738
|
+
result = await copySkillFromSource(skill, pluginDir, sourceResult);
|
|
739
|
+
}
|
|
740
|
+
completed++;
|
|
741
|
+
onProgress?.(completed, total);
|
|
742
|
+
return result;
|
|
743
|
+
})
|
|
744
|
+
);
|
|
745
|
+
return results.filter((r) => r !== null);
|
|
499
746
|
}
|
|
500
747
|
function getFlattenedSkillDestPath(skill, localSkillsDir) {
|
|
501
|
-
return
|
|
748
|
+
return resolveSkillPath(localSkillsDir, skill.id);
|
|
502
749
|
}
|
|
503
750
|
async function copySkillToLocalFlattened(skill, localSkillsDir, sourceResult) {
|
|
504
751
|
const sourcePath = getSkillSourcePathFromSource(skill, sourceResult);
|
|
@@ -515,31 +762,30 @@ async function copySkillToLocalFlattened(skill, localSkillsDir, sourceResult) {
|
|
|
515
762
|
};
|
|
516
763
|
}
|
|
517
764
|
async function copySkillsToLocalFlattened(selectedSkillIds, localSkillsDir, matrix, sourceResult, sourceSelections) {
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
return copiedSkills;
|
|
765
|
+
const results = await Promise.all(
|
|
766
|
+
selectedSkillIds.map(async (skillId) => {
|
|
767
|
+
const skill = matrix.skills[skillId];
|
|
768
|
+
if (!skill) {
|
|
769
|
+
warn(`Skill not found in matrix: '${skillId}'`);
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
const selectedSource = sourceSelections?.[skillId];
|
|
773
|
+
const userSelectedRemote = selectedSource && selectedSource !== "local";
|
|
774
|
+
if (skill.local && skill.localPath && !userSelectedRemote) {
|
|
775
|
+
const localSkillPath = path4.join(process.cwd(), skill.localPath);
|
|
776
|
+
const contentHash = await generateSkillHash(localSkillPath);
|
|
777
|
+
return {
|
|
778
|
+
skillId: skill.id,
|
|
779
|
+
sourcePath: skill.localPath,
|
|
780
|
+
destPath: skill.localPath,
|
|
781
|
+
contentHash,
|
|
782
|
+
local: true
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
return copySkillToLocalFlattened(skill, localSkillsDir, sourceResult);
|
|
786
|
+
})
|
|
787
|
+
);
|
|
788
|
+
return results.filter((r) => r !== null);
|
|
543
789
|
}
|
|
544
790
|
|
|
545
791
|
// src/cli/lib/skills/skill-agent-mappings.ts
|
|
@@ -550,7 +796,7 @@ init_esm_shims();
|
|
|
550
796
|
|
|
551
797
|
// src/cli/lib/loading/loader.ts
|
|
552
798
|
init_esm_shims();
|
|
553
|
-
import { parse as
|
|
799
|
+
import { parse as parseYaml3 } from "yaml";
|
|
554
800
|
import path5 from "path";
|
|
555
801
|
import { unique } from "remeda";
|
|
556
802
|
var FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/;
|
|
@@ -558,12 +804,10 @@ function parseFrontmatter(content, filePath) {
|
|
|
558
804
|
const match = content.match(FRONTMATTER_REGEX);
|
|
559
805
|
if (!match) return null;
|
|
560
806
|
const yamlContent = match[1];
|
|
561
|
-
const parsed = skillFrontmatterLoaderSchema.safeParse(
|
|
807
|
+
const parsed = skillFrontmatterLoaderSchema.safeParse(parseYaml3(yamlContent));
|
|
562
808
|
if (!parsed.success) {
|
|
563
809
|
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
|
-
);
|
|
810
|
+
warn(`Invalid SKILL.md frontmatter in '${location}': ${formatZodErrors(parsed.error.issues)}`);
|
|
567
811
|
return null;
|
|
568
812
|
}
|
|
569
813
|
return parsed.data;
|
|
@@ -576,7 +820,7 @@ async function loadAllAgents(projectRoot) {
|
|
|
576
820
|
const fullPath = path5.join(agentSourcesDir, file);
|
|
577
821
|
try {
|
|
578
822
|
const content = await readFile(fullPath);
|
|
579
|
-
const config = agentYamlConfigSchema.parse(
|
|
823
|
+
const config = agentYamlConfigSchema.parse(parseYaml3(content));
|
|
580
824
|
const agentPath = path5.dirname(file);
|
|
581
825
|
agents[config.id] = {
|
|
582
826
|
title: config.title,
|
|
@@ -588,9 +832,7 @@ async function loadAllAgents(projectRoot) {
|
|
|
588
832
|
};
|
|
589
833
|
verbose(`Loaded agent: ${config.id} from ${file}`);
|
|
590
834
|
} catch (error) {
|
|
591
|
-
warn(
|
|
592
|
-
`Skipping invalid agent.yaml at ${fullPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
593
|
-
);
|
|
835
|
+
warn(`Skipping invalid agent.yaml at '${fullPath}': ${getErrorMessage(error)}`);
|
|
594
836
|
}
|
|
595
837
|
}
|
|
596
838
|
return agents;
|
|
@@ -607,7 +849,7 @@ async function loadProjectAgents(projectRoot) {
|
|
|
607
849
|
const fullPath = path5.join(projectAgentsDir, file);
|
|
608
850
|
try {
|
|
609
851
|
const content = await readFile(fullPath);
|
|
610
|
-
const config = agentYamlConfigSchema.parse(
|
|
852
|
+
const config = agentYamlConfigSchema.parse(parseYaml3(content));
|
|
611
853
|
const agentPath = path5.dirname(file);
|
|
612
854
|
agents[config.id] = {
|
|
613
855
|
title: config.title,
|
|
@@ -621,9 +863,7 @@ async function loadProjectAgents(projectRoot) {
|
|
|
621
863
|
};
|
|
622
864
|
verbose(`Loaded project agent: ${config.id} from ${file}`);
|
|
623
865
|
} catch (error) {
|
|
624
|
-
warn(
|
|
625
|
-
`Skipping invalid agent.yaml at ${fullPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
626
|
-
);
|
|
866
|
+
warn(`Skipping invalid agent.yaml at '${fullPath}': ${getErrorMessage(error)}`);
|
|
627
867
|
}
|
|
628
868
|
}
|
|
629
869
|
return agents;
|
|
@@ -655,13 +895,13 @@ async function loadSkillsByIds(skillIds, projectRoot) {
|
|
|
655
895
|
} else {
|
|
656
896
|
const childSkills = allSkillIds.filter((id) => {
|
|
657
897
|
const dirPath = idToDirectoryPath[id];
|
|
658
|
-
return dirPath.startsWith(skillId
|
|
898
|
+
return dirPath.startsWith(`${skillId}/`);
|
|
659
899
|
});
|
|
660
900
|
if (childSkills.length > 0) {
|
|
661
901
|
expandedSkillIds.push(...childSkills);
|
|
662
902
|
verbose(`Expanded directory '${skillId}' to ${childSkills.length} skills`);
|
|
663
903
|
} else {
|
|
664
|
-
|
|
904
|
+
warn(`Unknown skill reference '${skillId}'`);
|
|
665
905
|
}
|
|
666
906
|
}
|
|
667
907
|
}
|
|
@@ -669,16 +909,16 @@ async function loadSkillsByIds(skillIds, projectRoot) {
|
|
|
669
909
|
for (const skillId of uniqueSkillIds) {
|
|
670
910
|
const directoryPath = idToDirectoryPath[skillId];
|
|
671
911
|
if (!directoryPath) {
|
|
672
|
-
|
|
912
|
+
warn(`Could not find skill '${skillId}': no matching skill found`);
|
|
673
913
|
continue;
|
|
674
914
|
}
|
|
675
915
|
const skillPath = path5.join(skillsDir, directoryPath);
|
|
676
|
-
const skillMdPath = path5.join(skillPath,
|
|
916
|
+
const skillMdPath = path5.join(skillPath, STANDARD_FILES.SKILL_MD);
|
|
677
917
|
try {
|
|
678
918
|
const content = await readFile(skillMdPath);
|
|
679
919
|
const frontmatter = parseFrontmatter(content, skillMdPath);
|
|
680
920
|
if (!frontmatter) {
|
|
681
|
-
warn(`Skipping ${skillId}:
|
|
921
|
+
warn(`Skipping '${skillId}': missing or invalid frontmatter`);
|
|
682
922
|
continue;
|
|
683
923
|
}
|
|
684
924
|
const canonicalId = frontmatter.name;
|
|
@@ -693,7 +933,7 @@ async function loadSkillsByIds(skillIds, projectRoot) {
|
|
|
693
933
|
}
|
|
694
934
|
verbose(`Loaded skill: ${canonicalId} (from ${directoryPath})`);
|
|
695
935
|
} catch (error) {
|
|
696
|
-
|
|
936
|
+
warn(`Could not load skill '${skillId}': ${error}`);
|
|
697
937
|
}
|
|
698
938
|
}
|
|
699
939
|
return skills;
|
|
@@ -710,7 +950,7 @@ async function loadPluginSkills(pluginDir) {
|
|
|
710
950
|
const content = await readFile(fullPath);
|
|
711
951
|
const frontmatter = parseFrontmatter(content, fullPath);
|
|
712
952
|
if (!frontmatter) {
|
|
713
|
-
warn(`Skipping ${file}:
|
|
953
|
+
warn(`Skipping '${file}': missing or invalid frontmatter`);
|
|
714
954
|
continue;
|
|
715
955
|
}
|
|
716
956
|
const folderPath = file.replace("/SKILL.md", "");
|
|
@@ -735,7 +975,7 @@ init_esm_shims();
|
|
|
735
975
|
|
|
736
976
|
// src/cli/lib/matrix/matrix-loader.ts
|
|
737
977
|
init_esm_shims();
|
|
738
|
-
import { parse as
|
|
978
|
+
import { parse as parseYaml4 } from "yaml";
|
|
739
979
|
import path6 from "path";
|
|
740
980
|
import { z } from "zod";
|
|
741
981
|
var rawMetadataSchema = z.object({
|
|
@@ -756,11 +996,11 @@ var rawMetadataSchema = z.object({
|
|
|
756
996
|
});
|
|
757
997
|
async function loadSkillsMatrix(configPath) {
|
|
758
998
|
const content = await readFile(configPath);
|
|
759
|
-
const raw =
|
|
999
|
+
const raw = parseYaml4(content);
|
|
760
1000
|
const result = skillsMatrixConfigSchema.safeParse(raw);
|
|
761
1001
|
if (!result.success) {
|
|
762
1002
|
throw new Error(
|
|
763
|
-
`Invalid skills matrix at ${configPath}: ${result.error.issues
|
|
1003
|
+
`Invalid skills matrix at '${configPath}': ${formatZodErrors(result.error.issues)}`
|
|
764
1004
|
);
|
|
765
1005
|
}
|
|
766
1006
|
verbose(`Loaded skills matrix: ${configPath}`);
|
|
@@ -768,21 +1008,21 @@ async function loadSkillsMatrix(configPath) {
|
|
|
768
1008
|
}
|
|
769
1009
|
async function extractAllSkills(skillsDir) {
|
|
770
1010
|
const skills = [];
|
|
771
|
-
const metadataFiles = await glob(
|
|
1011
|
+
const metadataFiles = await glob(`**/${STANDARD_FILES.METADATA_YAML}`, skillsDir);
|
|
772
1012
|
for (const metadataFile of metadataFiles) {
|
|
773
1013
|
const skillDir = path6.dirname(metadataFile);
|
|
774
|
-
const skillMdPath = path6.join(skillsDir, skillDir,
|
|
1014
|
+
const skillMdPath = path6.join(skillsDir, skillDir, STANDARD_FILES.SKILL_MD);
|
|
775
1015
|
const metadataPath = path6.join(skillsDir, metadataFile);
|
|
776
1016
|
if (!await fileExists(skillMdPath)) {
|
|
777
|
-
verbose(`Skipping ${metadataFile}: No
|
|
1017
|
+
verbose(`Skipping ${metadataFile}: No ${STANDARD_FILES.SKILL_MD} found`);
|
|
778
1018
|
continue;
|
|
779
1019
|
}
|
|
780
1020
|
const metadataContent = await readFile(metadataPath);
|
|
781
|
-
const rawMetadata =
|
|
1021
|
+
const rawMetadata = parseYaml4(metadataContent);
|
|
782
1022
|
const metadataResult = rawMetadataSchema.safeParse(rawMetadata);
|
|
783
1023
|
if (!metadataResult.success) {
|
|
784
1024
|
warn(
|
|
785
|
-
`Skipping ${metadataFile}:
|
|
1025
|
+
`Skipping '${metadataFile}': invalid metadata.yaml \u2014 ${formatZodErrors(metadataResult.error.issues)}`
|
|
786
1026
|
);
|
|
787
1027
|
continue;
|
|
788
1028
|
}
|
|
@@ -795,7 +1035,7 @@ async function extractAllSkills(skillsDir) {
|
|
|
795
1035
|
}
|
|
796
1036
|
if (!metadata.cli_name) {
|
|
797
1037
|
throw new Error(
|
|
798
|
-
`Skill at ${metadataFile} is missing required '
|
|
1038
|
+
`Skill at ${metadataFile} is missing required '${METADATA_KEYS.CLI_NAME}' field in metadata.yaml`
|
|
799
1039
|
);
|
|
800
1040
|
}
|
|
801
1041
|
const skillId = frontmatter.name;
|
|
@@ -849,10 +1089,13 @@ function buildAliasTargetToSkillIdMap(displayNameToId, skills) {
|
|
|
849
1089
|
}
|
|
850
1090
|
const aliasTargets = new Set(Object.values(displayNameToId));
|
|
851
1091
|
for (const skill of skills) {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1092
|
+
let slashIdx = skill.id.indexOf("/");
|
|
1093
|
+
while (slashIdx !== -1) {
|
|
1094
|
+
const suffix = skill.id.slice(slashIdx + 1);
|
|
1095
|
+
if (suffix && aliasTargets.has(suffix) && suffix !== skill.id) {
|
|
1096
|
+
map[suffix] = skill.id;
|
|
855
1097
|
}
|
|
1098
|
+
slashIdx = skill.id.indexOf("/", slashIdx + 1);
|
|
856
1099
|
}
|
|
857
1100
|
}
|
|
858
1101
|
return map;
|
|
@@ -911,110 +1154,99 @@ async function mergeMatrixWithSkills(matrix, skills) {
|
|
|
911
1154
|
};
|
|
912
1155
|
return merged;
|
|
913
1156
|
}
|
|
914
|
-
function
|
|
915
|
-
const
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
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,
|
|
1157
|
+
function resolveConflicts(skillId, metadataConflicts, conflictRules, resolve) {
|
|
1158
|
+
const conflicts = [];
|
|
1159
|
+
for (const conflictRef of metadataConflicts) {
|
|
1160
|
+
conflicts.push({
|
|
1161
|
+
skillId: resolve(conflictRef, "conflictsWith"),
|
|
931
1162
|
reason: "Defined in skill metadata"
|
|
932
1163
|
});
|
|
933
1164
|
}
|
|
934
|
-
for (const
|
|
935
|
-
const
|
|
936
|
-
if (
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
conflictsWith.push({
|
|
941
|
-
skillId: otherSkill,
|
|
942
|
-
reason: conflictRule.reason
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
}
|
|
1165
|
+
for (const rule of conflictRules) {
|
|
1166
|
+
const resolved = rule.skills.map((id) => resolve(id, "conflicts"));
|
|
1167
|
+
if (!resolved.includes(skillId)) continue;
|
|
1168
|
+
for (const other of resolved) {
|
|
1169
|
+
if (other !== skillId && !conflicts.some((c) => c.skillId === other)) {
|
|
1170
|
+
conflicts.push({ skillId: other, reason: rule.reason });
|
|
946
1171
|
}
|
|
947
1172
|
}
|
|
948
1173
|
}
|
|
949
|
-
|
|
950
|
-
|
|
1174
|
+
return conflicts;
|
|
1175
|
+
}
|
|
1176
|
+
function resolveRecommends(skillId, compatibleWith, recommendRules, resolve) {
|
|
1177
|
+
const recommends = [];
|
|
1178
|
+
for (const compatRef of compatibleWith) {
|
|
951
1179
|
recommends.push({
|
|
952
|
-
skillId:
|
|
1180
|
+
skillId: resolve(compatRef, "compatibleWith"),
|
|
953
1181
|
reason: "Compatible with this skill"
|
|
954
1182
|
});
|
|
955
1183
|
}
|
|
956
|
-
for (const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
recommends.push({
|
|
963
|
-
skillId: canonicalId,
|
|
964
|
-
reason: recommendRule.reason
|
|
965
|
-
});
|
|
966
|
-
}
|
|
1184
|
+
for (const rule of recommendRules) {
|
|
1185
|
+
if (resolve(rule.when, "recommends.when") !== skillId) continue;
|
|
1186
|
+
for (const suggested of rule.suggest) {
|
|
1187
|
+
const canonicalId = resolve(suggested, "recommends.suggest");
|
|
1188
|
+
if (!recommends.some((r) => r.skillId === canonicalId)) {
|
|
1189
|
+
recommends.push({ skillId: canonicalId, reason: rule.reason });
|
|
967
1190
|
}
|
|
968
1191
|
}
|
|
969
1192
|
}
|
|
970
|
-
|
|
1193
|
+
return recommends;
|
|
1194
|
+
}
|
|
1195
|
+
function resolveRequirements(skillId, metadataRequires, requireRules, resolve) {
|
|
1196
|
+
const requires = [];
|
|
1197
|
+
if (metadataRequires.length > 0) {
|
|
971
1198
|
requires.push({
|
|
972
|
-
skillIds:
|
|
1199
|
+
skillIds: metadataRequires.map((id) => resolve(id, "requires")),
|
|
973
1200
|
needsAny: false,
|
|
974
1201
|
reason: "Defined in skill metadata"
|
|
975
1202
|
});
|
|
976
1203
|
}
|
|
977
|
-
for (const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
requires.
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
});
|
|
985
|
-
}
|
|
1204
|
+
for (const rule of requireRules) {
|
|
1205
|
+
if (resolve(rule.skill, "requires.skill") !== skillId) continue;
|
|
1206
|
+
requires.push({
|
|
1207
|
+
skillIds: rule.needs.map((id) => resolve(id, "requires.needs")),
|
|
1208
|
+
needsAny: rule.needs_any ?? false,
|
|
1209
|
+
reason: rule.reason
|
|
1210
|
+
});
|
|
986
1211
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
}
|
|
1212
|
+
return requires;
|
|
1213
|
+
}
|
|
1214
|
+
function resolveAlternatives(skillId, alternativeGroups, resolve) {
|
|
1215
|
+
const alternatives = [];
|
|
1216
|
+
for (const group of alternativeGroups) {
|
|
1217
|
+
const resolved = group.skills.map((id) => resolve(id, "alternatives"));
|
|
1218
|
+
if (!resolved.includes(skillId)) continue;
|
|
1219
|
+
for (const alt of resolved) {
|
|
1220
|
+
if (alt !== skillId) {
|
|
1221
|
+
alternatives.push({ skillId: alt, purpose: group.purpose });
|
|
997
1222
|
}
|
|
998
1223
|
}
|
|
999
1224
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1225
|
+
return alternatives;
|
|
1226
|
+
}
|
|
1227
|
+
function resolveDiscourages(skillId, discourageRules, resolve) {
|
|
1228
|
+
if (!discourageRules) return [];
|
|
1229
|
+
const discourages = [];
|
|
1230
|
+
for (const rule of discourageRules) {
|
|
1231
|
+
const resolved = rule.skills.map((id) => resolve(id, "discourages"));
|
|
1232
|
+
if (!resolved.includes(skillId)) continue;
|
|
1233
|
+
for (const other of resolved) {
|
|
1234
|
+
if (other !== skillId && !discourages.some((d) => d.skillId === other)) {
|
|
1235
|
+
discourages.push({ skillId: other, reason: rule.reason });
|
|
1014
1236
|
}
|
|
1015
1237
|
}
|
|
1016
1238
|
}
|
|
1017
|
-
|
|
1239
|
+
return discourages;
|
|
1240
|
+
}
|
|
1241
|
+
function buildResolvedSkill(skill, matrix, displayNameToId, displayNames, directoryPathToId, aliasTargetToSkillId) {
|
|
1242
|
+
const resolve = (id, context) => resolveToCanonicalId(
|
|
1243
|
+
id,
|
|
1244
|
+
displayNameToId,
|
|
1245
|
+
directoryPathToId,
|
|
1246
|
+
aliasTargetToSkillId,
|
|
1247
|
+
context ? `${skill.id} ${context}` : void 0
|
|
1248
|
+
);
|
|
1249
|
+
const { relationships } = matrix;
|
|
1018
1250
|
return {
|
|
1019
1251
|
id: skill.id,
|
|
1020
1252
|
displayName: displayNames[skill.id],
|
|
@@ -1024,12 +1256,22 @@ function buildResolvedSkill(skill, matrix, displayNameToId, displayNames, direct
|
|
|
1024
1256
|
categoryExclusive: skill.categoryExclusive,
|
|
1025
1257
|
tags: skill.tags,
|
|
1026
1258
|
author: skill.author,
|
|
1027
|
-
conflictsWith
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1259
|
+
conflictsWith: resolveConflicts(
|
|
1260
|
+
skill.id,
|
|
1261
|
+
skill.conflictsWith,
|
|
1262
|
+
relationships.conflicts,
|
|
1263
|
+
resolve
|
|
1264
|
+
),
|
|
1265
|
+
recommends: resolveRecommends(
|
|
1266
|
+
skill.id,
|
|
1267
|
+
skill.compatibleWith,
|
|
1268
|
+
relationships.recommends,
|
|
1269
|
+
resolve
|
|
1270
|
+
),
|
|
1271
|
+
requires: resolveRequirements(skill.id, skill.requires, relationships.requires, resolve),
|
|
1272
|
+
alternatives: resolveAlternatives(skill.id, relationships.alternatives, resolve),
|
|
1273
|
+
discourages: resolveDiscourages(skill.id, relationships.discourages, resolve),
|
|
1274
|
+
compatibleWith: skill.compatibleWith.map((id) => resolve(id, "compatibleWith")),
|
|
1033
1275
|
requiresSetup: skill.requiresSetup.map((id) => resolve(id, "requiresSetup")),
|
|
1034
1276
|
providesSetupFor: skill.providesSetupFor.map((id) => resolve(id, "providesSetupFor")),
|
|
1035
1277
|
path: skill.path
|
|
@@ -1048,6 +1290,11 @@ function getLabel(skill, fallback) {
|
|
|
1048
1290
|
function resolveAlias(aliasOrId, matrix) {
|
|
1049
1291
|
return matrix.displayNameToId[aliasOrId] || aliasOrId;
|
|
1050
1292
|
}
|
|
1293
|
+
function initializeSelectionContext(currentSelections, matrix) {
|
|
1294
|
+
const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
|
|
1295
|
+
const selectedSet = new Set(resolvedSelections);
|
|
1296
|
+
return { resolvedSelections, selectedSet };
|
|
1297
|
+
}
|
|
1051
1298
|
function isDisabled(skillId, currentSelections, matrix, options) {
|
|
1052
1299
|
if (options?.expertMode) {
|
|
1053
1300
|
return false;
|
|
@@ -1063,19 +1310,19 @@ function isDisabled(skillId, currentSelections, matrix, options) {
|
|
|
1063
1310
|
return true;
|
|
1064
1311
|
}
|
|
1065
1312
|
const selectedSkill = matrix.skills[selectedFullId];
|
|
1066
|
-
if (selectedSkill
|
|
1313
|
+
if (selectedSkill?.conflictsWith.some((c) => c.skillId === fullId)) {
|
|
1067
1314
|
return true;
|
|
1068
1315
|
}
|
|
1069
1316
|
}
|
|
1070
|
-
const
|
|
1317
|
+
const { selectedSet } = initializeSelectionContext(currentSelections, matrix);
|
|
1071
1318
|
for (const requirement of skill.requires) {
|
|
1072
1319
|
if (requirement.needsAny) {
|
|
1073
|
-
const hasAny = requirement.skillIds.some((reqId) =>
|
|
1320
|
+
const hasAny = requirement.skillIds.some((reqId) => selectedSet.has(reqId));
|
|
1074
1321
|
if (!hasAny) {
|
|
1075
1322
|
return true;
|
|
1076
1323
|
}
|
|
1077
1324
|
} else {
|
|
1078
|
-
const hasAll = requirement.skillIds.every((reqId) =>
|
|
1325
|
+
const hasAll = requirement.skillIds.every((reqId) => selectedSet.has(reqId));
|
|
1079
1326
|
if (!hasAll) {
|
|
1080
1327
|
return true;
|
|
1081
1328
|
}
|
|
@@ -1089,7 +1336,7 @@ function getDisableReason(skillId, currentSelections, matrix) {
|
|
|
1089
1336
|
if (!skill) {
|
|
1090
1337
|
return void 0;
|
|
1091
1338
|
}
|
|
1092
|
-
const resolvedSelections
|
|
1339
|
+
const { resolvedSelections, selectedSet } = initializeSelectionContext(currentSelections, matrix);
|
|
1093
1340
|
for (const selectedId of resolvedSelections) {
|
|
1094
1341
|
const conflict = skill.conflictsWith.find((c) => c.skillId === selectedId);
|
|
1095
1342
|
if (conflict) {
|
|
@@ -1106,15 +1353,13 @@ function getDisableReason(skillId, currentSelections, matrix) {
|
|
|
1106
1353
|
}
|
|
1107
1354
|
for (const requirement of skill.requires) {
|
|
1108
1355
|
if (requirement.needsAny) {
|
|
1109
|
-
const hasAny = requirement.skillIds.some((reqId) =>
|
|
1356
|
+
const hasAny = requirement.skillIds.some((reqId) => selectedSet.has(reqId));
|
|
1110
1357
|
if (!hasAny) {
|
|
1111
1358
|
const requiredNames = requirement.skillIds.map((id) => getLabel(matrix.skills[id], id)).join(" or ");
|
|
1112
1359
|
return `${requirement.reason} (requires ${requiredNames})`;
|
|
1113
1360
|
}
|
|
1114
1361
|
} else {
|
|
1115
|
-
const missingIds = requirement.skillIds.filter(
|
|
1116
|
-
(reqId) => !resolvedSelections.includes(reqId)
|
|
1117
|
-
);
|
|
1362
|
+
const missingIds = requirement.skillIds.filter((reqId) => !selectedSet.has(reqId));
|
|
1118
1363
|
if (missingIds.length > 0) {
|
|
1119
1364
|
const missingNames = missingIds.map((id) => getLabel(matrix.skills[id], id)).join(", ");
|
|
1120
1365
|
return `${requirement.reason} (requires ${missingNames})`;
|
|
@@ -1129,10 +1374,10 @@ function isDiscouraged(skillId, currentSelections, matrix) {
|
|
|
1129
1374
|
if (!skill) {
|
|
1130
1375
|
return false;
|
|
1131
1376
|
}
|
|
1132
|
-
const resolvedSelections = currentSelections
|
|
1377
|
+
const { resolvedSelections } = initializeSelectionContext(currentSelections, matrix);
|
|
1133
1378
|
for (const selectedId of resolvedSelections) {
|
|
1134
1379
|
const selectedSkill = matrix.skills[selectedId];
|
|
1135
|
-
if (selectedSkill
|
|
1380
|
+
if (selectedSkill?.discourages.some((d) => d.skillId === fullId)) {
|
|
1136
1381
|
return true;
|
|
1137
1382
|
}
|
|
1138
1383
|
if (skill.discourages.some((d) => d.skillId === selectedId)) {
|
|
@@ -1147,7 +1392,7 @@ function getDiscourageReason(skillId, currentSelections, matrix) {
|
|
|
1147
1392
|
if (!skill) {
|
|
1148
1393
|
return void 0;
|
|
1149
1394
|
}
|
|
1150
|
-
const resolvedSelections = currentSelections
|
|
1395
|
+
const { resolvedSelections } = initializeSelectionContext(currentSelections, matrix);
|
|
1151
1396
|
for (const selectedId of resolvedSelections) {
|
|
1152
1397
|
const selectedSkill = matrix.skills[selectedId];
|
|
1153
1398
|
if (selectedSkill) {
|
|
@@ -1169,10 +1414,10 @@ function isRecommended(skillId, currentSelections, matrix) {
|
|
|
1169
1414
|
if (!skill) {
|
|
1170
1415
|
return false;
|
|
1171
1416
|
}
|
|
1172
|
-
const resolvedSelections = currentSelections
|
|
1417
|
+
const { resolvedSelections } = initializeSelectionContext(currentSelections, matrix);
|
|
1173
1418
|
for (const selectedId of resolvedSelections) {
|
|
1174
1419
|
const selectedSkill = matrix.skills[selectedId];
|
|
1175
|
-
if (selectedSkill
|
|
1420
|
+
if (selectedSkill?.recommends.some((r) => r.skillId === fullId)) {
|
|
1176
1421
|
return true;
|
|
1177
1422
|
}
|
|
1178
1423
|
}
|
|
@@ -1184,7 +1429,7 @@ function getRecommendReason(skillId, currentSelections, matrix) {
|
|
|
1184
1429
|
if (!skill) {
|
|
1185
1430
|
return void 0;
|
|
1186
1431
|
}
|
|
1187
|
-
const resolvedSelections = currentSelections
|
|
1432
|
+
const { resolvedSelections } = initializeSelectionContext(currentSelections, matrix);
|
|
1188
1433
|
for (const selectedId of resolvedSelections) {
|
|
1189
1434
|
const selectedSkill = matrix.skills[selectedId];
|
|
1190
1435
|
if (selectedSkill) {
|
|
@@ -1196,10 +1441,8 @@ function getRecommendReason(skillId, currentSelections, matrix) {
|
|
|
1196
1441
|
}
|
|
1197
1442
|
return void 0;
|
|
1198
1443
|
}
|
|
1199
|
-
function
|
|
1444
|
+
function validateConflicts(resolvedSelections, matrix) {
|
|
1200
1445
|
const errors = [];
|
|
1201
|
-
const warnings = [];
|
|
1202
|
-
const resolvedSelections = selections.map((s) => resolveAlias(s, matrix));
|
|
1203
1446
|
for (let i = 0; i < resolvedSelections.length; i++) {
|
|
1204
1447
|
const skillA = matrix.skills[resolvedSelections[i]];
|
|
1205
1448
|
if (!skillA) continue;
|
|
@@ -1215,12 +1458,16 @@ function validateSelection(selections, matrix) {
|
|
|
1215
1458
|
}
|
|
1216
1459
|
}
|
|
1217
1460
|
}
|
|
1461
|
+
return { errors, warnings: [] };
|
|
1462
|
+
}
|
|
1463
|
+
function validateRequirements(resolvedSelections, selectedSet, matrix) {
|
|
1464
|
+
const errors = [];
|
|
1218
1465
|
for (const skillId of resolvedSelections) {
|
|
1219
1466
|
const skill = matrix.skills[skillId];
|
|
1220
1467
|
if (!skill) continue;
|
|
1221
1468
|
for (const requirement of skill.requires) {
|
|
1222
1469
|
if (requirement.needsAny) {
|
|
1223
|
-
const hasAny = requirement.skillIds.some((reqId) =>
|
|
1470
|
+
const hasAny = requirement.skillIds.some((reqId) => selectedSet.has(reqId));
|
|
1224
1471
|
if (!hasAny) {
|
|
1225
1472
|
errors.push({
|
|
1226
1473
|
type: "missing_requirement",
|
|
@@ -1229,9 +1476,7 @@ function validateSelection(selections, matrix) {
|
|
|
1229
1476
|
});
|
|
1230
1477
|
}
|
|
1231
1478
|
} else {
|
|
1232
|
-
const missingIds = requirement.skillIds.filter(
|
|
1233
|
-
(reqId) => !resolvedSelections.includes(reqId)
|
|
1234
|
-
);
|
|
1479
|
+
const missingIds = requirement.skillIds.filter((reqId) => !selectedSet.has(reqId));
|
|
1235
1480
|
if (missingIds.length > 0) {
|
|
1236
1481
|
errors.push({
|
|
1237
1482
|
type: "missing_requirement",
|
|
@@ -1242,6 +1487,10 @@ function validateSelection(selections, matrix) {
|
|
|
1242
1487
|
}
|
|
1243
1488
|
}
|
|
1244
1489
|
}
|
|
1490
|
+
return { errors, warnings: [] };
|
|
1491
|
+
}
|
|
1492
|
+
function validateExclusivity(resolvedSelections, matrix) {
|
|
1493
|
+
const errors = [];
|
|
1245
1494
|
const validSkills = resolvedSelections.map((skillId) => ({ skillId, skill: matrix.skills[skillId] })).filter((entry) => entry.skill != null);
|
|
1246
1495
|
const categorySelections = groupBy(validSkills, (entry) => entry.skill.category);
|
|
1247
1496
|
for (const [categoryId, entries] of typedEntries(categorySelections)) {
|
|
@@ -1257,15 +1506,19 @@ function validateSelection(selections, matrix) {
|
|
|
1257
1506
|
}
|
|
1258
1507
|
}
|
|
1259
1508
|
}
|
|
1509
|
+
return { errors, warnings: [] };
|
|
1510
|
+
}
|
|
1511
|
+
function validateRecommendations(resolvedSelections, selectedSet, matrix) {
|
|
1512
|
+
const warnings = [];
|
|
1260
1513
|
for (const skillId of resolvedSelections) {
|
|
1261
1514
|
const skill = matrix.skills[skillId];
|
|
1262
1515
|
if (!skill) continue;
|
|
1263
1516
|
for (const recommendation of skill.recommends) {
|
|
1264
|
-
if (!
|
|
1517
|
+
if (!selectedSet.has(recommendation.skillId)) {
|
|
1265
1518
|
const recommendedSkill = matrix.skills[recommendation.skillId];
|
|
1266
1519
|
if (recommendedSkill) {
|
|
1267
1520
|
const hasConflict = recommendedSkill.conflictsWith.some(
|
|
1268
|
-
(c) =>
|
|
1521
|
+
(c) => selectedSet.has(c.skillId)
|
|
1269
1522
|
);
|
|
1270
1523
|
if (!hasConflict) {
|
|
1271
1524
|
warnings.push({
|
|
@@ -1278,12 +1531,14 @@ function validateSelection(selections, matrix) {
|
|
|
1278
1531
|
}
|
|
1279
1532
|
}
|
|
1280
1533
|
}
|
|
1534
|
+
return { errors: [], warnings };
|
|
1535
|
+
}
|
|
1536
|
+
function validateSetupUsage(resolvedSelections, selectedSet, matrix) {
|
|
1537
|
+
const warnings = [];
|
|
1281
1538
|
for (const skillId of resolvedSelections) {
|
|
1282
1539
|
const skill = matrix.skills[skillId];
|
|
1283
1540
|
if (!skill || skill.providesSetupFor.length === 0) continue;
|
|
1284
|
-
const hasUsageSkill = skill.providesSetupFor.some(
|
|
1285
|
-
(usageId) => resolvedSelections.includes(usageId)
|
|
1286
|
-
);
|
|
1541
|
+
const hasUsageSkill = skill.providesSetupFor.some((usageId) => selectedSet.has(usageId));
|
|
1287
1542
|
if (!hasUsageSkill) {
|
|
1288
1543
|
warnings.push({
|
|
1289
1544
|
type: "unused_setup",
|
|
@@ -1292,6 +1547,23 @@ function validateSelection(selections, matrix) {
|
|
|
1292
1547
|
});
|
|
1293
1548
|
}
|
|
1294
1549
|
}
|
|
1550
|
+
return { errors: [], warnings };
|
|
1551
|
+
}
|
|
1552
|
+
function mergeValidationResults(results) {
|
|
1553
|
+
return {
|
|
1554
|
+
errors: results.flatMap((r) => r.errors),
|
|
1555
|
+
warnings: results.flatMap((r) => r.warnings)
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
function validateSelection(selections, matrix) {
|
|
1559
|
+
const { resolvedSelections, selectedSet } = initializeSelectionContext(selections, matrix);
|
|
1560
|
+
const { errors, warnings } = mergeValidationResults([
|
|
1561
|
+
validateConflicts(resolvedSelections, matrix),
|
|
1562
|
+
validateRequirements(resolvedSelections, selectedSet, matrix),
|
|
1563
|
+
validateExclusivity(resolvedSelections, matrix),
|
|
1564
|
+
validateRecommendations(resolvedSelections, selectedSet, matrix),
|
|
1565
|
+
validateSetupUsage(resolvedSelections, selectedSet, matrix)
|
|
1566
|
+
]);
|
|
1295
1567
|
return {
|
|
1296
1568
|
valid: errors.length === 0,
|
|
1297
1569
|
errors,
|
|
@@ -1300,7 +1572,7 @@ function validateSelection(selections, matrix) {
|
|
|
1300
1572
|
}
|
|
1301
1573
|
function getAvailableSkills(categoryId, currentSelections, matrix, options) {
|
|
1302
1574
|
const skillOptions = [];
|
|
1303
|
-
const
|
|
1575
|
+
const { selectedSet } = initializeSelectionContext(currentSelections, matrix);
|
|
1304
1576
|
for (const skill of Object.values(matrix.skills)) {
|
|
1305
1577
|
if (!skill) continue;
|
|
1306
1578
|
if (skill.category !== categoryId) {
|
|
@@ -1319,7 +1591,7 @@ function getAvailableSkills(categoryId, currentSelections, matrix, options) {
|
|
|
1319
1591
|
discouragedReason: discouraged ? getDiscourageReason(skill.id, currentSelections, matrix) : void 0,
|
|
1320
1592
|
recommended,
|
|
1321
1593
|
recommendedReason: recommended ? getRecommendReason(skill.id, currentSelections, matrix) : void 0,
|
|
1322
|
-
selected:
|
|
1594
|
+
selected: selectedSet.has(skill.id),
|
|
1323
1595
|
alternatives: skill.alternatives.map((a) => a.skillId)
|
|
1324
1596
|
});
|
|
1325
1597
|
}
|
|
@@ -1473,8 +1745,8 @@ init_esm_shims();
|
|
|
1473
1745
|
// src/cli/lib/plugins/plugin-manifest.ts
|
|
1474
1746
|
init_esm_shims();
|
|
1475
1747
|
import path7 from "path";
|
|
1476
|
-
var PLUGIN_DIR_NAME =
|
|
1477
|
-
var PLUGIN_MANIFEST_FILE2 =
|
|
1748
|
+
var PLUGIN_DIR_NAME = PLUGIN_MANIFEST_DIR;
|
|
1749
|
+
var PLUGIN_MANIFEST_FILE2 = STANDARD_FILES.PLUGIN_JSON;
|
|
1478
1750
|
var SKILL_PLUGIN_PREFIX = "";
|
|
1479
1751
|
var AGENT_PLUGIN_PREFIX = "agent-";
|
|
1480
1752
|
function buildAuthor(name, email) {
|
|
@@ -1567,9 +1839,11 @@ async function findPluginManifest(startDir) {
|
|
|
1567
1839
|
// src/cli/lib/plugins/plugin-finder.ts
|
|
1568
1840
|
init_esm_shims();
|
|
1569
1841
|
import path9 from "path";
|
|
1842
|
+
import { last, zip } from "remeda";
|
|
1843
|
+
var MAX_SKILL_NAME_LENGTH = 100;
|
|
1570
1844
|
function getCollectivePluginDir(projectDir) {
|
|
1571
1845
|
const dir = projectDir ?? process.cwd();
|
|
1572
|
-
return path9.join(dir, CLAUDE_DIR, PLUGINS_SUBDIR,
|
|
1846
|
+
return path9.join(dir, CLAUDE_DIR, PLUGINS_SUBDIR, DEFAULT_PLUGIN_NAME);
|
|
1573
1847
|
}
|
|
1574
1848
|
function getProjectPluginsDir(projectDir) {
|
|
1575
1849
|
const dir = projectDir ?? process.cwd();
|
|
@@ -1591,7 +1865,7 @@ async function readPluginManifest(pluginDir) {
|
|
|
1591
1865
|
return null;
|
|
1592
1866
|
}
|
|
1593
1867
|
try {
|
|
1594
|
-
const content = await
|
|
1868
|
+
const content = await readFileSafe(manifestPath, MAX_PLUGIN_FILE_SIZE);
|
|
1595
1869
|
const manifest = pluginManifestSchema.parse(JSON.parse(content));
|
|
1596
1870
|
if (!manifest.name || typeof manifest.name !== "string") {
|
|
1597
1871
|
verbose(` Invalid manifest at ${manifestPath}: missing name`);
|
|
@@ -1607,29 +1881,40 @@ async function getPluginSkillIds(pluginSkillsDir, matrix) {
|
|
|
1607
1881
|
const skillFiles = await glob("**/SKILL.md", pluginSkillsDir);
|
|
1608
1882
|
const skillIds = [];
|
|
1609
1883
|
const aliasToId = /* @__PURE__ */ new Map();
|
|
1610
|
-
for (const [id, skill] of
|
|
1884
|
+
for (const [id, skill] of typedEntries(matrix.skills)) {
|
|
1611
1885
|
if (!skill) continue;
|
|
1612
1886
|
if (skill.displayName) {
|
|
1613
1887
|
aliasToId.set(skill.displayName.toLowerCase(), id);
|
|
1614
1888
|
}
|
|
1615
1889
|
}
|
|
1616
1890
|
const dirToId = /* @__PURE__ */ new Map();
|
|
1617
|
-
for (const [id] of
|
|
1891
|
+
for (const [id] of typedEntries(matrix.skills)) {
|
|
1618
1892
|
const idParts = id.split("/");
|
|
1619
|
-
const lastPart = idParts
|
|
1893
|
+
const lastPart = last(idParts);
|
|
1620
1894
|
if (lastPart) {
|
|
1621
1895
|
dirToId.set(lastPart.toLowerCase(), id);
|
|
1622
1896
|
}
|
|
1623
1897
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1898
|
+
const fileContents = await Promise.all(
|
|
1899
|
+
skillFiles.map((skillFile) => readFile(path9.join(pluginSkillsDir, skillFile)))
|
|
1900
|
+
);
|
|
1901
|
+
for (const [skillFile, content] of zip(skillFiles, fileContents)) {
|
|
1627
1902
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1628
1903
|
if (frontmatterMatch) {
|
|
1629
1904
|
const frontmatter = frontmatterMatch[1];
|
|
1630
1905
|
const nameMatch = frontmatter.match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
1631
1906
|
if (nameMatch) {
|
|
1632
1907
|
const skillName = nameMatch[1].trim();
|
|
1908
|
+
if (skillName.length === 0) {
|
|
1909
|
+
warn(`Skipping plugin skill '${skillFile}': empty name in frontmatter`);
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
if (skillName.length > MAX_SKILL_NAME_LENGTH) {
|
|
1913
|
+
warn(
|
|
1914
|
+
`Skipping plugin skill '${skillFile}': name exceeds ${MAX_SKILL_NAME_LENGTH} characters`
|
|
1915
|
+
);
|
|
1916
|
+
continue;
|
|
1917
|
+
}
|
|
1633
1918
|
if (matrix.skills[skillName]) {
|
|
1634
1919
|
skillIds.push(skillName);
|
|
1635
1920
|
continue;
|
|
@@ -1663,8 +1948,8 @@ init_esm_shims();
|
|
|
1663
1948
|
init_esm_shims();
|
|
1664
1949
|
import path10 from "path";
|
|
1665
1950
|
async function detectInstallation(projectDir = process.cwd()) {
|
|
1666
|
-
const srcConfigPath = path10.join(projectDir, CLAUDE_SRC_DIR,
|
|
1667
|
-
const legacyConfigPath = path10.join(projectDir, CLAUDE_DIR,
|
|
1951
|
+
const srcConfigPath = path10.join(projectDir, CLAUDE_SRC_DIR, STANDARD_FILES.CONFIG_YAML);
|
|
1952
|
+
const legacyConfigPath = path10.join(projectDir, CLAUDE_DIR, STANDARD_FILES.CONFIG_YAML);
|
|
1668
1953
|
const localConfigPath = await fileExists(srcConfigPath) ? srcConfigPath : await fileExists(legacyConfigPath) ? legacyConfigPath : null;
|
|
1669
1954
|
if (localConfigPath) {
|
|
1670
1955
|
const loaded = await loadProjectConfig(projectDir);
|
|
@@ -1680,7 +1965,7 @@ async function detectInstallation(projectDir = process.cwd()) {
|
|
|
1680
1965
|
}
|
|
1681
1966
|
}
|
|
1682
1967
|
const pluginDir = getCollectivePluginDir(projectDir);
|
|
1683
|
-
const pluginConfigPath = path10.join(pluginDir,
|
|
1968
|
+
const pluginConfigPath = path10.join(pluginDir, STANDARD_FILES.CONFIG_YAML);
|
|
1684
1969
|
if (await directoryExists(pluginDir)) {
|
|
1685
1970
|
return {
|
|
1686
1971
|
mode: "plugin",
|
|
@@ -1696,41 +1981,59 @@ async function detectInstallation(projectDir = process.cwd()) {
|
|
|
1696
1981
|
// src/cli/lib/installation/local-installer.ts
|
|
1697
1982
|
init_esm_shims();
|
|
1698
1983
|
import path15 from "path";
|
|
1699
|
-
import { stringify as
|
|
1984
|
+
import { stringify as stringifyYaml3 } from "yaml";
|
|
1700
1985
|
|
|
1701
1986
|
// src/cli/lib/stacks/index.ts
|
|
1702
1987
|
init_esm_shims();
|
|
1703
1988
|
|
|
1704
1989
|
// src/cli/lib/stacks/stacks-loader.ts
|
|
1705
1990
|
init_esm_shims();
|
|
1706
|
-
import { parse as
|
|
1991
|
+
import { parse as parseYaml5 } from "yaml";
|
|
1707
1992
|
import path11 from "path";
|
|
1708
|
-
import { mapValues } from "remeda";
|
|
1993
|
+
import { mapValues, pipe, flatMap, unique as unique2 } from "remeda";
|
|
1709
1994
|
var STACKS_FILE = "config/stacks.yaml";
|
|
1710
1995
|
var stacksCache = /* @__PURE__ */ new Map();
|
|
1711
|
-
|
|
1712
|
-
|
|
1996
|
+
function normalizeAgentConfig(agentConfig) {
|
|
1997
|
+
return mapValues(agentConfig, (value) => {
|
|
1998
|
+
const items = Array.isArray(value) ? value : [value];
|
|
1999
|
+
return items.map(
|
|
2000
|
+
(item) => typeof item === "string" ? { id: item, preloaded: false } : item
|
|
2001
|
+
);
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
function normalizeStackRecord(rawStack) {
|
|
2005
|
+
return mapValues(rawStack, (agentConfig) => normalizeAgentConfig(agentConfig));
|
|
2006
|
+
}
|
|
2007
|
+
async function loadStacks(configDir, stacksFile) {
|
|
2008
|
+
const resolvedStacksFile = stacksFile ?? STACKS_FILE;
|
|
2009
|
+
const cacheKey = `${configDir}:${resolvedStacksFile}`;
|
|
1713
2010
|
const cached = stacksCache.get(cacheKey);
|
|
1714
2011
|
if (cached) return cached;
|
|
1715
|
-
const stacksPath = path11.join(configDir,
|
|
2012
|
+
const stacksPath = path11.join(configDir, resolvedStacksFile);
|
|
1716
2013
|
if (!await fileExists(stacksPath)) {
|
|
1717
2014
|
verbose(`No stacks file found at ${stacksPath}`);
|
|
1718
2015
|
return [];
|
|
1719
2016
|
}
|
|
1720
2017
|
try {
|
|
1721
2018
|
const content = await readFile(stacksPath);
|
|
1722
|
-
const result = stacksConfigSchema.safeParse(
|
|
2019
|
+
const result = stacksConfigSchema.safeParse(parseYaml5(content));
|
|
1723
2020
|
if (!result.success) {
|
|
1724
2021
|
throw new Error(
|
|
1725
|
-
`Invalid stacks.yaml at ${stacksPath}: ${result.error.issues
|
|
2022
|
+
`Invalid stacks.yaml at '${stacksPath}': ${formatZodErrors(result.error.issues)}`
|
|
1726
2023
|
);
|
|
1727
2024
|
}
|
|
1728
|
-
const
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
2025
|
+
const stacks = result.data.stacks.map((stack) => ({
|
|
2026
|
+
...stack,
|
|
2027
|
+
agents: mapValues(
|
|
2028
|
+
stack.agents,
|
|
2029
|
+
(agentConfig) => normalizeAgentConfig(agentConfig)
|
|
2030
|
+
)
|
|
2031
|
+
}));
|
|
2032
|
+
stacksCache.set(cacheKey, stacks);
|
|
2033
|
+
verbose(`Loaded ${stacks.length} stacks from ${stacksPath}`);
|
|
2034
|
+
return stacks;
|
|
1732
2035
|
} catch (error) {
|
|
1733
|
-
const errorMessage =
|
|
2036
|
+
const errorMessage = getErrorMessage(error);
|
|
1734
2037
|
throw new Error(`Failed to load stacks from '${stacksPath}': ${errorMessage}`);
|
|
1735
2038
|
}
|
|
1736
2039
|
}
|
|
@@ -1744,25 +2047,36 @@ async function loadStackById(stackId, configDir) {
|
|
|
1744
2047
|
verbose(`Found stack: ${stack.name} (${stackId})`);
|
|
1745
2048
|
return stack;
|
|
1746
2049
|
}
|
|
1747
|
-
function resolveAgentConfigToSkills(agentConfig
|
|
2050
|
+
function resolveAgentConfigToSkills(agentConfig) {
|
|
1748
2051
|
const skillRefs = [];
|
|
1749
|
-
for (const [subcategory,
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
)
|
|
1755
|
-
|
|
2052
|
+
for (const [subcategory, assignments] of typedEntries(
|
|
2053
|
+
agentConfig
|
|
2054
|
+
)) {
|
|
2055
|
+
if (!assignments) continue;
|
|
2056
|
+
for (const assignment of assignments) {
|
|
2057
|
+
if (!SKILL_ID_PATTERN.test(assignment.id)) {
|
|
2058
|
+
warn(
|
|
2059
|
+
`Invalid skill ID '${assignment.id}' for subcategory '${subcategory}' in stack config. Skipping.`
|
|
2060
|
+
);
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
skillRefs.push({
|
|
2064
|
+
id: assignment.id,
|
|
2065
|
+
usage: `when working with ${subcategory}`,
|
|
2066
|
+
preloaded: assignment.preloaded ?? false
|
|
2067
|
+
});
|
|
1756
2068
|
}
|
|
1757
|
-
const isKeySkill = KEY_SUBCATEGORIES.has(subcategory);
|
|
1758
|
-
skillRefs.push({
|
|
1759
|
-
id: fullSkillId,
|
|
1760
|
-
usage: `when working with ${subcategory}`,
|
|
1761
|
-
preloaded: isKeySkill
|
|
1762
|
-
});
|
|
1763
2069
|
}
|
|
1764
2070
|
return skillRefs;
|
|
1765
2071
|
}
|
|
2072
|
+
function getStackSkillIds(stack) {
|
|
2073
|
+
return pipe(
|
|
2074
|
+
Object.values(stack),
|
|
2075
|
+
flatMap(resolveAgentConfigToSkills),
|
|
2076
|
+
(refs) => refs.map((r) => r.id),
|
|
2077
|
+
unique2()
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
1766
2080
|
|
|
1767
2081
|
// src/cli/lib/stacks/stack-installer.ts
|
|
1768
2082
|
init_esm_shims();
|
|
@@ -1777,7 +2091,7 @@ import path13 from "path";
|
|
|
1777
2091
|
init_esm_shims();
|
|
1778
2092
|
import { Liquid } from "liquidjs";
|
|
1779
2093
|
import path12 from "path";
|
|
1780
|
-
import { pipe, flatMap, filter, uniqueBy } from "remeda";
|
|
2094
|
+
import { pipe as pipe2, flatMap as flatMap2, filter, uniqueBy } from "remeda";
|
|
1781
2095
|
|
|
1782
2096
|
// src/cli/lib/resolver.ts
|
|
1783
2097
|
init_esm_shims();
|
|
@@ -1797,17 +2111,9 @@ function resolveSkillReferences(skillRefs, skills) {
|
|
|
1797
2111
|
return skillRefs.map((ref) => resolveSkillReference(ref, skills)).filter((skill) => skill !== null);
|
|
1798
2112
|
}
|
|
1799
2113
|
function buildSkillRefsFromConfig(agentStack) {
|
|
1800
|
-
|
|
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;
|
|
2114
|
+
return resolveAgentConfigToSkills(agentStack);
|
|
1809
2115
|
}
|
|
1810
|
-
function resolveAgentSkillsFromStack(agentName, stack
|
|
2116
|
+
function resolveAgentSkillsFromStack(agentName, stack) {
|
|
1811
2117
|
const agentConfig = stack.agents[agentName];
|
|
1812
2118
|
if (!agentConfig) {
|
|
1813
2119
|
verbose(`Agent '${agentName}' not found in stack '${stack.id}'`);
|
|
@@ -1817,33 +2123,16 @@ function resolveAgentSkillsFromStack(agentName, stack, displayNameToId) {
|
|
|
1817
2123
|
verbose(`Agent '${agentName}' has no technology config in stack '${stack.id}'`);
|
|
1818
2124
|
return [];
|
|
1819
2125
|
}
|
|
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
|
-
}
|
|
2126
|
+
const skillRefs = resolveAgentConfigToSkills(agentConfig);
|
|
1838
2127
|
verbose(`Resolved ${skillRefs.length} skills for agent '${agentName}' from stack '${stack.id}'`);
|
|
1839
2128
|
return skillRefs;
|
|
1840
2129
|
}
|
|
1841
|
-
async function
|
|
2130
|
+
async function resolveAgentSkillRefs(agentName, agentConfig, stack) {
|
|
1842
2131
|
if (agentConfig.skills && agentConfig.skills.length > 0) {
|
|
1843
2132
|
return agentConfig.skills;
|
|
1844
2133
|
}
|
|
1845
|
-
if (stack
|
|
1846
|
-
const stackSkills = resolveAgentSkillsFromStack(agentName, stack
|
|
2134
|
+
if (stack) {
|
|
2135
|
+
const stackSkills = resolveAgentSkillsFromStack(agentName, stack);
|
|
1847
2136
|
if (stackSkills.length > 0) {
|
|
1848
2137
|
verbose(`Resolved ${stackSkills.length} skills from stack for ${agentName}`);
|
|
1849
2138
|
return stackSkills;
|
|
@@ -1851,7 +2140,7 @@ async function getAgentSkills(agentName, agentConfig, stack, displayNameToId) {
|
|
|
1851
2140
|
}
|
|
1852
2141
|
return [];
|
|
1853
2142
|
}
|
|
1854
|
-
async function resolveAgents(agents, skills, compileConfig, _projectRoot, stack
|
|
2143
|
+
async function resolveAgents(agents, skills, compileConfig, _projectRoot, stack) {
|
|
1855
2144
|
const resolved = {};
|
|
1856
2145
|
const agentNames = typedKeys(compileConfig.agents);
|
|
1857
2146
|
for (const agentName of agentNames) {
|
|
@@ -1864,7 +2153,7 @@ async function resolveAgents(agents, skills, compileConfig, _projectRoot, stack,
|
|
|
1864
2153
|
);
|
|
1865
2154
|
}
|
|
1866
2155
|
const agentConfig = compileConfig.agents[agentName];
|
|
1867
|
-
const skillRefs = await
|
|
2156
|
+
const skillRefs = await resolveAgentSkillRefs(agentName, agentConfig, stack);
|
|
1868
2157
|
const resolvedSkills = resolveSkillReferences(skillRefs, skills);
|
|
1869
2158
|
resolved[agentName] = {
|
|
1870
2159
|
name: agentName,
|
|
@@ -1880,7 +2169,7 @@ async function resolveAgents(agents, skills, compileConfig, _projectRoot, stack,
|
|
|
1880
2169
|
}
|
|
1881
2170
|
return resolved;
|
|
1882
2171
|
}
|
|
1883
|
-
function
|
|
2172
|
+
function convertStackToCompileConfig(stackId, stack) {
|
|
1884
2173
|
const agents = {};
|
|
1885
2174
|
for (const agentId of stack.agents) {
|
|
1886
2175
|
agents[agentId] = {};
|
|
@@ -1895,7 +2184,7 @@ function stackToCompileConfig(stackId, stack) {
|
|
|
1895
2184
|
|
|
1896
2185
|
// src/cli/utils/frontmatter.ts
|
|
1897
2186
|
init_esm_shims();
|
|
1898
|
-
import { parse as
|
|
2187
|
+
import { parse as parseYaml6 } from "yaml";
|
|
1899
2188
|
function extractFrontmatter(content) {
|
|
1900
2189
|
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
1901
2190
|
const match = content.match(frontmatterRegex);
|
|
@@ -1903,13 +2192,75 @@ function extractFrontmatter(content) {
|
|
|
1903
2192
|
return null;
|
|
1904
2193
|
}
|
|
1905
2194
|
try {
|
|
1906
|
-
return
|
|
2195
|
+
return parseYaml6(match[1]);
|
|
1907
2196
|
} catch {
|
|
1908
2197
|
return null;
|
|
1909
2198
|
}
|
|
1910
2199
|
}
|
|
1911
2200
|
|
|
1912
2201
|
// src/cli/lib/compiler.ts
|
|
2202
|
+
var LIQUID_SYNTAX_PATTERN = /\{\{|\}\}|\{%|%\}/g;
|
|
2203
|
+
function sanitizeLiquidSyntax(value, fieldName) {
|
|
2204
|
+
if (!LIQUID_SYNTAX_PATTERN.test(value)) return value;
|
|
2205
|
+
LIQUID_SYNTAX_PATTERN.lastIndex = 0;
|
|
2206
|
+
const sanitized = value.replace(LIQUID_SYNTAX_PATTERN, "");
|
|
2207
|
+
warn(`Stripped Liquid template syntax from '${fieldName}' \u2014 possible template injection attempt`);
|
|
2208
|
+
return sanitized;
|
|
2209
|
+
}
|
|
2210
|
+
function sanitizeString(value, fieldName) {
|
|
2211
|
+
if (value === void 0) return void 0;
|
|
2212
|
+
return sanitizeLiquidSyntax(value, fieldName);
|
|
2213
|
+
}
|
|
2214
|
+
function sanitizeStringArray(values, fieldName) {
|
|
2215
|
+
if (!values) return values;
|
|
2216
|
+
return values.map((v) => sanitizeLiquidSyntax(v, fieldName));
|
|
2217
|
+
}
|
|
2218
|
+
function sanitizeSkills(skills) {
|
|
2219
|
+
return skills.map((s) => ({
|
|
2220
|
+
...s,
|
|
2221
|
+
id: sanitizeLiquidSyntax(s.id, "skill.id"),
|
|
2222
|
+
description: sanitizeLiquidSyntax(s.description, "skill.description"),
|
|
2223
|
+
usage: sanitizeLiquidSyntax(s.usage, "skill.usage"),
|
|
2224
|
+
pluginRef: sanitizeString(s.pluginRef, "skill.pluginRef")
|
|
2225
|
+
}));
|
|
2226
|
+
}
|
|
2227
|
+
function sanitizeCompiledAgentData(data) {
|
|
2228
|
+
const sanitizedAgent = {
|
|
2229
|
+
...data.agent,
|
|
2230
|
+
name: sanitizeLiquidSyntax(data.agent.name, "agent.name"),
|
|
2231
|
+
title: sanitizeLiquidSyntax(data.agent.title, "agent.title"),
|
|
2232
|
+
description: sanitizeLiquidSyntax(data.agent.description, "agent.description"),
|
|
2233
|
+
tools: sanitizeStringArray(data.agent.tools, "agent.tools") ?? data.agent.tools,
|
|
2234
|
+
disallowed_tools: sanitizeStringArray(data.agent.disallowed_tools, "agent.disallowed_tools"),
|
|
2235
|
+
model: sanitizeString(data.agent.model, "agent.model"),
|
|
2236
|
+
permission_mode: sanitizeString(
|
|
2237
|
+
data.agent.permission_mode,
|
|
2238
|
+
"agent.permission_mode"
|
|
2239
|
+
)
|
|
2240
|
+
};
|
|
2241
|
+
const sanitizedSkills = sanitizeSkills(data.skills);
|
|
2242
|
+
const sanitizedPreloaded = sanitizeSkills(data.preloadedSkills);
|
|
2243
|
+
const sanitizedDynamic = sanitizeSkills(data.dynamicSkills);
|
|
2244
|
+
const sanitizedPreloadedIds = data.preloadedSkillIds.map(
|
|
2245
|
+
(id) => sanitizeLiquidSyntax(String(id), "preloadedSkillId")
|
|
2246
|
+
);
|
|
2247
|
+
return {
|
|
2248
|
+
agent: sanitizedAgent,
|
|
2249
|
+
intro: sanitizeLiquidSyntax(data.intro, "intro"),
|
|
2250
|
+
workflow: sanitizeLiquidSyntax(data.workflow, "workflow"),
|
|
2251
|
+
examples: sanitizeLiquidSyntax(data.examples, "examples"),
|
|
2252
|
+
criticalRequirementsTop: sanitizeLiquidSyntax(
|
|
2253
|
+
data.criticalRequirementsTop,
|
|
2254
|
+
"criticalRequirementsTop"
|
|
2255
|
+
),
|
|
2256
|
+
criticalReminders: sanitizeLiquidSyntax(data.criticalReminders, "criticalReminders"),
|
|
2257
|
+
outputFormat: sanitizeLiquidSyntax(data.outputFormat, "outputFormat"),
|
|
2258
|
+
skills: sanitizedSkills,
|
|
2259
|
+
preloadedSkills: sanitizedPreloaded,
|
|
2260
|
+
dynamicSkills: sanitizedDynamic,
|
|
2261
|
+
preloadedSkillIds: sanitizedPreloadedIds
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
1913
2264
|
async function createLiquidEngine(projectDir) {
|
|
1914
2265
|
const roots = [];
|
|
1915
2266
|
if (projectDir) {
|
|
@@ -1934,42 +2285,48 @@ async function createLiquidEngine(projectDir) {
|
|
|
1934
2285
|
}
|
|
1935
2286
|
|
|
1936
2287
|
// src/cli/lib/stacks/stack-plugin-compiler.ts
|
|
1937
|
-
import { unique as
|
|
2288
|
+
import { unique as unique3 } from "remeda";
|
|
1938
2289
|
function hashStackConfig(stack) {
|
|
1939
|
-
const stackSkillIds = stack.stack ?
|
|
2290
|
+
const stackSkillIds = stack.stack ? getStackSkillIds(stack.stack).sort() : [];
|
|
1940
2291
|
const parts = [
|
|
1941
2292
|
`name:${stack.name}`,
|
|
1942
2293
|
`description:${stack.description ?? ""}`,
|
|
1943
2294
|
`skills:${stackSkillIds.join(",")}`,
|
|
1944
2295
|
`agents:${(stack.agents || []).sort().join(",")}`
|
|
1945
2296
|
];
|
|
1946
|
-
return
|
|
2297
|
+
return computeStringHash(parts.join("\n"));
|
|
1947
2298
|
}
|
|
1948
2299
|
async function compileAgentForPlugin(name, agent, fallbackRoot, engine, installMode) {
|
|
1949
2300
|
verbose(`Compiling agent: ${name}`);
|
|
1950
2301
|
const agentSourceRoot = agent.sourceRoot || fallbackRoot;
|
|
1951
2302
|
const agentBaseDir = agent.agentBaseDir || DIRS.agents;
|
|
1952
2303
|
const agentDir = path13.join(agentSourceRoot, agentBaseDir, agent.path || name);
|
|
1953
|
-
const intro = await readFile(path13.join(agentDir,
|
|
1954
|
-
const workflow = await readFile(path13.join(agentDir,
|
|
2304
|
+
const intro = await readFile(path13.join(agentDir, STANDARD_FILES.INTRO_MD));
|
|
2305
|
+
const workflow = await readFile(path13.join(agentDir, STANDARD_FILES.WORKFLOW_MD));
|
|
1955
2306
|
const examples = await readFileOptional(
|
|
1956
|
-
path13.join(agentDir,
|
|
2307
|
+
path13.join(agentDir, STANDARD_FILES.EXAMPLES_MD),
|
|
1957
2308
|
"## Examples\n\n_No examples defined._"
|
|
1958
2309
|
);
|
|
1959
2310
|
const criticalRequirementsTop = await readFileOptional(
|
|
1960
|
-
path13.join(agentDir,
|
|
2311
|
+
path13.join(agentDir, STANDARD_FILES.CRITICAL_REQUIREMENTS_MD),
|
|
1961
2312
|
""
|
|
1962
2313
|
);
|
|
1963
2314
|
const criticalReminders = await readFileOptional(
|
|
1964
|
-
path13.join(agentDir,
|
|
2315
|
+
path13.join(agentDir, STANDARD_FILES.CRITICAL_REMINDERS_MD),
|
|
1965
2316
|
""
|
|
1966
2317
|
);
|
|
1967
2318
|
const agentPath = agent.path || name;
|
|
1968
2319
|
const category = agentPath.split("/")[0];
|
|
1969
2320
|
const categoryDir = path13.join(agentSourceRoot, agentBaseDir, category);
|
|
1970
|
-
let outputFormat = await readFileOptional(
|
|
2321
|
+
let outputFormat = await readFileOptional(
|
|
2322
|
+
path13.join(agentDir, STANDARD_FILES.OUTPUT_FORMAT_MD),
|
|
2323
|
+
""
|
|
2324
|
+
);
|
|
1971
2325
|
if (!outputFormat) {
|
|
1972
|
-
outputFormat = await readFileOptional(
|
|
2326
|
+
outputFormat = await readFileOptional(
|
|
2327
|
+
path13.join(categoryDir, STANDARD_FILES.OUTPUT_FORMAT_MD),
|
|
2328
|
+
""
|
|
2329
|
+
);
|
|
1973
2330
|
}
|
|
1974
2331
|
const skills = installMode === "plugin" ? agent.skills.map((s) => ({ ...s, pluginRef: `${s.id}:${s.id}` })) : agent.skills;
|
|
1975
2332
|
const preloadedSkills = skills.filter((s) => s.preloaded);
|
|
@@ -1991,7 +2348,7 @@ async function compileAgentForPlugin(name, agent, fallbackRoot, engine, installM
|
|
|
1991
2348
|
dynamicSkills,
|
|
1992
2349
|
preloadedSkillIds
|
|
1993
2350
|
};
|
|
1994
|
-
return engine.renderFile("agent", data);
|
|
2351
|
+
return engine.renderFile("agent", sanitizeCompiledAgentData(data));
|
|
1995
2352
|
}
|
|
1996
2353
|
function generateStackReadme(stackId, stack, agents, skillPlugins) {
|
|
1997
2354
|
const lines = [];
|
|
@@ -2004,9 +2361,9 @@ function generateStackReadme(stackId, stack, agents, skillPlugins) {
|
|
|
2004
2361
|
lines.push("Add this plugin to your Claude Code configuration:");
|
|
2005
2362
|
lines.push("");
|
|
2006
2363
|
lines.push("```json");
|
|
2007
|
-
lines.push(
|
|
2364
|
+
lines.push("{");
|
|
2008
2365
|
lines.push(` "plugins": ["${stackId}"]`);
|
|
2009
|
-
lines.push(
|
|
2366
|
+
lines.push("}");
|
|
2010
2367
|
lines.push("```");
|
|
2011
2368
|
lines.push("");
|
|
2012
2369
|
lines.push("## Agents");
|
|
@@ -2022,7 +2379,7 @@ function generateStackReadme(stackId, stack, agents, skillPlugins) {
|
|
|
2022
2379
|
lines.push("");
|
|
2023
2380
|
lines.push("This stack includes the following skills:");
|
|
2024
2381
|
lines.push("");
|
|
2025
|
-
const uniqueSkills =
|
|
2382
|
+
const uniqueSkills = unique3(skillPlugins).sort();
|
|
2026
2383
|
for (const skill of uniqueSkills) {
|
|
2027
2384
|
lines.push(`- \`${skill}\``);
|
|
2028
2385
|
}
|
|
@@ -2049,19 +2406,8 @@ async function compileStackPlugin(options) {
|
|
|
2049
2406
|
);
|
|
2050
2407
|
let newStack = options.stack || await loadStackById(stackId, projectRoot);
|
|
2051
2408
|
if (!newStack) {
|
|
2052
|
-
newStack = await loadStackById(stackId, PROJECT_ROOT);
|
|
2053
|
-
}
|
|
2054
|
-
const sourceMatrixPath = path13.join(projectRoot, SKILLS_MATRIX_PATH);
|
|
2055
|
-
const cliMatrixPath = path13.join(PROJECT_ROOT, SKILLS_MATRIX_PATH);
|
|
2056
|
-
let matrix;
|
|
2057
|
-
try {
|
|
2058
|
-
matrix = await loadSkillsMatrix(
|
|
2059
|
-
await fileExists(sourceMatrixPath) ? sourceMatrixPath : cliMatrixPath
|
|
2060
|
-
);
|
|
2061
|
-
} catch {
|
|
2062
|
-
matrix = await loadSkillsMatrix(cliMatrixPath);
|
|
2409
|
+
newStack = await loadStackById(stackId, PROJECT_ROOT);
|
|
2063
2410
|
}
|
|
2064
|
-
const skillAliases = matrix.skill_aliases || {};
|
|
2065
2411
|
let stack;
|
|
2066
2412
|
if (newStack) {
|
|
2067
2413
|
verbose(` Found stack: ${newStack.name}`);
|
|
@@ -2069,7 +2415,7 @@ async function compileStackPlugin(options) {
|
|
|
2069
2415
|
for (const agentName of typedKeys(newStack.agents)) {
|
|
2070
2416
|
const agentConfig = newStack.agents[agentName];
|
|
2071
2417
|
if (!agentConfig) continue;
|
|
2072
|
-
const skillRefs = resolveAgentConfigToSkills(agentConfig
|
|
2418
|
+
const skillRefs = resolveAgentConfigToSkills(agentConfig);
|
|
2073
2419
|
for (const ref of skillRefs) {
|
|
2074
2420
|
agentSkillIds.add(ref.id);
|
|
2075
2421
|
}
|
|
@@ -2079,25 +2425,18 @@ async function compileStackPlugin(options) {
|
|
|
2079
2425
|
description: newStack.description,
|
|
2080
2426
|
agents: typedKeys(newStack.agents),
|
|
2081
2427
|
skills: [...agentSkillIds],
|
|
2082
|
-
stack: buildStackProperty(newStack
|
|
2428
|
+
stack: buildStackProperty(newStack)
|
|
2083
2429
|
};
|
|
2084
2430
|
} else {
|
|
2085
2431
|
throw new Error(`Stack '${stackId}' not found in config/stacks.yaml`);
|
|
2086
2432
|
}
|
|
2087
|
-
const stackSkillIds = stack.stack ?
|
|
2433
|
+
const stackSkillIds = stack.stack ? getStackSkillIds(stack.stack) : [];
|
|
2088
2434
|
const skills = await loadSkillsByIds(
|
|
2089
2435
|
stackSkillIds.map((id) => ({ id })),
|
|
2090
2436
|
projectRoot
|
|
2091
2437
|
);
|
|
2092
|
-
const compileConfig =
|
|
2093
|
-
const resolvedAgents = await resolveAgents(
|
|
2094
|
-
agents,
|
|
2095
|
-
skills,
|
|
2096
|
-
compileConfig,
|
|
2097
|
-
projectRoot,
|
|
2098
|
-
newStack,
|
|
2099
|
-
skillAliases
|
|
2100
|
-
);
|
|
2438
|
+
const compileConfig = convertStackToCompileConfig(stackId, stack);
|
|
2439
|
+
const resolvedAgents = await resolveAgents(agents, skills, compileConfig, projectRoot, newStack);
|
|
2101
2440
|
const pluginDir = path13.join(outputDir, stackId);
|
|
2102
2441
|
const agentsDir = path13.join(pluginDir, "agents");
|
|
2103
2442
|
await ensureDir(pluginDir);
|
|
@@ -2132,11 +2471,11 @@ async function compileStackPlugin(options) {
|
|
|
2132
2471
|
verbose(` Compiled agent: ${name}`);
|
|
2133
2472
|
}
|
|
2134
2473
|
const stackDir = path13.join(projectRoot, DIRS.stacks, stackId);
|
|
2135
|
-
const claudeMdPath = path13.join(stackDir,
|
|
2474
|
+
const claudeMdPath = path13.join(stackDir, STANDARD_FILES.CLAUDE_MD);
|
|
2136
2475
|
if (await fileExists(claudeMdPath)) {
|
|
2137
2476
|
const claudeContent = await readFile(claudeMdPath);
|
|
2138
|
-
await writeFile(path13.join(pluginDir,
|
|
2139
|
-
verbose(` Copied
|
|
2477
|
+
await writeFile(path13.join(pluginDir, STANDARD_FILES.CLAUDE_MD), claudeContent);
|
|
2478
|
+
verbose(` Copied ${STANDARD_FILES.CLAUDE_MD}`);
|
|
2140
2479
|
}
|
|
2141
2480
|
const newHash = hashStackConfig(stack);
|
|
2142
2481
|
const { version, contentHash } = await determinePluginVersion(
|
|
@@ -2144,7 +2483,7 @@ async function compileStackPlugin(options) {
|
|
|
2144
2483
|
pluginDir,
|
|
2145
2484
|
getPluginManifestPath
|
|
2146
2485
|
);
|
|
2147
|
-
const uniqueSkillPlugins =
|
|
2486
|
+
const uniqueSkillPlugins = unique3(allSkillPlugins);
|
|
2148
2487
|
const manifest = generateStackPluginManifest({
|
|
2149
2488
|
stackName: stackId,
|
|
2150
2489
|
description: stack.description,
|
|
@@ -2160,7 +2499,7 @@ async function compileStackPlugin(options) {
|
|
|
2160
2499
|
verbose(` Wrote plugin.json (v${version})`);
|
|
2161
2500
|
const readme = generateStackReadme(stackId, stack, compiledAgentNames, uniqueSkillPlugins);
|
|
2162
2501
|
await writeFile(path13.join(pluginDir, "README.md"), readme);
|
|
2163
|
-
verbose(
|
|
2502
|
+
verbose(" Generated README.md");
|
|
2164
2503
|
return {
|
|
2165
2504
|
pluginPath: pluginDir,
|
|
2166
2505
|
manifest,
|
|
@@ -2171,27 +2510,111 @@ async function compileStackPlugin(options) {
|
|
|
2171
2510
|
};
|
|
2172
2511
|
}
|
|
2173
2512
|
function printStackCompilationSummary(result) {
|
|
2174
|
-
|
|
2513
|
+
log(`
|
|
2175
2514
|
Stack plugin compiled: ${result.stackName}`);
|
|
2176
|
-
|
|
2177
|
-
|
|
2515
|
+
log(` Path: ${result.pluginPath}`);
|
|
2516
|
+
log(` Agents: ${result.agents.length}`);
|
|
2178
2517
|
for (const agent of result.agents) {
|
|
2179
|
-
|
|
2518
|
+
log(` - ${agent}`);
|
|
2180
2519
|
}
|
|
2181
2520
|
if (result.skillPlugins.length > 0) {
|
|
2182
|
-
|
|
2521
|
+
log(` Skills included: ${result.skillPlugins.length}`);
|
|
2183
2522
|
for (const skill of result.skillPlugins) {
|
|
2184
|
-
|
|
2523
|
+
log(` - ${skill}`);
|
|
2185
2524
|
}
|
|
2186
2525
|
}
|
|
2187
2526
|
if (result.hasHooks) {
|
|
2188
|
-
|
|
2527
|
+
log(" Hooks: enabled");
|
|
2189
2528
|
}
|
|
2190
2529
|
}
|
|
2191
2530
|
|
|
2192
2531
|
// src/cli/utils/exec.ts
|
|
2193
2532
|
init_esm_shims();
|
|
2194
2533
|
import { spawn } from "child_process";
|
|
2534
|
+
var MAX_PLUGIN_PATH_LENGTH = 1024;
|
|
2535
|
+
var MAX_GITHUB_REPO_LENGTH = 256;
|
|
2536
|
+
var MAX_MARKETPLACE_NAME_LENGTH = 128;
|
|
2537
|
+
var MAX_PLUGIN_NAME_LENGTH = 256;
|
|
2538
|
+
var GITHUB_REPO_PATTERN = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/;
|
|
2539
|
+
var SAFE_NAME_PATTERN = /^[a-zA-Z0-9._@/-]+$/;
|
|
2540
|
+
var SAFE_PLUGIN_PATH_PATTERN = /^[a-zA-Z0-9._@/:~-]+$/;
|
|
2541
|
+
var CONTROL_CHAR_PATTERN = /[\x00-\x08\x0E-\x1F\x7F]/u;
|
|
2542
|
+
function validatePluginPath(pluginPath) {
|
|
2543
|
+
if (!pluginPath || pluginPath.trim().length === 0) {
|
|
2544
|
+
throw new Error("Plugin path must not be empty.");
|
|
2545
|
+
}
|
|
2546
|
+
if (pluginPath.length > MAX_PLUGIN_PATH_LENGTH) {
|
|
2547
|
+
throw new Error(
|
|
2548
|
+
`Plugin path is too long (${pluginPath.length} characters, max ${MAX_PLUGIN_PATH_LENGTH}).`
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
if (CONTROL_CHAR_PATTERN.test(pluginPath)) {
|
|
2552
|
+
throw new Error("Plugin path contains invalid control characters.");
|
|
2553
|
+
}
|
|
2554
|
+
if (!SAFE_PLUGIN_PATH_PATTERN.test(pluginPath)) {
|
|
2555
|
+
throw new Error(
|
|
2556
|
+
`Plugin path contains invalid characters: "${pluginPath}"
|
|
2557
|
+
Plugin paths may only contain alphanumeric characters, dashes, underscores, dots, slashes, @, and colons.`
|
|
2558
|
+
);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
function validateGithubRepo(githubRepo) {
|
|
2562
|
+
if (!githubRepo || githubRepo.trim().length === 0) {
|
|
2563
|
+
throw new Error("GitHub repository must not be empty.");
|
|
2564
|
+
}
|
|
2565
|
+
if (githubRepo.length > MAX_GITHUB_REPO_LENGTH) {
|
|
2566
|
+
throw new Error(
|
|
2567
|
+
`GitHub repository is too long (${githubRepo.length} characters, max ${MAX_GITHUB_REPO_LENGTH}).`
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2570
|
+
if (CONTROL_CHAR_PATTERN.test(githubRepo)) {
|
|
2571
|
+
throw new Error("GitHub repository contains invalid control characters.");
|
|
2572
|
+
}
|
|
2573
|
+
if (!GITHUB_REPO_PATTERN.test(githubRepo)) {
|
|
2574
|
+
throw new Error(
|
|
2575
|
+
`Invalid GitHub repository format: "${githubRepo}"
|
|
2576
|
+
Expected format: owner/repo (e.g., 'my-org/my-skills').`
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
function validateMarketplaceName(name) {
|
|
2581
|
+
if (!name || name.trim().length === 0) {
|
|
2582
|
+
throw new Error("Marketplace name must not be empty.");
|
|
2583
|
+
}
|
|
2584
|
+
if (name.length > MAX_MARKETPLACE_NAME_LENGTH) {
|
|
2585
|
+
throw new Error(
|
|
2586
|
+
`Marketplace name is too long (${name.length} characters, max ${MAX_MARKETPLACE_NAME_LENGTH}).`
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2589
|
+
if (CONTROL_CHAR_PATTERN.test(name)) {
|
|
2590
|
+
throw new Error("Marketplace name contains invalid control characters.");
|
|
2591
|
+
}
|
|
2592
|
+
if (!SAFE_NAME_PATTERN.test(name)) {
|
|
2593
|
+
throw new Error(
|
|
2594
|
+
`Marketplace name contains invalid characters: "${name}"
|
|
2595
|
+
Names may only contain alphanumeric characters, dashes, underscores, dots, @, and slashes.`
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
function validatePluginName(pluginName) {
|
|
2600
|
+
if (!pluginName || pluginName.trim().length === 0) {
|
|
2601
|
+
throw new Error("Plugin name must not be empty.");
|
|
2602
|
+
}
|
|
2603
|
+
if (pluginName.length > MAX_PLUGIN_NAME_LENGTH) {
|
|
2604
|
+
throw new Error(
|
|
2605
|
+
`Plugin name is too long (${pluginName.length} characters, max ${MAX_PLUGIN_NAME_LENGTH}).`
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
if (CONTROL_CHAR_PATTERN.test(pluginName)) {
|
|
2609
|
+
throw new Error("Plugin name contains invalid control characters.");
|
|
2610
|
+
}
|
|
2611
|
+
if (!SAFE_NAME_PATTERN.test(pluginName)) {
|
|
2612
|
+
throw new Error(
|
|
2613
|
+
`Plugin name contains invalid characters: "${pluginName}"
|
|
2614
|
+
Names may only contain alphanumeric characters, dashes, underscores, dots, @, and slashes.`
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2195
2618
|
async function execCommand(command, args, options) {
|
|
2196
2619
|
return new Promise((resolve, reject) => {
|
|
2197
2620
|
const proc = spawn(command, args, {
|
|
@@ -2220,6 +2643,7 @@ async function execCommand(command, args, options) {
|
|
|
2220
2643
|
});
|
|
2221
2644
|
}
|
|
2222
2645
|
async function claudePluginInstall(pluginPath, scope, projectDir) {
|
|
2646
|
+
validatePluginPath(pluginPath);
|
|
2223
2647
|
const args = ["plugin", "install", pluginPath, "--scope", scope];
|
|
2224
2648
|
const result = await execCommand("claude", args, { cwd: projectDir });
|
|
2225
2649
|
if (result.exitCode !== 0) {
|
|
@@ -2262,14 +2686,14 @@ async function claudePluginMarketplaceExists(name) {
|
|
|
2262
2686
|
return marketplaces.some((m) => m.name === name);
|
|
2263
2687
|
}
|
|
2264
2688
|
async function claudePluginMarketplaceAdd(githubRepo, name) {
|
|
2689
|
+
validateGithubRepo(githubRepo);
|
|
2690
|
+
validateMarketplaceName(name);
|
|
2265
2691
|
const args = ["plugin", "marketplace", "add", githubRepo, "--name", name];
|
|
2266
2692
|
let result;
|
|
2267
2693
|
try {
|
|
2268
2694
|
result = await execCommand("claude", args, {});
|
|
2269
2695
|
} catch (err) {
|
|
2270
|
-
throw new Error(
|
|
2271
|
-
`Failed to add marketplace: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
2272
|
-
);
|
|
2696
|
+
throw new Error(`Failed to add marketplace: ${getErrorMessage(err)}`);
|
|
2273
2697
|
}
|
|
2274
2698
|
if (result.exitCode !== 0) {
|
|
2275
2699
|
const errorMessage = result.stderr || result.stdout || "Unknown error";
|
|
@@ -2280,6 +2704,7 @@ async function claudePluginMarketplaceAdd(githubRepo, name) {
|
|
|
2280
2704
|
}
|
|
2281
2705
|
}
|
|
2282
2706
|
async function claudePluginUninstall(pluginName, scope, projectDir) {
|
|
2707
|
+
validatePluginName(pluginName);
|
|
2283
2708
|
const args = ["plugin", "uninstall", pluginName, "--scope", scope];
|
|
2284
2709
|
const result = await execCommand("claude", args, { cwd: projectDir });
|
|
2285
2710
|
if (result.exitCode !== 0) {
|
|
@@ -2351,9 +2776,33 @@ async function installStackAsPlugin(options) {
|
|
|
2351
2776
|
}
|
|
2352
2777
|
|
|
2353
2778
|
// src/cli/lib/installation/local-installer.ts
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2779
|
+
function resolveInstallPaths(projectDir) {
|
|
2780
|
+
return {
|
|
2781
|
+
skillsDir: path15.join(projectDir, LOCAL_SKILLS_PATH),
|
|
2782
|
+
agentsDir: path15.join(projectDir, CLAUDE_DIR, "agents"),
|
|
2783
|
+
configPath: path15.join(projectDir, CLAUDE_SRC_DIR, STANDARD_FILES.CONFIG_YAML)
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
async function prepareDirectories(paths) {
|
|
2787
|
+
await ensureDir(paths.skillsDir);
|
|
2788
|
+
await ensureDir(paths.agentsDir);
|
|
2789
|
+
await ensureDir(path15.dirname(paths.configPath));
|
|
2790
|
+
}
|
|
2791
|
+
async function archiveAndCopySkills(wizardResult, sourceResult, projectDir, skillsDir) {
|
|
2792
|
+
for (const skillId of wizardResult.selectedSkills) {
|
|
2793
|
+
const selectedSource = wizardResult.sourceSelections?.[skillId];
|
|
2794
|
+
if (selectedSource && selectedSource !== "public") {
|
|
2795
|
+
verbose(`Using alternate source '${selectedSource}' for ${skillId}`);
|
|
2796
|
+
await archiveLocalSkill(projectDir, skillId);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
return copySkillsToLocalFlattened(
|
|
2800
|
+
wizardResult.selectedSkills,
|
|
2801
|
+
skillsDir,
|
|
2802
|
+
sourceResult.matrix,
|
|
2803
|
+
sourceResult
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2357
2806
|
function buildLocalSkillsMap(copiedSkills, matrix) {
|
|
2358
2807
|
const localSkillsForResolution = {};
|
|
2359
2808
|
for (const copiedSkill of copiedSkills) {
|
|
@@ -2370,6 +2819,11 @@ function buildLocalSkillsMap(copiedSkills, matrix) {
|
|
|
2370
2819
|
}
|
|
2371
2820
|
return localSkillsForResolution;
|
|
2372
2821
|
}
|
|
2822
|
+
async function loadMergedAgents(sourcePath) {
|
|
2823
|
+
const cliAgents = await loadAllAgents(PROJECT_ROOT);
|
|
2824
|
+
const sourceAgents = await loadAllAgents(sourcePath);
|
|
2825
|
+
return { ...cliAgents, ...sourceAgents };
|
|
2826
|
+
}
|
|
2373
2827
|
async function buildLocalConfig(wizardResult, sourceResult) {
|
|
2374
2828
|
let loadedStack = null;
|
|
2375
2829
|
if (wizardResult.selectedStackId) {
|
|
@@ -2382,7 +2836,7 @@ async function buildLocalConfig(wizardResult, sourceResult) {
|
|
|
2382
2836
|
if (wizardResult.selectedStackId) {
|
|
2383
2837
|
if (loadedStack) {
|
|
2384
2838
|
localConfig = generateProjectConfigFromSkills(
|
|
2385
|
-
|
|
2839
|
+
DEFAULT_PLUGIN_NAME,
|
|
2386
2840
|
wizardResult.selectedSkills,
|
|
2387
2841
|
sourceResult.matrix
|
|
2388
2842
|
);
|
|
@@ -2401,7 +2855,7 @@ async function buildLocalConfig(wizardResult, sourceResult) {
|
|
|
2401
2855
|
}
|
|
2402
2856
|
} else {
|
|
2403
2857
|
localConfig = generateProjectConfigFromSkills(
|
|
2404
|
-
|
|
2858
|
+
DEFAULT_PLUGIN_NAME,
|
|
2405
2859
|
wizardResult.selectedSkills,
|
|
2406
2860
|
sourceResult.matrix
|
|
2407
2861
|
);
|
|
@@ -2419,6 +2873,21 @@ function setConfigMetadata(config, wizardResult, sourceResult, sourceFlag) {
|
|
|
2419
2873
|
config.marketplace = sourceResult.marketplace;
|
|
2420
2874
|
}
|
|
2421
2875
|
}
|
|
2876
|
+
async function buildAndMergeConfig(wizardResult, sourceResult, projectDir, sourceFlag) {
|
|
2877
|
+
const { config } = await buildLocalConfig(wizardResult, sourceResult);
|
|
2878
|
+
setConfigMetadata(config, wizardResult, sourceResult, sourceFlag);
|
|
2879
|
+
return mergeWithExistingConfig(config, { projectDir });
|
|
2880
|
+
}
|
|
2881
|
+
async function writeConfigFile(config, configPath) {
|
|
2882
|
+
const schemaComment = `${yamlSchemaComment(SCHEMA_PATHS.projectConfig)}
|
|
2883
|
+
`;
|
|
2884
|
+
const serializable = config.stack ? { ...config, stack: compactStackForYaml(config.stack) } : config;
|
|
2885
|
+
const configYaml = stringifyYaml3(serializable, {
|
|
2886
|
+
indent: YAML_FORMATTING.INDENT,
|
|
2887
|
+
lineWidth: YAML_FORMATTING.LINE_WIDTH
|
|
2888
|
+
});
|
|
2889
|
+
await writeFile(configPath, `${schemaComment}${configYaml}`);
|
|
2890
|
+
}
|
|
2422
2891
|
function buildCompileAgents(config, agents) {
|
|
2423
2892
|
const compileAgents = {};
|
|
2424
2893
|
for (const agentId of config.agents) {
|
|
@@ -2453,42 +2922,22 @@ async function compileAndWriteAgents(compileConfig, agents, localSkills, sourceR
|
|
|
2453
2922
|
}
|
|
2454
2923
|
async function installLocal(options) {
|
|
2455
2924
|
const { wizardResult, sourceResult, projectDir, sourceFlag } = options;
|
|
2456
|
-
const
|
|
2457
|
-
|
|
2458
|
-
const
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
for (const skillId of wizardResult.selectedSkills) {
|
|
2464
|
-
const selectedSource = wizardResult.sourceSelections?.[skillId];
|
|
2465
|
-
if (selectedSource && selectedSource !== "public") {
|
|
2466
|
-
verbose(`Using alternate source '${selectedSource}' for ${skillId}`);
|
|
2467
|
-
await archiveLocalSkill(projectDir, skillId);
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
|
-
const copiedSkills = await copySkillsToLocalFlattened(
|
|
2471
|
-
wizardResult.selectedSkills,
|
|
2472
|
-
localSkillsDir,
|
|
2473
|
-
matrix,
|
|
2474
|
-
sourceResult
|
|
2925
|
+
const paths = resolveInstallPaths(projectDir);
|
|
2926
|
+
await prepareDirectories(paths);
|
|
2927
|
+
const copiedSkills = await archiveAndCopySkills(
|
|
2928
|
+
wizardResult,
|
|
2929
|
+
sourceResult,
|
|
2930
|
+
projectDir,
|
|
2931
|
+
paths.skillsDir
|
|
2475
2932
|
);
|
|
2476
|
-
const localSkillsForResolution = buildLocalSkillsMap(copiedSkills, matrix);
|
|
2477
|
-
const
|
|
2478
|
-
const
|
|
2479
|
-
const agents = { ...cliAgents, ...localAgents };
|
|
2480
|
-
const { config: builtConfig } = await buildLocalConfig(wizardResult, sourceResult);
|
|
2481
|
-
setConfigMetadata(builtConfig, wizardResult, sourceResult, sourceFlag);
|
|
2482
|
-
const mergeResult = await mergeWithExistingConfig(builtConfig, { projectDir });
|
|
2933
|
+
const localSkillsForResolution = buildLocalSkillsMap(copiedSkills, sourceResult.matrix);
|
|
2934
|
+
const agents = await loadMergedAgents(sourceResult.sourcePath);
|
|
2935
|
+
const mergeResult = await buildAndMergeConfig(wizardResult, sourceResult, projectDir, sourceFlag);
|
|
2483
2936
|
const finalConfig = mergeResult.config;
|
|
2484
|
-
|
|
2485
|
-
indent: YAML_INDENT,
|
|
2486
|
-
lineWidth: YAML_LINE_WIDTH
|
|
2487
|
-
});
|
|
2488
|
-
await writeFile(localConfigPath, configYaml);
|
|
2937
|
+
await writeConfigFile(finalConfig, paths.configPath);
|
|
2489
2938
|
const compileAgentsConfig = buildCompileAgents(finalConfig, agents);
|
|
2490
2939
|
const compileConfig = {
|
|
2491
|
-
name:
|
|
2940
|
+
name: DEFAULT_PLUGIN_NAME,
|
|
2492
2941
|
description: finalConfig.description || `Local setup with ${wizardResult.selectedSkills.length} skills`,
|
|
2493
2942
|
agents: compileAgentsConfig
|
|
2494
2943
|
};
|
|
@@ -2498,23 +2947,22 @@ async function installLocal(options) {
|
|
|
2498
2947
|
localSkillsForResolution,
|
|
2499
2948
|
sourceResult,
|
|
2500
2949
|
projectDir,
|
|
2501
|
-
|
|
2950
|
+
paths.agentsDir,
|
|
2502
2951
|
wizardResult.installMode
|
|
2503
2952
|
);
|
|
2504
2953
|
return {
|
|
2505
2954
|
copiedSkills,
|
|
2506
2955
|
config: finalConfig,
|
|
2507
|
-
configPath:
|
|
2956
|
+
configPath: paths.configPath,
|
|
2508
2957
|
compiledAgents: compiledAgentNames,
|
|
2509
2958
|
wasMerged: mergeResult.merged,
|
|
2510
2959
|
mergedConfigPath: mergeResult.existingConfigPath,
|
|
2511
|
-
skillsDir:
|
|
2512
|
-
agentsDir:
|
|
2960
|
+
skillsDir: paths.skillsDir,
|
|
2961
|
+
agentsDir: paths.agentsDir
|
|
2513
2962
|
};
|
|
2514
2963
|
}
|
|
2515
2964
|
|
|
2516
2965
|
// src/cli/lib/plugins/plugin-info.ts
|
|
2517
|
-
var DEFAULT_NAME = "claude-collective";
|
|
2518
2966
|
async function getInstallationInfo() {
|
|
2519
2967
|
const installation = await detectInstallation();
|
|
2520
2968
|
if (!installation) {
|
|
@@ -2522,7 +2970,7 @@ async function getInstallationInfo() {
|
|
|
2522
2970
|
}
|
|
2523
2971
|
let skillCount = 0;
|
|
2524
2972
|
let agentCount = 0;
|
|
2525
|
-
let name =
|
|
2973
|
+
let name = DEFAULT_PLUGIN_NAME;
|
|
2526
2974
|
let version = DEFAULT_DISPLAY_VERSION;
|
|
2527
2975
|
if (await directoryExists(installation.skillsDir)) {
|
|
2528
2976
|
try {
|
|
@@ -2545,14 +2993,14 @@ async function getInstallationInfo() {
|
|
|
2545
2993
|
if (installation.mode === "local") {
|
|
2546
2994
|
const loaded = await loadProjectConfig(installation.projectDir);
|
|
2547
2995
|
if (loaded?.config) {
|
|
2548
|
-
name = loaded.config.name ||
|
|
2996
|
+
name = loaded.config.name || DEFAULT_PLUGIN_NAME;
|
|
2549
2997
|
version = "local";
|
|
2550
2998
|
}
|
|
2551
2999
|
} else {
|
|
2552
3000
|
const pluginDir = getCollectivePluginDir(installation.projectDir);
|
|
2553
3001
|
const manifest = await readPluginManifest(pluginDir);
|
|
2554
3002
|
if (manifest) {
|
|
2555
|
-
name = manifest.name ||
|
|
3003
|
+
name = manifest.name || DEFAULT_PLUGIN_NAME;
|
|
2556
3004
|
version = manifest.version || DEFAULT_DISPLAY_VERSION;
|
|
2557
3005
|
}
|
|
2558
3006
|
}
|
|
@@ -2587,7 +3035,7 @@ function parseVersion(version) {
|
|
|
2587
3035
|
}
|
|
2588
3036
|
async function bumpPluginVersion(pluginDir, type) {
|
|
2589
3037
|
const manifestPath = path16.join(pluginDir, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
|
|
2590
|
-
const content = await
|
|
3038
|
+
const content = await readFileSafe(manifestPath, MAX_PLUGIN_FILE_SIZE);
|
|
2591
3039
|
const manifest = pluginManifestSchema.parse(JSON.parse(content));
|
|
2592
3040
|
const [major, minor, patch] = parseVersion(manifest.version || DEFAULT_VERSION);
|
|
2593
3041
|
let newVersion;
|
|
@@ -2601,6 +3049,10 @@ async function bumpPluginVersion(pluginDir, type) {
|
|
|
2601
3049
|
case "patch":
|
|
2602
3050
|
newVersion = `${major}.${minor}.${patch + 1}`;
|
|
2603
3051
|
break;
|
|
3052
|
+
default: {
|
|
3053
|
+
const exhaustiveCheck = type;
|
|
3054
|
+
throw new Error(`Unknown version bump type: ${exhaustiveCheck}`);
|
|
3055
|
+
}
|
|
2604
3056
|
}
|
|
2605
3057
|
manifest.version = newVersion;
|
|
2606
3058
|
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
@@ -2608,7 +3060,7 @@ async function bumpPluginVersion(pluginDir, type) {
|
|
|
2608
3060
|
}
|
|
2609
3061
|
async function getPluginVersion(pluginDir) {
|
|
2610
3062
|
const manifestPath = path16.join(pluginDir, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
|
|
2611
|
-
const content = await
|
|
3063
|
+
const content = await readFileSafe(manifestPath, MAX_PLUGIN_FILE_SIZE);
|
|
2612
3064
|
const manifest = pluginManifestSchema.parse(JSON.parse(content));
|
|
2613
3065
|
return manifest.version || DEFAULT_VERSION;
|
|
2614
3066
|
}
|
|
@@ -2619,8 +3071,8 @@ import { z as z2 } from "zod";
|
|
|
2619
3071
|
import path17 from "path";
|
|
2620
3072
|
import fg from "fast-glob";
|
|
2621
3073
|
import { countBy as countBy2 } from "remeda";
|
|
2622
|
-
var PLUGIN_DIR =
|
|
2623
|
-
var PLUGIN_MANIFEST =
|
|
3074
|
+
var PLUGIN_DIR = PLUGIN_MANIFEST_DIR;
|
|
3075
|
+
var PLUGIN_MANIFEST = STANDARD_FILES.PLUGIN_JSON;
|
|
2624
3076
|
var KEBAB_CASE_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2625
3077
|
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-]+)*))?$/;
|
|
2626
3078
|
var pluginManifestValidationSchema = z2.object({
|
|
@@ -2634,13 +3086,13 @@ var pluginManifestValidationSchema = z2.object({
|
|
|
2634
3086
|
skills: z2.union([z2.string(), z2.array(z2.string())]).optional(),
|
|
2635
3087
|
hooks: z2.union([z2.string(), hooksRecordSchema]).optional()
|
|
2636
3088
|
}).strict();
|
|
2637
|
-
function
|
|
3089
|
+
function formatZodErrors2(error) {
|
|
2638
3090
|
return error.issues.map((issue) => {
|
|
2639
|
-
const
|
|
3091
|
+
const path25 = issue.path.join(".");
|
|
2640
3092
|
if (issue.code === "unrecognized_keys") {
|
|
2641
3093
|
return `Unrecognized key: "${issue.keys.join('", "')}"`;
|
|
2642
3094
|
}
|
|
2643
|
-
return
|
|
3095
|
+
return path25 ? `${path25}: ${issue.message}` : issue.message;
|
|
2644
3096
|
});
|
|
2645
3097
|
}
|
|
2646
3098
|
function isKebabCase(str) {
|
|
@@ -2689,19 +3141,18 @@ async function validatePluginManifest(manifestPath) {
|
|
|
2689
3141
|
}
|
|
2690
3142
|
let manifest;
|
|
2691
3143
|
try {
|
|
2692
|
-
const content = await
|
|
3144
|
+
const content = await readFileSafe(manifestPath, MAX_PLUGIN_FILE_SIZE);
|
|
2693
3145
|
manifest = JSON.parse(content);
|
|
2694
3146
|
} catch (err) {
|
|
2695
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2696
3147
|
return {
|
|
2697
3148
|
valid: false,
|
|
2698
|
-
errors: [`Invalid JSON in ${PLUGIN_MANIFEST}: ${
|
|
3149
|
+
errors: [`Invalid JSON in ${PLUGIN_MANIFEST}: ${getErrorMessage(err)}`],
|
|
2699
3150
|
warnings: []
|
|
2700
3151
|
};
|
|
2701
3152
|
}
|
|
2702
3153
|
const result = pluginManifestValidationSchema.safeParse(manifest);
|
|
2703
3154
|
if (!result.success) {
|
|
2704
|
-
errors.push(...
|
|
3155
|
+
errors.push(...formatZodErrors2(result.error));
|
|
2705
3156
|
}
|
|
2706
3157
|
if (manifest.name && typeof manifest.name === "string") {
|
|
2707
3158
|
if (!isKebabCase(manifest.name)) {
|
|
@@ -2758,7 +3209,7 @@ async function validateSkillFrontmatter(skillPath) {
|
|
|
2758
3209
|
}
|
|
2759
3210
|
const result = skillFrontmatterValidationSchema.safeParse(frontmatter);
|
|
2760
3211
|
if (!result.success) {
|
|
2761
|
-
errors.push(...
|
|
3212
|
+
errors.push(...formatZodErrors2(result.error));
|
|
2762
3213
|
}
|
|
2763
3214
|
return {
|
|
2764
3215
|
valid: errors.length === 0,
|
|
@@ -2787,7 +3238,7 @@ async function validateAgentFrontmatter(agentPath) {
|
|
|
2787
3238
|
}
|
|
2788
3239
|
const result = agentFrontmatterValidationSchema.safeParse(frontmatter);
|
|
2789
3240
|
if (!result.success) {
|
|
2790
|
-
errors.push(...
|
|
3241
|
+
errors.push(...formatZodErrors2(result.error));
|
|
2791
3242
|
}
|
|
2792
3243
|
const fm = frontmatter;
|
|
2793
3244
|
if (fm.name && typeof fm.name === "string") {
|
|
@@ -2801,74 +3252,72 @@ async function validateAgentFrontmatter(agentPath) {
|
|
|
2801
3252
|
warnings
|
|
2802
3253
|
};
|
|
2803
3254
|
}
|
|
2804
|
-
|
|
2805
|
-
const errors =
|
|
2806
|
-
const warnings =
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
3255
|
+
function mergeResults(results) {
|
|
3256
|
+
const errors = results.flatMap((r) => r.errors);
|
|
3257
|
+
const warnings = results.flatMap((r) => r.warnings);
|
|
3258
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
3259
|
+
}
|
|
3260
|
+
var EMPTY_RESULT = { valid: true, errors: [], warnings: [] };
|
|
3261
|
+
function prefixResult(result, prefix) {
|
|
3262
|
+
return {
|
|
3263
|
+
valid: result.valid,
|
|
3264
|
+
errors: result.valid ? [] : result.errors.map((e) => `${prefix}: ${e}`),
|
|
3265
|
+
warnings: result.warnings.map((w) => `${prefix}: ${w}`)
|
|
3266
|
+
};
|
|
3267
|
+
}
|
|
3268
|
+
async function validatePluginSkillFiles(pluginPath, skillsRelPath) {
|
|
3269
|
+
const skillsDir = path17.join(pluginPath, skillsRelPath);
|
|
3270
|
+
if (!await directoryExists(skillsDir)) return EMPTY_RESULT;
|
|
3271
|
+
const files = await fg("**/SKILL.md", { cwd: skillsDir, absolute: true });
|
|
3272
|
+
if (files.length === 0) {
|
|
3273
|
+
return {
|
|
3274
|
+
valid: true,
|
|
3275
|
+
errors: [],
|
|
3276
|
+
warnings: [`Skills directory exists but contains no SKILL.md files: ${skillsRelPath}`]
|
|
3277
|
+
};
|
|
2812
3278
|
}
|
|
2813
|
-
const
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
3279
|
+
const results = await Promise.all(
|
|
3280
|
+
files.map(
|
|
3281
|
+
async (f) => prefixResult(await validateSkillFrontmatter(f), path17.relative(pluginPath, f))
|
|
3282
|
+
)
|
|
3283
|
+
);
|
|
3284
|
+
return mergeResults(results);
|
|
3285
|
+
}
|
|
3286
|
+
async function validatePluginAgentFiles(pluginPath, agentsRelPath) {
|
|
3287
|
+
const agentsDir = path17.join(pluginPath, agentsRelPath);
|
|
3288
|
+
if (!await directoryExists(agentsDir)) return EMPTY_RESULT;
|
|
3289
|
+
const files = await fg("*.md", { cwd: agentsDir, absolute: true });
|
|
3290
|
+
if (files.length === 0) {
|
|
3291
|
+
return {
|
|
3292
|
+
valid: true,
|
|
3293
|
+
errors: [],
|
|
3294
|
+
warnings: [`Agents directory exists but contains no .md files: ${agentsRelPath}`]
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
const results = await Promise.all(
|
|
3298
|
+
files.map(
|
|
3299
|
+
async (f) => prefixResult(await validateAgentFrontmatter(f), path17.relative(pluginPath, f))
|
|
3300
|
+
)
|
|
3301
|
+
);
|
|
3302
|
+
return mergeResults(results);
|
|
3303
|
+
}
|
|
3304
|
+
async function loadManifestForValidation(manifestPath) {
|
|
2818
3305
|
try {
|
|
2819
|
-
const content = await
|
|
2820
|
-
|
|
3306
|
+
const content = await readFileSafe(manifestPath, MAX_PLUGIN_FILE_SIZE);
|
|
3307
|
+
return JSON.parse(content);
|
|
2821
3308
|
} catch {
|
|
3309
|
+
return null;
|
|
2822
3310
|
}
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
`Skills directory exists but contains no SKILL.md files: ${manifest.skills}`
|
|
2834
|
-
);
|
|
2835
|
-
}
|
|
2836
|
-
for (const skillFile of skillFiles) {
|
|
2837
|
-
const relativePath = path17.relative(pluginPath, skillFile);
|
|
2838
|
-
const skillResult = await validateSkillFrontmatter(skillFile);
|
|
2839
|
-
if (!skillResult.valid) {
|
|
2840
|
-
errors.push(...skillResult.errors.map((e) => `${relativePath}: ${e}`));
|
|
2841
|
-
}
|
|
2842
|
-
warnings.push(...skillResult.warnings.map((w) => `${relativePath}: ${w}`));
|
|
2843
|
-
}
|
|
2844
|
-
}
|
|
2845
|
-
}
|
|
2846
|
-
if (manifest.agents && typeof manifest.agents === "string") {
|
|
2847
|
-
const agentsDir = path17.join(pluginPath, manifest.agents);
|
|
2848
|
-
if (await directoryExists(agentsDir)) {
|
|
2849
|
-
const agentFiles = await fg("*.md", {
|
|
2850
|
-
cwd: agentsDir,
|
|
2851
|
-
absolute: true
|
|
2852
|
-
});
|
|
2853
|
-
if (agentFiles.length === 0) {
|
|
2854
|
-
warnings.push(`Agents directory exists but contains no .md files: ${manifest.agents}`);
|
|
2855
|
-
}
|
|
2856
|
-
for (const agentFile of agentFiles) {
|
|
2857
|
-
const relativePath = path17.relative(pluginPath, agentFile);
|
|
2858
|
-
const agentResult = await validateAgentFrontmatter(agentFile);
|
|
2859
|
-
if (!agentResult.valid) {
|
|
2860
|
-
errors.push(...agentResult.errors.map((e) => `${relativePath}: ${e}`));
|
|
2861
|
-
}
|
|
2862
|
-
warnings.push(...agentResult.warnings.map((w) => `${relativePath}: ${w}`));
|
|
2863
|
-
}
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
return {
|
|
2868
|
-
valid: errors.length === 0,
|
|
2869
|
-
errors,
|
|
2870
|
-
warnings
|
|
2871
|
-
};
|
|
3311
|
+
}
|
|
3312
|
+
async function validatePlugin(pluginPath) {
|
|
3313
|
+
const structureResult = await validatePluginStructure(pluginPath);
|
|
3314
|
+
if (!structureResult.valid) return structureResult;
|
|
3315
|
+
const manifestPath = path17.join(pluginPath, PLUGIN_DIR, PLUGIN_MANIFEST);
|
|
3316
|
+
const manifestResult = await validatePluginManifest(manifestPath);
|
|
3317
|
+
const manifest = await loadManifestForValidation(manifestPath);
|
|
3318
|
+
const skillsResult = manifest?.skills && typeof manifest.skills === "string" ? await validatePluginSkillFiles(pluginPath, manifest.skills) : EMPTY_RESULT;
|
|
3319
|
+
const agentsResult = manifest?.agents && typeof manifest.agents === "string" ? await validatePluginAgentFiles(pluginPath, manifest.agents) : EMPTY_RESULT;
|
|
3320
|
+
return mergeResults([structureResult, manifestResult, skillsResult, agentsResult]);
|
|
2872
3321
|
}
|
|
2873
3322
|
async function validateAllPlugins(pluginsDir) {
|
|
2874
3323
|
const results = [];
|
|
@@ -2936,21 +3385,21 @@ function printPluginValidationResult(name, result, verbose2 = false) {
|
|
|
2936
3385
|
if (result.valid && result.warnings.length === 0 && !verbose2) {
|
|
2937
3386
|
return;
|
|
2938
3387
|
}
|
|
2939
|
-
|
|
3388
|
+
log(`
|
|
2940
3389
|
${status} ${name}`);
|
|
2941
3390
|
if (result.errors.length > 0) {
|
|
2942
|
-
|
|
2943
|
-
result.errors.forEach((e) =>
|
|
3391
|
+
log(" Errors:");
|
|
3392
|
+
result.errors.forEach((e) => log(` - ${e}`));
|
|
2944
3393
|
}
|
|
2945
3394
|
if (result.warnings.length > 0) {
|
|
2946
|
-
|
|
2947
|
-
result.warnings.forEach((w) =>
|
|
3395
|
+
log(" Warnings:");
|
|
3396
|
+
result.warnings.forEach((w) => log(` - ${w}`));
|
|
2948
3397
|
}
|
|
2949
3398
|
}
|
|
2950
3399
|
|
|
2951
3400
|
// src/cli/lib/loading/multi-source-loader.ts
|
|
2952
3401
|
var PUBLIC_SOURCE_NAME = "public";
|
|
2953
|
-
async function loadSkillsFromAllSources(primaryMatrix,
|
|
3402
|
+
async function loadSkillsFromAllSources(primaryMatrix, _sourceConfig, projectDir) {
|
|
2954
3403
|
tagPrimarySourceSkills(primaryMatrix);
|
|
2955
3404
|
tagLocalSkills(primaryMatrix);
|
|
2956
3405
|
await tagPluginSkills(primaryMatrix, projectDir);
|
|
@@ -2958,9 +3407,7 @@ async function loadSkillsFromAllSources(primaryMatrix, sourceConfig, projectDir)
|
|
|
2958
3407
|
setActiveSources(primaryMatrix);
|
|
2959
3408
|
}
|
|
2960
3409
|
function tagPrimarySourceSkills(matrix) {
|
|
2961
|
-
for (const [, skill] of typedEntries(
|
|
2962
|
-
matrix.skills
|
|
2963
|
-
)) {
|
|
3410
|
+
for (const [, skill] of typedEntries(matrix.skills)) {
|
|
2964
3411
|
if (!skill) continue;
|
|
2965
3412
|
const source = {
|
|
2966
3413
|
name: PUBLIC_SOURCE_NAME,
|
|
@@ -2974,9 +3421,7 @@ function tagPrimarySourceSkills(matrix) {
|
|
|
2974
3421
|
}
|
|
2975
3422
|
function tagLocalSkills(matrix) {
|
|
2976
3423
|
let count = 0;
|
|
2977
|
-
for (const [, skill] of typedEntries(
|
|
2978
|
-
matrix.skills
|
|
2979
|
-
)) {
|
|
3424
|
+
for (const [, skill] of typedEntries(matrix.skills)) {
|
|
2980
3425
|
if (!skill) continue;
|
|
2981
3426
|
if (!skill.local) continue;
|
|
2982
3427
|
const source = {
|
|
@@ -3063,14 +3508,12 @@ async function tagExtraSources(matrix, projectDir) {
|
|
|
3063
3508
|
`Extra source '${extraSource.name}': ${skills.length} skills found, ${matchCount} matching`
|
|
3064
3509
|
);
|
|
3065
3510
|
} catch (error) {
|
|
3066
|
-
warn(`Failed to load extra source '${extraSource.name}' (${extraSource.url}): ${error}`);
|
|
3511
|
+
warn(`Failed to load extra source '${extraSource.name}' ('${extraSource.url}'): ${error}`);
|
|
3067
3512
|
}
|
|
3068
3513
|
}
|
|
3069
3514
|
}
|
|
3070
3515
|
function setActiveSources(matrix) {
|
|
3071
|
-
for (const [, skill] of typedEntries(
|
|
3072
|
-
matrix.skills
|
|
3073
|
-
)) {
|
|
3516
|
+
for (const [, skill] of typedEntries(matrix.skills)) {
|
|
3074
3517
|
if (!skill) continue;
|
|
3075
3518
|
if (!skill.availableSources || skill.availableSources.length === 0) continue;
|
|
3076
3519
|
const installedSource = skill.availableSources.find((s) => s.installed);
|
|
@@ -3102,7 +3545,7 @@ async function searchExtraSources(alias, configuredSources) {
|
|
|
3102
3545
|
}
|
|
3103
3546
|
}
|
|
3104
3547
|
} catch (error) {
|
|
3105
|
-
warn(`Failed to search extra source '${source.name}' (${source.url}): ${error}`);
|
|
3548
|
+
warn(`Failed to search extra source '${source.name}' ('${source.url}'): ${error}`);
|
|
3106
3549
|
}
|
|
3107
3550
|
}
|
|
3108
3551
|
return candidates;
|
|
@@ -3141,30 +3584,7 @@ async function loadFromLocal(source, sourceConfig) {
|
|
|
3141
3584
|
skillsPath = PROJECT_ROOT;
|
|
3142
3585
|
}
|
|
3143
3586
|
verbose(`Loading skills from local path: ${skillsPath}`);
|
|
3144
|
-
const
|
|
3145
|
-
const cliMatrixPath = path19.join(PROJECT_ROOT, SKILLS_MATRIX_PATH);
|
|
3146
|
-
let matrixPath;
|
|
3147
|
-
if (await fileExists(sourceMatrixPath)) {
|
|
3148
|
-
matrixPath = sourceMatrixPath;
|
|
3149
|
-
verbose(`Matrix from source: ${matrixPath}`);
|
|
3150
|
-
} else {
|
|
3151
|
-
matrixPath = cliMatrixPath;
|
|
3152
|
-
verbose(`Matrix from CLI (source has no matrix): ${matrixPath}`);
|
|
3153
|
-
}
|
|
3154
|
-
const skillsDir = path19.join(skillsPath, SKILLS_DIR_PATH);
|
|
3155
|
-
verbose(`Skills from source: ${skillsDir}`);
|
|
3156
|
-
const matrix = await loadSkillsMatrix(matrixPath);
|
|
3157
|
-
const skills = await extractAllSkills(skillsDir);
|
|
3158
|
-
const mergedMatrix = await mergeMatrixWithSkills(matrix, skills);
|
|
3159
|
-
const sourceStacks = await loadStacks(skillsPath);
|
|
3160
|
-
const stacks = sourceStacks.length > 0 ? sourceStacks : await loadStacks(PROJECT_ROOT);
|
|
3161
|
-
if (stacks.length > 0) {
|
|
3162
|
-
mergedMatrix.suggestedStacks = stacks.map(
|
|
3163
|
-
(stack) => stackToResolvedStack(stack, mergedMatrix.displayNameToId)
|
|
3164
|
-
);
|
|
3165
|
-
const stackSource = sourceStacks.length > 0 ? "source" : "CLI";
|
|
3166
|
-
verbose(`Loaded ${stacks.length} stacks from ${stackSource}`);
|
|
3167
|
-
}
|
|
3587
|
+
const mergedMatrix = await loadAndMergeFromBasePath(skillsPath);
|
|
3168
3588
|
return {
|
|
3169
3589
|
matrix: mergedMatrix,
|
|
3170
3590
|
sourceConfig,
|
|
@@ -3177,7 +3597,21 @@ async function loadFromRemote(source, sourceConfig, forceRefresh) {
|
|
|
3177
3597
|
verbose(`Fetching skills from remote source: ${source}`);
|
|
3178
3598
|
const fetchResult = await fetchFromSource(source, { forceRefresh });
|
|
3179
3599
|
verbose(`Fetched to: ${fetchResult.path}`);
|
|
3180
|
-
const
|
|
3600
|
+
const mergedMatrix = await loadAndMergeFromBasePath(fetchResult.path);
|
|
3601
|
+
return {
|
|
3602
|
+
matrix: mergedMatrix,
|
|
3603
|
+
sourceConfig,
|
|
3604
|
+
sourcePath: fetchResult.path,
|
|
3605
|
+
isLocal: false,
|
|
3606
|
+
marketplace: sourceConfig.marketplace
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
async function loadAndMergeFromBasePath(basePath) {
|
|
3610
|
+
const sourceProjectConfig = await loadProjectSourceConfig(basePath);
|
|
3611
|
+
const matrixRelPath = sourceProjectConfig?.matrix_file ?? SKILLS_MATRIX_PATH;
|
|
3612
|
+
const skillsDirRelPath = sourceProjectConfig?.skills_dir ?? SKILLS_DIR_PATH;
|
|
3613
|
+
const stacksRelFile = sourceProjectConfig?.stacks_file;
|
|
3614
|
+
const sourceMatrixPath = path19.join(basePath, matrixRelPath);
|
|
3181
3615
|
const cliMatrixPath = path19.join(PROJECT_ROOT, SKILLS_MATRIX_PATH);
|
|
3182
3616
|
let matrixPath;
|
|
3183
3617
|
if (await fileExists(sourceMatrixPath)) {
|
|
@@ -3187,35 +3621,27 @@ async function loadFromRemote(source, sourceConfig, forceRefresh) {
|
|
|
3187
3621
|
matrixPath = cliMatrixPath;
|
|
3188
3622
|
verbose(`Matrix from CLI (source has no matrix): ${matrixPath}`);
|
|
3189
3623
|
}
|
|
3190
|
-
const skillsDir = path19.join(
|
|
3624
|
+
const skillsDir = path19.join(basePath, skillsDirRelPath);
|
|
3191
3625
|
verbose(`Skills from source: ${skillsDir}`);
|
|
3192
3626
|
const matrix = await loadSkillsMatrix(matrixPath);
|
|
3193
3627
|
const skills = await extractAllSkills(skillsDir);
|
|
3194
3628
|
const mergedMatrix = await mergeMatrixWithSkills(matrix, skills);
|
|
3195
|
-
const sourceStacks = await loadStacks(
|
|
3629
|
+
const sourceStacks = await loadStacks(basePath, stacksRelFile);
|
|
3196
3630
|
const stacks = sourceStacks.length > 0 ? sourceStacks : await loadStacks(PROJECT_ROOT);
|
|
3197
3631
|
if (stacks.length > 0) {
|
|
3198
|
-
mergedMatrix.suggestedStacks = stacks.map(
|
|
3199
|
-
(stack) => stackToResolvedStack(stack, mergedMatrix.displayNameToId)
|
|
3200
|
-
);
|
|
3632
|
+
mergedMatrix.suggestedStacks = stacks.map((stack) => convertStackToResolvedStack(stack));
|
|
3201
3633
|
const stackSource = sourceStacks.length > 0 ? "source" : "CLI";
|
|
3202
3634
|
verbose(`Loaded ${stacks.length} stacks from ${stackSource}`);
|
|
3203
3635
|
}
|
|
3204
|
-
return
|
|
3205
|
-
matrix: mergedMatrix,
|
|
3206
|
-
sourceConfig,
|
|
3207
|
-
sourcePath: fetchResult.path,
|
|
3208
|
-
isLocal: false,
|
|
3209
|
-
marketplace: sourceConfig.marketplace
|
|
3210
|
-
};
|
|
3636
|
+
return mergedMatrix;
|
|
3211
3637
|
}
|
|
3212
|
-
function
|
|
3638
|
+
function convertStackToResolvedStack(stack) {
|
|
3213
3639
|
const allSkillIds = [];
|
|
3214
3640
|
const seenSkillIds = /* @__PURE__ */ new Set();
|
|
3215
3641
|
for (const agentId of typedKeys(stack.agents)) {
|
|
3216
3642
|
const agentConfig = stack.agents[agentId];
|
|
3217
3643
|
if (!agentConfig) continue;
|
|
3218
|
-
const skillRefs = resolveAgentConfigToSkills(agentConfig
|
|
3644
|
+
const skillRefs = resolveAgentConfigToSkills(agentConfig);
|
|
3219
3645
|
for (const ref of skillRefs) {
|
|
3220
3646
|
if (!seenSkillIds.has(ref.id)) {
|
|
3221
3647
|
seenSkillIds.add(ref.id);
|
|
@@ -3237,6 +3663,26 @@ function stackToResolvedStack(stack, displayNameToId) {
|
|
|
3237
3663
|
philosophy: stack.philosophy || ""
|
|
3238
3664
|
};
|
|
3239
3665
|
}
|
|
3666
|
+
function extractSourceName(source) {
|
|
3667
|
+
const withoutProtocol = source.replace(/^(?:github|gh|gitlab|bitbucket|sourcehut):/, "");
|
|
3668
|
+
const withoutUrl = withoutProtocol.replace(/^https?:\/\/[^/]+\//, "");
|
|
3669
|
+
const firstSegment = withoutUrl.split("/")[0];
|
|
3670
|
+
return firstSegment || source;
|
|
3671
|
+
}
|
|
3672
|
+
function getMarketplaceLabel(sourceResult) {
|
|
3673
|
+
if (sourceResult.isLocal) return void 0;
|
|
3674
|
+
const { marketplace } = sourceResult;
|
|
3675
|
+
if (!marketplace) {
|
|
3676
|
+
const name = extractSourceName(sourceResult.sourceConfig.source);
|
|
3677
|
+
return `${name} (public)`;
|
|
3678
|
+
}
|
|
3679
|
+
const PUBLIC_MARKETPLACE_COUNT = 1;
|
|
3680
|
+
const isDefaultSource = sourceResult.sourceConfig.source === DEFAULT_SOURCE;
|
|
3681
|
+
if (!isDefaultSource) {
|
|
3682
|
+
return `${marketplace} + ${PUBLIC_MARKETPLACE_COUNT} public`;
|
|
3683
|
+
}
|
|
3684
|
+
return marketplace;
|
|
3685
|
+
}
|
|
3240
3686
|
function mergeLocalSkillsIntoMatrix(matrix, localResult) {
|
|
3241
3687
|
for (const metadata of localResult.skills) {
|
|
3242
3688
|
const existingSkill = matrix.skills[metadata.id];
|
|
@@ -3250,7 +3696,7 @@ function mergeLocalSkillsIntoMatrix(matrix, localResult) {
|
|
|
3250
3696
|
category,
|
|
3251
3697
|
categoryExclusive: metadata.categoryExclusive,
|
|
3252
3698
|
tags: metadata.tags ?? [],
|
|
3253
|
-
author:
|
|
3699
|
+
author: LOCAL_DEFAULTS.AUTHOR,
|
|
3254
3700
|
conflictsWith: existingSkill?.conflictsWith ?? [],
|
|
3255
3701
|
recommends: existingSkill?.recommends ?? [],
|
|
3256
3702
|
requires: existingSkill?.requires ?? [],
|
|
@@ -3271,7 +3717,6 @@ function mergeLocalSkillsIntoMatrix(matrix, localResult) {
|
|
|
3271
3717
|
|
|
3272
3718
|
// src/cli/lib/loading/defaults-loader.ts
|
|
3273
3719
|
init_esm_shims();
|
|
3274
|
-
import { parse as parseYaml8 } from "yaml";
|
|
3275
3720
|
var cachedDefaults = null;
|
|
3276
3721
|
function getCachedDefaults() {
|
|
3277
3722
|
return cachedDefaults;
|
|
@@ -3389,7 +3834,7 @@ function getEffectiveSkillToAgents() {
|
|
|
3389
3834
|
}
|
|
3390
3835
|
return SKILL_TO_AGENTS;
|
|
3391
3836
|
}
|
|
3392
|
-
function getAgentsForSkill(skillPath, category,
|
|
3837
|
+
function getAgentsForSkill(skillPath, category, _projectConfig) {
|
|
3393
3838
|
const normalizedPath = skillPath.replace(/^skills\//, "").replace(/\/$/, "");
|
|
3394
3839
|
const skillToAgents = getEffectiveSkillToAgents();
|
|
3395
3840
|
if (skillToAgents[category]) {
|
|
@@ -3414,14 +3859,12 @@ function getAgentsForSkill(skillPath, category, projectConfig) {
|
|
|
3414
3859
|
// src/cli/lib/skills/skill-plugin-compiler.ts
|
|
3415
3860
|
init_esm_shims();
|
|
3416
3861
|
import path20 from "path";
|
|
3417
|
-
import { parse as
|
|
3418
|
-
var SKILL_FILES = ["SKILL.md", "reference.md"];
|
|
3419
|
-
var SKILL_DIRS = ["examples", "scripts"];
|
|
3862
|
+
import { parse as parseYaml7 } from "yaml";
|
|
3420
3863
|
function sanitizeSkillName(name) {
|
|
3421
3864
|
return name.replace(/\+/g, "-");
|
|
3422
3865
|
}
|
|
3423
3866
|
async function readSkillMetadata(skillPath) {
|
|
3424
|
-
const metadataPath = path20.join(skillPath,
|
|
3867
|
+
const metadataPath = path20.join(skillPath, STANDARD_FILES.METADATA_YAML);
|
|
3425
3868
|
if (!await fileExists(metadataPath)) {
|
|
3426
3869
|
return null;
|
|
3427
3870
|
}
|
|
@@ -3429,16 +3872,14 @@ async function readSkillMetadata(skillPath) {
|
|
|
3429
3872
|
const content = await readFile(metadataPath);
|
|
3430
3873
|
const lines = content.split("\n");
|
|
3431
3874
|
const yamlContent = lines[0]?.startsWith("# yaml-language-server:") ? lines.slice(1).join("\n") : content;
|
|
3432
|
-
const result = skillMetadataLoaderSchema.safeParse(
|
|
3875
|
+
const result = skillMetadataLoaderSchema.safeParse(parseYaml7(yamlContent));
|
|
3433
3876
|
if (!result.success) {
|
|
3434
|
-
warn(
|
|
3435
|
-
`Invalid metadata.yaml at ${skillPath}: ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
3436
|
-
);
|
|
3877
|
+
warn(`Invalid metadata.yaml at '${skillPath}': ${formatZodErrors(result.error.issues)}`);
|
|
3437
3878
|
return null;
|
|
3438
3879
|
}
|
|
3439
3880
|
return result.data;
|
|
3440
3881
|
} catch (error) {
|
|
3441
|
-
warn(`Failed to read metadata.yaml at ${skillPath}: ${error}`);
|
|
3882
|
+
warn(`Failed to read metadata.yaml at '${skillPath}': ${error}`);
|
|
3442
3883
|
return null;
|
|
3443
3884
|
}
|
|
3444
3885
|
}
|
|
@@ -3459,17 +3900,17 @@ function generateReadme(skillName, frontmatter, metadata) {
|
|
|
3459
3900
|
lines.push("Add this plugin to your Claude Code configuration:");
|
|
3460
3901
|
lines.push("");
|
|
3461
3902
|
lines.push("```json");
|
|
3462
|
-
lines.push(
|
|
3903
|
+
lines.push("{");
|
|
3463
3904
|
lines.push(` "plugins": ["${skillName}"]`);
|
|
3464
|
-
lines.push(
|
|
3905
|
+
lines.push("}");
|
|
3465
3906
|
lines.push("```");
|
|
3466
3907
|
lines.push("");
|
|
3467
3908
|
lines.push("## Usage");
|
|
3468
3909
|
lines.push("");
|
|
3469
|
-
lines.push(
|
|
3910
|
+
lines.push("This skill is automatically available when installed.");
|
|
3470
3911
|
if (metadata?.requires && metadata.requires.length > 0) {
|
|
3471
3912
|
lines.push("");
|
|
3472
|
-
lines.push(
|
|
3913
|
+
lines.push(`**Requires:** ${metadata.requires.join(", ")}`);
|
|
3473
3914
|
}
|
|
3474
3915
|
lines.push("");
|
|
3475
3916
|
lines.push("---");
|
|
@@ -3481,17 +3922,17 @@ function generateReadme(skillName, frontmatter, metadata) {
|
|
|
3481
3922
|
async function compileSkillPlugin(options) {
|
|
3482
3923
|
const { skillPath, outputDir, skillName: overrideName } = options;
|
|
3483
3924
|
const dirBasename = path20.basename(skillPath);
|
|
3484
|
-
const skillMdPath = path20.join(skillPath,
|
|
3925
|
+
const skillMdPath = path20.join(skillPath, STANDARD_FILES.SKILL_MD);
|
|
3485
3926
|
if (!await fileExists(skillMdPath)) {
|
|
3486
3927
|
throw new Error(
|
|
3487
|
-
`Skill '${dirBasename}' is missing required
|
|
3928
|
+
`Skill '${dirBasename}' is missing required ${STANDARD_FILES.SKILL_MD} file. Expected at: ${skillMdPath}`
|
|
3488
3929
|
);
|
|
3489
3930
|
}
|
|
3490
3931
|
const skillMdContent = await readFile(skillMdPath);
|
|
3491
3932
|
const frontmatter = parseFrontmatter(skillMdContent, skillMdPath);
|
|
3492
3933
|
if (!frontmatter) {
|
|
3493
3934
|
throw new Error(
|
|
3494
|
-
`Skill '${dirBasename}' has invalid or missing YAML frontmatter in
|
|
3935
|
+
`Skill '${dirBasename}' has invalid or missing YAML frontmatter in ${STANDARD_FILES.SKILL_MD}. Required fields: 'name' and 'description'. File: ${skillMdPath}`
|
|
3495
3936
|
);
|
|
3496
3937
|
}
|
|
3497
3938
|
const skillName = overrideName ?? sanitizeSkillName(frontmatter.name);
|
|
@@ -3501,7 +3942,7 @@ async function compileSkillPlugin(options) {
|
|
|
3501
3942
|
const skillsDir = path20.join(pluginDir, "skills", skillName);
|
|
3502
3943
|
await ensureDir(pluginDir);
|
|
3503
3944
|
await ensureDir(skillsDir);
|
|
3504
|
-
const newHash = await
|
|
3945
|
+
const newHash = await computeSkillFolderHash(skillPath);
|
|
3505
3946
|
const { version, contentHash } = await determinePluginVersion(
|
|
3506
3947
|
newHash,
|
|
3507
3948
|
pluginDir,
|
|
@@ -3517,10 +3958,10 @@ async function compileSkillPlugin(options) {
|
|
|
3517
3958
|
await writePluginManifest(pluginDir, manifest);
|
|
3518
3959
|
await writeContentHash(pluginDir, contentHash, getPluginManifestPath);
|
|
3519
3960
|
verbose(` Wrote plugin.json for ${skillName} (v${version})`);
|
|
3520
|
-
await writeFile(path20.join(skillsDir,
|
|
3521
|
-
verbose(` Copied
|
|
3522
|
-
for (const fileName of
|
|
3523
|
-
if (fileName ===
|
|
3961
|
+
await writeFile(path20.join(skillsDir, STANDARD_FILES.SKILL_MD), skillMdContent);
|
|
3962
|
+
verbose(` Copied ${STANDARD_FILES.SKILL_MD}`);
|
|
3963
|
+
for (const fileName of SKILL_CONTENT_FILES) {
|
|
3964
|
+
if (fileName === STANDARD_FILES.SKILL_MD) continue;
|
|
3524
3965
|
const sourcePath = path20.join(skillPath, fileName);
|
|
3525
3966
|
if (await fileExists(sourcePath)) {
|
|
3526
3967
|
const content = await readFile(sourcePath);
|
|
@@ -3528,7 +3969,7 @@ async function compileSkillPlugin(options) {
|
|
|
3528
3969
|
verbose(` Copied ${fileName}`);
|
|
3529
3970
|
}
|
|
3530
3971
|
}
|
|
3531
|
-
for (const dirName of
|
|
3972
|
+
for (const dirName of SKILL_CONTENT_DIRS) {
|
|
3532
3973
|
const sourceDir = path20.join(skillPath, dirName);
|
|
3533
3974
|
if (await fileExists(sourceDir)) {
|
|
3534
3975
|
await copy(sourceDir, path20.join(skillsDir, dirName));
|
|
@@ -3537,7 +3978,7 @@ async function compileSkillPlugin(options) {
|
|
|
3537
3978
|
}
|
|
3538
3979
|
const readme = generateReadme(skillName, frontmatter, metadata);
|
|
3539
3980
|
await writeFile(path20.join(pluginDir, "README.md"), readme);
|
|
3540
|
-
verbose(
|
|
3981
|
+
verbose(" Generated README.md");
|
|
3541
3982
|
return {
|
|
3542
3983
|
pluginPath: pluginDir,
|
|
3543
3984
|
manifest,
|
|
@@ -3546,7 +3987,7 @@ async function compileSkillPlugin(options) {
|
|
|
3546
3987
|
}
|
|
3547
3988
|
async function compileAllSkillPlugins(skillsDir, outputDir) {
|
|
3548
3989
|
const results = [];
|
|
3549
|
-
const skillMdFiles = await glob(
|
|
3990
|
+
const skillMdFiles = await glob(`**/${STANDARD_FILES.SKILL_MD}`, skillsDir);
|
|
3550
3991
|
for (const skillMdFile of skillMdFiles) {
|
|
3551
3992
|
const skillPath = path20.join(skillsDir, path20.dirname(skillMdFile));
|
|
3552
3993
|
try {
|
|
@@ -3555,29 +3996,27 @@ async function compileAllSkillPlugins(skillsDir, outputDir) {
|
|
|
3555
3996
|
outputDir
|
|
3556
3997
|
});
|
|
3557
3998
|
results.push(result);
|
|
3558
|
-
|
|
3999
|
+
log(` [OK] ${result.skillName}`);
|
|
3559
4000
|
} catch (error) {
|
|
3560
|
-
const errorMessage =
|
|
4001
|
+
const errorMessage = getErrorMessage(error);
|
|
3561
4002
|
const dirBasename = path20.basename(skillPath);
|
|
3562
|
-
|
|
4003
|
+
warn(`Failed to compile skill from '${dirBasename}': ${errorMessage}`);
|
|
3563
4004
|
}
|
|
3564
4005
|
}
|
|
3565
4006
|
return results;
|
|
3566
4007
|
}
|
|
3567
4008
|
function printCompilationSummary(results) {
|
|
3568
|
-
|
|
4009
|
+
log(`
|
|
3569
4010
|
Compiled ${results.length} skill plugins:`);
|
|
3570
4011
|
for (const result of results) {
|
|
3571
|
-
|
|
4012
|
+
log(` - ${result.skillName} (v${result.manifest.version})`);
|
|
3572
4013
|
}
|
|
3573
4014
|
}
|
|
3574
4015
|
|
|
3575
4016
|
// src/cli/lib/skills/local-skill-loader.ts
|
|
3576
4017
|
init_esm_shims();
|
|
3577
|
-
import { parse as
|
|
4018
|
+
import { parse as parseYaml8 } from "yaml";
|
|
3578
4019
|
import path21 from "path";
|
|
3579
|
-
var LOCAL_CATEGORY = "local";
|
|
3580
|
-
var LOCAL_AUTHOR = "@local";
|
|
3581
4020
|
async function discoverLocalSkills(projectDir) {
|
|
3582
4021
|
const localSkillsPath = path21.join(projectDir, LOCAL_SKILLS_PATH);
|
|
3583
4022
|
if (!await directoryExists(localSkillsPath)) {
|
|
@@ -3600,8 +4039,8 @@ async function discoverLocalSkills(projectDir) {
|
|
|
3600
4039
|
}
|
|
3601
4040
|
async function extractLocalSkill(localSkillsPath, skillDirName) {
|
|
3602
4041
|
const skillDir = path21.join(localSkillsPath, skillDirName);
|
|
3603
|
-
const metadataPath = path21.join(skillDir,
|
|
3604
|
-
const skillMdPath = path21.join(skillDir,
|
|
4042
|
+
const metadataPath = path21.join(skillDir, STANDARD_FILES.METADATA_YAML);
|
|
4043
|
+
const skillMdPath = path21.join(skillDir, STANDARD_FILES.SKILL_MD);
|
|
3605
4044
|
if (!await fileExists(metadataPath)) {
|
|
3606
4045
|
verbose(`Skipping local skill '${skillDirName}': No metadata.yaml found`);
|
|
3607
4046
|
return null;
|
|
@@ -3611,30 +4050,32 @@ async function extractLocalSkill(localSkillsPath, skillDirName) {
|
|
|
3611
4050
|
return null;
|
|
3612
4051
|
}
|
|
3613
4052
|
const metadataContent = await readFile(metadataPath);
|
|
3614
|
-
const parsed = localRawMetadataSchema.safeParse(
|
|
4053
|
+
const parsed = localRawMetadataSchema.safeParse(parseYaml8(metadataContent));
|
|
3615
4054
|
if (!parsed.success) {
|
|
3616
4055
|
warn(
|
|
3617
|
-
`Skipping local skill '${skillDirName}':
|
|
4056
|
+
`Skipping local skill '${skillDirName}': invalid metadata.yaml \u2014 ${formatZodErrors(parsed.error.issues)}`
|
|
3618
4057
|
);
|
|
3619
4058
|
return null;
|
|
3620
4059
|
}
|
|
3621
4060
|
const metadata = parsed.data;
|
|
3622
4061
|
if (!metadata.cli_name) {
|
|
3623
|
-
warn(
|
|
4062
|
+
warn(
|
|
4063
|
+
`Skipping local skill '${skillDirName}': missing required '${METADATA_KEYS.CLI_NAME}' in metadata.yaml`
|
|
4064
|
+
);
|
|
3624
4065
|
return null;
|
|
3625
4066
|
}
|
|
3626
4067
|
const skillMdContent = await readFile(skillMdPath);
|
|
3627
4068
|
const frontmatter = parseFrontmatter(skillMdContent, skillMdPath);
|
|
3628
4069
|
if (!frontmatter) {
|
|
3629
|
-
warn(`Skipping local skill '${skillDirName}':
|
|
4070
|
+
warn(`Skipping local skill '${skillDirName}': invalid SKILL.md frontmatter`);
|
|
3630
4071
|
return null;
|
|
3631
4072
|
}
|
|
3632
4073
|
const relativePath = `${LOCAL_SKILLS_PATH}/${skillDirName}/`;
|
|
3633
4074
|
const skillId = frontmatter.name;
|
|
3634
|
-
const category = metadata.category ||
|
|
4075
|
+
const category = metadata.category || LOCAL_DEFAULTS.CATEGORY;
|
|
3635
4076
|
if (!metadata.category) {
|
|
3636
4077
|
warn(
|
|
3637
|
-
`Local skill '${skillDirName}' has no category in metadata.yaml \u2014 defaulting to '${
|
|
4078
|
+
`Local skill '${skillDirName}' has no category in metadata.yaml \u2014 defaulting to '${LOCAL_DEFAULTS.CATEGORY}' (will not appear in wizard domain views)`
|
|
3638
4079
|
);
|
|
3639
4080
|
}
|
|
3640
4081
|
const extracted = {
|
|
@@ -3644,7 +4085,7 @@ async function extractLocalSkill(localSkillsPath, skillDirName) {
|
|
|
3644
4085
|
usageGuidance: metadata.usage_guidance,
|
|
3645
4086
|
category,
|
|
3646
4087
|
categoryExclusive: metadata.category_exclusive ?? false,
|
|
3647
|
-
author:
|
|
4088
|
+
author: LOCAL_DEFAULTS.AUTHOR,
|
|
3648
4089
|
tags: metadata.tags ?? [],
|
|
3649
4090
|
compatibleWith: metadata.compatible_with ?? [],
|
|
3650
4091
|
conflictsWith: metadata.conflicts_with ?? [],
|
|
@@ -3662,38 +4103,65 @@ async function extractLocalSkill(localSkillsPath, skillDirName) {
|
|
|
3662
4103
|
// src/cli/lib/skills/source-switcher.ts
|
|
3663
4104
|
init_esm_shims();
|
|
3664
4105
|
import path22 from "path";
|
|
4106
|
+
function validateSkillId(skillId) {
|
|
4107
|
+
if (!SKILL_ID_PATTERN.test(skillId)) {
|
|
4108
|
+
return false;
|
|
4109
|
+
}
|
|
4110
|
+
return !(skillId.includes("\0") || skillId.includes("..") || skillId.includes("/") || skillId.includes("\\"));
|
|
4111
|
+
}
|
|
4112
|
+
function validatePathBoundary(resolvedPath, expectedParent) {
|
|
4113
|
+
const normalizedPath = path22.resolve(resolvedPath);
|
|
4114
|
+
const normalizedParent = path22.resolve(expectedParent);
|
|
4115
|
+
return normalizedPath.startsWith(normalizedParent + path22.sep);
|
|
4116
|
+
}
|
|
3665
4117
|
async function archiveLocalSkill(projectDir, skillId) {
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
4118
|
+
if (!validateSkillId(skillId)) {
|
|
4119
|
+
warn(`Invalid skill ID for archiving: '${skillId}'`);
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
4122
|
+
const skillsDir = path22.resolve(path22.join(projectDir, LOCAL_SKILLS_PATH));
|
|
4123
|
+
const skillPath = path22.resolve(path22.join(skillsDir, skillId));
|
|
4124
|
+
const archivedDir = path22.resolve(path22.join(skillsDir, ARCHIVED_SKILLS_DIR_NAME));
|
|
4125
|
+
const archivedSkillPath = path22.resolve(path22.join(archivedDir, skillId));
|
|
4126
|
+
if (!validatePathBoundary(skillPath, skillsDir) || !validatePathBoundary(archivedSkillPath, archivedDir)) {
|
|
4127
|
+
warn(`Skill ID '${skillId}' resolves outside the skills directory.`);
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
try {
|
|
4131
|
+
await ensureDir(archivedDir);
|
|
4132
|
+
await copy(skillPath, archivedSkillPath);
|
|
4133
|
+
await remove(skillPath);
|
|
4134
|
+
} catch (error) {
|
|
4135
|
+
warn(`Failed to archive skill '${skillId}': ${getErrorMessage(error)}`);
|
|
3671
4136
|
return;
|
|
3672
4137
|
}
|
|
3673
|
-
await ensureDir(archivedDir);
|
|
3674
|
-
await copy(skillPath, archivedSkillPath);
|
|
3675
|
-
await remove(skillPath);
|
|
3676
4138
|
verbose(`Archived local skill '${skillId}' to ${ARCHIVED_SKILLS_DIR_NAME}/`);
|
|
3677
4139
|
}
|
|
3678
4140
|
async function restoreArchivedSkill(projectDir, skillId) {
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
);
|
|
3686
|
-
|
|
4141
|
+
if (!validateSkillId(skillId)) {
|
|
4142
|
+
warn(`Invalid skill ID for restoring: '${skillId}'`);
|
|
4143
|
+
return false;
|
|
4144
|
+
}
|
|
4145
|
+
const skillsDir = path22.resolve(path22.join(projectDir, LOCAL_SKILLS_PATH));
|
|
4146
|
+
const skillPath = path22.resolve(path22.join(skillsDir, skillId));
|
|
4147
|
+
const archivedDir = path22.resolve(path22.join(skillsDir, ARCHIVED_SKILLS_DIR_NAME));
|
|
4148
|
+
const archivedSkillPath = path22.resolve(path22.join(archivedDir, skillId));
|
|
4149
|
+
if (!validatePathBoundary(skillPath, skillsDir) || !validatePathBoundary(archivedSkillPath, archivedDir)) {
|
|
4150
|
+
warn(`Skill ID '${skillId}' resolves outside the skills directory.`);
|
|
4151
|
+
return false;
|
|
4152
|
+
}
|
|
4153
|
+
try {
|
|
4154
|
+
await copy(archivedSkillPath, skillPath);
|
|
4155
|
+
await remove(archivedSkillPath);
|
|
4156
|
+
} catch {
|
|
3687
4157
|
return false;
|
|
3688
4158
|
}
|
|
3689
|
-
await copy(archivedSkillPath, skillPath);
|
|
3690
|
-
await remove(archivedSkillPath);
|
|
3691
4159
|
verbose(`Restored archived skill '${skillId}' from ${ARCHIVED_SKILLS_DIR_NAME}/`);
|
|
3692
4160
|
return true;
|
|
3693
4161
|
}
|
|
3694
4162
|
|
|
3695
4163
|
// src/cli/lib/configuration/config-generator.ts
|
|
3696
|
-
function
|
|
4164
|
+
function extractSubcategoryFromPath(categoryPath) {
|
|
3697
4165
|
if (categoryPath === "local") return void 0;
|
|
3698
4166
|
const parts = categoryPath.split("/");
|
|
3699
4167
|
return parts.length >= 2 ? parts[1] : parts[0];
|
|
@@ -3709,14 +4177,14 @@ function generateProjectConfigFromSkills(name, selectedSkillIds, matrix, options
|
|
|
3709
4177
|
const skillPath = skill.path;
|
|
3710
4178
|
const category = skill.category;
|
|
3711
4179
|
const agents = getAgentsForSkill(skillPath, category);
|
|
3712
|
-
const subcategory =
|
|
4180
|
+
const subcategory = extractSubcategoryFromPath(category);
|
|
3713
4181
|
for (const agentId of agents) {
|
|
3714
4182
|
neededAgents.add(agentId);
|
|
3715
4183
|
if (subcategory) {
|
|
3716
4184
|
if (!stackProperty[agentId]) {
|
|
3717
4185
|
stackProperty[agentId] = {};
|
|
3718
4186
|
}
|
|
3719
|
-
stackProperty[agentId][subcategory] = skillId;
|
|
4187
|
+
stackProperty[agentId][subcategory] = [{ id: skillId, preloaded: false }];
|
|
3720
4188
|
}
|
|
3721
4189
|
}
|
|
3722
4190
|
}
|
|
@@ -3736,23 +4204,18 @@ function generateProjectConfigFromSkills(name, selectedSkillIds, matrix, options
|
|
|
3736
4204
|
}
|
|
3737
4205
|
return config;
|
|
3738
4206
|
}
|
|
3739
|
-
function buildStackProperty(stack
|
|
4207
|
+
function buildStackProperty(stack) {
|
|
3740
4208
|
const result = {};
|
|
3741
4209
|
for (const [agentId, agentConfig] of typedEntries(stack.agents)) {
|
|
3742
4210
|
if (!agentConfig || Object.keys(agentConfig).length === 0) {
|
|
3743
4211
|
continue;
|
|
3744
4212
|
}
|
|
3745
4213
|
const resolvedMappings = {};
|
|
3746
|
-
for (const [subcategoryId,
|
|
4214
|
+
for (const [subcategoryId, assignments] of typedEntries(
|
|
3747
4215
|
agentConfig
|
|
3748
4216
|
)) {
|
|
3749
|
-
if (!
|
|
3750
|
-
|
|
3751
|
-
if (skillId) {
|
|
3752
|
-
resolvedMappings[subcategoryId] = skillId;
|
|
3753
|
-
} else {
|
|
3754
|
-
resolvedMappings[subcategoryId] = displayName;
|
|
3755
|
-
}
|
|
4217
|
+
if (!assignments || assignments.length === 0) continue;
|
|
4218
|
+
resolvedMappings[subcategoryId] = assignments;
|
|
3756
4219
|
}
|
|
3757
4220
|
if (Object.keys(resolvedMappings).length > 0) {
|
|
3758
4221
|
result[agentId] = resolvedMappings;
|
|
@@ -3760,6 +4223,33 @@ function buildStackProperty(stack, displayNameToId) {
|
|
|
3760
4223
|
}
|
|
3761
4224
|
return result;
|
|
3762
4225
|
}
|
|
4226
|
+
function compactStackForYaml(stack) {
|
|
4227
|
+
const result = {};
|
|
4228
|
+
for (const [agentId, agentConfig] of Object.entries(stack)) {
|
|
4229
|
+
const compacted = {};
|
|
4230
|
+
for (const [subcategory, assignments] of typedEntries(
|
|
4231
|
+
agentConfig
|
|
4232
|
+
)) {
|
|
4233
|
+
if (!assignments || assignments.length === 0) continue;
|
|
4234
|
+
if (assignments.length === 1) {
|
|
4235
|
+
const assignment = assignments[0];
|
|
4236
|
+
if (!assignment.preloaded) {
|
|
4237
|
+
compacted[subcategory] = assignment.id;
|
|
4238
|
+
} else {
|
|
4239
|
+
compacted[subcategory] = { id: assignment.id, preloaded: true };
|
|
4240
|
+
}
|
|
4241
|
+
} else {
|
|
4242
|
+
compacted[subcategory] = assignments.map(
|
|
4243
|
+
(a) => !a.preloaded ? a.id : { id: a.id, preloaded: true }
|
|
4244
|
+
);
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
if (Object.keys(compacted).length > 0) {
|
|
4248
|
+
result[agentId] = compacted;
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
return result;
|
|
4252
|
+
}
|
|
3763
4253
|
|
|
3764
4254
|
// src/cli/lib/configuration/config-merger.ts
|
|
3765
4255
|
init_esm_shims();
|
|
@@ -3768,7 +4258,6 @@ import { difference } from "remeda";
|
|
|
3768
4258
|
// src/cli/lib/configuration/project-config.ts
|
|
3769
4259
|
init_esm_shims();
|
|
3770
4260
|
import path23 from "path";
|
|
3771
|
-
import { parse as parseYaml11 } from "yaml";
|
|
3772
4261
|
var CONFIG_PATH = `${CLAUDE_SRC_DIR}/config.yaml`;
|
|
3773
4262
|
var LEGACY_CONFIG_PATH = `${CLAUDE_DIR}/config.yaml`;
|
|
3774
4263
|
async function loadProjectConfig(projectDir) {
|
|
@@ -3784,38 +4273,29 @@ async function loadProjectConfig(projectDir) {
|
|
|
3784
4273
|
return null;
|
|
3785
4274
|
}
|
|
3786
4275
|
}
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
if (!config.name) {
|
|
3801
|
-
warn(
|
|
3802
|
-
`Project config at ${configPath} is missing required 'name' field \u2014 defaulting to directory name`
|
|
3803
|
-
);
|
|
3804
|
-
config.name = path23.basename(projectDir);
|
|
3805
|
-
}
|
|
3806
|
-
if (!config.skills) {
|
|
3807
|
-
warn(`Project config at ${configPath} is missing 'skills' array \u2014 defaulting to empty`);
|
|
3808
|
-
config.skills = [];
|
|
3809
|
-
}
|
|
3810
|
-
verbose(`Loaded project config from ${configPath}`);
|
|
3811
|
-
return {
|
|
3812
|
-
config,
|
|
3813
|
-
configPath
|
|
3814
|
-
};
|
|
3815
|
-
} catch (error) {
|
|
3816
|
-
warn(`Failed to parse project config at ${configPath}: ${error}`);
|
|
3817
|
-
return null;
|
|
4276
|
+
const data = await safeLoadYamlFile(configPath, projectConfigLoaderSchema);
|
|
4277
|
+
if (!data) return null;
|
|
4278
|
+
const config = data;
|
|
4279
|
+
if (config.stack) {
|
|
4280
|
+
config.stack = normalizeStackRecord(
|
|
4281
|
+
config.stack
|
|
4282
|
+
);
|
|
4283
|
+
}
|
|
4284
|
+
if (!config.name) {
|
|
4285
|
+
warn(
|
|
4286
|
+
`Project config at '${configPath}' is missing required 'name' field \u2014 defaulting to directory name`
|
|
4287
|
+
);
|
|
4288
|
+
config.name = path23.basename(projectDir);
|
|
3818
4289
|
}
|
|
4290
|
+
if (!config.skills) {
|
|
4291
|
+
warn(`Project config at '${configPath}' is missing 'skills' array \u2014 defaulting to empty`);
|
|
4292
|
+
config.skills = [];
|
|
4293
|
+
}
|
|
4294
|
+
verbose(`Loaded project config from ${configPath}`);
|
|
4295
|
+
return {
|
|
4296
|
+
config,
|
|
4297
|
+
configPath
|
|
4298
|
+
};
|
|
3819
4299
|
}
|
|
3820
4300
|
function validateProjectConfig(config) {
|
|
3821
4301
|
const errors = [];
|
|
@@ -3899,42 +4379,22 @@ async function mergeWithExistingConfig(newConfig, context) {
|
|
|
3899
4379
|
|
|
3900
4380
|
// src/cli/lib/configuration/config-saver.ts
|
|
3901
4381
|
init_esm_shims();
|
|
3902
|
-
import path24 from "path";
|
|
3903
|
-
import { parse as parseYaml12, stringify as stringifyYaml5 } from "yaml";
|
|
3904
|
-
var YAML_INDENT2 = 2;
|
|
3905
4382
|
async function saveSourceToProjectConfig(projectDir, source) {
|
|
3906
|
-
const
|
|
3907
|
-
|
|
3908
|
-
if (await fileExists(configPath)) {
|
|
3909
|
-
const content = await readFile(configPath);
|
|
3910
|
-
try {
|
|
3911
|
-
const parsed = parseYaml12(content);
|
|
3912
|
-
const result = projectSourceConfigSchema.safeParse(parsed);
|
|
3913
|
-
config = result.success ? result.data : {};
|
|
3914
|
-
if (!result.success) {
|
|
3915
|
-
warn(
|
|
3916
|
-
`Invalid config at ${configPath}: ${result.error.issues.map((i) => i.message).join(", ")}. Starting with empty config.`
|
|
3917
|
-
);
|
|
3918
|
-
}
|
|
3919
|
-
} catch (error) {
|
|
3920
|
-
warn(
|
|
3921
|
-
`Failed to parse existing config at ${configPath}: ${error instanceof Error ? error.message : String(error)}. Starting with empty config.`
|
|
3922
|
-
);
|
|
3923
|
-
}
|
|
3924
|
-
}
|
|
3925
|
-
config.source = source;
|
|
3926
|
-
await ensureDir(path24.join(projectDir, CLAUDE_SRC_DIR));
|
|
3927
|
-
const configYaml = stringifyYaml5(config, { indent: YAML_INDENT2 });
|
|
3928
|
-
await writeFile(configPath, configYaml);
|
|
4383
|
+
const existing = await loadProjectSourceConfig(projectDir) ?? {};
|
|
4384
|
+
await saveProjectConfig(projectDir, { ...existing, source });
|
|
3929
4385
|
}
|
|
3930
4386
|
|
|
3931
4387
|
// src/cli/lib/loading/source-fetcher.ts
|
|
4388
|
+
var SAFE_NAME_PATTERN2 = /^[a-zA-Z0-9@._/ -]+$/;
|
|
4389
|
+
var MAX_NAME_LENGTH = 200;
|
|
3932
4390
|
function sanitizeSourceForCache(source) {
|
|
3933
|
-
|
|
4391
|
+
const hash = createHash2("sha256").update(source).digest("hex").slice(0, CACHE_HASH_LENGTH);
|
|
4392
|
+
const readable = source.replace(/[^a-zA-Z0-9]/g, "-").replace(/--+/g, "-").replace(/^-|-$/g, "").slice(0, CACHE_READABLE_PREFIX_LENGTH);
|
|
4393
|
+
return readable ? `${readable}-${hash}` : hash;
|
|
3934
4394
|
}
|
|
3935
4395
|
function getCacheDir(source) {
|
|
3936
|
-
const sanitized = sanitizeSourceForCache(source);
|
|
3937
|
-
return
|
|
4396
|
+
const sanitized = sanitizeSourceForCache(source) || "unknown";
|
|
4397
|
+
return path24.join(CACHE_DIR, "sources", sanitized);
|
|
3938
4398
|
}
|
|
3939
4399
|
async function fetchFromSource(source, options = {}) {
|
|
3940
4400
|
const { forceRefresh = false, subdir } = options;
|
|
@@ -3944,10 +4404,10 @@ async function fetchFromSource(source, options = {}) {
|
|
|
3944
4404
|
return fetchFromRemoteSource(source, { forceRefresh, subdir });
|
|
3945
4405
|
}
|
|
3946
4406
|
async function fetchFromLocalSource(source, subdir) {
|
|
3947
|
-
const fullPath = subdir ?
|
|
3948
|
-
const absolutePath =
|
|
4407
|
+
const fullPath = subdir ? path24.join(source, subdir) : source;
|
|
4408
|
+
const absolutePath = path24.isAbsolute(fullPath) ? fullPath : path24.resolve(process.cwd(), fullPath);
|
|
3949
4409
|
if (!await directoryExists(absolutePath)) {
|
|
3950
|
-
throw new Error(`Local source not found: ${absolutePath}`);
|
|
4410
|
+
throw new Error(`Local source not found: '${absolutePath}'`);
|
|
3951
4411
|
}
|
|
3952
4412
|
verbose(`Using local source: ${absolutePath}`);
|
|
3953
4413
|
return {
|
|
@@ -3970,7 +4430,7 @@ async function fetchFromRemoteSource(source, options) {
|
|
|
3970
4430
|
source: fullSource
|
|
3971
4431
|
};
|
|
3972
4432
|
}
|
|
3973
|
-
await ensureDir(
|
|
4433
|
+
await ensureDir(path24.dirname(cacheDir));
|
|
3974
4434
|
try {
|
|
3975
4435
|
const result = await downloadTemplate(fullSource, {
|
|
3976
4436
|
dir: cacheDir,
|
|
@@ -3985,11 +4445,11 @@ async function fetchFromRemoteSource(source, options) {
|
|
|
3985
4445
|
source: fullSource
|
|
3986
4446
|
};
|
|
3987
4447
|
} catch (error) {
|
|
3988
|
-
throw
|
|
4448
|
+
throw createDetailedFetchError(error, source);
|
|
3989
4449
|
}
|
|
3990
4450
|
}
|
|
3991
|
-
function
|
|
3992
|
-
const message =
|
|
4451
|
+
function createDetailedFetchError(error, source) {
|
|
4452
|
+
const message = getErrorMessage(error);
|
|
3993
4453
|
if (message.includes("404") || message.includes("Not Found")) {
|
|
3994
4454
|
return new Error(
|
|
3995
4455
|
`Repository not found: ${source}
|
|
@@ -4040,25 +4500,71 @@ async function fetchMarketplace(source, options = {}) {
|
|
|
4040
4500
|
subdir: ""
|
|
4041
4501
|
// Root of repo
|
|
4042
4502
|
});
|
|
4043
|
-
const marketplacePath =
|
|
4044
|
-
if (!await directoryExists(
|
|
4503
|
+
const marketplacePath = path24.join(result.path, ".claude-plugin", "marketplace.json");
|
|
4504
|
+
if (!await directoryExists(path24.dirname(marketplacePath))) {
|
|
4045
4505
|
throw new Error(
|
|
4046
|
-
`Marketplace not found
|
|
4506
|
+
`Marketplace not found for source: ${source}
|
|
4507
|
+
|
|
4508
|
+
The .claude-plugin/marketplace.json file is missing from this repository.
|
|
4047
4509
|
|
|
4048
|
-
|
|
4510
|
+
Possible causes:
|
|
4511
|
+
- The source URL may be incorrect
|
|
4512
|
+
- The repository may not have a marketplace configured
|
|
4513
|
+
|
|
4514
|
+
To create a marketplace, add a .claude-plugin/marketplace.json file to your source repository.`
|
|
4049
4515
|
);
|
|
4050
4516
|
}
|
|
4051
|
-
const content = await
|
|
4517
|
+
const content = await readFileSafe(marketplacePath, MAX_MARKETPLACE_FILE_SIZE);
|
|
4052
4518
|
const parsed = JSON.parse(content);
|
|
4519
|
+
if (!validateNestingDepth(parsed, MAX_JSON_NESTING_DEPTH)) {
|
|
4520
|
+
throw new Error(
|
|
4521
|
+
`Invalid marketplace.json at: ${marketplacePath}
|
|
4522
|
+
|
|
4523
|
+
JSON structure exceeds maximum nesting depth of ${MAX_JSON_NESTING_DEPTH}.`
|
|
4524
|
+
);
|
|
4525
|
+
}
|
|
4053
4526
|
const validation = marketplaceSchema.safeParse(parsed);
|
|
4054
4527
|
if (!validation.success) {
|
|
4055
4528
|
throw new Error(
|
|
4056
4529
|
`Invalid marketplace.json at: ${marketplacePath}
|
|
4057
4530
|
|
|
4058
|
-
Validation errors: ${validation.error.issues
|
|
4531
|
+
Validation errors: ${formatZodErrors(validation.error.issues)}`
|
|
4059
4532
|
);
|
|
4060
4533
|
}
|
|
4061
4534
|
const marketplace = validation.data;
|
|
4535
|
+
const EXPECTED_MARKETPLACE_KEYS = [
|
|
4536
|
+
"$schema",
|
|
4537
|
+
"name",
|
|
4538
|
+
"version",
|
|
4539
|
+
"description",
|
|
4540
|
+
"owner",
|
|
4541
|
+
"metadata",
|
|
4542
|
+
"plugins"
|
|
4543
|
+
];
|
|
4544
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
4545
|
+
warnUnknownFields(
|
|
4546
|
+
parsed,
|
|
4547
|
+
EXPECTED_MARKETPLACE_KEYS,
|
|
4548
|
+
"marketplace.json"
|
|
4549
|
+
);
|
|
4550
|
+
}
|
|
4551
|
+
if (marketplace.plugins.length > MAX_MARKETPLACE_PLUGINS) {
|
|
4552
|
+
throw new Error(
|
|
4553
|
+
`Invalid marketplace.json at: ${marketplacePath}
|
|
4554
|
+
|
|
4555
|
+
Too many plugins: ${marketplace.plugins.length} (limit: ${MAX_MARKETPLACE_PLUGINS}).`
|
|
4556
|
+
);
|
|
4557
|
+
}
|
|
4558
|
+
for (const plugin of marketplace.plugins) {
|
|
4559
|
+
if (plugin.name.length > MAX_NAME_LENGTH) {
|
|
4560
|
+
warn(
|
|
4561
|
+
`Marketplace plugin name too long (${plugin.name.length} chars): '${plugin.name.slice(0, 50)}...'`
|
|
4562
|
+
);
|
|
4563
|
+
}
|
|
4564
|
+
if (!SAFE_NAME_PATTERN2.test(plugin.name)) {
|
|
4565
|
+
warn(`Marketplace plugin name contains unsafe characters: '${plugin.name.slice(0, 50)}'`);
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4062
4568
|
verbose(`Loaded marketplace: ${marketplace.name} v${marketplace.version}`);
|
|
4063
4569
|
return {
|
|
4064
4570
|
marketplace,
|
|
@@ -4140,6 +4646,8 @@ export {
|
|
|
4140
4646
|
generateAgentPluginManifest,
|
|
4141
4647
|
writePluginManifest,
|
|
4142
4648
|
findPluginManifest,
|
|
4649
|
+
typedEntries,
|
|
4650
|
+
typedKeys,
|
|
4143
4651
|
getCollectivePluginDir,
|
|
4144
4652
|
getProjectPluginsDir,
|
|
4145
4653
|
getPluginSkillsDir,
|
|
@@ -4156,13 +4664,15 @@ export {
|
|
|
4156
4664
|
formatOrigin,
|
|
4157
4665
|
resolveAuthor,
|
|
4158
4666
|
resolveAllSources,
|
|
4667
|
+
IMPORT_DEFAULTS,
|
|
4668
|
+
LOCAL_DEFAULTS,
|
|
4159
4669
|
getCurrentDate,
|
|
4160
|
-
|
|
4161
|
-
|
|
4670
|
+
computeStringHash,
|
|
4671
|
+
computeFileHash,
|
|
4162
4672
|
determinePluginVersion,
|
|
4163
4673
|
writeContentHash,
|
|
4164
4674
|
readForkedFromMetadata,
|
|
4165
|
-
|
|
4675
|
+
compareLocalSkillsWithSource,
|
|
4166
4676
|
injectForkedFromMetadata,
|
|
4167
4677
|
copySkillsToPluginFromSource,
|
|
4168
4678
|
copySkillsToLocalFlattened,
|
|
@@ -4175,7 +4685,9 @@ export {
|
|
|
4175
4685
|
getAvailableSkills,
|
|
4176
4686
|
fetchFromSource,
|
|
4177
4687
|
searchExtraSources,
|
|
4688
|
+
normalizeStackRecord,
|
|
4178
4689
|
loadStacks,
|
|
4690
|
+
getStackSkillIds,
|
|
4179
4691
|
buildSkillRefsFromConfig,
|
|
4180
4692
|
resolveAgents,
|
|
4181
4693
|
extractFrontmatter,
|
|
@@ -4190,6 +4702,7 @@ export {
|
|
|
4190
4702
|
claudePluginUninstall,
|
|
4191
4703
|
installStackAsPlugin,
|
|
4192
4704
|
loadSkillsMatrixFromSource,
|
|
4705
|
+
getMarketplaceLabel,
|
|
4193
4706
|
compileSkillPlugin,
|
|
4194
4707
|
compileAllSkillPlugins,
|
|
4195
4708
|
printCompilationSummary,
|
|
@@ -4212,4 +4725,4 @@ export {
|
|
|
4212
4725
|
validateAllPlugins,
|
|
4213
4726
|
printPluginValidationResult
|
|
4214
4727
|
};
|
|
4215
|
-
//# sourceMappingURL=chunk-
|
|
4728
|
+
//# sourceMappingURL=chunk-SSHG7MEE.js.map
|