@groupby/ai-dev 0.5.20 → 0.5.22

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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import fs4 from "fs";
5
- import path5 from "path";
4
+ import fs7 from "fs";
5
+ import path7 from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
  import { Command } from "commander";
8
8
 
@@ -11,10 +11,10 @@ import chalk from "chalk";
11
11
  import fg2 from "fast-glob";
12
12
 
13
13
  // src/lib/discovery.ts
14
- import path from "path";
14
+ import path2 from "path";
15
15
  import { fileURLToPath } from "url";
16
16
  import fg from "fast-glob";
17
- import fs from "fs-extra";
17
+ import fs2 from "fs-extra";
18
18
 
19
19
  // src/lib/frontmatter.ts
20
20
  import matter from "gray-matter";
@@ -32,44 +32,128 @@ See \`${aiDir}/skills/${skillName}/SKILL.md\` and follow closely.
32
32
  return matter.stringify(body, frontmatter);
33
33
  }
34
34
 
35
+ // src/lib/skill-meta.ts
36
+ import path from "path";
37
+ import fs from "fs-extra";
38
+ import matter2 from "gray-matter";
39
+ var SKILL_META_FILENAME = "skill-meta.yml";
40
+ var VALID_STATUSES = [
41
+ "evaluating",
42
+ "recommended",
43
+ "deprecated"
44
+ ];
45
+ var META_DELIMITER = "~~~skill-meta~~~";
46
+ function parseSkillMeta(yamlText) {
47
+ const parsed = matter2(
48
+ `${META_DELIMITER}
49
+ ${yamlText.trim()}
50
+ ${META_DELIMITER}
51
+ `,
52
+ { delimiters: META_DELIMITER }
53
+ );
54
+ return parsed.data;
55
+ }
56
+ var META_FIELD_ORDER = [
57
+ "source",
58
+ "upstreamName",
59
+ "pinnedCommit",
60
+ "vendoredOn",
61
+ "status",
62
+ "agents",
63
+ "reviewedBy",
64
+ "notes"
65
+ ];
66
+ function serializeSkillMeta(meta) {
67
+ const lines = [];
68
+ for (const key of META_FIELD_ORDER) {
69
+ const value = meta[key];
70
+ if (value === void 0 || value === null) continue;
71
+ if (key === "agents") {
72
+ const agents = value;
73
+ if (agents.length === 0) continue;
74
+ lines.push(`agents: [${agents.map(yamlScalar).join(", ")}]`);
75
+ continue;
76
+ }
77
+ if (typeof value === "string" && value.length === 0) continue;
78
+ lines.push(`${key}: ${yamlScalar(value)}`);
79
+ }
80
+ return `${lines.join("\n")}
81
+ `;
82
+ }
83
+ function yamlScalar(value) {
84
+ const text = String(value);
85
+ const isPlainToken = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(text);
86
+ const looksBoolOrNull = /^(true|false|null|yes|no|on|off)$/i.test(text);
87
+ const looksDate = /^\d{4}-\d{2}-\d{2}/.test(text);
88
+ const looksNumber = /^[-+]?(\d[\d_]*\.?\d*|\.\d+)([eE][-+]?\d+)?$/.test(text) || /^0x[0-9a-fA-F_]+$/.test(text) || /^0o[0-7_]+$/.test(text) || /^[-+]?\.(inf|nan)$/i.test(text);
89
+ if (isPlainToken && !looksBoolOrNull && !looksDate && !looksNumber) {
90
+ return text;
91
+ }
92
+ return JSON.stringify(text);
93
+ }
94
+ async function readSkillMeta(skillFolder) {
95
+ const metaPath = path.join(skillFolder, SKILL_META_FILENAME);
96
+ if (!await fs.pathExists(metaPath)) return void 0;
97
+ const text = await fs.readFile(metaPath, "utf-8");
98
+ try {
99
+ return parseSkillMeta(text);
100
+ } catch (error) {
101
+ const reason = error instanceof Error ? error.message : String(error);
102
+ console.warn(`Failed to parse ${metaPath}: ${reason}`);
103
+ return void 0;
104
+ }
105
+ }
106
+
35
107
  // src/lib/discovery.ts
36
- var __dirname = path.dirname(fileURLToPath(import.meta.url));
37
- var PACKAGE_ROOT = path.resolve(__dirname, "..");
108
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
109
+ var PACKAGE_ROOT = path2.resolve(__dirname, "..");
38
110
  async function discoverSkills() {
39
111
  const libraryPattern = "skills/library/*/SKILL.md";
40
112
  const teamPattern = "teams/*/skills/*/SKILL.md";
41
113
  const toolsetPattern = "toolsets/*/skills/*/SKILL.md";
42
- const matches = await fg([libraryPattern, teamPattern, toolsetPattern], {
43
- cwd: PACKAGE_ROOT,
44
- absolute: true
45
- });
114
+ const thirdPartyPattern = "teams/*/third-party/*/*/SKILL.md";
115
+ const matches = await fg(
116
+ [libraryPattern, teamPattern, toolsetPattern, thirdPartyPattern],
117
+ {
118
+ cwd: PACKAGE_ROOT,
119
+ absolute: true
120
+ }
121
+ );
46
122
  const skills = [];
47
123
  for (const match of matches) {
48
- const content = await fs.readFile(match, "utf-8");
124
+ const content = await fs2.readFile(match, "utf-8");
49
125
  const { data } = parseFrontmatter(content);
50
- const skillFolder = path.dirname(match);
51
- const relativePath = path.relative(PACKAGE_ROOT, match);
126
+ const skillFolder = path2.dirname(match);
127
+ const relativePath = path2.relative(PACKAGE_ROOT, match);
128
+ const parts = relativePath.split(path2.sep);
52
129
  let sourceType;
53
130
  let teamName;
54
131
  let toolsetName;
132
+ let lib;
133
+ let meta;
55
134
  if (relativePath.startsWith("skills/library/")) {
56
135
  sourceType = "library";
57
136
  } else if (relativePath.startsWith("toolsets/")) {
58
137
  sourceType = "toolset";
59
- const parts = relativePath.split(path.sep);
60
138
  toolsetName = parts[1];
139
+ } else if (parts[2] === "third-party") {
140
+ sourceType = "third-party";
141
+ teamName = parts[1];
142
+ lib = parts[3];
143
+ meta = await readSkillMeta(skillFolder);
61
144
  } else {
62
145
  sourceType = "team";
63
- const parts = relativePath.split(path.sep);
64
146
  teamName = parts[1];
65
147
  }
66
148
  skills.push({
67
- name: data.name || path.basename(skillFolder),
149
+ name: data.name || path2.basename(skillFolder),
68
150
  description: data.description || "",
69
151
  sourcePath: skillFolder,
70
152
  sourceType,
71
153
  teamName,
72
154
  toolsetName,
155
+ lib,
156
+ meta,
73
157
  frontmatter: data
74
158
  });
75
159
  }
@@ -83,13 +167,13 @@ async function discoverTeamAssetFolders() {
83
167
  });
84
168
  const assetFolders = [];
85
169
  for (const teamDir of teamDirs) {
86
- const teamName = path.basename(teamDir);
87
- const entries = await fs.readdir(teamDir, { withFileTypes: true });
170
+ const teamName = path2.basename(teamDir);
171
+ const entries = await fs2.readdir(teamDir, { withFileTypes: true });
88
172
  for (const entry of entries) {
89
- if (!entry.isDirectory() || entry.name === "skills") continue;
173
+ if (!entry.isDirectory() || entry.name === "skills" || entry.name === "third-party") continue;
90
174
  assetFolders.push({
91
175
  name: entry.name,
92
- sourcePath: path.join(teamDir, entry.name),
176
+ sourcePath: path2.join(teamDir, entry.name),
93
177
  teamName
94
178
  });
95
179
  }
@@ -107,13 +191,16 @@ async function discoverTeams() {
107
191
  const ensureTeam = (name) => {
108
192
  const existing = teamMap.get(name);
109
193
  if (existing) return existing;
110
- const team = { name, skills: [], assetFolders: [] };
194
+ const team = { name, skills: [], thirdPartySkills: [], assetFolders: [] };
111
195
  teamMap.set(name, team);
112
196
  return team;
113
197
  };
114
198
  for (const skill of skills) {
115
- if (skill.sourceType === "team" && skill.teamName) {
199
+ if (!skill.teamName) continue;
200
+ if (skill.sourceType === "team") {
116
201
  ensureTeam(skill.teamName).skills.push(skill);
202
+ } else if (skill.sourceType === "third-party") {
203
+ ensureTeam(skill.teamName).thirdPartySkills.push(skill);
117
204
  }
118
205
  }
119
206
  for (const folder of assetFolders) {
@@ -133,9 +220,9 @@ async function discoverToolsets() {
133
220
  }
134
221
  const toolsets = [];
135
222
  for (const [name, toolsetSkills] of toolsetMap) {
136
- const resourcesPath = path.join(PACKAGE_ROOT, "toolsets", name, "resources");
223
+ const resourcesPath = path2.join(PACKAGE_ROOT, "toolsets", name, "resources");
137
224
  let hasResources = false;
138
- if (await fs.pathExists(resourcesPath)) {
225
+ if (await fs2.pathExists(resourcesPath)) {
139
226
  const resourceFiles = await fg("**/*", {
140
227
  cwd: resourcesPath,
141
228
  onlyFiles: true
@@ -146,9 +233,26 @@ async function discoverToolsets() {
146
233
  }
147
234
  return toolsets;
148
235
  }
149
- async function findSkill(name) {
236
+ var SKILL_TIER_ORDER = [
237
+ "library",
238
+ "team",
239
+ "toolset",
240
+ "third-party"
241
+ ];
242
+ function resolveSkillMatches(matches) {
243
+ for (const tier of SKILL_TIER_ORDER) {
244
+ const inTier = matches.filter((m) => m.sourceType === tier);
245
+ if (inTier.length === 1) return { kind: "found", skill: inTier[0] };
246
+ if (inTier.length > 1) return { kind: "ambiguous", matches: inTier };
247
+ }
248
+ return { kind: "none" };
249
+ }
250
+ async function findSkillMatches(name, team) {
150
251
  const skills = await discoverSkills();
151
- return skills.find((s) => s.name === name && s.sourceType === "library") || skills.find((s) => s.name === name && s.sourceType === "team") || skills.find((s) => s.name === name);
252
+ const teamLc = team?.toLowerCase();
253
+ return skills.filter(
254
+ (s) => s.name === name && (!teamLc || s.teamName?.toLowerCase() === teamLc)
255
+ );
152
256
  }
153
257
  async function findToolset(name) {
154
258
  const toolsets = await discoverToolsets();
@@ -168,6 +272,9 @@ function isResourceFolder(folderName) {
168
272
  }
169
273
  function formatTeamContents(team) {
170
274
  const parts = [`${team.skills.length} skill${team.skills.length !== 1 ? "s" : ""}`];
275
+ if (team.thirdPartySkills.length > 0) {
276
+ parts.push(`${team.thirdPartySkills.length} third-party`);
277
+ }
171
278
  const promptCount = team.assetFolders.filter((folder) => isPromptFolder(folder.name)).length;
172
279
  const resourceCount = team.assetFolders.filter((folder) => isResourceFolder(folder.name)).length;
173
280
  const genericCount = team.assetFolders.length - promptCount - resourceCount;
@@ -207,6 +314,24 @@ function truncate(str, max) {
207
314
  if (str.length <= max) return str;
208
315
  return str.slice(0, max - 3) + "...";
209
316
  }
317
+ function formatStatus(status) {
318
+ switch (status) {
319
+ case "recommended":
320
+ return chalk.green("recommended");
321
+ case "evaluating":
322
+ return chalk.yellow("evaluating");
323
+ case "deprecated":
324
+ return chalk.red("deprecated");
325
+ default:
326
+ return chalk.dim("no status");
327
+ }
328
+ }
329
+ function formatProvenance(skill) {
330
+ const meta = skill.meta;
331
+ const sha = meta?.pinnedCommit ? meta.pinnedCommit.slice(0, 7) : "unpinned";
332
+ const source = meta?.source ? `${meta.source}@${sha}` : "unknown source";
333
+ return `[${formatStatus(meta?.status)}] ${chalk.dim(source)}`;
334
+ }
210
335
  async function countResourceFiles(resourcesPath) {
211
336
  const files = await fg2("**/*", {
212
337
  cwd: resourcesPath,
@@ -223,6 +348,7 @@ async function listSkills() {
223
348
  const librarySkills = skills.filter((s) => s.sourceType === "library");
224
349
  const teamSkills = skills.filter((s) => s.sourceType === "team");
225
350
  const toolsetSkills = skills.filter((s) => s.sourceType === "toolset");
351
+ const thirdPartySkills = skills.filter((s) => s.sourceType === "third-party");
226
352
  if (librarySkills.length > 0) {
227
353
  console.log(chalk.bold("\nLibrary"));
228
354
  for (const s of librarySkills) {
@@ -236,12 +362,27 @@ async function listSkills() {
236
362
  existing.push(s);
237
363
  teamMap.set(team, existing);
238
364
  }
239
- for (const [team, skillList] of teamMap) {
365
+ const thirdPartyMap = /* @__PURE__ */ new Map();
366
+ for (const s of thirdPartySkills) {
367
+ const team = s.teamName || "unknown";
368
+ const existing = thirdPartyMap.get(team) || [];
369
+ existing.push(s);
370
+ thirdPartyMap.set(team, existing);
371
+ }
372
+ const teamNames = /* @__PURE__ */ new Set([...teamMap.keys(), ...thirdPartyMap.keys()]);
373
+ for (const team of teamNames) {
240
374
  console.log(chalk.bold(`
