@kolatts/pncli 1.10.1 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/copilot-instructions.md +34 -1
  2. package/dist/{chunk-O3LWAWSN.js → chunk-AWSXVWRN.js} +2 -1
  3. package/dist/{chunk-O3LWAWSN.js.map → chunk-AWSXVWRN.js.map} +1 -1
  4. package/dist/{chunk-3XHTRFRX.js → chunk-VPUWMP2M.js} +3 -2
  5. package/dist/chunk-VPUWMP2M.js.map +1 -0
  6. package/dist/cli.js +376 -7
  7. package/dist/cli.js.map +1 -1
  8. package/dist/{config-NCI2BTQG.js → config-OFLPO5DN.js} +4 -2
  9. package/dist/{http-L2P23H2D.js → http-LNDRNPP7.js} +2 -2
  10. package/package.json +1 -1
  11. package/skills/pncli/SKILL.md +92 -0
  12. package/skills/pncli/ado.md +33 -0
  13. package/skills/pncli/artifactory.md +35 -0
  14. package/skills/pncli/bitbucket.md +30 -0
  15. package/skills/pncli/checkmarx.md +31 -0
  16. package/skills/pncli/confluence.md +24 -0
  17. package/skills/pncli/contrast.md +39 -0
  18. package/skills/pncli/jenkins.md +29 -0
  19. package/skills/pncli/jira.md +35 -0
  20. package/skills/pncli/marketplace.md +41 -0
  21. package/skills/pncli/sde.md +29 -0
  22. package/skills/pncli/servicenow.md +33 -0
  23. package/skills/pncli/sonarqube.md +30 -0
  24. package/skills/pncli/sonatypeiq.md +39 -0
  25. package/skills/pncli/udeploy.md +48 -0
  26. package/dist/chunk-3XHTRFRX.js.map +0 -1
  27. package/skills/address-pr-feedback/SKILL.md +0 -87
  28. package/skills/code-review/SKILL.md +0 -82
  29. package/skills/local-setup/SKILL.md +0 -225
  30. package/skills/plan/SKILL.md +0 -120
  31. package/skills/security-review/SKILL.md +0 -108
  32. package/skills/ship/SKILL.md +0 -170
  33. /package/dist/{config-NCI2BTQG.js.map → config-OFLPO5DN.js.map} +0 -0
  34. /package/dist/{http-L2P23H2D.js.map → http-LNDRNPP7.js.map} +0 -0
package/dist/cli.js CHANGED
@@ -2,12 +2,13 @@
2
2
  import {
3
3
  getGlobalConfigPath,
4
4
  loadConfig,
5
+ loadJsonFile,
5
6
  maskConfig,
6
7
  setConfigValue,
7
8
  setRepoConfigValue,
8
9
  writeGlobalConfig,
9
10
  writeRepoConfig
10
- } from "./chunk-O3LWAWSN.js";
11
+ } from "./chunk-AWSXVWRN.js";
11
12
  import {
12
13
  ExitCode,
13
14
  PncliError,
@@ -18,7 +19,7 @@ import {
18
19
  setGlobalUser,
19
20
  success,
20
21
  warn
21
- } from "./chunk-3XHTRFRX.js";
22
+ } from "./chunk-VPUWMP2M.js";
22
23
  import {
23
24
  AdoCoreClient
24
25
  } from "./chunk-VDL32PSG.js";
@@ -161,6 +162,201 @@ function getBranches(root) {
161
162
  }
162
163
  return { current, local, remote };
163
164
  }
