@rbbtsn0w/adg 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +308 -0
- package/bin/adg.ts +758 -0
- package/docs/agents-spec.md +132 -0
- package/docs/authoring.md +352 -0
- package/package.json +50 -0
- package/schemas/adg-plugin.schema.json +77 -0
- package/schemas/marketplace.schema.json +86 -0
- package/schemas/plugin-lock.schema.json +90 -0
- package/src/adapters/anthropic.ts +54 -0
- package/src/adapters/index.ts +24 -0
- package/src/adapters/openai.ts +37 -0
- package/src/adapters/reverse.ts +60 -0
- package/src/agents/claude.ts +124 -0
- package/src/agents/codex.ts +67 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/registry.ts +30 -0
- package/src/agents/types.ts +47 -0
- package/src/commands/adapt.ts +36 -0
- package/src/commands/import.ts +69 -0
- package/src/commands/init.ts +146 -0
- package/src/commands/install.ts +411 -0
- package/src/commands/link.ts +61 -0
- package/src/commands/list.ts +28 -0
- package/src/commands/marketplace.ts +198 -0
- package/src/commands/migrate.ts +84 -0
- package/src/commands/multiselect-skills.ts +137 -0
- package/src/commands/remove.ts +136 -0
- package/src/commands/select-agents.ts +45 -0
- package/src/commands/select-components.ts +66 -0
- package/src/commands/select-plugins.ts +28 -0
- package/src/commands/select-scope.ts +21 -0
- package/src/commands/update.ts +85 -0
- package/src/commands/validate.ts +57 -0
- package/src/components.ts +90 -0
- package/src/deps.ts +64 -0
- package/src/fsutil.ts +38 -0
- package/src/hash.ts +61 -0
- package/src/lock.ts +57 -0
- package/src/manifest.ts +113 -0
- package/src/marketplace.ts +41 -0
- package/src/package.ts +74 -0
- package/src/paths.ts +129 -0
- package/src/semver.ts +67 -0
- package/src/skills.ts +88 -0
- package/src/sources.ts +159 -0
- package/src/types.ts +140 -0
- package/vendor/skills/LICENSE +29 -0
- package/vendor/skills/PROVENANCE.md +60 -0
- package/vendor/skills/ThirdPartyNoticeText.txt +117 -0
- package/vendor/skills/package.json +143 -0
- package/vendor/skills/src/add.ts +1999 -0
- package/vendor/skills/src/agents.ts +755 -0
- package/vendor/skills/src/blob.ts +567 -0
- package/vendor/skills/src/cli.ts +387 -0
- package/vendor/skills/src/constants.ts +3 -0
- package/vendor/skills/src/detect-agent.ts +62 -0
- package/vendor/skills/src/find.ts +357 -0
- package/vendor/skills/src/frontmatter.ts +16 -0
- package/vendor/skills/src/git-tree.ts +36 -0
- package/vendor/skills/src/git.ts +277 -0
- package/vendor/skills/src/install.ts +91 -0
- package/vendor/skills/src/installer.ts +1097 -0
- package/vendor/skills/src/list.ts +231 -0
- package/vendor/skills/src/local-lock.ts +182 -0
- package/vendor/skills/src/plugin-manifest.ts +183 -0
- package/vendor/skills/src/prompts/search-multiselect.ts +387 -0
- package/vendor/skills/src/providers/index.ts +14 -0
- package/vendor/skills/src/providers/registry.ts +51 -0
- package/vendor/skills/src/providers/types.ts +97 -0
- package/vendor/skills/src/providers/wellknown.ts +804 -0
- package/vendor/skills/src/remove.ts +323 -0
- package/vendor/skills/src/sanitize.ts +65 -0
- package/vendor/skills/src/self-cli.ts +20 -0
- package/vendor/skills/src/skill-lock.ts +329 -0
- package/vendor/skills/src/skills.ts +316 -0
- package/vendor/skills/src/source-parser.ts +438 -0
- package/vendor/skills/src/sync.ts +478 -0
- package/vendor/skills/src/telemetry.ts +186 -0
- package/vendor/skills/src/test-utils.ts +73 -0
- package/vendor/skills/src/types.ts +128 -0
- package/vendor/skills/src/update-source.ts +90 -0
- package/vendor/skills/src/update.ts +749 -0
- package/vendor/skills/src/use.ts +675 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { readdir, stat } from 'fs/promises';
|
|
4
|
+
import { join, sep } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { parseSkillMd } from './skills.ts';
|
|
7
|
+
import { installSkillForAgent, getCanonicalPath } from './installer.ts';
|
|
8
|
+
import {
|
|
9
|
+
detectInstalledAgents,
|
|
10
|
+
agents,
|
|
11
|
+
getUniversalAgents,
|
|
12
|
+
getVisibleUniversalAgents,
|
|
13
|
+
getNonUniversalAgents,
|
|
14
|
+
} from './agents.ts';
|
|
15
|
+
import { searchMultiselect } from './prompts/search-multiselect.ts';
|
|
16
|
+
import { addSkillToLocalLock, computeSkillFolderHash, readLocalLock } from './local-lock.ts';
|
|
17
|
+
import type { Skill, AgentType } from './types.ts';
|
|
18
|
+
import { track } from './telemetry.ts';
|
|
19
|
+
import { detectAgent, getAgentType } from './detect-agent.ts';
|
|
20
|
+
|
|
21
|
+
const isCancelled = (value: unknown): value is symbol => typeof value === 'symbol';
|
|
22
|
+
|
|
23
|
+
export interface SyncOptions {
|
|
24
|
+
agent?: string[];
|
|
25
|
+
yes?: boolean;
|
|
26
|
+
force?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Shortens a path for display: replaces homedir with ~ and cwd with .
|
|
31
|
+
*/
|
|
32
|
+
function shortenPath(fullPath: string, cwd: string): string {
|
|
33
|
+
const home = homedir();
|
|
34
|
+
if (fullPath === home || fullPath.startsWith(home + sep)) {
|
|
35
|
+
return '~' + fullPath.slice(home.length);
|
|
36
|
+
}
|
|
37
|
+
if (fullPath === cwd || fullPath.startsWith(cwd + sep)) {
|
|
38
|
+
return '.' + fullPath.slice(cwd.length);
|
|
39
|
+
}
|
|
40
|
+
return fullPath;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Crawl node_modules for SKILL.md files.
|
|
45
|
+
* Searches both top-level packages and scoped packages (@org/pkg).
|
|
46
|
+
* Returns discovered skills with their source package name.
|
|
47
|
+
*/
|
|
48
|
+
async function discoverNodeModuleSkills(
|
|
49
|
+
cwd: string
|
|
50
|
+
): Promise<Array<Skill & { packageName: string }>> {
|
|
51
|
+
const nodeModulesDir = join(cwd, 'node_modules');
|
|
52
|
+
const skills: Array<Skill & { packageName: string }> = [];
|
|
53
|
+
|
|
54
|
+
let topNames: string[];
|
|
55
|
+
try {
|
|
56
|
+
topNames = await readdir(nodeModulesDir);
|
|
57
|
+
} catch {
|
|
58
|
+
return skills;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const processPackageDir = async (pkgDir: string, packageName: string) => {
|
|
62
|
+
// Check for SKILL.md at package root
|
|
63
|
+
const rootSkill = await parseSkillMd(join(pkgDir, 'SKILL.md'));
|
|
64
|
+
if (rootSkill) {
|
|
65
|
+
skills.push({ ...rootSkill, packageName });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check common skill locations within the package
|
|
70
|
+
const searchDirs = [pkgDir, join(pkgDir, 'skills'), join(pkgDir, '.agents', 'skills')];
|
|
71
|
+
|
|
72
|
+
for (const searchDir of searchDirs) {
|
|
73
|
+
try {
|
|
74
|
+
const entries = await readdir(searchDir);
|
|
75
|
+
for (const name of entries) {
|
|
76
|
+
const skillDir = join(searchDir, name);
|
|
77
|
+
try {
|
|
78
|
+
const s = await stat(skillDir);
|
|
79
|
+
if (!s.isDirectory()) continue;
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const skill = await parseSkillMd(join(skillDir, 'SKILL.md'));
|
|
84
|
+
if (skill) {
|
|
85
|
+
skills.push({ ...skill, packageName });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Directory doesn't exist
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
await Promise.all(
|
|
95
|
+
topNames.map(async (name) => {
|
|
96
|
+
if (name.startsWith('.')) return;
|
|
97
|
+
|
|
98
|
+
const fullPath = join(nodeModulesDir, name);
|
|
99
|
+
try {
|
|
100
|
+
const s = await stat(fullPath);
|
|
101
|
+
if (!s.isDirectory()) return;
|
|
102
|
+
} catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (name.startsWith('@')) {
|
|
107
|
+
// Scoped package: read @org/* entries
|
|
108
|
+
try {
|
|
109
|
+
const scopeNames = await readdir(fullPath);
|
|
110
|
+
await Promise.all(
|
|
111
|
+
scopeNames.map(async (scopedName) => {
|
|
112
|
+
const scopedPath = join(fullPath, scopedName);
|
|
113
|
+
try {
|
|
114
|
+
const s = await stat(scopedPath);
|
|
115
|
+
if (!s.isDirectory()) return;
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await processPackageDir(scopedPath, `${name}/${scopedName}`);
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
} catch {
|
|
123
|
+
// Scope directory not readable
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
await processPackageDir(fullPath, name);
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return skills;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function runSync(args: string[], options: SyncOptions = {}): Promise<void> {
|
|
135
|
+
const cwd = process.cwd();
|
|
136
|
+
|
|
137
|
+
// Auto-enable non-interactive mode when running inside an AI agent
|
|
138
|
+
const agentResult = await detectAgent();
|
|
139
|
+
if (agentResult.isAgent) {
|
|
140
|
+
options.yes = true;
|
|
141
|
+
if (!options.agent || options.agent.length === 0) {
|
|
142
|
+
const mappedAgent = getAgentType(agentResult.agent.name);
|
|
143
|
+
if (mappedAgent) {
|
|
144
|
+
const agentList: AgentType[] = [mappedAgent];
|
|
145
|
+
for (const ua of getUniversalAgents()) {
|
|
146
|
+
if (!agentList.includes(ua)) agentList.push(ua);
|
|
147
|
+
}
|
|
148
|
+
options.agent = agentList;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log();
|
|
154
|
+
if (!agentResult.isAgent) {
|
|
155
|
+
p.intro(pc.bgCyan(pc.black(' skills experimental_sync ')));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (agentResult.isAgent) {
|
|
159
|
+
p.log.info(
|
|
160
|
+
pc.bgCyan(pc.black(pc.bold(` ${agentResult.agent.name} `))) +
|
|
161
|
+
' ' +
|
|
162
|
+
'Agent detected — installing non-interactively'
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const spinner = p.spinner();
|
|
167
|
+
|
|
168
|
+
// 1. Discover skills from node_modules
|
|
169
|
+
spinner.start('Scanning node_modules for skills...');
|
|
170
|
+
const discoveredSkills = await discoverNodeModuleSkills(cwd);
|
|
171
|
+
|
|
172
|
+
if (discoveredSkills.length === 0) {
|
|
173
|
+
spinner.stop(pc.yellow('No skills found'));
|
|
174
|
+
p.outro(pc.dim('No SKILL.md files found in node_modules.'));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
spinner.stop(
|
|
179
|
+
`Found ${pc.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? 's' : ''} in node_modules`
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Show discovered skills
|
|
183
|
+
for (const skill of discoveredSkills) {
|
|
184
|
+
p.log.info(`${pc.cyan(skill.name)} ${pc.dim(`from ${skill.packageName}`)}`);
|
|
185
|
+
if (skill.description) {
|
|
186
|
+
p.log.message(pc.dim(` ${skill.description}`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 2. Check which skills are already up-to-date via local lock
|
|
191
|
+
const localLock = await readLocalLock(cwd);
|
|
192
|
+
const toInstall: Array<Skill & { packageName: string }> = [];
|
|
193
|
+
const upToDate: string[] = [];
|
|
194
|
+
|
|
195
|
+
if (options.force) {
|
|
196
|
+
toInstall.push(...discoveredSkills);
|
|
197
|
+
p.log.info(pc.dim('Force mode: reinstalling all skills'));
|
|
198
|
+
} else {
|
|
199
|
+
for (const skill of discoveredSkills) {
|
|
200
|
+
const existingEntry = localLock.skills[skill.name];
|
|
201
|
+
if (existingEntry) {
|
|
202
|
+
// Compute current hash and compare
|
|
203
|
+
const currentHash = await computeSkillFolderHash(skill.path);
|
|
204
|
+
if (currentHash === existingEntry.computedHash) {
|
|
205
|
+
upToDate.push(skill.name);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
toInstall.push(skill);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (upToDate.length > 0) {
|
|
213
|
+
p.log.info(
|
|
214
|
+
pc.dim(`${upToDate.length} skill${upToDate.length !== 1 ? 's' : ''} already up to date`)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (toInstall.length === 0) {
|
|
219
|
+
console.log();
|
|
220
|
+
p.outro(pc.green('All skills are up to date.'));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
p.log.info(`${toInstall.length} skill${toInstall.length !== 1 ? 's' : ''} to install/update`);
|
|
226
|
+
|
|
227
|
+
// 3. Select agents
|
|
228
|
+
let targetAgents: AgentType[];
|
|
229
|
+
const validAgents = Object.keys(agents);
|
|
230
|
+
const universalAgents = getUniversalAgents();
|
|
231
|
+
const visibleUniversalAgents = getVisibleUniversalAgents();
|
|
232
|
+
|
|
233
|
+
if (options.agent?.includes('*')) {
|
|
234
|
+
targetAgents = validAgents as AgentType[];
|
|
235
|
+
p.log.info(`Installing to all ${targetAgents.length} agents`);
|
|
236
|
+
} else if (options.agent && options.agent.length > 0) {
|
|
237
|
+
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
238
|
+
if (invalidAgents.length > 0) {
|
|
239
|
+
p.log.error(`Invalid agents: ${invalidAgents.join(', ')}`);
|
|
240
|
+
p.log.info(`Valid agents: ${validAgents.join(', ')}`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
targetAgents = options.agent as AgentType[];
|
|
244
|
+
} else {
|
|
245
|
+
spinner.start('Loading agents...');
|
|
246
|
+
const installedAgents = await detectInstalledAgents();
|
|
247
|
+
const totalAgents = Object.keys(agents).length;
|
|
248
|
+
spinner.stop(`${totalAgents} agents`);
|
|
249
|
+
|
|
250
|
+
if (installedAgents.length === 0) {
|
|
251
|
+
if (options.yes) {
|
|
252
|
+
targetAgents = universalAgents;
|
|
253
|
+
p.log.info('Installing to universal agents');
|
|
254
|
+
} else {
|
|
255
|
+
const otherAgents = getNonUniversalAgents();
|
|
256
|
+
|
|
257
|
+
const otherChoices = otherAgents.map((a) => ({
|
|
258
|
+
value: a,
|
|
259
|
+
label: agents[a].displayName,
|
|
260
|
+
hint: agents[a].skillsDir,
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
const selected = await searchMultiselect({
|
|
264
|
+
message: 'Which agents do you want to install to?',
|
|
265
|
+
items: otherChoices,
|
|
266
|
+
initialSelected: [],
|
|
267
|
+
lockedSection: {
|
|
268
|
+
title: 'Universal (.agents/skills)',
|
|
269
|
+
items: visibleUniversalAgents.map((a) => ({
|
|
270
|
+
value: a,
|
|
271
|
+
label: agents[a].displayName,
|
|
272
|
+
})),
|
|
273
|
+
hiddenCount: universalAgents.length - visibleUniversalAgents.length,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (isCancelled(selected)) {
|
|
278
|
+
p.cancel('Sync cancelled');
|
|
279
|
+
process.exit(0);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
targetAgents = selected as AgentType[];
|
|
283
|
+
}
|
|
284
|
+
} else if (installedAgents.length === 1 || options.yes) {
|
|
285
|
+
// Ensure universal agents are included
|
|
286
|
+
targetAgents = [...installedAgents];
|
|
287
|
+
for (const ua of universalAgents) {
|
|
288
|
+
if (!targetAgents.includes(ua)) {
|
|
289
|
+
targetAgents.push(ua);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
const otherAgents = getNonUniversalAgents().filter((a) => installedAgents.includes(a));
|
|
294
|
+
|
|
295
|
+
const otherChoices = otherAgents.map((a) => ({
|
|
296
|
+
value: a,
|
|
297
|
+
label: agents[a].displayName,
|
|
298
|
+
hint: agents[a].skillsDir,
|
|
299
|
+
}));
|
|
300
|
+
|
|
301
|
+
const selected = await searchMultiselect({
|
|
302
|
+
message: 'Which agents do you want to install to?',
|
|
303
|
+
items: otherChoices,
|
|
304
|
+
initialSelected: installedAgents.filter((a) => !universalAgents.includes(a)),
|
|
305
|
+
lockedSection: {
|
|
306
|
+
title: 'Universal (.agents/skills)',
|
|
307
|
+
items: visibleUniversalAgents.map((a) => ({
|
|
308
|
+
value: a,
|
|
309
|
+
label: agents[a].displayName,
|
|
310
|
+
})),
|
|
311
|
+
hiddenCount: universalAgents.length - visibleUniversalAgents.length,
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (isCancelled(selected)) {
|
|
316
|
+
p.cancel('Sync cancelled');
|
|
317
|
+
process.exit(0);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
targetAgents = selected as AgentType[];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 4. Build summary
|
|
325
|
+
const summaryLines: string[] = [];
|
|
326
|
+
for (const skill of toInstall) {
|
|
327
|
+
const canonicalPath = getCanonicalPath(skill.name, { global: false });
|
|
328
|
+
const shortCanonical = shortenPath(canonicalPath, cwd);
|
|
329
|
+
summaryLines.push(`${pc.cyan(skill.name)} ${pc.dim(`← ${skill.packageName}`)}`);
|
|
330
|
+
summaryLines.push(` ${pc.dim(shortCanonical)}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
console.log();
|
|
334
|
+
p.note(summaryLines.join('\n'), 'Sync Summary');
|
|
335
|
+
|
|
336
|
+
if (!options.yes) {
|
|
337
|
+
const confirmed = await p.confirm({ message: 'Proceed with sync?' });
|
|
338
|
+
|
|
339
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
340
|
+
p.cancel('Sync cancelled');
|
|
341
|
+
process.exit(0);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 5. Install skills (always project-scoped, always symlink)
|
|
346
|
+
spinner.start('Syncing skills...');
|
|
347
|
+
|
|
348
|
+
const results: Array<{
|
|
349
|
+
skill: string;
|
|
350
|
+
packageName: string;
|
|
351
|
+
agent: string;
|
|
352
|
+
success: boolean;
|
|
353
|
+
path: string;
|
|
354
|
+
canonicalPath?: string;
|
|
355
|
+
error?: string;
|
|
356
|
+
}> = [];
|
|
357
|
+
|
|
358
|
+
for (const skill of toInstall) {
|
|
359
|
+
for (const agent of targetAgents) {
|
|
360
|
+
const result = await installSkillForAgent(skill, agent, {
|
|
361
|
+
global: false,
|
|
362
|
+
cwd,
|
|
363
|
+
mode: 'symlink',
|
|
364
|
+
});
|
|
365
|
+
results.push({
|
|
366
|
+
skill: skill.name,
|
|
367
|
+
packageName: skill.packageName,
|
|
368
|
+
agent: agents[agent].displayName,
|
|
369
|
+
success: result.success,
|
|
370
|
+
path: result.path,
|
|
371
|
+
canonicalPath: result.canonicalPath,
|
|
372
|
+
error: result.error,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
spinner.stop('Sync complete');
|
|
378
|
+
|
|
379
|
+
// 6. Update local lock file
|
|
380
|
+
const successful = results.filter((r) => r.success);
|
|
381
|
+
const failed = results.filter((r) => !r.success);
|
|
382
|
+
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
383
|
+
|
|
384
|
+
for (const skill of toInstall) {
|
|
385
|
+
if (successfulSkillNames.has(skill.name)) {
|
|
386
|
+
try {
|
|
387
|
+
const computedHash = await computeSkillFolderHash(skill.path);
|
|
388
|
+
await addSkillToLocalLock(
|
|
389
|
+
skill.name,
|
|
390
|
+
{
|
|
391
|
+
source: skill.packageName,
|
|
392
|
+
sourceType: 'node_modules',
|
|
393
|
+
computedHash,
|
|
394
|
+
},
|
|
395
|
+
cwd
|
|
396
|
+
);
|
|
397
|
+
} catch {
|
|
398
|
+
// Don't fail sync if lock file update fails
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 7. Display results
|
|
404
|
+
console.log();
|
|
405
|
+
|
|
406
|
+
if (successful.length > 0) {
|
|
407
|
+
const bySkill = new Map<string, typeof results>();
|
|
408
|
+
for (const r of successful) {
|
|
409
|
+
const skillResults = bySkill.get(r.skill) || [];
|
|
410
|
+
skillResults.push(r);
|
|
411
|
+
bySkill.set(r.skill, skillResults);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const resultLines: string[] = [];
|
|
415
|
+
for (const [skillName, skillResults] of bySkill) {
|
|
416
|
+
const firstResult = skillResults[0]!;
|
|
417
|
+
const pkg = toInstall.find((s) => s.name === skillName)?.packageName;
|
|
418
|
+
if (firstResult.canonicalPath) {
|
|
419
|
+
const shortPath = shortenPath(firstResult.canonicalPath, cwd);
|
|
420
|
+
resultLines.push(`${pc.green('✓')} ${skillName} ${pc.dim(`← ${pkg}`)}`);
|
|
421
|
+
resultLines.push(` ${pc.dim(shortPath)}`);
|
|
422
|
+
} else {
|
|
423
|
+
resultLines.push(`${pc.green('✓')} ${skillName} ${pc.dim(`← ${pkg}`)}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const skillCount = bySkill.size;
|
|
428
|
+
const title = pc.green(`Synced ${skillCount} skill${skillCount !== 1 ? 's' : ''}`);
|
|
429
|
+
p.note(resultLines.join('\n'), title);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (failed.length > 0) {
|
|
433
|
+
console.log();
|
|
434
|
+
p.log.error(pc.red(`Failed to install ${failed.length}`));
|
|
435
|
+
for (const r of failed) {
|
|
436
|
+
p.log.message(` ${pc.red('✗')} ${r.skill} → ${r.agent}: ${pc.dim(r.error)}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Track telemetry
|
|
441
|
+
track({
|
|
442
|
+
event: 'experimental_sync',
|
|
443
|
+
skillCount: String(toInstall.length),
|
|
444
|
+
successCount: String(successfulSkillNames.size),
|
|
445
|
+
agents: targetAgents.join(','),
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
console.log();
|
|
449
|
+
p.outro(
|
|
450
|
+
pc.green('Done!') + pc.dim(' Review skills before use; they run with full agent permissions.')
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function parseSyncOptions(args: string[]): { options: SyncOptions } {
|
|
455
|
+
const options: SyncOptions = {};
|
|
456
|
+
|
|
457
|
+
for (let i = 0; i < args.length; i++) {
|
|
458
|
+
const arg = args[i];
|
|
459
|
+
|
|
460
|
+
if (arg === '-y' || arg === '--yes') {
|
|
461
|
+
options.yes = true;
|
|
462
|
+
} else if (arg === '-f' || arg === '--force') {
|
|
463
|
+
options.force = true;
|
|
464
|
+
} else if (arg === '-a' || arg === '--agent') {
|
|
465
|
+
options.agent = options.agent || [];
|
|
466
|
+
i++;
|
|
467
|
+
let nextArg = args[i];
|
|
468
|
+
while (i < args.length && nextArg && !nextArg.startsWith('-')) {
|
|
469
|
+
options.agent.push(nextArg);
|
|
470
|
+
i++;
|
|
471
|
+
nextArg = args[i];
|
|
472
|
+
}
|
|
473
|
+
i--;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return { options };
|
|
478
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
const TELEMETRY_URL = 'https://add-skill.vercel.sh/t';
|
|
2
|
+
const AUDIT_URL = 'https://add-skill.vercel.sh/audit';
|
|
3
|
+
|
|
4
|
+
interface InstallTelemetryData {
|
|
5
|
+
event: 'install';
|
|
6
|
+
source: string;
|
|
7
|
+
skills: string;
|
|
8
|
+
agents: string;
|
|
9
|
+
global?: '1';
|
|
10
|
+
skillFiles?: string; // JSON stringified { skillName: relativePath }
|
|
11
|
+
/**
|
|
12
|
+
* Source type for different hosts:
|
|
13
|
+
* - 'github': GitHub repository (default, uses raw.githubusercontent.com)
|
|
14
|
+
* - 'raw': Direct URL to SKILL.md (generic raw URL)
|
|
15
|
+
* - Provider IDs like 'mintlify', 'huggingface', etc.
|
|
16
|
+
*/
|
|
17
|
+
sourceType?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface RemoveTelemetryData {
|
|
21
|
+
event: 'remove';
|
|
22
|
+
source?: string;
|
|
23
|
+
skills: string;
|
|
24
|
+
agents: string;
|
|
25
|
+
global?: '1';
|
|
26
|
+
sourceType?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface UpdateTelemetryData {
|
|
30
|
+
event: 'update';
|
|
31
|
+
scope?: string;
|
|
32
|
+
skillCount: string;
|
|
33
|
+
successCount: string;
|
|
34
|
+
failCount: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface FindTelemetryData {
|
|
38
|
+
event: 'find';
|
|
39
|
+
query: string;
|
|
40
|
+
resultCount: string;
|
|
41
|
+
interactive?: '1';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface SyncTelemetryData {
|
|
45
|
+
event: 'experimental_sync';
|
|
46
|
+
skillCount: string;
|
|
47
|
+
successCount: string;
|
|
48
|
+
agents: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type TelemetryData =
|
|
52
|
+
| InstallTelemetryData
|
|
53
|
+
| RemoveTelemetryData
|
|
54
|
+
| UpdateTelemetryData
|
|
55
|
+
| FindTelemetryData
|
|
56
|
+
| SyncTelemetryData;
|
|
57
|
+
|
|
58
|
+
let cliVersion: string | null = null;
|
|
59
|
+
let detectedAgentName: string | null = null;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set the detected AI agent name for telemetry tracking.
|
|
63
|
+
* Called once during agent detection, then included in all telemetry events.
|
|
64
|
+
*/
|
|
65
|
+
export function setDetectedAgent(agentName: string | null): void {
|
|
66
|
+
detectedAgentName = agentName;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isCI(): boolean {
|
|
70
|
+
return !!(
|
|
71
|
+
process.env.CI ||
|
|
72
|
+
process.env.GITHUB_ACTIONS ||
|
|
73
|
+
process.env.GITLAB_CI ||
|
|
74
|
+
process.env.CIRCLECI ||
|
|
75
|
+
process.env.TRAVIS ||
|
|
76
|
+
process.env.BUILDKITE ||
|
|
77
|
+
process.env.JENKINS_URL ||
|
|
78
|
+
process.env.TEAMCITY_VERSION
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isEnabled(): boolean {
|
|
83
|
+
return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function setVersion(version: string): void {
|
|
87
|
+
cliVersion = version;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Security audit data ───
|
|
91
|
+
|
|
92
|
+
export interface PartnerAudit {
|
|
93
|
+
risk: 'safe' | 'low' | 'medium' | 'high' | 'critical' | 'unknown';
|
|
94
|
+
alerts?: number;
|
|
95
|
+
score?: number;
|
|
96
|
+
analyzedAt: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type SkillAuditData = Record<string, PartnerAudit>;
|
|
100
|
+
export type AuditResponse = Record<string, SkillAuditData>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Fetch security audit results for skills from the audit API.
|
|
104
|
+
* Returns null on any error or timeout — never blocks installation.
|
|
105
|
+
*/
|
|
106
|
+
export async function fetchAuditData(
|
|
107
|
+
source: string,
|
|
108
|
+
skillSlugs: string[],
|
|
109
|
+
timeoutMs = 3000
|
|
110
|
+
): Promise<AuditResponse | null> {
|
|
111
|
+
if (skillSlugs.length === 0) return null;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const params = new URLSearchParams({
|
|
115
|
+
source,
|
|
116
|
+
skills: skillSlugs.join(','),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
121
|
+
|
|
122
|
+
const response = await fetch(`${AUDIT_URL}?${params.toString()}`, {
|
|
123
|
+
signal: controller.signal,
|
|
124
|
+
});
|
|
125
|
+
clearTimeout(timeout);
|
|
126
|
+
|
|
127
|
+
if (!response.ok) return null;
|
|
128
|
+
return (await response.json()) as AuditResponse;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Pending telemetry promises — awaited before CLI exit so we don't lose data,
|
|
135
|
+
// but never block the main workflow.
|
|
136
|
+
const pendingTelemetry: Promise<void>[] = [];
|
|
137
|
+
|
|
138
|
+
export function track(data: TelemetryData): void {
|
|
139
|
+
if (!isEnabled()) return;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const params = new URLSearchParams();
|
|
143
|
+
|
|
144
|
+
// Add version
|
|
145
|
+
if (cliVersion) {
|
|
146
|
+
params.set('v', cliVersion);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Add CI flag if running in CI
|
|
150
|
+
if (isCI()) {
|
|
151
|
+
params.set('ci', '1');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Add detected AI agent name
|
|
155
|
+
if (detectedAgentName) {
|
|
156
|
+
params.set('agent', detectedAgentName);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Add event data
|
|
160
|
+
for (const [key, value] of Object.entries(data)) {
|
|
161
|
+
if (value !== undefined && value !== null) {
|
|
162
|
+
params.set(key, String(value));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fire and forget during the workflow, but track the promise so
|
|
167
|
+
// flushTelemetry() can await it before the process exits.
|
|
168
|
+
const p = fetch(`${TELEMETRY_URL}?${params.toString()}`)
|
|
169
|
+
.catch(() => {})
|
|
170
|
+
.then(() => {});
|
|
171
|
+
pendingTelemetry.push(p);
|
|
172
|
+
} catch {
|
|
173
|
+
// Silently fail - telemetry should never break the CLI
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Wait for all in-flight telemetry requests to settle.
|
|
179
|
+
* Called once at CLI exit so the process doesn't hang on open sockets
|
|
180
|
+
* but also doesn't drop data by exiting too early.
|
|
181
|
+
*/
|
|
182
|
+
export async function flushTelemetry(timeoutMs = 5000): Promise<void> {
|
|
183
|
+
if (pendingTelemetry.length === 0) return;
|
|
184
|
+
const timeout = new Promise<void>((resolve) => setTimeout(resolve, timeoutMs));
|
|
185
|
+
await Promise.race([Promise.all(pendingTelemetry), timeout]);
|
|
186
|
+
}
|