241
375
  ${formatTeamName(team)}`));
242
- for (const s of skillList) {
376
+ for (const s of teamMap.get(team) || []) {
243
377
  console.log(` ${chalk.cyan(s.name.padEnd(24))} ${truncate(s.description, 60)}`);
244
378
  }
379
+ const teamThirdParty = thirdPartyMap.get(team) || [];
380
+ if (teamThirdParty.length > 0) {
381
+ console.log(` ${chalk.dim("third-party:")}`);
382
+ for (const s of teamThirdParty) {
383
+ console.log(` ${chalk.cyan(s.name.padEnd(22))} ${formatProvenance(s)}`);
384
+ }
385
+ }
245
386
  }
246
387
  const toolsetMap = /* @__PURE__ */ new Map();
247
388
  for (const s of toolsetSkills) {
@@ -273,6 +414,50 @@ async function listTeams() {
273
414
  }
274
415
  console.log();
275
416
  }
417
+ function formatTeamDetail(team) {
418
+ const lines = [];
419
+ lines.push(`${chalk.bold(formatTeamName(team.name))} ${chalk.dim(`(${formatTeamContents(team)})`)}`);
420
+ if (team.skills.length > 0) {
421
+ lines.push(chalk.bold("\n Skills"));
422
+ for (const s of team.skills) {
423
+ lines.push(` ${chalk.cyan(s.name.padEnd(24))} ${truncate(s.description, 56)}`);
424
+ }
425
+ }
426
+ if (team.thirdPartySkills.length > 0) {
427
+ lines.push(`${chalk.bold("\n Third-party skills")} ${chalk.dim("(vendored)")}`);
428
+ for (const s of team.thirdPartySkills) {
429
+ lines.push(` ${chalk.cyan(s.name.padEnd(24))} ${formatProvenance(s)}`);
430
+ if (s.description) {
431
+ lines.push(` ${" ".repeat(24)} ${chalk.dim(truncate(s.description, 56))}`);
432
+ }
433
+ }
434
+ }
435
+ if (team.assetFolders.length > 0) {
436
+ lines.push(chalk.bold("\n Content"));
437
+ for (const f of team.assetFolders) {
438
+ lines.push(` ${chalk.cyan(f.name)}`);
439
+ }
440
+ }
441
+ return lines;
442
+ }
443
+ async function listTeam(name) {
444
+ const teams = await discoverTeams();
445
+ const team = teams.find(
446
+ (t) => t.name === name || t.name.toLowerCase() === name.toLowerCase()
447
+ );
448
+ if (!team) {
449
+ console.log(chalk.yellow(`Team "${name}" not found.`));
450
+ if (teams.length > 0) {
451
+ console.log(`Available teams: ${teams.map((t) => t.name).join(", ")}`);
452
+ }
453
+ process.exit(1);
454
+ }
455
+ console.log();
456
+ for (const line of formatTeamDetail(team)) {
457
+ console.log(line);
458
+ }
459
+ console.log();
460
+ }
276
461
  async function listToolsets() {
277
462
  const toolsets = await discoverToolsets();
278
463
  if (toolsets.length === 0) {
@@ -320,17 +505,19 @@ function registerListCommand(program2) {
320
505
  const list = program2.command("list").description("List available skills, teams, and toolsets");
321
506
  list.command("skills").description("List all available skills").action(listSkills);
322
507
  list.command("teams").description("List teams with skill and content counts").action(listTeams);
508
+ list.command("team <name>").description("List one team's skills, vendored third-party skills, and content").action(listTeam);
323
509
  list.command("toolsets").description("List toolsets with skill and resource counts").action(listToolsets);
324
510
  list.action(listAll);
325
511
  }
326
512
 
327
513
  // src/commands/install.ts
328
- import path4 from "path";
514
+ import path5 from "path";
515
+ import fs5 from "fs-extra";
329
516
  import chalk3 from "chalk";
330
517
 
331
518
  // src/lib/clients.ts
332
- import path2 from "path";
333
- import fs2 from "fs-extra";
519
+ import path3 from "path";
520
+ import fs3 from "fs-extra";
334
521
  var ALL_CLIENTS = [
335
522
  { name: "Copilot", skillsDir: ".github/skills", detectDir: ".github" },
336
523
  { name: "Claude Code", skillsDir: ".claude/skills", detectDir: ".claude" },
@@ -339,32 +526,59 @@ var ALL_CLIENTS = [
339
526
  async function detectClients(targetDir) {
340
527
  const detected = [];
341
528
  for (const client of ALL_CLIENTS) {
342
- const dirPath = path2.join(targetDir, client.detectDir);
343
- if (await fs2.pathExists(dirPath)) {
529
+ const dirPath = path3.join(targetDir, client.detectDir);
530
+ if (await fs3.pathExists(dirPath)) {
344
531
  detected.push(client);
345
532
  }
346
533
  }
347
534
  return detected;
348
535
  }
536
+ var CLIENT_ALIASES = {
537
+ copilot: ALL_CLIENTS[0],
538
+ claude: ALL_CLIENTS[1],
539
+ "claude code": ALL_CLIENTS[1],
540
+ "claude-code": ALL_CLIENTS[1],
541
+ codex: ALL_CLIENTS[2]
542
+ };
543
+ function resolveClients(list) {
544
+ const tokens = list.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
545
+ if (tokens.includes("all")) return [...ALL_CLIENTS];
546
+ const result = [];
547
+ for (const token of tokens) {
548
+ const client = CLIENT_ALIASES[token];
549
+ if (!client) {
550
+ throw new Error(
551
+ `Unknown client "${token}". Valid values: copilot, claude, codex, all.`
552
+ );
553
+ }
554
+ if (!result.includes(client)) result.push(client);
555
+ }
556
+ if (result.length === 0) {
557
+ throw new Error(
558
+ "--clients requires at least one of: copilot, claude, codex, all."
559
+ );
560
+ }
561
+ return result;
562
+ }
349
563
 
350
564
  // src/lib/installer.ts
351
- import path3 from "path";
352
- import fs3 from "fs-extra";
565
+ import path4 from "path";
566
+ import fs4 from "fs-extra";
353
567
  import fg3 from "fast-glob";
354
568
  var DEFAULT_AI_DIR = "docs/ai";
355
569
  async function handleFile(srcPath, destPath, options, result, onConflict, contentOverride) {
356
- const relativeDest = path3.relative(options.targetDir, destPath);
357
- if (await fs3.pathExists(destPath)) {
358
- const existingContent = await fs3.readFile(destPath, "utf-8");
359
- const newContent = contentOverride ?? await fs3.readFile(srcPath, "utf-8");
570
+ const relativeDest = path4.relative(options.targetDir, destPath);
571
+ if (await fs4.pathExists(destPath)) {
572
+ const existingContent = await fs4.readFile(destPath, "utf-8");
573
+ const newContent = contentOverride ?? await fs4.readFile(srcPath, "utf-8");
360
574
  if (existingContent === newContent) {
361
575
  result.skipped.push(relativeDest);
362
576
  return;
363
577
  }
364
578
  if (options.force) {
365
579
  if (!options.dryRun) {
366
- await fs3.ensureDir(path3.dirname(destPath));
367
- await fs3.writeFile(destPath, newContent);
580
+ await fs4.ensureDir(path4.dirname(destPath));
581
+ await fs4.writeFile(destPath, newContent);
368
582
  }
369
583
  result.overwritten.push(relativeDest);
370
584
  } else if (options.skipExisting) {
@@ -373,8 +587,8 @@ async function handleFile(srcPath, destPath, options, result, onConflict, conten
373
587
  const choice = await onConflict(relativeDest);
374
588
  if (choice === "overwrite") {
375
589
  if (!options.dryRun) {
376
- await fs3.ensureDir(path3.dirname(destPath));
377
- await fs3.writeFile(destPath, newContent);
590
+ await fs4.ensureDir(path4.dirname(destPath));
591
+ await fs4.writeFile(destPath, newContent);
378
592
  }
379
593
  result.overwritten.push(relativeDest);
380
594
  } else {
@@ -385,11 +599,11 @@ async function handleFile(srcPath, destPath, options, result, onConflict, conten
385
599
  }
386
600
  } else {
387
601
  if (!options.dryRun) {
388
- await fs3.ensureDir(path3.dirname(destPath));
602
+ await fs4.ensureDir(path4.dirname(destPath));
389
603
  if (contentOverride) {
390
- await fs3.writeFile(destPath, contentOverride);
604
+ await fs4.writeFile(destPath, contentOverride);
391
605
  } else {
392
- await fs3.copy(srcPath, destPath);
606
+ await fs4.copy(srcPath, destPath);
393
607
  }
394
608
  }
395
609
  result.created.push(relativeDest);
@@ -398,7 +612,7 @@ async function handleFile(srcPath, destPath, options, result, onConflict, conten
398
612
  async function patchContent(srcPath, aiDir) {
399
613
  if (aiDir === DEFAULT_AI_DIR) return void 0;
400
614
  if (!srcPath.endsWith(".md")) return void 0;
401
- const content = await fs3.readFile(srcPath, "utf-8");
615
+ const content = await fs4.readFile(srcPath, "utf-8");
402
616
  return content.replaceAll(`${DEFAULT_AI_DIR}/`, `${aiDir}/`);
403
617
  }
404
618
  async function installDirectoryFiles(sourceDir, destDir, options, result, onConflict) {
@@ -408,8 +622,8 @@ async function installDirectoryFiles(sourceDir, destDir, options, result, onConf
408
622
  dot: true
409
623
  });
410
624
  for (const file of files) {
411
- const srcFile = path3.join(sourceDir, file);
412
- const destFile = path3.join(destDir, file);
625
+ const srcFile = path4.join(sourceDir, file);
626
+ const destFile = path4.join(destDir, file);
413
627
  const contentOverride = await patchContent(srcFile, options.aiDir);
414
628
  await handleFile(srcFile, destFile, options, result, onConflict, contentOverride);
415
629
  }
@@ -421,19 +635,19 @@ async function installSkill(skill, options, onConflict) {
421
635
  skipped: [],
422
636
  overwritten: []
423
637
  };
424
- const destFolder = path3.join(options.targetDir, options.aiDir, "skills", skill.name);
425
- const sourceFiles = await fs3.readdir(skill.sourcePath);
638
+ const destFolder = path4.join(options.targetDir, options.aiDir, "skills", skill.name);
639
+ const sourceFiles = await fs4.readdir(skill.sourcePath);
426
640
  for (const file of sourceFiles) {
427
- const srcFile = path3.join(skill.sourcePath, file);
428
- const stat = await fs3.stat(srcFile);
641
+ const srcFile = path4.join(skill.sourcePath, file);
642
+ const stat = await fs4.stat(srcFile);
429
643
  if (stat.isFile()) {
430
- const destFile = path3.join(destFolder, file);
644
+ const destFile = path4.join(destFolder, file);
431
645
  const contentOverride = await patchContent(srcFile, options.aiDir);
432
646
  await handleFile(srcFile, destFile, options, result, onConflict, contentOverride);
433
647
  }
434
648
  }
435
649
  for (const client of options.clients) {
436
- const stubPath = path3.join(
650
+ const stubPath = path4.join(
437
651
  options.targetDir,
438
652
  client.skillsDir,
439
653
  skill.name,
@@ -461,7 +675,7 @@ async function installTeamAssetFolders(assetFolders, options, onConflict) {
461
675
  skipped: [],
462
676
  overwritten: []
463
677
  };
464
- const destFolder = path3.join(options.targetDir, options.aiDir, folder.name);
678
+ const destFolder = path4.join(options.targetDir, options.aiDir, folder.name);
465
679
  await installDirectoryFiles(folder.sourcePath, destFolder, options, result, onConflict);
466
680
  results.push(result);
467
681
  }
@@ -475,7 +689,7 @@ async function installResources(toolset, options, onConflict) {
475
689
  overwritten: []
476
690
  };
477
691
  if (!toolset.hasResources) return result;
478
- const destDir = path3.join(options.targetDir, options.aiDir, "resources");
692
+ const destDir = path4.join(options.targetDir, options.aiDir, "resources");
479
693
  const fileResult = { created: [], skipped: [], overwritten: [] };
480
694
  await installDirectoryFiles(toolset.resourcesPath, destDir, options, fileResult, onConflict);
481
695
  result.created = fileResult.created;
@@ -653,12 +867,137 @@ function formatTeamName2(name) {
653
867
  // src/commands/install.ts
654
868
  function normalizeAiDir(raw) {
655
869
  const trimmed = raw.replace(/\/+$/, "") || DEFAULT_AI_DIR;
656
- if (path4.isAbsolute(trimmed)) {
870
+ if (path5.isAbsolute(trimmed)) {
657
871
  console.log(chalk3.red(`--ai-dir must be a relative path, got: ${trimmed}`));
658
872
  process.exit(1);
659
873
  }
660
874
  return trimmed;
661
875
  }
876
+ async function resolveClientSelection(parentOpts, targetDir, force) {
877
+ if (parentOpts.clients) {
878
+ try {
879
+ return resolveClients(String(parentOpts.clients));
880
+ } catch (err) {
881
+ console.log(chalk3.red(err.message));
882
+ process.exit(1);
883
+ }
884
+ }
885
+ const detected = await detectClients(targetDir);
886
+ if (force || !process.stdin.isTTY) {
887
+ const fallback = detected.length > 0 ? detected : ALL_CLIENTS;
888
+ if (!force) {
889
+ console.log(
890
+ chalk3.dim(
891
+ `Non-interactive terminal: writing stubs for ${fallback.map((c) => c.name).join(", ")}. Pass --clients to choose explicitly.`
892
+ )
893
+ );
894
+ }
895
+ return fallback;
896
+ }
897
+ return promptSelectClients(ALL_CLIENTS, detected);
898
+ }
899
+ function gateSkillStatus(skill, opts) {
900
+ if (skill.sourceType !== "third-party") return { allowed: true };
901
+ const status = skill.meta?.status;
902
+ switch (status) {
903
+ case "recommended":
904
+ return { allowed: true };
905
+ case "evaluating":
906
+ return opts.includeEvaluating ? { allowed: true } : {
907
+ allowed: false,
908
+ message: `Third-party skill "${skill.name}" is evaluating. Re-run with --include-evaluating to install it.`
909
+ };
910
+ case "deprecated":
911
+ return opts.includeDeprecated ? { allowed: true } : {
912
+ allowed: false,
913
+ message: `Third-party skill "${skill.name}" is deprecated. Re-run with --include-deprecated to install it.`
914
+ };
915
+ default:
916
+ return {
917
+ allowed: false,
918
+ message: `Third-party skill "${skill.name}" has no valid status in skill-meta.yml; refusing to install.`
919
+ };
920
+ }
921
+ }
922
+ function describeExplicitSkillStatus(skill) {
923
+ if (skill.sourceType !== "third-party") return void 0;
924
+ switch (skill.meta?.status) {
925
+ case "recommended":
926
+ return void 0;
927
+ case "evaluating":
928
+ return chalk3.yellow(`Note: "${skill.name}" is a third-party skill still under evaluation.`);
929
+ case "deprecated":
930
+ return chalk3.yellow(`Note: "${skill.name}" is a deprecated third-party skill.`);
931
+ default:
932
+ return chalk3.yellow(`Note: "${skill.name}" is a third-party skill with no recorded status.`);
933
+ }
934
+ }
935
+ function canonicalSkillPath(targetDir, aiDir, skillName) {
936
+ return path5.join(targetDir, aiDir, "skills", skillName);
937
+ }
938
+ function planTeamThirdPartySkills(skills, opts) {
939
+ const toInstall = [];
940
+ const evaluatingRefused = [];
941
+ const deprecated = [];
942
+ for (const skill of skills) {
943
+ const gate = gateSkillStatus(skill, opts);
944
+ if (gate.allowed) {
945
+ toInstall.push(skill);
946
+ continue;
947
+ }
948
+ const status = skill.meta?.status;
949
+ if (status === "evaluating") {
950
+ evaluatingRefused.push(skill);
951
+ } else if (status === "deprecated") {
952
+ deprecated.push(skill);
953
+ }
954
+ }
955
+ return { toInstall, evaluatingRefused, deprecated };
956
+ }
957
+ async function resolveTeamThirdPartyNotices(plan, options) {
958
+ const evaluating = [];
959
+ for (const skill of plan.evaluatingRefused) {
960
+ const alreadyInstalled = await fs5.pathExists(
961
+ canonicalSkillPath(options.targetDir, options.aiDir, skill.name)
962
+ );
963
+ evaluating.push({ skill, alreadyInstalled });
964
+ }
965
+ const deprecatedPresent = [];
966
+ for (const skill of plan.deprecated) {
967
+ if (await fs5.pathExists(
968
+ canonicalSkillPath(options.targetDir, options.aiDir, skill.name)
969
+ )) {
970
+ deprecatedPresent.push(skill);
971
+ }
972
+ }
973
+ return { evaluating, deprecatedPresent };
974
+ }
975
+ function printTeamThirdPartyNotices(notices, teamName, aiDir) {
976
+ if (notices.evaluating.length > 0) {
977
+ console.log(chalk3.bold("Third-party skills in evaluation (not installed by default)"));
978
+ for (const { skill, alreadyInstalled } of notices.evaluating) {
979
+ const state = alreadyInstalled ? chalk3.dim("installed, not updated (re-run with --include-evaluating)") : chalk3.dim("not installed");
980
+ console.log(` ${chalk3.yellow(skill.name.padEnd(24))} ${state}`);
981
+ }
982
+ console.log(`
983
+ These skills are still being evaluated. To install them and help evaluate, run:`);
984
+ console.log(chalk3.cyan(` npx @groupby/ai-dev install team ${teamName} --include-evaluating`));
985
+ console.log();
986
+ }
987
+ if (notices.deprecatedPresent.length > 0) {
988
+ console.log(chalk3.yellow("Deprecated third-party skills"));
989
+ for (const skill of notices.deprecatedPresent) {
990
+ const canonicalRel = path5.join(aiDir, "skills", skill.name);
991
+ const stubPaths = ALL_CLIENTS.map((c) => path5.join(c.skillsDir, skill.name));
992
+ console.log(` ${chalk3.yellow(skill.name)} is now deprecated and was not updated.`);
993
+ console.log(` To stop agents from using it, remove these folders:`);
994
+ for (const p of [canonicalRel, ...stubPaths]) {
995
+ console.log(` ${chalk3.cyan(p)}`);
996
+ }
997
+ }
998
+ console.log();
999
+ }
1000
+ }
662
1001
  function formatFileCounts(result, options) {
663
1002
  const createdWord = options.dryRun ? "would create" : "created";
664
1003
  const skippedWord = "up to date";
@@ -722,29 +1061,81 @@ ${prefix}${chalk3.bold("Install complete")}
722
1061
  console.log(` Clients: ${options.clients.map((c) => c.name).join(", ") || chalk3.dim("none")}`);
723
1062
  console.log();
724
1063
  }
725
- async function installSkillCmd(name, _opts, cmd) {
1064
+ function skillScopeLabel(skill) {
1065
+ return skill.teamName ?? skill.toolsetName ?? "library";
1066
+ }
1067
+ function describeSkillOrigin(skill) {
1068
+ if (skill.sourceType === "third-party") {
1069
+ const meta = skill.meta;
1070
+ const sha = meta?.pinnedCommit ? meta.pinnedCommit.slice(0, 7) : "unpinned";
1071
+ const source = meta?.source ? `${meta.source}@${sha}` : "unknown source";
1072
+ const status = meta?.status ?? "no status";
1073
+ const vendored = meta?.vendoredOn ? `, vendored ${meta.vendoredOn}` : "";
1074
+ return `third-party [${status}] ${source}${vendored}`;
1075
+ }
1076
+ if (skill.sourceType === "toolset") return `toolset ${skill.toolsetName}`;
1077
+ if (skill.sourceType === "library") return "library skill";
1078
+ return "team skill";
1079
+ }
1080
+ function parseSkillRef(raw) {
1081
+ const slash = raw.indexOf("/");
1082
+ if (slash === -1) return { name: raw };
1083
+ return { team: raw.slice(0, slash) || void 0, name: raw.slice(slash + 1) };
1084
+ }
1085
+ async function installSkillCmd(name, opts, cmd) {
726
1086
  const parentOpts = cmd.parent.opts();
727
1087
  const force = Boolean(parentOpts.force);
728
1088
  const skipExisting = Boolean(parentOpts.skipExisting);
729
1089
  const dryRun = Boolean(parentOpts.dryRun);
730
- const targetDir = path4.resolve(
1090
+ const targetDir = path5.resolve(
731
1091
  parentOpts.target || process.cwd()
732
1092
  );
733
1093
  const aiDir = normalizeAiDir(parentOpts.aiDir || DEFAULT_AI_DIR);
734
- const skill = await findSkill(name);
735
- if (!skill) {
1094
+ const ref = parseSkillRef(name);
1095
+ const teamOpt = typeof opts.team === "string" ? opts.team : void 0;
1096
+ if (ref.team && teamOpt && ref.team.toLowerCase() !== teamOpt.toLowerCase()) {
1097
+ console.log(
1098
+ chalk3.red(
1099
+ `Conflicting team scope: "${ref.team}" in the name vs "${teamOpt}" from --team.`
1100
+ )
1101
+ );
1102
+ process.exit(1);
1103
+ }
1104
+ const teamScope = ref.team ?? teamOpt;
1105
+ const skillName = ref.name;
1106
+ const matches = await findSkillMatches(skillName, teamScope);
1107
+ const resolution = resolveSkillMatches(matches);
1108
+ if (resolution.kind === "none") {
736
1109
  const all = await discoverSkills();
737
- console.log(chalk3.red(`Skill "${name}" not found.`));
1110
+ console.log(
1111
+ chalk3.red(
1112
+ teamScope ? `Skill "${skillName}" not found in team "${teamScope}".` : `Skill "${skillName}" not found.`
1113
+ )
1114
+ );
738
1115
  console.log(`Available skills: ${all.map((s) => s.name).join(", ")}`);
739
1116
  process.exit(1);
740
1117
  }
741
- const detected = await detectClients(targetDir);
742
- let clients;
743
- if (force) {
744
- clients = detected.length > 0 ? detected : ALL_CLIENTS;
745
- } else {
746
- clients = await promptSelectClients(ALL_CLIENTS, detected);
1118
+ if (resolution.kind === "ambiguous") {
1119
+ console.log(
1120
+ chalk3.red(
1121
+ `Skill "${skillName}" is ambiguous \u2014 ${resolution.matches.length} skills match:`
1122
+ )
1123
+ );
1124
+ for (const m of resolution.matches) {
1125
+ console.log(
1126
+ ` ${chalk3.cyan(`${skillScopeLabel(m)}/${m.name}`)} ${chalk3.dim(describeSkillOrigin(m))}`
1127
+ );
1128
+ }
1129
+ const example = resolution.matches[0];
1130
+ console.log("Disambiguate with a team scope, e.g.:");
1131
+ console.log(` ai-dev install skill ${skillScopeLabel(example)}/${example.name}`);
1132
+ console.log(` ai-dev install skill ${example.name} --team ${skillScopeLabel(example)}`);
1133
+ process.exit(1);
747
1134
  }
1135
+ const skill = resolution.skill;
1136
+ const statusNotice = describeExplicitSkillStatus(skill);
1137
+ if (statusNotice) console.log(statusNotice);
1138
+ const clients = await resolveClientSelection(parentOpts, targetDir, force);
748
1139
  const options = {
749
1140
  targetDir,
750
1141
  aiDir,
@@ -763,7 +1154,7 @@ async function installTeamCmd(name, _opts, cmd) {
763
1154
  const force = Boolean(parentOpts.force);
764
1155
  const skipExisting = Boolean(parentOpts.skipExisting);
765
1156
  const dryRun = Boolean(parentOpts.dryRun);
766
- const targetDir = path4.resolve(
1157
+ const targetDir = path5.resolve(
767
1158
  parentOpts.target || process.cwd()
768
1159
  );
769
1160
  const aiDir = normalizeAiDir(parentOpts.aiDir || DEFAULT_AI_DIR);
@@ -777,19 +1168,19 @@ async function installTeamCmd(name, _opts, cmd) {
777
1168
  process.exit(1);
778
1169
  }
779
1170
  let skillsToInstall = [...team.skills];
780
- const includeLibrary = await promptIncludeLibrary();
1171
+ const thirdPartyPlan = planTeamThirdPartySkills(team.thirdPartySkills, {
1172
+ includeEvaluating: Boolean(parentOpts.includeEvaluating),
1173
+ includeDeprecated: Boolean(parentOpts.includeDeprecated)
1174
+ });
1175
+ skillsToInstall.push(...thirdPartyPlan.toInstall);
1176
+ const nonInteractive = Boolean(parentOpts.clients) || force || !process.stdin.isTTY;
1177
+ const includeLibrary = parentOpts.includeLibrary ? true : nonInteractive ? false : await promptIncludeLibrary();
781
1178
  if (includeLibrary) {
782
1179
  const allSkills = await discoverSkills();
783
1180
  const librarySkills = allSkills.filter((s) => s.sourceType === "library");
784
1181
  skillsToInstall.push(...librarySkills);
785
1182
  }
786
- const detected = await detectClients(targetDir);
787
- let clients;
788
- if (force) {
789
- clients = detected.length > 0 ? detected : ALL_CLIENTS;
790
- } else {
791
- clients = await promptSelectClients(ALL_CLIENTS, detected);
792
- }
1183
+ const clients = await resolveClientSelection(parentOpts, targetDir, force);
793
1184
  const options = {
794
1185
  targetDir,
795
1186
  aiDir,
@@ -810,13 +1201,15 @@ async function installTeamCmd(name, _opts, cmd) {
810
1201
  conflictHandler
811
1202
  );
812
1203
  printResults(results, options, [], assetResults);
1204
+ const notices = await resolveTeamThirdPartyNotices(thirdPartyPlan, options);
1205
+ printTeamThirdPartyNotices(notices, team.name, options.aiDir);
813
1206
  }
814
1207
  async function installToolsetCmd(name, _opts, cmd) {
815
1208
  const parentOpts = cmd.parent.opts();
816
1209
  const force = Boolean(parentOpts.force);
817
1210
  const skipExisting = Boolean(parentOpts.skipExisting);
818
1211
  const dryRun = Boolean(parentOpts.dryRun);
819
- const targetDir = path4.resolve(
1212
+ const targetDir = path5.resolve(
820
1213
  parentOpts.target || process.cwd()
821
1214
  );
822
1215
  const aiDir = normalizeAiDir(parentOpts.aiDir || DEFAULT_AI_DIR);
@@ -829,13 +1222,7 @@ async function installToolsetCmd(name, _opts, cmd) {
829
1222
  }
830
1223
  process.exit(1);
831
1224
  }
832
- const detected = await detectClients(targetDir);
833
- let clients;
834
- if (force) {
835
- clients = detected.length > 0 ? detected : ALL_CLIENTS;
836
- } else {
837
- clients = await promptSelectClients(ALL_CLIENTS, detected);
838
- }
1225
+ const clients = await resolveClientSelection(parentOpts, targetDir, force);
839
1226
  const options = {
840
1227
  targetDir,
841
1228
  aiDir,
@@ -854,41 +1241,407 @@ async function installToolsetCmd(name, _opts, cmd) {
854
1241
  }
855
1242
  var INHERITED_OPTIONS_HELP = `