165
+ var COMMIT_PREFIX = "__PNCLI_COMMIT__";
166
+ function getWeekMonday(dateStr) {
167
+ const d = new Date(dateStr);
168
+ const day = d.getUTCDay();
169
+ const diff = day === 0 ? -6 : 1 - day;
170
+ const monday = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + diff));
171
+ return monday.toISOString().slice(0, 10);
172
+ }
173
+ function getWeekSunday(mondayStr) {
174
+ const d = new Date(mondayStr);
175
+ const sunday = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 6));
176
+ return sunday.toISOString().slice(0, 10);
177
+ }
178
+ function getISOWeekLabel(mondayStr) {
179
+ const d = new Date(mondayStr);
180
+ const thursday = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 3));
181
+ const year = thursday.getUTCFullYear();
182
+ const jan4 = new Date(Date.UTC(year, 0, 4));
183
+ const jan4Monday = new Date(Date.UTC(year, 0, 4 - (jan4.getUTCDay() === 0 ? 6 : jan4.getUTCDay() - 1)));
184
+ const weekNum = Math.round((d.getTime() - jan4Monday.getTime()) / (7 * 864e5)) + 1;
185
+ return `${year}-W${String(weekNum).padStart(2, "0")}`;
186
+ }
187
+ function buildWeeklyBreakdown(commits) {
188
+ const weekMap = /* @__PURE__ */ new Map();
189
+ for (const commit of commits) {
190
+ const monday = getWeekMonday(commit.date);
191
+ if (!weekMap.has(monday)) weekMap.set(monday, /* @__PURE__ */ new Map());
192
+ const authorMap = weekMap.get(monday);
193
+ if (!authorMap.has(commit.author)) {
194
+ authorMap.set(commit.author, {
195
+ author: commit.author,
196
+ commitCount: 0,
197
+ insertions: 0,
198
+ deletions: 0,
199
+ netLines: 0,
200
+ filesChanged: 0
201
+ });
202
+ }
203
+ const stats = authorMap.get(commit.author);
204
+ stats.commitCount++;
205
+ stats.insertions += commit.insertions;
206
+ stats.deletions += commit.deletions;
207
+ stats.netLines += commit.netLines;
208
+ stats.filesChanged += commit.filesChanged;
209
+ }
210
+ return [...weekMap.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([monday, authorMap]) => {
211
+ const authors = [...authorMap.values()].sort((a, b) => a.author.localeCompare(b.author));
212
+ return {
213
+ week: getISOWeekLabel(monday),
214
+ weekStart: monday,
215
+ weekEnd: getWeekSunday(monday),
216
+ authors,
217
+ summary: {
218
+ commitCount: authors.reduce((s, a) => s + a.commitCount, 0),
219
+ totalInsertions: authors.reduce((s, a) => s + a.insertions, 0),
220
+ totalDeletions: authors.reduce((s, a) => s + a.deletions, 0),
221
+ totalNetLines: authors.reduce((s, a) => s + a.netLines, 0),
222
+ totalFilesChanged: authors.reduce((s, a) => s + a.filesChanged, 0)
223
+ }
224
+ };
225
+ });
226
+ }
227
+ function buildByAuthor(commits) {
228
+ const map = /* @__PURE__ */ new Map();
229
+ for (const commit of commits) {
230
+ if (!map.has(commit.author)) {
231
+ map.set(commit.author, {
232
+ author: commit.author,
233
+ commitCount: 0,
234
+ insertions: 0,
235
+ deletions: 0,
236
+ netLines: 0,
237
+ filesChanged: 0
238
+ });
239
+ }
240
+ const stats = map.get(commit.author);
241
+ stats.commitCount++;
242
+ stats.insertions += commit.insertions;
243
+ stats.deletions += commit.deletions;
244
+ stats.netLines += commit.netLines;
245
+ stats.filesChanged += commit.filesChanged;
246
+ }
247
+ return [...map.values()].sort((a, b) => a.author.localeCompare(b.author));
248
+ }
249
+ function parseBranchReportOutput(raw, branchName, opts) {
250
+ const commits = [];
251
+ let current = null;
252
+ for (const line of raw.split("\n")) {
253
+ if (line.startsWith(COMMIT_PREFIX)) {
254
+ if (current) commits.push(current);
255
+ const parts = line.slice(COMMIT_PREFIX.length).split(" ");
256
+ current = {
257
+ hash: parts[0] ?? "",
258
+ author: parts[1] ?? "",
259
+ date: parts[2] ?? "",
260
+ message: parts[3] ?? "",
261
+ filesChanged: 0,
262
+ insertions: 0,
263
+ deletions: 0,
264
+ netLines: 0
265
+ };
266
+ } else if (current && /^(\d+|-)\t(\d+|-)/.test(line)) {
267
+ const [ins, del] = line.split(" ");
268
+ const insertions = ins === "-" ? 0 : parseInt(ins ?? "0", 10);
269
+ const deletions = del === "-" ? 0 : parseInt(del ?? "0", 10);
270
+ if (!isNaN(insertions) && !isNaN(deletions)) {
271
+ current.insertions += insertions;
272
+ current.deletions += deletions;
273
+ current.filesChanged++;
274
+ current.netLines = current.insertions - current.deletions;
275
+ }
276
+ }
277
+ }
278
+ if (current) commits.push(current);
279
+ const totalInsertions = commits.reduce((sum, c) => sum + c.insertions, 0);
280
+ const totalDeletions = commits.reduce((sum, c) => sum + c.deletions, 0);
281
+ return {
282
+ branch: branchName,
283
+ since: opts.since ?? null,
284
+ until: opts.until ?? null,
285
+ commits,
286
+ weeks: buildWeeklyBreakdown(commits),
287
+ byAuthor: buildByAuthor(commits),
288
+ summary: {
289
+ commitCount: commits.length,
290
+ totalInsertions,
291
+ totalDeletions,
292
+ totalNetLines: totalInsertions - totalDeletions,
293
+ totalFilesChanged: commits.reduce((sum, c) => sum + c.filesChanged, 0)
294
+ }
295
+ };
296
+ }
297
+ function buildReportArgs(opts) {
298
+ const fmt = `${COMMIT_PREFIX}%H %an %aI %s`;
299
+ const args = ["log", `--format=${fmt}`, "--numstat"];
300
+ if (opts.since) args.push(`--since=${opts.since}`);
301
+ if (opts.until) args.push(`--until=${opts.until}`);
302
+ if (opts.base && opts.branch) {
303
+ args.push(`${opts.base}..${opts.branch}`);
304
+ } else if (opts.base) {
305
+ args.push(`${opts.base}..HEAD`);
306
+ } else if (opts.branch) {
307
+ args.push(opts.branch);
308
+ }
309
+ return args;
310
+ }
311
+ function getBranchReport(root, opts) {
312
+ const args = buildReportArgs(opts);
313
+ let raw = "";
314
+ try {
315
+ raw = execFileSync("git", args, { encoding: "utf8", cwd: root }).trim();
316
+ } catch {
317
+ }
318
+ let branchName = opts.branch ?? "";
319
+ if (!branchName) {
320
+ try {
321
+ branchName = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
322
+ encoding: "utf8",
323
+ cwd: root
324
+ }).trim();
325
+ } catch {
326
+ branchName = "HEAD";
327
+ }
328
+ }
329
+ return parseBranchReportOutput(raw, branchName, opts);
330
+ }
331
+ function escapeCsvField(value) {
332
+ if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
333
+ return `"${value.replace(/"/g, '""')}"`;
334
+ }
335
+ return value;
336
+ }
337
+ function formatBranchReportCsv(report) {
338
+ const header = "week,week_start,author,commits,insertions,deletions,net_lines,files_changed";
339
+ const rows = [];
340
+ for (const week of report.weeks) {
341
+ for (const a of week.authors) {
342
+ rows.push(
343
+ [
344
+ week.week,
345
+ week.weekStart,
346
+ escapeCsvField(a.author),
347
+ a.commitCount,
348
+ a.insertions,
349
+ a.deletions,
350
+ a.netLines,
351
+ a.filesChanged
352
+ ].join(",")
353
+ );
354
+ }
355
+ }
356
+ const s = report.summary;
357
+ const totalRow = ["TOTAL", "", "", s.commitCount, s.totalInsertions, s.totalDeletions, s.totalNetLines, s.totalFilesChanged].join(",");
358
+ return [header, ...rows, totalRow].join("\n") + "\n";
359
+ }
164
360
 
