@groupby/ai-dev 0.4.2 → 0.5.1

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,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import fs4 from "fs";
5
+ import path5 from "path";
6
+ import { fileURLToPath as fileURLToPath2 } from "url";
4
7
  import { Command } from "commander";
5
8
 
6
9
  // src/commands/list.ts
7
10
  import chalk from "chalk";
8
- import fs2 from "fs-extra";
11
+ import fg2 from "fast-glob";
9
12
 
10
13
  // src/lib/discovery.ts
11
14
  import path from "path";
@@ -72,20 +75,51 @@ async function discoverSkills() {
72
75
  }
73
76
  return skills;
74
77
  }
78
+ async function discoverTeamAssetFolders() {
79
+ const teamDirs = await fg("teams/*", {
80
+ cwd: PACKAGE_ROOT,
81
+ absolute: true,
82
+ onlyDirectories: true
83
+ });
84
+ const assetFolders = [];
85
+ for (const teamDir of teamDirs) {
86
+ const teamName = path.basename(teamDir);
87
+ const entries = await fs.readdir(teamDir, { withFileTypes: true });
88
+ for (const entry of entries) {
89
+ if (!entry.isDirectory() || entry.name === "skills") continue;
90
+ assetFolders.push({
91
+ name: entry.name,
92
+ sourcePath: path.join(teamDir, entry.name),
93
+ teamName
94
+ });
95
+ }
96
+ }
97
+ return assetFolders.sort(
98
+ (a, b) => a.teamName.localeCompare(b.teamName) || a.name.localeCompare(b.name)
99
+ );
100
+ }
75
101
  async function discoverTeams() {
76
- const skills = await discoverSkills();
102
+ const [skills, assetFolders] = await Promise.all([
103
+ discoverSkills(),
104
+ discoverTeamAssetFolders()
105
+ ]);
77
106
  const teamMap = /* @__PURE__ */ new Map();
107
+ const ensureTeam = (name) => {
108
+ const existing = teamMap.get(name);
109
+ if (existing) return existing;
110
+ const team = { name, skills: [], assetFolders: [] };
111
+ teamMap.set(name, team);
112
+ return team;
113
+ };
78
114
  for (const skill of skills) {
79
115
  if (skill.sourceType === "team" && skill.teamName) {
80
- const existing = teamMap.get(skill.teamName) || [];
81
- existing.push(skill);
82
- teamMap.set(skill.teamName, existing);
116
+ ensureTeam(skill.teamName).skills.push(skill);
83
117
  }
84
118
  }
85
- return Array.from(teamMap.entries()).map(([name, skills2]) => ({
86
- name,
87
- skills: skills2
88
- }));
119
+ for (const folder of assetFolders) {
120
+ ensureTeam(folder.teamName).assetFolders.push(folder);
121
+ }
122
+ return Array.from(teamMap.values()).sort((a, b) => a.name.localeCompare(b.name));
89
123
  }
90
124
  async function discoverToolsets() {
91
125
  const skills = await discoverSkills();
@@ -102,8 +136,11 @@ async function discoverToolsets() {
102
136
  const resourcesPath = path.join(PACKAGE_ROOT, "toolsets", name, "resources");
103
137
  let hasResources = false;
104
138
  if (await fs.pathExists(resourcesPath)) {
105
- const entries = await fs.readdir(resourcesPath);
106
- hasResources = entries.some((e) => e !== "README.md");
139
+ const resourceFiles = await fg("**/*", {
140
+ cwd: resourcesPath,
141
+ onlyFiles: true
142
+ });
143
+ hasResources = resourceFiles.length > 0;
107
144
  }
108
145
  toolsets.push({ name, skills: toolsetSkills, resourcesPath, hasResources });
109
146
  }
@@ -120,6 +157,48 @@ async function findToolset(name) {
120
157
  );
121
158
  }
122
159
 
160
+ // src/lib/content-types.ts
161
+ var PROMPTS_FOLDER = "prompts";
162
+ var RESOURCES_FOLDER = "resources";
163
+ function isPromptFolder(folderName) {
164
+ return folderName === PROMPTS_FOLDER;
165
+ }
166
+ function isResourceFolder(folderName) {
167
+ return folderName === RESOURCES_FOLDER;
168
+ }
169
+ function formatTeamContents(team) {
170
+ const parts = [`${team.skills.length} skill${team.skills.length !== 1 ? "s" : ""}`];
171
+ const promptCount = team.assetFolders.filter((folder) => isPromptFolder(folder.name)).length;
172
+ const resourceCount = team.assetFolders.filter((folder) => isResourceFolder(folder.name)).length;
173
+ const genericCount = team.assetFolders.length - promptCount - resourceCount;
174
+ if (promptCount > 0) {
175
+ parts.push(`${promptCount} prompt${promptCount !== 1 ? "s" : ""}`);
176
+ }
177
+ if (resourceCount > 0) {
178
+ parts.push(`${resourceCount} resource${resourceCount !== 1 ? "s" : ""}`);
179
+ }
180
+ if (genericCount > 0) {
181
+ parts.push(`${genericCount} content folder${genericCount !== 1 ? "s" : ""}`);
182
+ }
183
+ return parts.join(", ");
184
+ }
185
+ function partitionTeamAssetFolders(folders) {
186
+ const prompts = [];
187
+ const resources = [];
188
+ const generic = [];
189
+ for (const folder of folders) {
190
+ const folderName = "name" in folder ? folder.name : folder.folder;
191
+ if (isPromptFolder(folderName)) {
192
+ prompts.push(folder);
193
+ } else if (isResourceFolder(folderName)) {
194
+ resources.push(folder);
195
+ } else {
196
+ generic.push(folder);
197
+ }
198
+ }
199
+ return { prompts, resources, generic };
200
+ }
201
+
123
202
  // src/commands/list.ts