856
1243
  Parent options (pass before subcommand):
857
- --force Overwrite existing files without prompting
858
- --skip-existing Skip files that already exist without prompting
859
- --dry-run Show what would be installed without writing files
860
- --target <dir> Install to a different directory (default: CWD)
861
- --ai-dir <path> Override the AI content directory (default: docs/ai)`;
1244
+ --force Overwrite existing files without prompting
1245
+ --skip-existing Skip files that already exist without prompting
1246
+ --dry-run Show what would be installed without writing files
1247
+ --include-evaluating Include evaluating third-party skills in a team install (a named skill always installs)
1248
+ --include-deprecated Include deprecated third-party skills in a team install (a named skill always installs)
1249
+ --clients <list> Client stubs to write non-interactively: copilot,claude,codex, or all
1250
+ --include-library Include shared library skills without prompting (use with --clients)
1251
+ --target <dir> Install to a different directory (default: CWD)
1252
+ --ai-dir <path> Override the AI content directory (default: docs/ai)`;
862
1253
  function registerInstallCommand(program2) {
863
- const install = program2.command("install").description("Install AI content").option("--force", "Overwrite existing files without prompting").option("--skip-existing", "Skip files that already exist without prompting").option("--dry-run", "Show what would be installed without writing files").option("--target <dir>", "Install to a different directory (default: CWD)").option("--ai-dir <path>", "Override the AI content directory (default: docs/ai)");
864
- install.command("skill <name>").description("Install a specific skill").addHelpText("after", INHERITED_OPTIONS_HELP).action(installSkillCmd);
1254
+ const install = program2.command("install").description("Install AI content").option("--force", "Overwrite existing files without prompting").option("--skip-existing", "Skip files that already exist without prompting").option("--dry-run", "Show what would be installed without writing files").option("--include-evaluating", "Include evaluating third-party skills in a team install (a named skill always installs)").option("--include-deprecated", "Include deprecated third-party skills in a team install (a named skill always installs)").option("--clients <list>", "Client stubs to write non-interactively: copilot,claude,codex, or all").option("--include-library", "Include shared library skills without prompting (use with --clients)").option("--target <dir>", "Install to a different directory (default: CWD)").option("--ai-dir <path>", "Override the AI content directory (default: docs/ai)");
1255
+ install.command("skill <name>").description("Install a specific skill (use <team>/<name> or --team to disambiguate)").option("--team <team>", "Scope the skill to a specific team when the name is ambiguous").addHelpText("after", INHERITED_OPTIONS_HELP).action(installSkillCmd);
865
1256
  install.command("team <name>").description("Install all skills, prompts, and content folders for a team").addHelpText("after", INHERITED_OPTIONS_HELP).action(installTeamCmd);
866
1257
  install.command("toolset <name>").description("Install all skills and resources for a toolset").addHelpText("after", INHERITED_OPTIONS_HELP).action(installToolsetCmd);
867
1258
  }
