MCPcopy Create free account
hub / github.com/QodeXcli/QodeX / copyInto

Function copyInto

src/skills/installer.ts:160–220  ·  view source on GitHub ↗
(srcRoot: string, originalSource: string, opts: { force?: boolean })

Source from the content-addressed store, hash-verified

158}
159
160async function copyInto(srcRoot: string, originalSource: string, opts: { force?: boolean }): Promise<InstallResult> {
161 const raw = await fs.readFile(path.join(srcRoot, 'SKILL.md'), 'utf-8');
162 // Parse to get the canonical name. If frontmatter has `name:`, prefer that;
163 // otherwise fall back to the directory name.
164 const fmName = extractFrontmatterName(raw);
165 const dirName = path.basename(srcRoot);
166 const name = (fmName ?? dirName).toLowerCase();
167 if (!NAME_RE.test(name)) {
168 throw new Error(`Skill name "${name}" is invalid. Names must match /^[a-z][a-z0-9-]*$/.`);
169 }
170 // Validate it parses cleanly before we commit
171 const parsed = parseSkill(raw, name, srcRoot, 'user');
172 if (!parsed) throw new Error('SKILL.md is missing a `description:` field.');
173
174 // Security scan BEFORE anything touches disk. A skill is instructions the agent
175 // reads and acts on, so a malicious one is close to running untrusted code:
176 // prompt injection, secret exfiltration, destructive shell, hidden unicode.
177 // `dangerous` findings hard-block even with --force; `suspicious` can be
178 // overridden with --force after the user reviews the report.
179 const scanText = await gatherSkillText(srcRoot);
180 const scan = scanSkillContent(scanText);
181 if (scan.severity !== 'clean') {
182 const report = formatScanReport(scan, name);
183 if (scan.severity === 'dangerous') {
184 throw new Error(
185 `${report}\n\nInstall blocked. Dangerous findings cannot be overridden with --force. ` +
186 `Inspect the skill source manually before trusting it.`,
187 );
188 }
189 if (!opts.force) {
190 throw new Error(
191 `${report}\n\nInstall paused. Review the skill, then re-run with --force to install anyway.`,
192 );
193 }
194 // suspicious + force → proceed, but the report was shown above by the caller path.
195 }
196
197 const target = path.join(userSkillsDir(), name);
198 try {
199 await fs.stat(target);
200 if (!opts.force) {
201 throw new Error(`Skill "${name}" is already installed at ${target}. Pass --force to overwrite, or use \`qodex skill remove ${name}\` first.`);
202 }
203 await fs.rm(target, { recursive: true, force: true });
204 } catch (e: any) {
205 // The ONLY case we swallow is a genuine "not installed yet": fs.stat threw
206 // ENOENT because the target dir doesn't exist. Everything else must
207 // propagate — the already-installed sentinel (so the caller sees it), and
208 // any real IO error including a failed fs.rm during --force overwrite.
209 // Swallowing an rm failure here would let us copy over a half-removed dir
210 // and produce a corrupt skill.
211 if (e?.code !== 'ENOENT') throw e;
212 }
213 await fs.mkdir(target, { recursive: true });
214 await copyDir(srcRoot, target);
215
216 // Stamp provenance into SKILL.md if not already present
217 await stampSource(path.join(target, 'SKILL.md'), originalSource);

Callers 3

installFromLocalFunction · 0.85
installFromGithubFunction · 0.85
installFromNpmFunction · 0.85

Calls 8

extractFrontmatterNameFunction · 0.85
parseSkillFunction · 0.85
gatherSkillTextFunction · 0.85
scanSkillContentFunction · 0.85
formatScanReportFunction · 0.85
userSkillsDirFunction · 0.85
copyDirFunction · 0.70
stampSourceFunction · 0.70

Tested by

no test coverage detected