124
203
  function formatTeamName(name) {
125
204
  return name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
@@ -128,6 +207,13 @@ function truncate(str, max) {
128
207
  if (str.length <= max) return str;
129
208
  return str.slice(0, max - 3) + "...";
130
209
  }
210
+ async function countResourceFiles(resourcesPath) {
211
+ const files = await fg2("**/*", {
212
+ cwd: resourcesPath,
213
+ onlyFiles: true
214
+ });
215
+ return files.length;
216
+ }
131
217
  async function listSkills() {
132
218
  const skills = await discoverSkills();
133
219
  if (skills.length === 0) {
@@ -182,7 +268,7 @@ async function listTeams() {
182
268
  console.log(chalk.bold("\nTeams"));
183
269
  for (const t of teams) {
184
270
  console.log(
185
- ` ${chalk.cyan(formatTeamName(t.name).padEnd(24))} ${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`
271
+ ` ${chalk.cyan(formatTeamName(t.name).padEnd(24))} ${formatTeamContents(t)}`
186
272
  );
187
273
  }
188
274
  console.log();
@@ -197,8 +283,7 @@ async function listToolsets() {
197
283
  for (const t of toolsets) {
198
284
  const parts = [`${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`];
199
285
  if (t.hasResources) {
200
- const entries = await fs2.readdir(t.resourcesPath);
201
- const count = entries.filter((e) => e !== "README.md").length;
286
+ const count = await countResourceFiles(t.resourcesPath);
202
287
  parts.push(`${count} resource${count !== 1 ? "s" : ""}`);
203
288
  }
204
289
  console.log(` ${chalk.cyan(t.name.padEnd(24))} ${parts.join(", ")}`);
@@ -212,7 +297,7 @@ async function listAll() {
212
297
  console.log(chalk.bold("Teams"));
213
298
  for (const t of teams) {
214
299
  console.log(
215
- ` ${chalk.cyan(formatTeamName(t.name).padEnd(24))} ${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`
300
+ ` ${chalk.cyan(formatTeamName(t.name).padEnd(24))} ${formatTeamContents(t)}`
216
301
  );
217
302
  }
218
303
  console.log();
@@ -223,8 +308,7 @@ async function listAll() {
223
308
  for (const t of toolsets) {
224
309
  const parts = [`${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`];
225
310
  if (t.hasResources) {
226
- const entries = await fs2.readdir(t.resourcesPath);
227
- const count = entries.filter((e) => e !== "README.md").length;
311
+ const count = await countResourceFiles(t.resourcesPath);
228
312
  parts.push(`${count} resource${count !== 1 ? "s" : ""}`);
229
313
  }
230
314
  console.log(` ${chalk.cyan(t.name.padEnd(24))} ${parts.join(", ")}`);
@@ -233,9 +317,9 @@ async function listAll() {
233
317
  }
234
318
  }
235
319
  function registerListCommand(program2) {
236
- const list = program2.command("list").description("List available skills and teams");
320
+ const list = program2.command("list").description("List available skills, teams, and toolsets");
237
321
  list.command("skills").description("List all available skills").action(listSkills);
238
- list.command("teams").description("List teams and their skill counts").action(listTeams);
322
+ list.command("teams").description("List teams with skill and content counts").action(listTeams);
239
323
  list.command("toolsets").description("List toolsets with skill and resource counts").action(listToolsets);
240
324
  list.action(listAll);
241
325
  }
@@ -246,7 +330,7 @@ import chalk3 from "chalk";
246
330
 
247
331
  // src/lib/clients.ts
248
332
  import path2 from "path";
249
- import fs3 from "fs-extra";
333
+ import fs2 from "fs-extra";
250
334
  var ALL_CLIENTS = [
251
335
  { name: "Copilot", skillsDir: ".github/skills", detectDir: ".github" },
252
336
  { name: "Claude Code", skillsDir: ".claude/skills", detectDir: ".claude" },
@@ -256,7 +340,7 @@ async function detectClients(targetDir) {
256
340
  const detected = [];
257
341
  for (const client of ALL_CLIENTS) {
258
342
  const dirPath = path2.join(targetDir, client.detectDir);
259
- if (await fs3.pathExists(dirPath)) {
343
+ if (await fs2.pathExists(dirPath)) {
260
344
  detected.push(client);
261
345
  }
262
346
  }
@@ -265,21 +349,22 @@ async function detectClients(targetDir) {
265
349
 
266
350
  // src/lib/installer.ts
267
351
  import path3 from "path";
268
- import fs4 from "fs-extra";
352
+ import fs3 from "fs-extra";
353
+ import fg3 from "fast-glob";
269
354
  var DEFAULT_AI_DIR = "docs/ai";
270
355
  async function handleFile(srcPath, destPath, options, result, onConflict, contentOverride) {
271
356
  const relativeDest = path3.relative(options.targetDir, destPath);
272
- if (await fs4.pathExists(destPath)) {
273
- const existingContent = await fs4.readFile(destPath, "utf-8");
274
- const newContent = contentOverride ?? await fs4.readFile(srcPath, "utf-8");
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");
275
360
  if (existingContent === newContent) {
276
361
  result.skipped.push(relativeDest);
277
362
  return;
278
363
  }
279
364
  if (options.force) {
280
365
  if (!options.dryRun) {
281
- await fs4.ensureDir(path3.dirname(destPath));
282
- await fs4.writeFile(destPath, newContent);
366
+ await fs3.ensureDir(path3.dirname(destPath));
367
+ await fs3.writeFile(destPath, newContent);
283
368
  }
284
369
  result.overwritten.push(relativeDest);
285
370
  } else if (options.skipExisting) {
@@ -288,8 +373,8 @@ async function handleFile(srcPath, destPath, options, result, onConflict, conten
288
373
  const choice = await onConflict(relativeDest);
289
374
  if (choice === "overwrite") {
290
375
  if (!options.dryRun) {
291
- await fs4.ensureDir(path3.dirname(destPath));
292
- await fs4.writeFile(destPath, newContent);
376
+ await fs3.ensureDir(path3.dirname(destPath));
377
+ await fs3.writeFile(destPath, newContent);
293
378
  }
294
379
  result.overwritten.push(relativeDest);
295
380
  } else {
@@ -300,11 +385,11 @@ async function handleFile(srcPath, destPath, options, result, onConflict, conten
300
385
  }
301
386
  } else {
302
387
  if (!options.dryRun) {
303
- await fs4.ensureDir(path3.dirname(destPath));
388
+ await fs3.ensureDir(path3.dirname(destPath));
304
389
  if (contentOverride) {
305
- await fs4.writeFile(destPath, contentOverride);
390
+ await fs3.writeFile(destPath, contentOverride);
306
391
  } else {
307
- await fs4.copy(srcPath, destPath);
392
+ await fs3.copy(srcPath, destPath);
308
393
  }
309
394
  }
310
395
  result.created.push(relativeDest);
@@ -313,9 +398,22 @@ async function handleFile(srcPath, destPath, options, result, onConflict, conten
313
398
  async function patchContent(srcPath, aiDir) {
314
399
  if (aiDir === DEFAULT_AI_DIR) return void 0;
315
400
  if (!srcPath.endsWith(".md")) return void 0;
316
- const content = await fs4.readFile(srcPath, "utf-8");
401
+ const content = await fs3.readFile(srcPath, "utf-8");
317
402
  return content.replaceAll(`${DEFAULT_AI_DIR}/`, `${aiDir}/`);
318
403
  }
404
+ async function installDirectoryFiles(sourceDir, destDir, options, result, onConflict) {
405
+ const files = await fg3("**/*", {
406
+ cwd: sourceDir,
407
+ onlyFiles: true,
408
+ dot: true
409
+ });
410
+ for (const file of files) {
411
+ const srcFile = path3.join(sourceDir, file);
412
+ const destFile = path3.join(destDir, file);
413
+ const contentOverride = await patchContent(srcFile, options.aiDir);
414
+ await handleFile(srcFile, destFile, options, result, onConflict, contentOverride);
415
+ }
416
+ }
319
417
  async function installSkill(skill, options, onConflict) {
320
418
  const result = {
321
419
  skill: skill.name,
@@ -324,10 +422,10 @@ async function installSkill(skill, options, onConflict) {
324
422
  overwritten: []
325
423
  };
326
424
  const destFolder = path3.join(options.targetDir, options.aiDir, "skills", skill.name);
327
- const sourceFiles = await fs4.readdir(skill.sourcePath);
425
+ const sourceFiles = await fs3.readdir(skill.sourcePath);
328
426
  for (const file of sourceFiles) {
329
427
  const srcFile = path3.join(skill.sourcePath, file);
330
- const stat = await fs4.stat(srcFile);
428
+ const stat = await fs3.stat(srcFile);
331
429
  if (stat.isFile()) {
332
430
  const destFile = path3.join(destFolder, file);
333
431
  const contentOverride = await patchContent(srcFile, options.aiDir);
@@ -353,6 +451,22 @@ async function installSkills(skills, options, onConflict) {
353
451
  }
354
452
  return results;
355
453
  }
454
+ async function installTeamAssetFolders(assetFolders, options, onConflict) {
455
+ const results = [];
456
+ for (const folder of assetFolders) {
457
+ const result = {
458
+ team: folder.teamName,
459
+ folder: folder.name,
460
+ created: [],
461
+ skipped: [],
462
+ overwritten: []
463
+ };
464
+ const destFolder = path3.join(options.targetDir, options.aiDir, folder.name);
465
+ await installDirectoryFiles(folder.sourcePath, destFolder, options, result, onConflict);
466
+ results.push(result);
467
+ }
468
+ return results;
469
+ }
356
470
  async function installResources(toolset, options, onConflict) {
357
471
  const result = {
358
472
  toolset: toolset.name,
@@ -361,24 +475,47 @@ async function installResources(toolset, options, onConflict) {
361
475
  overwritten: []
362
476
  };
363
477
  if (!toolset.hasResources) return result;
364
- const entries = await fs4.readdir(toolset.resourcesPath);
365
- const resourceFiles = entries.filter((e) => e !== "README.md");
366
478
  const destDir = path3.join(options.targetDir, options.aiDir, "resources");
367
- const fileResult = { skill: toolset.name, created: [], skipped: [], overwritten: [] };
368
- for (const file of resourceFiles) {
369
- const srcFile = path3.join(toolset.resourcesPath, file);
370
- const stat = await fs4.stat(srcFile);
371
- if (!stat.isFile()) continue;
372
- const destFile = path3.join(destDir, file);
373
- const contentOverride = await patchContent(srcFile, options.aiDir);
374
- await handleFile(srcFile, destFile, options, fileResult, onConflict, contentOverride);
375
- }
479
+ const fileResult = { created: [], skipped: [], overwritten: [] };
480
+ await installDirectoryFiles(toolset.resourcesPath, destDir, options, fileResult, onConflict);
376
481
  result.created = fileResult.created;
377
482
  result.skipped = fileResult.skipped;
378
483
  result.overwritten = fileResult.overwritten;
379
484
  return result;
380
485
  }
381
486
 
487
+ // src/lib/skill-resource-dependencies.ts
488
+ function findTeamResourceFoldersForSkills(skills, teams) {
489
+ const teamNames = new Set(
490
+ skills.filter((skill) => skill.sourceType === "team" && skill.teamName).map((skill) => skill.teamName)
491
+ );
492
+ const folders = /* @__PURE__ */ new Map();
493
+ for (const team of teams) {
494
+ if (!teamNames.has(team.name)) continue;
495
+ for (const folder of team.assetFolders.filter((f) => isResourceFolder(f.name))) {
496
+ folders.set(`${folder.teamName}/${folder.name}`, folder);
497
+ }
498
+ }
499
+ return Array.from(folders.values());
500
+ }
501
+ function findToolsetsForSkills(skills, toolsets) {
502
+ const toolsetNames = new Set(
503
+ skills.filter((skill) => skill.sourceType === "toolset" && skill.toolsetName).map((skill) => skill.toolsetName)
504
+ );
505
+ return toolsets.filter((toolset) => toolsetNames.has(toolset.name) && toolset.hasResources);
506
+ }
507
+ async function installResourceDependenciesForSkills(skills, options, onConflict) {
508
+ const [teams, toolsets] = await Promise.all([discoverTeams(), discoverToolsets()]);
509
+ const teamResourceFolders = findTeamResourceFoldersForSkills(skills, teams);
510
+ const matchingToolsets = findToolsetsForSkills(skills, toolsets);
511
+ const assetResults = await installTeamAssetFolders(teamResourceFolders, options, onConflict);
512
+ const resourceResults = [];
513
+ for (const toolset of matchingToolsets) {
514
+ resourceResults.push(await installResources(toolset, options, onConflict));
515
+ }
516
+ return { resourceResults, assetResults };
517
+ }
518
+
382
519
  // src/lib/prompts.ts
383
520
  import { select, checkbox, confirm, input } from "@inquirer/prompts";
384
521
  import chalk2 from "chalk";
@@ -435,7 +572,7 @@ async function promptSelectTeam(teams) {
435
572
  return select({
436
573
  message: "Select a team:",
437
574
  choices: teams.map((t) => ({
438
- name: `${formatTeamName2(t.name)} (${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""})`,
575
+ name: `${formatTeamName2(t.name)} (${formatTeamContents(t)})`,
439
576
  value: t
440
577
  }))
441
578
  });
@@ -468,20 +605,30 @@ async function promptIncludeLibrary() {
468
605
  }
469
606
  async function promptInstallDir() {
470
607
  const raw = await input({
471
- message: "Install location for AI skills and resources:",
608
+ message: "Install location for AI content:",
472
609
  default: "docs/ai"
473
610
  });
474
611
  return raw.replace(/\/+$/, "") || "docs/ai";
475
612
  }
476
- async function promptConfirmInstall(skills, clients, targetDir, aiDir = "docs/ai") {
613
+ async function promptConfirmInstall(skills, clients, targetDir, aiDir = "docs/ai", assetFolders = []) {
477
614
  console.log();
478
615
  console.log(chalk2.bold("Install summary:"));
479
- console.log(` Skills: ${skills.map((s) => s.name).join(", ")}`);
616
+ console.log(` Skills: ${skills.map((s) => s.name).join(", ") || chalk2.dim("none")}`);
617
+ if (assetFolders.length > 0) {
618
+ const { prompts, resources, generic } = partitionTeamAssetFolders(assetFolders);
619
+ if (prompts.length > 0) {
620
+ console.log(` Prompts: ${prompts.map((f) => f.name).join(", ")}`);
621
+ }
622
+ if (resources.length > 0) {
623
+ console.log(` Resources: ${resources.map((f) => f.name).join(", ")}`);
624
+ }
625
+ if (generic.length > 0) {
626
+ console.log(` Content folders: ${generic.map((f) => f.name).join(", ")}`);
627
+ }
628
+ }
480
629
  console.log(` Clients: ${clients.map((c) => c.name).join(", ") || chalk2.dim("none")}`);
481
630
  console.log(` Target: ${targetDir}`);
482
- if (aiDir !== "docs/ai") {
483
- console.log(` AI directory: ${aiDir}`);
484
- }
631
+ console.log(` AI directory: ${aiDir}`);
485
632
  console.log();
486
633
  return confirm({ message: "Proceed with installation?", default: true });
487
634
  }
@@ -512,35 +659,66 @@ function normalizeAiDir(raw) {
512
659
  }
513
660
  return trimmed;
514
661
  }
515
- function printResults(results, options, resourceResult) {
516
- const prefix = options.dryRun ? chalk3.yellow("[DRY RUN] ") : "";
662
+ function formatFileCounts(result, options) {
517
663
  const createdWord = options.dryRun ? "would create" : "created";
518
664
  const skippedWord = "up to date";
519
665
  const overwrittenWord = options.dryRun ? "would overwrite" : "overwritten";
666
+ const parts = [];
667
+ if (result.created.length > 0) parts.push(chalk3.green(`${result.created.length} ${createdWord}`));
668
+ if (result.skipped.length > 0) parts.push(chalk3.dim(`${result.skipped.length} ${skippedWord}`));
669
+ if (result.overwritten.length > 0) parts.push(chalk3.yellow(`${result.overwritten.length} ${overwrittenWord}`));
670
+ return parts.join(", ");
671
+ }
672
+ function printResults(results, options, resourceResults = [], assetResults = []) {
673
+ const prefix = options.dryRun ? chalk3.yellow("[DRY RUN] ") : "";
520
674
  console.log(`
521
675
  ${prefix}${chalk3.bold("Install complete")}
522
676
  `);
523
- if (resourceResult) {
524
- const rParts = [];
525
- if (resourceResult.created.length > 0) rParts.push(chalk3.green(`${resourceResult.created.length} ${createdWord}`));
526
- if (resourceResult.skipped.length > 0) rParts.push(chalk3.dim(`${resourceResult.skipped.length} ${skippedWord}`));
527
- if (resourceResult.overwritten.length > 0) rParts.push(chalk3.yellow(`${resourceResult.overwritten.length} ${overwrittenWord}`));
528
- if (rParts.length > 0) {
529
- console.log(` ${chalk3.bold("Resources")} ${rParts.join(", ")}`);
677
+ if (resourceResults.length > 0) {
678
+ console.log(" Toolset resources Status");
679
+ console.log(" " + "\u2500".repeat(50));
680
+ for (const r of resourceResults) {
681
+ console.log(` ${chalk3.cyan(r.toolset.padEnd(24))} ${formatFileCounts(r, options)}`);
682
+ }
683
+ console.log();
684
+ }
685
+ if (assetResults.length > 0) {
686
+ const { prompts, resources, generic } = partitionTeamAssetFolders(assetResults);
687
+ if (prompts.length > 0) {
688
+ console.log(" Prompt Status");
689
+ console.log(" " + "\u2500".repeat(50));
690
+ for (const r of prompts) {
691
+ console.log(` ${chalk3.cyan(r.folder.padEnd(24))} ${formatFileCounts(r, options)}`);
692
+ }
693
+ console.log();
694
+ }
695
+ if (resources.length > 0) {
696
+ console.log(" Resource Status");
697
+ console.log(" " + "\u2500".repeat(50));
698
+ for (const r of resources) {
699
+ console.log(` ${chalk3.cyan(r.folder.padEnd(24))} ${formatFileCounts(r, options)}`);
700
+ }
701
+ console.log();
702
+ }
703
+ if (generic.length > 0) {
704
+ console.log(" Content folder Status");
705
+ console.log(" " + "\u2500".repeat(50));
706
+ for (const r of generic) {
707
+ console.log(` ${chalk3.cyan(r.folder.padEnd(24))} ${formatFileCounts(r, options)}`);
708
+ }
530
709
  console.log();
531
710
  }
532
711
  }
533
- console.log(" Skill Status");
534
- console.log(" " + "\u2500".repeat(50));
535
- for (const r of results) {
536
- const parts = [];
537
- if (r.created.length > 0) parts.push(chalk3.green(`${r.created.length} ${createdWord}`));
538
- if (r.skipped.length > 0) parts.push(chalk3.dim(`${r.skipped.length} ${skippedWord}`));
539
- if (r.overwritten.length > 0) parts.push(chalk3.yellow(`${r.overwritten.length} ${overwrittenWord}`));
540
- console.log(` ${chalk3.cyan(r.skill.padEnd(24))} ${parts.join(", ")}`);
712
+ if (results.length > 0) {
713
+ console.log(" Skill Status");
714
+ console.log(" " + "\u2500".repeat(50));
715
+ for (const r of results) {
716
+ console.log(` ${chalk3.cyan(r.skill.padEnd(24))} ${formatFileCounts(r, options)}`);
717
+ }
718
+ console.log();
541
719
  }
542
- console.log(`
543
- Target: ${options.targetDir}`);
720
+ console.log(` Target: ${options.targetDir}`);
721
+ console.log(` AI directory: ${options.aiDir}`);
544
722
  console.log(` Clients: ${options.clients.map((c) => c.name).join(", ") || chalk3.dim("none")}`);
545
723
  console.log();
546
724
  }
@@ -575,15 +753,10 @@ async function installSkillCmd(name, _opts, cmd) {
575
753
  skipExisting,
576
754
  dryRun
577
755
  };
578
- const result = await installSkill(skill, options, force || skipExisting ? void 0 : promptConflict);
579
- let resourceResult;
580
- if (skill.sourceType === "toolset" && skill.toolsetName) {
581
- const toolset = await findToolset(skill.toolsetName);
582
- if (toolset && toolset.hasResources) {
583
- resourceResult = await installResources(toolset, options, force || skipExisting ? void 0 : promptConflict);
584
- }
585
- }
586
- printResults([result], options, resourceResult);
756
+ const conflictHandler = force || skipExisting ? void 0 : promptConflict;
757
+ const resourceDependencies = await installResourceDependenciesForSkills([skill], options, conflictHandler);
758
+ const result = await installSkill(skill, options, conflictHandler);
759
+ printResults([result], options, resourceDependencies.resourceResults, resourceDependencies.assetResults);
587
760
  }
588
761
  async function installTeamCmd(name, _opts, cmd) {
589
762
  const parentOpts = cmd.parent.opts();
@@ -625,12 +798,18 @@ async function installTeamCmd(name, _opts, cmd) {
625
798
  skipExisting,
626
799
  dryRun
627
800
  };
801
+ const conflictHandler = force || skipExisting ? void 0 : promptConflict;
802
+ const assetResults = await installTeamAssetFolders(
803
+ team.assetFolders,
804
+ options,
805
+ conflictHandler
806
+ );
628
807
  const results = await installSkills(
629
808
  skillsToInstall,
630
809
  options,
631
- force || skipExisting ? void 0 : promptConflict
810
+ conflictHandler
632
811
  );
633
- printResults(results, options);
812
+ printResults(results, options, [], assetResults);
634
813
  }
635
814
  async function installToolsetCmd(name, _opts, cmd) {
636
815
  const parentOpts = cmd.parent.opts();
@@ -666,12 +845,12 @@ async function installToolsetCmd(name, _opts, cmd) {
666
845
  dryRun
667
846
  };
668
847
  const conflictHandler = force || skipExisting ? void 0 : promptConflict;
669
- let resourceResult;
848
+ const resourceResults = [];
670
849
  if (toolset.hasResources) {
671
- resourceResult = await installResources(toolset, options, conflictHandler);
850
+ resourceResults.push(await installResources(toolset, options, conflictHandler));
672
851
  }
673
852
  const results = await installSkills(toolset.skills, options, conflictHandler);
674
- printResults(results, options, resourceResult);
853
+ printResults(results, options, resourceResults);
675
854
  }
676
855
  var INHERITED_OPTIONS_HELP = `
677
856
  Parent options (pass before subcommand):
@@ -679,46 +858,77 @@ Parent options (pass before subcommand):
679
858
  --skip-existing Skip files that already exist without prompting
680
859
  --dry-run Show what would be installed without writing files
681
860
  --target <dir> Install to a different directory (default: CWD)
682
- --ai-dir <path> Override the AI skills/resources directory (default: docs/ai)`;
861
+ --ai-dir <path> Override the AI content directory (default: docs/ai)`;
683
862
  function registerInstallCommand(program2) {
684
- const install = program2.command("install").description("Install skills").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 skills/resources directory (default: docs/ai)");
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)");
685
864
  install.command("skill <name>").description("Install a specific skill").addHelpText("after", INHERITED_OPTIONS_HELP).action(installSkillCmd);
686
- install.command("team <name>").description("Install all skills for a team").addHelpText("after", INHERITED_OPTIONS_HELP).action(installTeamCmd);
865
+ install.command("team <name>").description("Install all skills, prompts, and content folders for a team").addHelpText("after", INHERITED_OPTIONS_HELP).action(installTeamCmd);
687
866
  install.command("toolset <name>").description("Install all skills and resources for a toolset").addHelpText("after", INHERITED_OPTIONS_HELP).action(installToolsetCmd);
688
867
  }
689
868
 
690
869
  // src/commands/interactive.ts
691
870
  import { select as select2 } from "@inquirer/prompts";
692
871
  import chalk4 from "chalk";
693
- function printResults2(results, options, resourceResult) {
694
- const prefix = options.dryRun ? chalk4.yellow("[DRY RUN] ") : "";
872
+ function formatFileCounts2(result, options) {
695
873
  const createdWord = options.dryRun ? "would create" : "created";
696
874
  const skippedWord = "up to date";
697
875
  const overwrittenWord = options.dryRun ? "would overwrite" : "overwritten";
876
+ 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}`));
880
+ return parts.join(", ");
881
+ }
882
+ function printResults2(results, options, resourceResults = [], assetResults = []) {
883
+ const prefix = options.dryRun ? chalk4.yellow("[DRY RUN] ") : "";
698
884
  console.log(`
699
885
  ${prefix}${chalk4.bold("Install complete")}
700
886
  `);
701
- if (resourceResult) {
702
- const rParts = [];
703
- if (resourceResult.created.length > 0) rParts.push(chalk4.green(`${resourceResult.created.length} ${createdWord}`));
704
- if (resourceResult.skipped.length > 0) rParts.push(chalk4.dim(`${resourceResult.skipped.length} ${skippedWord}`));
705
- if (resourceResult.overwritten.length > 0) rParts.push(chalk4.yellow(`${resourceResult.overwritten.length} ${overwrittenWord}`));
706
- if (rParts.length > 0) {
707
- console.log(` ${chalk4.bold("Resources")} ${rParts.join(", ")}`);
887
+ if (resourceResults.length > 0) {
888
+ console.log(" Toolset resources Status");
889
+ console.log(" " + "\u2500".repeat(50));
890
+ for (const r of resourceResults) {
891
+ console.log(` ${chalk4.cyan(r.toolset.padEnd(24))} ${formatFileCounts2(r, options)}`);
892
+ }
893
+ console.log();
894
+ }
895
+ if (assetResults.length > 0) {
896
+ const { prompts, resources, generic } = partitionTeamAssetFolders(assetResults);
897
+ if (prompts.length > 0) {
898
+ console.log(" Prompt Status");
899
+ console.log(" " + "\u2500".repeat(50));
900
+ for (const r of prompts) {
901
+ console.log(` ${chalk4.cyan(r.folder.padEnd(24))} ${formatFileCounts2(r, options)}`);
902
+ }
903
+ console.log();
904
+ }
905
+ if (resources.length > 0) {
906
+ console.log(" Resource Status");
907
+ console.log(" " + "\u2500".repeat(50));
908
+ for (const r of resources) {
909
+ console.log(` ${chalk4.cyan(r.folder.padEnd(24))} ${formatFileCounts2(r, options)}`);
910
+ }
911
+ console.log();
912
+ }
913
+ if (generic.length > 0) {
914
+ console.log(" Content folder Status");
915
+ console.log(" " + "\u2500".repeat(50));
916
+ for (const r of generic) {
917
+ console.log(` ${chalk4.cyan(r.folder.padEnd(24))} ${formatFileCounts2(r, options)}`);
918
+ }
708
919
  console.log();
709
920
  }
710
921
  }
711
- console.log(" Skill Status");
712
- console.log(" " + "\u2500".repeat(50));
713
- for (const r of results) {
714
- const parts = [];
715
- if (r.created.length > 0) parts.push(chalk4.green(`${r.created.length} ${createdWord}`));
716
- if (r.skipped.length > 0) parts.push(chalk4.dim(`${r.skipped.length} ${skippedWord}`));
717
- if (r.overwritten.length > 0) parts.push(chalk4.yellow(`${r.overwritten.length} ${overwrittenWord}`));
718
- console.log(` ${chalk4.cyan(r.skill.padEnd(24))} ${parts.join(", ")}`);
922
+ if (results.length > 0) {
923
+ console.log(" Skill Status");
924
+ console.log(" " + "\u2500".repeat(50));
925
+ for (const r of results) {
926
+ console.log(` ${chalk4.cyan(r.skill.padEnd(24))} ${formatFileCounts2(r, options)}`);
927
+ }
928
+ console.log();
719
929
  }
720
- console.log(`
721
- Target: ${options.targetDir}`);
930
+ console.log(` Target: ${options.targetDir}`);
931
+ console.log(` AI directory: ${options.aiDir}`);
722
932
  console.log(` Clients: ${options.clients.map((c) => c.name).join(", ") || chalk4.dim("none")}`);
723
933
  console.log();
724
934
  }
@@ -757,7 +967,7 @@ ${formatTeamName3(team)}`));
757
967
  console.log(chalk4.bold("\nTeams"));
758
968
  for (const t of teams) {
759
969
  console.log(
760
- ` ${chalk4.cyan(formatTeamName3(t.name).padEnd(24))} ${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`
970
+ ` ${chalk4.cyan(formatTeamName3(t.name).padEnd(24))} ${formatTeamContents(t)}`
761
971
  );
762
972
  }
763
973
  }
@@ -778,7 +988,7 @@ async function runInteractive() {
778
988
  message: "What would you like to do?",
779
989
  choices: [
780
990
  { name: "Install skill(s)", value: "install-skills" },
781
- { name: "Install team skills", value: "install-team" },
991
+ { name: "Install team content", value: "install-team" },
782
992
  { name: "Install toolset", value: "install-toolset" },
783
993
  { name: "List available skills & teams", value: "list" }
784
994
  ]
@@ -811,18 +1021,9 @@ async function runInteractive() {
811
1021
  skipExisting: false,
812
1022
  dryRun: false
813
1023
  };
1024
+ const resourceDependencies = await installResourceDependenciesForSkills(selectedSkills, options, promptConflict);
814
1025
  const results = await installSkills(selectedSkills, options, promptConflict);
815
- let resourceResult;
816
- const toolsetNames = new Set(
817
- selectedSkills.filter((s) => s.sourceType === "toolset" && s.toolsetName).map((s) => s.toolsetName)
818
- );
819
- for (const tsName of toolsetNames) {
820
- const toolset = await findToolset(tsName);
821
- if (toolset && toolset.hasResources) {
822
- resourceResult = await installResources(toolset, options, promptConflict);
823
- }
824
- }
825
- printResults2(results, options, resourceResult);
1026
+ printResults2(results, options, resourceDependencies.resourceResults, resourceDependencies.assetResults);
826
1027
  } else if (action === "install-team") {
827
1028
  const teams = await discoverTeams();
828
1029
  if (teams.length === 0) {
@@ -840,7 +1041,7 @@ async function runInteractive() {
840
1041
  const detected = await detectClients(targetDir);
841
1042
  const clients = await promptSelectClients(ALL_CLIENTS, detected);
842
1043
  const aiDir = await promptInstallDir();
843
- const confirmed = await promptConfirmInstall(skillsToInstall, clients, targetDir, aiDir);
1044
+ const confirmed = await promptConfirmInstall(skillsToInstall, clients, targetDir, aiDir, team.assetFolders);
844
1045
  if (!confirmed) {
845
1046
  console.log(chalk4.dim("Cancelled."));
846
1047
  return;
@@ -853,18 +1054,9 @@ async function runInteractive() {
853
1054
  skipExisting: false,
854
1055
  dryRun: false
855
1056
  };
1057
+ const assetResults = await installTeamAssetFolders(team.assetFolders, options, promptConflict);
856
1058
  const results = await installSkills(skillsToInstall, options, promptConflict);
857
- let resourceResult;
858
- const toolsetNames = new Set(
859
- skillsToInstall.filter((s) => s.sourceType === "toolset" && s.toolsetName).map((s) => s.toolsetName)
860
- );
861
- for (const tsName of toolsetNames) {
862
- const toolset = await findToolset(tsName);
863
- if (toolset && toolset.hasResources) {
864
- resourceResult = await installResources(toolset, options, promptConflict);
865
- }
866
- }
867
- printResults2(results, options, resourceResult);
1059
+ printResults2(results, options, [], assetResults);
868
1060
  } else if (action === "install-toolset") {
869
1061
  const toolsets = await discoverToolsets();
870
1062
  if (toolsets.length === 0) {
@@ -888,18 +1080,24 @@ async function runInteractive() {
888
1080
  skipExisting: false,
889
1081
  dryRun: false
890
1082
  };
891
- let resourceResult;
1083
+ const resourceResults = [];
892
1084
  if (toolset.hasResources) {
893
- resourceResult = await installResources(toolset, options, promptConflict);
1085
+ resourceResults.push(await installResources(toolset, options, promptConflict));
894
1086
  }
895
1087
  const results = await installSkills(toolset.skills, options, promptConflict);
896
- printResults2(results, options, resourceResult);
1088
+ printResults2(results, options, resourceResults);
897
1089
  }
898
1090
  }
899
1091
 
900
1092
  // src/index.ts
1093
+ 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"));
1097
+ return packageJson.version || "0.0.0";
1098
+ }
901
1099
  var program = new Command();
902
- program.name("ai-dev").description("Interactive installer for GroupBy AI development skills").version("0.1.0").action(runInteractive);
1100
+ program.name("ai-dev").description("Interactive installer for GroupBy AI development content").version(getPackageVersion()).action(runInteractive);
903
1101
  registerListCommand(program);
904
1102
  registerInstallCommand(program);
905
1103
  program.addHelpText(
@@ -910,7 +1108,7 @@ Examples:
910
1108
  $ npx @groupby/ai-dev list List skills, teams, and toolsets
911
1109
  $ npx @groupby/ai-dev list toolsets List available toolsets
912
1110
  $ npx @groupby/ai-dev install skill frontend-design Install a single skill
913
- $ npx @groupby/ai-dev install team brain-studio Install all team skills
1111
+ $ npx @groupby/ai-dev install team brain-studio Install all team content
914
1112
  $ npx @groupby/ai-dev install toolset rzlv-flow Install all skills + resources for a toolset
915
1113
  $ npx @groupby/ai-dev install team brain-studio --dry-run
916
1114
  $ npx @groupby/ai-dev install skill frontend-design --ai-dir .ai`