868
1259
 
1260
+ // src/commands/vendor.ts
1261
+ import { execFileSync } from "child_process";
1262
+ import chalk4 from "chalk";
1263
+
1264
+ // src/lib/vendor.ts
1265
+ import path6 from "path";
1266
+ import fs6 from "fs-extra";
1267
+
1268
+ // src/lib/github.ts
1269
+ var URL_HOST_ALLOWLIST = /* @__PURE__ */ new Set(["github.com", "www.github.com"]);
1270
+ var FETCH_HOST_ALLOWLIST = /* @__PURE__ */ new Set(["api.github.com", "raw.githubusercontent.com"]);
1271
+ var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
1272
+ var SHA_RE = /^[0-9a-f]{40}$/i;
1273
+ function parseGitHubUrl(input2) {
1274
+ let url;
1275
+ try {
1276
+ url = new URL(input2);
1277
+ } catch {
1278
+ throw new Error(`Invalid URL: ${input2}`);
1279
+ }
1280
+ if (url.protocol !== "https:") {
1281
+ throw new Error(`Only https:// GitHub URLs are supported (got ${url.protocol}).`);
1282
+ }
1283
+ if (!URL_HOST_ALLOWLIST.has(url.hostname)) {
1284
+ throw new Error(`Only github.com URLs are supported (got ${url.hostname}).`);
1285
+ }
1286
+ const segments = url.pathname.split("/").filter(Boolean);
1287
+ if (segments.length < 5) {
1288
+ throw new Error(
1289
+ "URL must point to a skill folder: https://github.com/<owner>/<repo>/tree/<ref>/<path>/<skill>"
1290
+ );
1291
+ }
1292
+ const [owner, repo, kind, ref, ...rest] = segments;
1293
+ if (kind !== "tree" && kind !== "blob") {
1294
+ throw new Error(`URL must contain /tree/ or /blob/ (got /${kind}/).`);
1295
+ }
1296
+ let pathSegments = rest;
1297
+ if (kind === "blob") {
1298
+ if (pathSegments[pathSegments.length - 1] !== "SKILL.md") {
1299
+ throw new Error("Blob URLs must point to a SKILL.md file.");
1300
+ }
1301
+ pathSegments = pathSegments.slice(0, -1);
1302
+ }
1303
+ if (pathSegments.length === 0) {
1304
+ throw new Error("URL must include a skill folder path after the ref.");
1305
+ }
1306
+ for (const seg of [owner, repo, ref, ...pathSegments]) {
1307
+ if (!SEGMENT_RE.test(seg)) {
1308
+ throw new Error(`Unsafe path segment "${seg}" in URL.`);
1309
+ }
1310
+ }
1311
+ return {
1312
+ owner,
1313
+ repo,
1314
+ ref,
1315
+ path: pathSegments.join("/"),
1316
+ skill: pathSegments[pathSegments.length - 1],
1317
+ source: `${owner}/${repo}`
1318
+ };
1319
+ }
1320
+ async function resolveCommitSha(parsed, token) {
1321
+ if (SHA_RE.test(parsed.ref)) {
1322
+ return parsed.ref.toLowerCase();
1323
+ }
1324
+ const { owner, repo, ref, path: path8 } = parsed;
1325
+ const commitsUrl = `https://api.github.com/repos/${owner}/${repo}/commits?path=${encodePath(path8)}&sha=${encodeURIComponent(ref)}&per_page=1`;
1326
+ const commits = await fetchJson(commitsUrl, token, { allowNotFound: true });
1327
+ if (Array.isArray(commits) && typeof commits[0]?.sha === "string" && SHA_RE.test(commits[0].sha)) {
1328
+ return commits[0].sha;
1329
+ }
1330
+ for (const refType of ["heads", "tags"]) {
1331
+ const refUrl = `https://api.github.com/repos/${owner}/${repo}/git/refs/${refType}/${encodeURIComponent(ref)}`;
1332
+ const data = await fetchJson(refUrl, token, { allowNotFound: true });
1333
+ const sha = data?.object?.sha;
1334
+ if (typeof sha === "string" && SHA_RE.test(sha)) return sha;
1335
+ }
1336
+ throw new Error(`Could not resolve "${ref}" to a commit SHA in ${owner}/${repo}.`);
1337
+ }
1338
+ async function fetchFolderContents(parsed, sha, subPath, token) {
1339
+ const folderPath = subPath ? `${parsed.path}/${subPath}` : parsed.path;
1340
+ const url = `https://api.github.com/repos/${parsed.owner}/${parsed.repo}/contents/${encodePath(folderPath)}?ref=${encodeURIComponent(sha)}`;
1341
+ const data = await fetchJson(url, token);
1342
+ if (!Array.isArray(data)) {
1343
+ throw new Error(`Expected a folder at ${folderPath}, but got a single file.`);
1344
+ }
1345
+ return data.map((entry) => ({
1346
+ name: String(entry.name),
1347
+ type: entry.type === "dir" ? "dir" : "file",
1348
+ path: String(entry.path),
1349
+ downloadUrl: typeof entry.download_url === "string" ? entry.download_url : null
1350
+ }));
1351
+ }
1352
+ async function fetchFileText(url, token) {
1353
+ assertAllowedHost(url);
1354
+ const res = await fetch(url, { headers: buildHeaders(token, "text"), redirect: "error" });
1355
+ if (!res.ok) {
1356
+ throw new Error(`Fetch failed (${res.status}) for ${url}`);
1357
+ }
1358
+ const text = await res.text();
1359
+ if (text.includes("\0")) {
1360
+ throw new Error(`Refusing to vendor binary content from ${url}`);
1361
+ }
1362
+ return text;
1363
+ }
1364
+ async function fetchJson(url, token, opts = {}) {
1365
+ assertAllowedHost(url);
1366
+ const res = await fetch(url, { headers: buildHeaders(token, "json"), redirect: "error" });
1367
+ if (res.status === 404 && opts.allowNotFound) return void 0;
1368
+ if (!res.ok) {
1369
+ throw new Error(`GitHub API request failed (${res.status}) for ${url}`);
1370
+ }
1371
+ return res.json();
1372
+ }
1373
+ function buildHeaders(token, kind) {
1374
+ const headers = {
1375
+ "User-Agent": "groupby-ai-dev-vendor",
1376
+ Accept: kind === "json" ? "application/vnd.github+json" : "text/plain"
1377
+ };
1378
+ if (token) headers.Authorization = `Bearer ${token}`;
1379
+ return headers;
1380
+ }
1381
+ function assertAllowedHost(rawUrl) {
1382
+ let host;
1383
+ try {
1384
+ host = new URL(rawUrl).hostname;
1385
+ } catch {
1386
+ throw new Error(`Invalid fetch URL: ${rawUrl}`);
1387
+ }
1388
+ if (!FETCH_HOST_ALLOWLIST.has(host)) {
1389
+ throw new Error(`Refusing to fetch from disallowed host "${host}".`);
1390
+ }
1391
+ }
1392
+ function encodePath(p) {
1393
+ return p.split("/").map(encodeURIComponent).join("/");
1394
+ }
1395
+
1396
+ // src/lib/vendor.ts
1397
+ var SAFE_NAME_RE = /^[A-Za-z0-9._-]+$/;
1398
+ var AGENT_FILE_CLIENT = {
1399
+ openai: "codex",
1400
+ codex: "codex",
1401
+ anthropic: "claude",
1402
+ claude: "claude",
1403
+ copilot: "copilot",
1404
+ github: "copilot"
1405
+ };
1406
+ async function vendorSkill(options) {
1407
+ const parsed = parseGitHubUrl(options.url);
1408
+ const skill = options.as ?? parsed.skill;
1409
+ if (!SAFE_NAME_RE.test(skill)) {
1410
+ throw new Error(`Unsafe skill name "${skill}".`);
1411
+ }
1412
+ const lib = options.lib ?? parsed.owner;
1413
+ if (!SAFE_NAME_RE.test(lib)) {
1414
+ throw new Error(`Unsafe lib name "${lib}".`);
1415
+ }
1416
+ const status = options.status ?? "evaluating";
1417
+ if (!SAFE_NAME_RE.test(options.team)) {
1418
+ throw new Error(`Unsafe team name "${options.team}".`);
1419
+ }
1420
+ const repoRoot = options.repoRoot ?? await findRepoRoot(process.cwd());
1421
+ const teamDir = path6.join(repoRoot, "teams", options.team);
1422
+ if (!await fs6.pathExists(teamDir)) {
1423
+ throw new Error(`Team "${options.team}" not found at teams/${options.team}.`);
1424
+ }
1425
+ const relativeFolder = path6.join("teams", options.team, "third-party", lib, skill);
1426
+ const skillFolder = path6.join(repoRoot, relativeFolder);
1427
+ const exists = await fs6.pathExists(skillFolder);
1428
+ if (exists && !options.update) {
1429
+ throw new Error(`${relativeFolder} already exists. Pass --update to re-vendor it.`);
1430
+ }
1431
+ if (!exists && options.update) {
1432
+ throw new Error(`${relativeFolder} does not exist. Run without --update to create it.`);
1433
+ }
1434
+ const sha = await resolveCommitSha(parsed, options.token);
1435
+ const entries = await fetchFolderContents(parsed, sha, void 0, options.token);
1436
+ const skillEntry = entries.find((e) => e.type === "file" && e.name === "SKILL.md");
1437
+ if (!skillEntry?.downloadUrl) {
1438
+ throw new Error(`No SKILL.md found in ${parsed.path}.`);
1439
+ }
1440
+ const skillMd = await fetchFileText(skillEntry.downloadUrl, options.token);
1441
+ const { data } = parseFrontmatter(skillMd);
1442
+ if (data.name !== skill) {
1443
+ throw new Error(
1444
+ `SKILL.md frontmatter name "${data.name}" does not match folder "${skill}". Use --as ${data.name} to match, or rename the target.`
1445
+ );
1446
+ }
1447
+ const { agents, droppedAgentsFiles } = await captureAgents(parsed, sha, entries, options.token);
1448
+ const warnings = collectActivationWarnings(data);
1449
+ const existingMeta = exists ? await readSkillMeta(skillFolder) : void 0;
1450
+ const meta = buildMeta({
1451
+ parsed,
1452
+ skill,
1453
+ sha,
1454
+ status,
1455
+ agents,
1456
+ droppedAgentsFiles,
1457
+ reviewedBy: options.reviewedBy,
1458
+ defaultReviewedBy: options.defaultReviewedBy,
1459
+ vendoredOn: options.vendoredOn ?? today(),
1460
+ existing: existingMeta,
1461
+ isUpdate: Boolean(options.update),
1462
+ statusExplicit: options.status !== void 0,
1463
+ reviewedByExplicit: options.reviewedBy !== void 0
1464
+ });
1465
+ let wrote = false;
1466
+ if (!options.dryRun) {
1467
+ await fs6.ensureDir(skillFolder);
1468
+ await fs6.writeFile(path6.join(skillFolder, "SKILL.md"), skillMd);
1469
+ await fs6.writeFile(path6.join(skillFolder, SKILL_META_FILENAME), serializeSkillMeta(meta));
1470
+ wrote = true;
1471
+ }
1472
+ return {
1473
+ relativeFolder,
1474
+ skillFolder,
1475
+ skill,
1476
+ pinnedCommit: sha,
1477
+ status: meta.status,
1478
+ agents: meta.agents ?? [],
1479
+ droppedAgentsFiles,
1480
+ wrote,
1481
+ warnings
1482
+ };
1483
+ }
1484
+ async function captureAgents(parsed, sha, entries, token) {
1485
+ const agentsDir = entries.find((e) => e.type === "dir" && e.name === "agents");
1486
+ if (!agentsDir) return { agents: [], droppedAgentsFiles: [] };
1487
+ const agentFiles = await fetchFolderContents(parsed, sha, "agents", token);
1488
+ const agents = /* @__PURE__ */ new Set();
1489
+ const droppedAgentsFiles = [];
1490
+ for (const file of agentFiles) {
1491
+ if (file.type !== "file" || !/\.ya?ml$/i.test(file.name)) continue;
1492
+ droppedAgentsFiles.push(`agents/${file.name}`);
1493
+ const stem = file.name.replace(/\.ya?ml$/i, "").toLowerCase();
1494
+ agents.add(AGENT_FILE_CLIENT[stem] ?? stem);
1495
+ }
1496
+ return { agents: Array.from(agents), droppedAgentsFiles };
1497
+ }
1498
+ function collectActivationWarnings(data) {
1499
+ const warnings = [];
1500
+ const applyTo = data.applyTo;
1501
+ const applyToValues = Array.isArray(applyTo) ? applyTo : [applyTo];
1502
+ if (applyToValues.some((v) => typeof v === "string" && v.includes("**"))) {
1503
+ warnings.push(`frontmatter applyTo: ${JSON.stringify(applyTo)} (broad auto-activation)`);
1504
+ }
1505
+ if (data.alwaysApply === true || data.alwaysApply === "true") {
1506
+ warnings.push("frontmatter alwaysApply: true (auto-activates on every request)");
1507
+ }
1508
+ return warnings;
1509
+ }
1510
+ function buildMeta(input2) {
1511
+ const dropped = input2.droppedAgentsFiles.length > 0 ? ` Upstream ships nested ${input2.droppedAgentsFiles.join(", ")}; captured into agents and not copied (the installer vendors top-level files only).` : "";
1512
+ const baseNote = `Vendored via \`ai-dev vendor\` from ${input2.parsed.source} at ${input2.parsed.path}.${dropped}`;
1513
+ const meta = {
1514
+ source: input2.parsed.source,
1515
+ upstreamName: input2.parsed.skill,
1516
+ pinnedCommit: input2.sha,
1517
+ vendoredOn: input2.vendoredOn,
1518
+ status: input2.status
1519
+ };
1520
+ if (input2.isUpdate && input2.existing) {
1521
+ if (!input2.statusExplicit && input2.existing.status) meta.status = input2.existing.status;
1522
+ const reviewedBy = input2.reviewedByExplicit ? input2.reviewedBy : input2.existing.reviewedBy;
1523
+ if (reviewedBy) meta.reviewedBy = reviewedBy;
1524
+ const agents = input2.existing.agents?.length ? input2.existing.agents : input2.agents;
1525
+ if (agents.length > 0) meta.agents = agents;
1526
+ const existingNotes = (input2.existing.notes ?? "").replace(/\s*Re-vendored on \d{4}-\d{2}-\d{2}\.?/g, "").trim();
1527
+ const base = existingNotes.length > 0 ? existingNotes : baseNote;
1528
+ meta.notes = `${base} Re-vendored on ${input2.vendoredOn}.`;
1529
+ } else {
1530
+ if (input2.agents.length > 0) meta.agents = input2.agents;
1531
+ const reviewedBy = input2.reviewedBy ?? input2.defaultReviewedBy;
1532
+ if (reviewedBy) meta.reviewedBy = reviewedBy;
1533
+ meta.notes = baseNote;
1534
+ }
1535
+ return meta;
1536
+ }
1537
+ function today() {
1538
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1539
+ }
1540
+ async function findRepoRoot(startDir) {
1541
+ let dir = path6.resolve(startDir);
1542
+ for (; ; ) {
1543
+ const hasTeams = await fs6.pathExists(path6.join(dir, "teams"));
1544
+ const hasConventions = await fs6.pathExists(
1545
+ path6.join(dir, "docs", "content-conventions.md")
1546
+ );
1547
+ if (hasTeams && hasConventions) return dir;
1548
+ const parent = path6.dirname(dir);
1549
+ if (parent === dir) break;
1550
+ dir = parent;
1551
+ }
1552
+ throw new Error(
1553
+ "Could not locate the ai-dev-shared repo root. Run this command from within the repo."
1554
+ );
1555
+ }
1556
+
1557
+ // src/commands/vendor.ts
1558
+ function resolveReviewedBy(explicit) {
1559
+ if (explicit) return explicit;
1560
+ try {
1561
+ const name = execFileSync("git", ["config", "user.name"], { encoding: "utf-8" }).trim();
1562
+ return name || void 0;
1563
+ } catch {
1564
+ return void 0;
1565
+ }
1566
+ }
1567
+ function printSummary(result, dryRun) {
1568
+ const prefix = dryRun ? chalk4.dim("[dry-run] ") : "";
1569
+ console.log(`
1570
+ ${prefix}Vendored: ${result.relativeFolder}/`);
1571
+ console.log(` SKILL.md (pinned to ${result.pinnedCommit.slice(0, 7)})`);
1572
+ console.log(` skill-meta.yml (status: ${result.status})`);
1573
+ console.log(
1574
+ `
1575
+ ${chalk4.yellow("\u26A0 REVIEW BEFORE USE")} \u2014 vendored content has not been audited.`
1576
+ );
1577
+ for (const warning of result.warnings) {
1578
+ console.log(chalk4.yellow(` ${warning}`));
1579
+ }
1580
+ console.log(
1581
+ `Next: ai-dev install skill ${result.skill}`
1582
+ );
1583
+ }
1584
+ async function vendorCmd(url, opts) {
1585
+ if (!opts.team) {
1586
+ console.log(chalk4.red("--team is required."));
1587
+ process.exit(1);
1588
+ }
1589
+ const status = opts.status;
1590
+ if (status && !VALID_STATUSES.includes(status)) {
1591
+ console.log(
1592
+ chalk4.red(`Invalid --status "${status}". Use one of: ${VALID_STATUSES.join(", ")}.`)
1593
+ );
1594
+ process.exit(1);
1595
+ }
1596
+ const options = {
1597
+ url,
1598
+ team: String(opts.team),
1599
+ lib: opts.lib ? String(opts.lib) : void 0,
1600
+ status,
1601
+ as: opts.as ? String(opts.as) : void 0,
1602
+ // Explicit --reviewed-by overrides a stored value; git config is only a
1603
+ // fallback for brand-new vendors, so it must not count as "explicit".
1604
+ reviewedBy: opts.reviewedBy ? String(opts.reviewedBy) : void 0,
1605
+ defaultReviewedBy: resolveReviewedBy(),
1606
+ update: Boolean(opts.update),
1607
+ dryRun: Boolean(opts.dryRun),
1608
+ token: process.env.GITHUB_TOKEN || void 0
1609
+ };
1610
+ try {
1611
+ const result = await vendorSkill(options);
1612
+ printSummary(result, options.dryRun ?? false);
1613
+ } catch (err) {
1614
+ console.log(chalk4.red(err.message));
1615
+ process.exit(1);
1616
+ }
1617
+ }
1618
+ function registerVendorCommand(program2) {
1619
+ program2.command("vendor <github-url>", { hidden: true }).description("Vendor a third-party skill from a GitHub URL (authoring)").option("--team <team>", "Target team folder (required)").option("--lib <lib>", "Override the <lib> path segment (default: repo owner)").option("--status <status>", "Initial status: evaluating|recommended|deprecated (default: evaluating)").option("--as <name>", "Override the skill folder name (must match SKILL.md name)").option("--reviewed-by <handle>", "Reviewer handle (default: git config user.name)").option("--update", "Re-vendor into an existing folder, refreshing the pinned commit").option("--dry-run", "Print what would be written without creating files").action(vendorCmd);
1620
+ }
1621
+
869
1622
  // src/commands/interactive.ts