165
361
  // src/lib/git-context.ts
166
362
  import { execSync as execSync2 } from "child_process";
@@ -529,6 +725,25 @@ function registerGitCommands(program2) {
529
725
  fail(err, "git", "branch", start);
530
726
  }
531
727
  });
728
+ git.command("report").description("Report lines of code and commit counts for a branch, optionally filtered by date").option("--branch <name>", "Branch to report on (defaults to current branch)").option("--base <ref>", 'Base ref to compare against, e.g. "main" (base..branch, or base..HEAD if --branch is omitted)').option("--since <date>", 'Include commits on or after this date (e.g. "2024-01-01")').option("--until <date>", 'Include commits on or before this date (e.g. "2024-12-31")').option("--csv", "Output as CSV instead of JSON").action((opts) => {
729
+ const start = Date.now();
730
+ try {
731
+ const root = requireRepoRoot();
732
+ const report = getBranchReport(root, {
733
+ branch: opts.branch,
734
+ base: opts.base,
735
+ since: opts.since,
736
+ until: opts.until
737
+ });
738
+ if (opts.csv) {
739
+ process.stdout.write(formatBranchReportCsv(report));
740
+ } else {
741
+ success(report, "git", "report", start);
742
+ }
743
+ } catch (err) {
744
+ fail(err, "git", "report", start);
745
+ }
746
+ });
532
747
  git.command("current-pr").description("Find the open PR for the current branch").action(async () => {
533
748
  const start = Date.now();
534
749
  try {
@@ -4793,7 +5008,8 @@ async function initGlobalConfig(start) {
4793
5008
  if (useContrast) {
4794
5009
  process.stderr.write(" Find your credentials in Contrast under User Settings \u2192 Your Keys.\n");
4795
5010
  contrastBaseUrl = await input({
4796
- message: "Contrast base URL (leave blank for cloud: https://app.contrastsecurity.com):",
5011
+ message: "Contrast base URL (e.g. https://app.contrastsecurity.com for cloud SaaS):",
5012
+ validate: (v) => v.trim() ? true : "Base URL is required",
4797
5013
  default: ""
4798
5014
  });
4799
5015
  contrastOrgUuid = await input({
@@ -5854,11 +6070,11 @@ function registerAdoRepoCommands(ado) {
5854
6070
  const start = Date.now();
5855
6071
  try {
5856
6072
  const globalOpts = ado.optsWithGlobals();
5857
- const { loadConfig: lc } = await import("./config-NCI2BTQG.js");
6073
+ const { loadConfig: lc } = await import("./config-OFLPO5DN.js");
5858
6074
  const config = lc({ configPath: globalOpts.config });
5859
6075
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
5860
6076
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
5861
- const { createHttpClient: createHttpClient2 } = await import("./http-L2P23H2D.js");
6077
+ const { createHttpClient: createHttpClient2 } = await import("./http-LNDRNPP7.js");
5862
6078
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
5863
6079
  const coreClient = new AdoCoreClient2(http);
5864
6080
  const me = await coreClient.getConnectionData(collection);
@@ -6966,6 +7182,19 @@ import fs3 from "fs";
6966
7182
  import path5 from "path";
6967
7183
  import os from "os";
6968
7184
  import { fileURLToPath } from "url";
7185
+ import { execFileSync as execFileSync3 } from "child_process";
7186
+ import { select } from "@inquirer/prompts";
7187
+ function injectTokenIntoUrl(url, token) {
7188
+ let parsed;
7189
+ try {
7190
+ parsed = new URL(url);
7191
+ } catch {
7192
+ throw new Error(`--token requires an HTTPS clone URL; got: ${url}`);
7193
+ }
7194
+ parsed.username = "x-token-auth";
7195
+ parsed.password = token;
7196
+ return parsed.toString();
7197
+ }
6969
7198
  var AGENT_PATHS = {
6970
7199
  "github-copilot": { project: ".agents/skills", user: path5.join(os.homedir(), ".copilot/skills") },
6971
7200
  "claude-code": { project: ".claude/skills", user: path5.join(os.homedir(), ".claude/skills") }
@@ -7025,9 +7254,12 @@ function registerSkillsCommands(program2) {
7025
7254
  continue;
7026
7255
  }
7027
7256
  try {
7028
- const content = fs3.readFileSync(path5.join(bundledDir, skillName, "SKILL.md"), "utf8");
7257
+ const mdFiles = fs3.readdirSync(path5.join(bundledDir, skillName)).filter((f) => f.endsWith(".md"));
7029
7258
  fs3.mkdirSync(skillDir, { recursive: true });
7030
- fs3.writeFileSync(path5.join(skillDir, "SKILL.md"), content, "utf8");
7259
+ for (const mdFile of mdFiles) {
7260
+ const content = fs3.readFileSync(path5.join(bundledDir, skillName, mdFile), "utf8");
7261
+ fs3.writeFileSync(path5.join(skillDir, mdFile), content, "utf8");
7262
+ }
7031
7263
  installed.push(skillName);
7032
7264
  } catch {
7033
7265
  failed.push(skillName);
@@ -7111,6 +7343,143 @@ function registerSkillsCommands(program2) {
7111
7343
  fail(err, "skills", "list", start);
7112
7344
  }
7113
7345
  });
7346
+ const marketplace = skills.command("marketplace").description("Manage a git-hosted skills marketplace");
7347
+ marketplace.command("setup").description("Clone a skills marketplace repo and register it in global config").argument("<url>", "Git clone URL of the marketplace repository").argument("<localPath>", "Local directory to clone the marketplace into").option("--branch <branch>", "Branch to clone", "master").option("--token <token>", "Bitbucket HTTP access token for authenticated clone and pull").action(async (url, localPath, opts) => {
7348
+ const start = Date.now();
7349
+ try {
7350
+ const resolvedPath = path5.resolve(localPath);
7351
+ const hasGit = fs3.existsSync(path5.join(resolvedPath, ".git"));
7352
+ if (fs3.existsSync(resolvedPath) && !hasGit && fs3.readdirSync(resolvedPath).length > 0) {
7353
+ throw new Error(`Directory already exists and is not a git repo: ${resolvedPath}`);
7354
+ }
7355
+ if (hasGit) {
7356
+ warn(`Directory already contains a git repo at ${resolvedPath} \u2014 skipping clone, updating config only.`);
7357
+ } else {
7358
+ warn(`Cloning ${url} (branch: ${opts.branch}) \u2192 ${resolvedPath}...`);
7359
+ const cloneUrl = opts.token ? injectTokenIntoUrl(url, opts.token) : url;
7360
+ try {
7361
+ execFileSync3("git", ["clone", "--branch", opts.branch, cloneUrl, resolvedPath], { stdio: ["inherit", "inherit", "pipe"] });
7362
+ } catch (e) {
7363
+ const msg = e instanceof Error ? e.message : String(e);
7364
+ throw new Error(msg.replace(/x-token-auth:[^@]+@/g, "x-token-auth:***@"));
7365
+ }
7366
+ }
7367
+ const configPath = getGlobalConfigPath();
7368
+ const existing = loadJsonFile(configPath) ?? {};
7369
+ existing.marketplace = { repoUrl: url, localPath: resolvedPath, ...opts.token ? { token: opts.token } : {} };
7370
+ writeGlobalConfig(existing, configPath);
7371
+ success({ repoUrl: url, localPath: resolvedPath, branch: opts.branch, tokenConfigured: !!opts.token }, "skills", "marketplace-setup", start);
7372
+ } catch (err) {
7373
+ fail(err, "skills", "marketplace-setup", start);
7374
+ }
7375
+ });
7376
+ marketplace.command("sync").description("Pull latest marketplace content and install a plugin's skills").argument("[plugin]", "Plugin name to install (skips interactive selection)").option("--claude", "Install to ~/.claude/skills instead of ~/.agents/skills").action(async (plugin, opts) => {
7377
+ const start = Date.now();
7378
+ try {
7379
+ const configPath = getGlobalConfigPath();
7380
+ const globalConfig = loadJsonFile(configPath) ?? {};
7381
+ const marketplacePath = globalConfig.marketplace?.localPath;
7382
+ if (!marketplacePath || !fs3.existsSync(marketplacePath)) {
7383
+ throw new Error("Marketplace not configured. Run: pncli skills marketplace setup <url> <localPath>");
7384
+ }
7385
+ warn("Pulling latest marketplace content...");
7386
+ const repoUrl = globalConfig.marketplace?.repoUrl;
7387
+ const token = globalConfig.marketplace?.token;
7388
+ const gitArgs = ["-C", marketplacePath];
7389
+ if (repoUrl && token) {
7390
+ gitArgs.push("-c", `remote.origin.url=${injectTokenIntoUrl(repoUrl, token)}`);
7391
+ }
7392
+ gitArgs.push("pull");
7393
+ try {
7394
+ execFileSync3("git", gitArgs, { stdio: ["inherit", "inherit", "pipe"] });
7395
+ } catch (e) {
7396
+ const msg = e instanceof Error ? e.message : String(e);
7397
+ throw new Error(msg.replace(/x-token-auth:[^@]+@/g, "x-token-auth:***@"));
7398
+ }
7399
+ const pluginChoices = resolvePluginChoices(marketplacePath);
7400
+ if (pluginChoices.length === 0) {
7401
+ throw new Error("No plugins found in marketplace. Check the marketplace repository structure.");
7402
+ }
7403
+ let selectedPlugin;
7404
+ if (plugin) {
7405
+ if (!pluginChoices.some((p) => p.name === plugin)) {
7406
+ throw new Error(`Plugin "${plugin}" not found. Available: ${pluginChoices.map((p) => p.name).join(", ")}`);
7407
+ }
7408
+ selectedPlugin = plugin;
7409
+ } else {
7410
+ selectedPlugin = await select({
7411
+ message: "Select a plugin to install:",
7412
+ choices: pluginChoices.map((p) => ({
7413
+ value: p.name,
7414
+ name: p.description ? `${p.name} \u2014 ${p.description}` : p.name
7415
+ }))
7416
+ });
7417
+ }
7418
+ const skillsSrc = resolveSkillsSrc(marketplacePath, selectedPlugin);
7419
+ if (!fs3.existsSync(skillsSrc)) {
7420
+ throw new Error(`No skills directory found at ${skillsSrc}`);
7421
+ }
7422
+ const targetDir = opts.claude ? path5.join(os.homedir(), ".claude", "skills") : path5.join(os.homedir(), ".agents", "skills");
7423
+ const { installed, failed } = copyPluginSkills(skillsSrc, targetDir);
7424
+ if (failed.length > 0) {
7425
+ warn(`Skipped ${failed.length} skill(s) with invalid names: ${failed.join(", ")}`);
7426
+ }
7427
+ success({
7428
+ plugin: selectedPlugin,
7429
+ installed,
7430
+ failed,
7431
+ total: installed.length,
7432
+ target: targetDir
7433
+ }, "skills", "marketplace-sync", start);
7434
+ } catch (err) {
7435
+ fail(err, "skills", "marketplace-sync", start);
7436
+ }
7437
+ });
7438
+ }
7439
+ function resolvePluginChoices(marketplacePath) {
7440
+ const marketplaceJsonPath = path5.join(marketplacePath, ".claude-plugin", "marketplace.json");
7441
+ if (fs3.existsSync(marketplaceJsonPath)) {
7442
+ try {
7443
+ const meta = JSON.parse(fs3.readFileSync(marketplaceJsonPath, "utf8"));
7444
+ if (Array.isArray(meta.plugins) && meta.plugins.length > 0) {
7445
+ return meta.plugins.map((p) => ({ name: p.name, description: p.description ?? "" }));
7446
+ }
7447
+ } catch {
7448
+ }
7449
+ }
7450
+ const pluginsDir = path5.join(marketplacePath, "plugins");
7451
+ if (fs3.existsSync(pluginsDir)) {
7452
+ return fs3.readdirSync(pluginsDir).filter((name) => fs3.statSync(path5.join(pluginsDir, name)).isDirectory()).map((name) => ({ name, description: "" }));
7453
+ }
7454
+ return [];
7455
+ }
7456
+ function resolveSkillsSrc(marketplacePath, selectedPlugin) {
7457
+ const pluginsBase = path5.resolve(marketplacePath, "plugins");
7458
+ const skillsSrc = path5.resolve(pluginsBase, selectedPlugin, "skills");
7459
+ if (!skillsSrc.startsWith(pluginsBase + path5.sep)) {
7460
+ throw new Error(`Invalid plugin name: "${selectedPlugin}"`);
7461
+ }
7462
+ return skillsSrc;
7463
+ }
7464
+ function copyPluginSkills(skillsSrc, targetDir) {
7465
+ fs3.mkdirSync(targetDir, { recursive: true });
7466
+ const resolvedTarget = path5.resolve(targetDir);
7467
+ const skillNames = fs3.readdirSync(skillsSrc).filter(
7468
+ (name) => fs3.statSync(path5.join(skillsSrc, name)).isDirectory()
7469
+ );
7470
+ const installed = [];
7471
+ const failed = [];
7472
+ for (const skillName of skillNames) {
7473
+ const dest = path5.resolve(targetDir, skillName);
7474
+ if (!dest.startsWith(resolvedTarget + path5.sep)) {
7475
+ failed.push(skillName);
7476
+ continue;
7477
+ }
7478
+ fs3.rmSync(dest, { recursive: true, force: true });
7479
+ fs3.cpSync(path5.join(skillsSrc, skillName), dest, { recursive: true });
7480
+ installed.push(skillName);
7481
+ }
7482
+ return { installed, failed };
7114
7483
  }
7115
7484
 
7116
7485
  // src/cli.ts