@groupby/ai-dev 0.5.19 → 0.5.21
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/README.md +56 -0
- package/dist/index.js +885 -124
- package/package.json +2 -2
- package/skills/README.md +42 -1
- package/teams/OOF/skills/oof-review/SKILL.md +319 -0
- package/teams/OOF/skills/oof-review/output-format.md +166 -0
- package/teams/OOF/skills/oof-review/reviewer-prompt.md +99 -0
- package/teams/OOF/skills/oof-review/summarize_review_config.py +226 -0
- package/teams/OOF/skills/oof-review/technology-profiles.md +54 -0
- package/teams/rangers/third-party/mattpocock/grilling/SKILL.md +28 -0
- package/teams/rangers/third-party/mattpocock/grilling/skill-meta.yml +14 -0
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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
|
|
14
|
+
import path2 from "path";
|
|
15
15
|
import { fileURLToPath } from "url";
|
|
16
16
|
import fg from "fast-glob";
|
|
17
|
-
import
|
|
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 =
|
|
37
|
-
var PACKAGE_ROOT =
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
124
|
+
const content = await fs2.readFile(match, "utf-8");
|
|
49
125
|
const { data } = parseFrontmatter(content);
|
|
50
|
-
const skillFolder =
|
|
51
|
-
const relativePath =
|
|
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 ||
|
|
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 =
|
|
87
|
-
const entries = await
|
|
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:
|
|
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.
|
|
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 =
|
|
223
|
+
const resourcesPath = path2.join(PACKAGE_ROOT, "toolsets", name, "resources");
|
|
137
224
|
let hasResources = false;
|
|
138
|
-
if (await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
333
|
-
import
|
|
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 =
|
|
343
|
-
if (await
|
|
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
|
|
352
|
-
import
|
|
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 =
|
|
357
|
-
if (await
|
|
358
|
-
const existingContent = await
|
|
359
|
-
const newContent = contentOverride ?? await
|
|
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
|
|
367
|
-
await
|
|
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
|
|
377
|
-
await
|
|
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
|
|
602
|
+
await fs4.ensureDir(path4.dirname(destPath));
|
|
389
603
|
if (contentOverride) {
|
|
390
|
-
await
|
|
604
|
+
await fs4.writeFile(destPath, contentOverride);
|
|
391
605
|
} else {
|
|
392
|
-
await
|
|
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
|
|
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 =
|
|
412
|
-
const destFile =
|
|
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 =
|
|
425
|
-
const sourceFiles = await
|
|
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 =
|
|
428
|
-
const stat = await
|
|
641
|
+
const srcFile = path4.join(skill.sourcePath, file);
|
|
642
|
+
const stat = await fs4.stat(srcFile);
|
|
429
643
|
if (stat.isFile()) {
|
|
430
|
-
const destFile =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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
|
|
735
|
-
|
|
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(
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
858
|
-
--skip-existing
|
|
859
|
-
--dry-run
|
|
860
|
-
--
|
|
861
|
-
--
|
|
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
|
|
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(
|
|
878
|
-
if (result.skipped.length > 0) parts.push(
|
|
879
|
-
if (result.overwritten.length > 0) parts.push(
|
|
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 ?
|
|
1636
|
+
const prefix = options.dryRun ? chalk5.yellow("[DRY RUN] ") : "";
|
|
884
1637
|
console.log(`
|
|
885
|
-
${prefix}${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(", ") ||
|
|
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(
|
|
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(
|
|
1700
|
+
console.log(chalk5.bold("\nLibrary"));
|
|
948
1701
|
for (const s of librarySkills) {
|
|
949
|
-
console.log(` ${
|
|
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(
|
|
1713
|
+
console.log(chalk5.bold(`
|
|
961
1714
|
${formatTeamName3(team)}`));
|
|
962
1715
|
for (const s of skillList) {
|
|
963
|
-
console.log(` ${
|
|
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(
|
|
1720
|
+
console.log(chalk5.bold("\nTeams"));
|
|
968
1721
|
for (const t of teams) {
|
|
969
1722
|
console.log(
|
|
970
|
-
` ${
|
|
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(
|
|
1729
|
+
console.log(chalk5.bold("\nToolsets"));
|
|
977
1730
|
for (const t of toolsets) {
|
|
978
1731
|
console.log(
|
|
979
|
-
` ${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
1095
|
-
const packageJsonPath =
|
|
1096
|
-
const packageJson = JSON.parse(
|
|
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
|
`
|