870
1623
  import { select as select2 } from "@inquirer/prompts";
871
- import chalk4 from "chalk";
1624
+ import chalk5 from "chalk";
872
1625
  function formatFileCounts2(result, options) {
873
1626
  const createdWord = options.dryRun ? "would create" : "created";
874
1627
  const skippedWord = "up to date";
875
1628
  const overwrittenWord = options.dryRun ? "would overwrite" : "overwritten";
876
1629
  const parts = [];
877
- if (result.created.length > 0) parts.push(chalk4.green(`${result.created.length} ${createdWord}`));
878
- if (result.skipped.length > 0) parts.push(chalk4.dim(`${result.skipped.length} ${skippedWord}`));
879
- if (result.overwritten.length > 0) parts.push(chalk4.yellow(`${result.overwritten.length} ${overwrittenWord}`));
1630
+ if (result.created.length > 0) parts.push(chalk5.green(`${result.created.length} ${createdWord}`));
1631
+ if (result.skipped.length > 0) parts.push(chalk5.dim(`${result.skipped.length} ${skippedWord}`));
1632
+ if (result.overwritten.length > 0) parts.push(chalk5.yellow(`${result.overwritten.length} ${overwrittenWord}`));
880
1633
  return parts.join(", ");
881
1634
  }
882
1635
  function printResults2(results, options, resourceResults = [], assetResults = []) {
883
- const prefix = options.dryRun ? chalk4.yellow("[DRY RUN] ") : "";
1636
+ const prefix = options.dryRun ? chalk5.yellow("[DRY RUN] ") : "";
884
1637
  console.log(`
885
- ${prefix}${chalk4.bold("Install complete")}
1638
+ ${prefix}${chalk5.bold("Install complete")}
886
1639
  `);
887
1640
  if (resourceResults.length > 0) {
888
1641
  console.log(" Toolset resources Status");
889
1642
  console.log(" " + "\u2500".repeat(50));
890
1643
  for (const r of resourceResults) {
891
- console.log(` ${chalk4.cyan(r.toolset.padEnd(24))} ${formatFileCounts2(r, options)}`);
1644
+ console.log(` ${chalk5.cyan(r.toolset.padEnd(24))} ${formatFileCounts2(r, options)}`);
892
1645
  }
893
1646
  console.log();
894
1647
  }
@@ -898,7 +1651,7 @@ ${prefix}${chalk4.bold("Install complete")}
898
1651
  console.log(" Prompt Status");
899
1652
  console.log(" " + "\u2500".repeat(50));
900
1653
  for (const r of prompts) {
901
- console.log(` ${chalk4.cyan(r.folder.padEnd(24))} ${formatFileCounts2(r, options)}`);
1654
+ console.log(` ${chalk5.cyan(r.folder.padEnd(24))} ${formatFileCounts2(r, options)}`);
902
1655
  }
903
1656
  console.log();
904
1657
  }
@@ -906,7 +1659,7 @@ ${prefix}${chalk4.bold("Install complete")}
906
1659
  console.log(" Resource Status");
907
1660
  console.log(" " + "\u2500".repeat(50));
908
1661
  for (const r of resources) {
909
- console.log(` ${chalk4.cyan(r.folder.padEnd(24))} ${formatFileCounts2(r, options)}`);
1662
+ console.log(` ${chalk5.cyan(r.folder.padEnd(24))} ${formatFileCounts2(r, options)}`);
910
1663
  }
911
1664
  console.log();
912
1665
  }
@@ -914,7 +1667,7 @@ ${prefix}${chalk4.bold("Install complete")}
914
1667
  console.log(" Content folder Status");
915
1668
  console.log(" " + "\u2500".repeat(50));
916
1669
  for (const r of generic) {
917
- console.log(` ${chalk4.cyan(r.folder.padEnd(24))} ${formatFileCounts2(r, options)}`);
1670
+ console.log(` ${chalk5.cyan(r.folder.padEnd(24))} ${formatFileCounts2(r, options)}`);
918
1671
  }
919
1672
  console.log();
920
1673
  }
@@ -923,20 +1676,20 @@ ${prefix}${chalk4.bold("Install complete")}
923
1676
  console.log(" Skill Status");
924
1677
  console.log(" " + "\u2500".repeat(50));
925
1678
  for (const r of results) {
926
- console.log(` ${chalk4.cyan(r.skill.padEnd(24))} ${formatFileCounts2(r, options)}`);
1679
+ console.log(` ${chalk5.cyan(r.skill.padEnd(24))} ${formatFileCounts2(r, options)}`);
927
1680
  }
928
1681
  console.log();
929
1682
  }
930
1683
  console.log(` Target: ${options.targetDir}`);
931
1684
  console.log(` AI directory: ${options.aiDir}`);
932
- console.log(` Clients: ${options.clients.map((c) => c.name).join(", ") || chalk4.dim("none")}`);
1685
+ console.log(` Clients: ${options.clients.map((c) => c.name).join(", ") || chalk5.dim("none")}`);
933
1686
  console.log();
934
1687
  }
935
1688
  async function listAll2() {
936
1689
  const skills = await discoverSkills();
937
1690
  const teams = await discoverTeams();
938
1691
  if (skills.length === 0) {
939
- console.log(chalk4.yellow("No skills found in package. This may indicate a build issue."));
1692
+ console.log(chalk5.yellow("No skills found in package. This may indicate a build issue."));
940
1693
  return;
941
1694
  }
942
1695
  const librarySkills = skills.filter((s) => s.sourceType === "library");
@@ -944,9 +1697,9 @@ async function listAll2() {
944
1697
  const formatTeamName3 = (name) => name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
945
1698
  const truncate3 = (str, max) => str.length <= max ? str : str.slice(0, max - 3) + "...";
946
1699
  if (librarySkills.length > 0) {
947
- console.log(chalk4.bold("\nLibrary"));
1700
+ console.log(chalk5.bold("\nLibrary"));
948
1701
  for (const s of librarySkills) {
949
- console.log(` ${chalk4.cyan(s.name.padEnd(24))} ${truncate3(s.description, 60)}`);
1702
+ console.log(` ${chalk5.cyan(s.name.padEnd(24))} ${truncate3(s.description, 60)}`);
950
1703
  }
951
1704
  }
952
1705
  const teamMap = /* @__PURE__ */ new Map();
@@ -957,33 +1710,33 @@ async function listAll2() {
957
1710
  teamMap.set(team, existing);
958
1711
  }
959
1712
  for (const [team, skillList] of teamMap) {
960
- console.log(chalk4.bold(`
1713
+ console.log(chalk5.bold(`
961
1714
  ${formatTeamName3(team)}`));
962
1715
  for (const s of skillList) {
963
- console.log(` ${chalk4.cyan(s.name.padEnd(24))} ${truncate3(s.description, 60)}`);
1716
+ console.log(` ${chalk5.cyan(s.name.padEnd(24))} ${truncate3(s.description, 60)}`);
964
1717
  }
965
1718
  }
966
1719
  if (teams.length > 0) {
967
- console.log(chalk4.bold("\nTeams"));
1720
+ console.log(chalk5.bold("\nTeams"));
968
1721
  for (const t of teams) {
969
1722
  console.log(
970
- ` ${chalk4.cyan(formatTeamName3(t.name).padEnd(24))} ${formatTeamContents(t)}`
1723
+ ` ${chalk5.cyan(formatTeamName3(t.name).padEnd(24))} ${formatTeamContents(t)}`
971
1724
  );
972
1725
  }
973
1726
  }
974
1727
  const toolsets = await discoverToolsets();
975
1728
  if (toolsets.length > 0) {
976
- console.log(chalk4.bold("\nToolsets"));
1729
+ console.log(chalk5.bold("\nToolsets"));
977
1730
  for (const t of toolsets) {
978
1731
  console.log(
979
- ` ${chalk4.cyan(t.name.padEnd(24))} ${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`
1732
+ ` ${chalk5.cyan(t.name.padEnd(24))} ${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`
980
1733
  );
981
1734
  }
982
1735
  }
983
1736
  console.log();
984
1737
  }
985
1738
  async function runInteractive() {
986
- console.log(chalk4.bold("\n@groupby/ai-dev \u2014 AI Skills Installer\n"));
1739
+ console.log(chalk5.bold("\n@groupby/ai-dev \u2014 AI Skills Installer\n"));
987
1740
  const action = await select2({
988
1741
  message: "What would you like to do?",
989
1742
  choices: [
@@ -1001,7 +1754,7 @@ async function runInteractive() {
1001
1754
  if (action === "install-skills") {
1002
1755
  const allSkills = await discoverSkills();
1003
1756
  if (allSkills.length === 0) {
1004
- console.log(chalk4.yellow("No skills found in package. This may indicate a build issue."));
1757
+ console.log(chalk5.yellow("No skills found in package. This may indicate a build issue."));
1005
1758
  process.exit(1);
1006
1759
  }
1007
1760
  const selectedSkills = await promptSelectSkills(allSkills);
@@ -1010,7 +1763,7 @@ async function runInteractive() {
1010
1763
  const aiDir = await promptInstallDir();
1011
1764
  const confirmed = await promptConfirmInstall(selectedSkills, clients, targetDir, aiDir);
1012
1765
  if (!confirmed) {
1013
- console.log(chalk4.dim("Cancelled."));
1766
+ console.log(chalk5.dim("Cancelled."));
1014
1767
  return;
1015
1768
  }
1016
1769
  const options = {
@@ -1027,11 +1780,18 @@ async function runInteractive() {
1027
1780
  } else if (action === "install-team") {
1028
1781
  const teams = await discoverTeams();
1029
1782
  if (teams.length === 0) {
1030
- console.log(chalk4.yellow("No teams found."));
1783
+ console.log(chalk5.yellow("No teams found."));
1031
1784
  return;
1032
1785
  }
1033
1786
  const team = await promptSelectTeam(teams);
1034
1787
  let skillsToInstall = [...team.skills];
1788
+ if (team.thirdPartySkills.length > 0) {
1789
+ console.log(
1790
+ chalk5.dim(
1791
+ `Note: this team has third-party skills. Interactive mode installs first-party content only; run 'npx @groupby/ai-dev install team ${team.name}' to manage third-party skills.`
1792
+ )
1793
+ );
1794
+ }
1035
1795
  const includeLibrary = await promptIncludeLibrary();
1036
1796
  if (includeLibrary) {
1037
1797
  const allSkills = await discoverSkills();
@@ -1043,7 +1803,7 @@ async function runInteractive() {
1043
1803
  const aiDir = await promptInstallDir();
1044
1804
  const confirmed = await promptConfirmInstall(skillsToInstall, clients, targetDir, aiDir, team.assetFolders);
1045
1805
  if (!confirmed) {
1046
- console.log(chalk4.dim("Cancelled."));
1806
+ console.log(chalk5.dim("Cancelled."));
1047
1807
  return;
1048
1808
  }
1049
1809
  const options = {
@@ -1060,7 +1820,7 @@ async function runInteractive() {
1060
1820
  } else if (action === "install-toolset") {
1061
1821
  const toolsets = await discoverToolsets();
1062
1822
  if (toolsets.length === 0) {
1063
- console.log(chalk4.yellow("No toolsets found."));
1823
+ console.log(chalk5.yellow("No toolsets found."));
1064
1824
  return;
1065
1825
  }
1066
1826
  const toolset = await promptSelectToolset(toolsets);
@@ -1069,7 +1829,7 @@ async function runInteractive() {
1069
1829
  const aiDir = await promptInstallDir();
1070
1830
  const confirmed = await promptConfirmInstall(toolset.skills, clients, targetDir, aiDir);
1071
1831
  if (!confirmed) {
1072
- console.log(chalk4.dim("Cancelled."));
1832
+ console.log(chalk5.dim("Cancelled."));
1073
1833
  return;
1074
1834
  }
1075
1835
  const options = {
@@ -1091,15 +1851,16 @@ async function runInteractive() {
1091
1851
 
1092
1852
  // src/index.ts
1093
1853
  function getPackageVersion() {
1094
- const packageRoot = path5.resolve(path5.dirname(fileURLToPath2(import.meta.url)), "..");
1095
- const packageJsonPath = path5.join(packageRoot, "package.json");
1096
- const packageJson = JSON.parse(fs4.readFileSync(packageJsonPath, "utf-8"));
1854
+ const packageRoot = path7.resolve(path7.dirname(fileURLToPath2(import.meta.url)), "..");
1855
+ const packageJsonPath = path7.join(packageRoot, "package.json");
1856
+ const packageJson = JSON.parse(fs7.readFileSync(packageJsonPath, "utf-8"));
1097
1857
  return packageJson.version || "0.0.0";
1098
1858
  }
1099
1859
  var program = new Command();
1100
1860
  program.name("ai-dev").description("Interactive installer for GroupBy AI development content").version(getPackageVersion()).action(runInteractive);
1101
1861
  registerListCommand(program);
1102
1862
  registerInstallCommand(program);
1863
+ registerVendorCommand(program);
1103
1864
  program.addHelpText(
1104
1865
  "after",
1105
1866
  `