@rbbtsn0w/adg 0.1.0-alpha.1 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/adg.js +703 -0
- package/dist/src/adapters/anthropic.js +54 -0
- package/dist/src/adapters/index.js +10 -0
- package/dist/src/adapters/openai.js +30 -0
- package/dist/src/adapters/reverse.js +53 -0
- package/dist/src/agents/claude.js +118 -0
- package/dist/src/agents/codex.js +61 -0
- package/{src/agents/index.ts → dist/src/agents/index.js} +6 -8
- package/dist/src/agents/registry.js +24 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/commands/adapt.js +26 -0
- package/dist/src/commands/import.js +51 -0
- package/dist/src/commands/init.js +104 -0
- package/dist/src/commands/install.js +257 -0
- package/dist/src/commands/link.js +34 -0
- package/dist/src/commands/list.js +19 -0
- package/dist/src/commands/marketplace.js +124 -0
- package/dist/src/commands/migrate.js +60 -0
- package/dist/src/commands/multiselect-skills.js +103 -0
- package/dist/src/commands/remove.js +102 -0
- package/dist/src/commands/select-agents.js +40 -0
- package/dist/src/commands/select-components.js +61 -0
- package/dist/src/commands/select-plugins.js +25 -0
- package/dist/src/commands/select-scope.js +20 -0
- package/dist/src/commands/update.js +50 -0
- package/dist/src/commands/validate.js +50 -0
- package/dist/src/components.js +90 -0
- package/dist/src/deps.js +46 -0
- package/dist/src/fsutil.js +32 -0
- package/dist/src/hash.js +51 -0
- package/dist/src/lock.js +51 -0
- package/dist/src/manifest.js +110 -0
- package/dist/src/marketplace.js +39 -0
- package/{src/package.ts → dist/src/package.js} +37 -42
- package/{src/paths.ts → dist/src/paths.js} +54 -60
- package/dist/src/semver.js +55 -0
- package/dist/src/skills.js +79 -0
- package/dist/src/sources.js +122 -0
- package/dist/src/types.js +19 -0
- package/dist/vendor/skills/package.json +143 -0
- package/dist/vendor/skills/src/add.js +1663 -0
- package/dist/vendor/skills/src/agents.js +729 -0
- package/dist/vendor/skills/src/blob.js +436 -0
- package/dist/vendor/skills/src/cli.js +340 -0
- package/dist/vendor/skills/src/constants.js +3 -0
- package/dist/vendor/skills/src/detect-agent.js +56 -0
- package/dist/vendor/skills/src/find.js +294 -0
- package/dist/vendor/skills/src/frontmatter.js +13 -0
- package/dist/vendor/skills/src/git-tree.js +32 -0
- package/dist/vendor/skills/src/git.js +235 -0
- package/dist/vendor/skills/src/install.js +75 -0
- package/dist/vendor/skills/src/installer.js +924 -0
- package/dist/vendor/skills/src/list.js +201 -0
- package/dist/vendor/skills/src/local-lock.js +109 -0
- package/dist/vendor/skills/src/plugin-manifest.js +152 -0
- package/dist/vendor/skills/src/prompts/search-multiselect.js +312 -0
- package/dist/vendor/skills/src/providers/index.js +4 -0
- package/dist/vendor/skills/src/providers/registry.js +42 -0
- package/dist/vendor/skills/src/providers/types.js +1 -0
- package/dist/vendor/skills/src/providers/wellknown.js +625 -0
- package/dist/vendor/skills/src/remove.js +263 -0
- package/dist/vendor/skills/src/sanitize.js +57 -0
- package/dist/vendor/skills/src/self-cli.js +15 -0
- package/dist/vendor/skills/src/skill-lock.js +237 -0
- package/dist/vendor/skills/src/skills.js +264 -0
- package/dist/vendor/skills/src/source-parser.js +367 -0
- package/dist/vendor/skills/src/sync.js +404 -0
- package/dist/vendor/skills/src/telemetry.js +101 -0
- package/dist/vendor/skills/src/test-utils.js +59 -0
- package/dist/vendor/skills/src/types.js +1 -0
- package/dist/vendor/skills/src/update-source.js +76 -0
- package/dist/vendor/skills/src/update.js +590 -0
- package/dist/vendor/skills/src/use.js +505 -0
- package/package.json +15 -7
- package/bin/adg.ts +0 -758
- package/src/adapters/anthropic.ts +0 -54
- package/src/adapters/index.ts +0 -24
- package/src/adapters/openai.ts +0 -37
- package/src/adapters/reverse.ts +0 -60
- package/src/agents/claude.ts +0 -124
- package/src/agents/codex.ts +0 -67
- package/src/agents/registry.ts +0 -30
- package/src/agents/types.ts +0 -47
- package/src/commands/adapt.ts +0 -36
- package/src/commands/import.ts +0 -69
- package/src/commands/init.ts +0 -146
- package/src/commands/install.ts +0 -411
- package/src/commands/link.ts +0 -61
- package/src/commands/list.ts +0 -28
- package/src/commands/marketplace.ts +0 -198
- package/src/commands/migrate.ts +0 -84
- package/src/commands/multiselect-skills.ts +0 -137
- package/src/commands/remove.ts +0 -136
- package/src/commands/select-agents.ts +0 -45
- package/src/commands/select-components.ts +0 -66
- package/src/commands/select-plugins.ts +0 -28
- package/src/commands/select-scope.ts +0 -21
- package/src/commands/update.ts +0 -85
- package/src/commands/validate.ts +0 -57
- package/src/components.ts +0 -90
- package/src/deps.ts +0 -64
- package/src/fsutil.ts +0 -38
- package/src/hash.ts +0 -61
- package/src/lock.ts +0 -57
- package/src/manifest.ts +0 -113
- package/src/marketplace.ts +0 -41
- package/src/semver.ts +0 -67
- package/src/skills.ts +0 -88
- package/src/sources.ts +0 -159
- package/src/types.ts +0 -140
|
@@ -0,0 +1,404 @@
|
|
|
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.js";
|
|
7
|
+
import { installSkillForAgent, getCanonicalPath } from "./installer.js";
|
|
8
|
+
import { detectInstalledAgents, agents, getUniversalAgents, getVisibleUniversalAgents, getNonUniversalAgents, } from "./agents.js";
|
|
9
|
+
import { searchMultiselect } from "./prompts/search-multiselect.js";
|
|
10
|
+
import { addSkillToLocalLock, computeSkillFolderHash, readLocalLock } from "./local-lock.js";
|
|
11
|
+
import { track } from "./telemetry.js";
|
|
12
|
+
import { detectAgent, getAgentType } from "./detect-agent.js";
|
|
13
|
+
const isCancelled = (value) => typeof value === 'symbol';
|
|
14
|
+
/**
|
|
15
|
+
* Shortens a path for display: replaces homedir with ~ and cwd with .
|
|
16
|
+
*/
|
|
17
|
+
function shortenPath(fullPath, cwd) {
|
|
18
|
+
const home = homedir();
|
|
19
|
+
if (fullPath === home || fullPath.startsWith(home + sep)) {
|
|
20
|
+
return '~' + fullPath.slice(home.length);
|
|
21
|
+
}
|
|
22
|
+
if (fullPath === cwd || fullPath.startsWith(cwd + sep)) {
|
|
23
|
+
return '.' + fullPath.slice(cwd.length);
|
|
24
|
+
}
|
|
25
|
+
return fullPath;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Crawl node_modules for SKILL.md files.
|
|
29
|
+
* Searches both top-level packages and scoped packages (@org/pkg).
|
|
30
|
+
* Returns discovered skills with their source package name.
|
|
31
|
+
*/
|
|
32
|
+
async function discoverNodeModuleSkills(cwd) {
|
|
33
|
+
const nodeModulesDir = join(cwd, 'node_modules');
|
|
34
|
+
const skills = [];
|
|
35
|
+
let topNames;
|
|
36
|
+
try {
|
|
37
|
+
topNames = await readdir(nodeModulesDir);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return skills;
|
|
41
|
+
}
|
|
42
|
+
const processPackageDir = async (pkgDir, packageName) => {
|
|
43
|
+
// Check for SKILL.md at package root
|
|
44
|
+
const rootSkill = await parseSkillMd(join(pkgDir, 'SKILL.md'));
|
|
45
|
+
if (rootSkill) {
|
|
46
|
+
skills.push({ ...rootSkill, packageName });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Check common skill locations within the package
|
|
50
|
+
const searchDirs = [pkgDir, join(pkgDir, 'skills'), join(pkgDir, '.agents', 'skills')];
|
|
51
|
+
for (const searchDir of searchDirs) {
|
|
52
|
+
try {
|
|
53
|
+
const entries = await readdir(searchDir);
|
|
54
|
+
for (const name of entries) {
|
|
55
|
+
const skillDir = join(searchDir, name);
|
|
56
|
+
try {
|
|
57
|
+
const s = await stat(skillDir);
|
|
58
|
+
if (!s.isDirectory())
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const skill = await parseSkillMd(join(skillDir, 'SKILL.md'));
|
|
65
|
+
if (skill) {
|
|
66
|
+
skills.push({ ...skill, packageName });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Directory doesn't exist
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
await Promise.all(topNames.map(async (name) => {
|
|
76
|
+
if (name.startsWith('.'))
|
|
77
|
+
return;
|
|
78
|
+
const fullPath = join(nodeModulesDir, name);
|
|
79
|
+
try {
|
|
80
|
+
const s = await stat(fullPath);
|
|
81
|
+
if (!s.isDirectory())
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (name.startsWith('@')) {
|
|
88
|
+
// Scoped package: read @org/* entries
|
|
89
|
+
try {
|
|
90
|
+
const scopeNames = await readdir(fullPath);
|
|
91
|
+
await Promise.all(scopeNames.map(async (scopedName) => {
|
|
92
|
+
const scopedPath = join(fullPath, scopedName);
|
|
93
|
+
try {
|
|
94
|
+
const s = await stat(scopedPath);
|
|
95
|
+
if (!s.isDirectory())
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await processPackageDir(scopedPath, `${name}/${scopedName}`);
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Scope directory not readable
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
await processPackageDir(fullPath, name);
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
return skills;
|
|
113
|
+
}
|
|
114
|
+
export async function runSync(args, options = {}) {
|
|
115
|
+
const cwd = process.cwd();
|
|
116
|
+
// Auto-enable non-interactive mode when running inside an AI agent
|
|
117
|
+
const agentResult = await detectAgent();
|
|
118
|
+
if (agentResult.isAgent) {
|
|
119
|
+
options.yes = true;
|
|
120
|
+
if (!options.agent || options.agent.length === 0) {
|
|
121
|
+
const mappedAgent = getAgentType(agentResult.agent.name);
|
|
122
|
+
if (mappedAgent) {
|
|
123
|
+
const agentList = [mappedAgent];
|
|
124
|
+
for (const ua of getUniversalAgents()) {
|
|
125
|
+
if (!agentList.includes(ua))
|
|
126
|
+
agentList.push(ua);
|
|
127
|
+
}
|
|
128
|
+
options.agent = agentList;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
console.log();
|
|
133
|
+
if (!agentResult.isAgent) {
|
|
134
|
+
p.intro(pc.bgCyan(pc.black(' skills experimental_sync ')));
|
|
135
|
+
}
|
|
136
|
+
if (agentResult.isAgent) {
|
|
137
|
+
p.log.info(pc.bgCyan(pc.black(pc.bold(` ${agentResult.agent.name} `))) +
|
|
138
|
+
' ' +
|
|
139
|
+
'Agent detected — installing non-interactively');
|
|
140
|
+
}
|
|
141
|
+
const spinner = p.spinner();
|
|
142
|
+
// 1. Discover skills from node_modules
|
|
143
|
+
spinner.start('Scanning node_modules for skills...');
|
|
144
|
+
const discoveredSkills = await discoverNodeModuleSkills(cwd);
|
|
145
|
+
if (discoveredSkills.length === 0) {
|
|
146
|
+
spinner.stop(pc.yellow('No skills found'));
|
|
147
|
+
p.outro(pc.dim('No SKILL.md files found in node_modules.'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
spinner.stop(`Found ${pc.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? 's' : ''} in node_modules`);
|
|
151
|
+
// Show discovered skills
|
|
152
|
+
for (const skill of discoveredSkills) {
|
|
153
|
+
p.log.info(`${pc.cyan(skill.name)} ${pc.dim(`from ${skill.packageName}`)}`);
|
|
154
|
+
if (skill.description) {
|
|
155
|
+
p.log.message(pc.dim(` ${skill.description}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// 2. Check which skills are already up-to-date via local lock
|
|
159
|
+
const localLock = await readLocalLock(cwd);
|
|
160
|
+
const toInstall = [];
|
|
161
|
+
const upToDate = [];
|
|
162
|
+
if (options.force) {
|
|
163
|
+
toInstall.push(...discoveredSkills);
|
|
164
|
+
p.log.info(pc.dim('Force mode: reinstalling all skills'));
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
for (const skill of discoveredSkills) {
|
|
168
|
+
const existingEntry = localLock.skills[skill.name];
|
|
169
|
+
if (existingEntry) {
|
|
170
|
+
// Compute current hash and compare
|
|
171
|
+
const currentHash = await computeSkillFolderHash(skill.path);
|
|
172
|
+
if (currentHash === existingEntry.computedHash) {
|
|
173
|
+
upToDate.push(skill.name);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
toInstall.push(skill);
|
|
178
|
+
}
|
|
179
|
+
if (upToDate.length > 0) {
|
|
180
|
+
p.log.info(pc.dim(`${upToDate.length} skill${upToDate.length !== 1 ? 's' : ''} already up to date`));
|
|
181
|
+
}
|
|
182
|
+
if (toInstall.length === 0) {
|
|
183
|
+
console.log();
|
|
184
|
+
p.outro(pc.green('All skills are up to date.'));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
p.log.info(`${toInstall.length} skill${toInstall.length !== 1 ? 's' : ''} to install/update`);
|
|
189
|
+
// 3. Select agents
|
|
190
|
+
let targetAgents;
|
|
191
|
+
const validAgents = Object.keys(agents);
|
|
192
|
+
const universalAgents = getUniversalAgents();
|
|
193
|
+
const visibleUniversalAgents = getVisibleUniversalAgents();
|
|
194
|
+
if (options.agent?.includes('*')) {
|
|
195
|
+
targetAgents = validAgents;
|
|
196
|
+
p.log.info(`Installing to all ${targetAgents.length} agents`);
|
|
197
|
+
}
|
|
198
|
+
else if (options.agent && options.agent.length > 0) {
|
|
199
|
+
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
200
|
+
if (invalidAgents.length > 0) {
|
|
201
|
+
p.log.error(`Invalid agents: ${invalidAgents.join(', ')}`);
|
|
202
|
+
p.log.info(`Valid agents: ${validAgents.join(', ')}`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
targetAgents = options.agent;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
spinner.start('Loading agents...');
|
|
209
|
+
const installedAgents = await detectInstalledAgents();
|
|
210
|
+
const totalAgents = Object.keys(agents).length;
|
|
211
|
+
spinner.stop(`${totalAgents} agents`);
|
|
212
|
+
if (installedAgents.length === 0) {
|
|
213
|
+
if (options.yes) {
|
|
214
|
+
targetAgents = universalAgents;
|
|
215
|
+
p.log.info('Installing to universal agents');
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
const otherAgents = getNonUniversalAgents();
|
|
219
|
+
const otherChoices = otherAgents.map((a) => ({
|
|
220
|
+
value: a,
|
|
221
|
+
label: agents[a].displayName,
|
|
222
|
+
hint: agents[a].skillsDir,
|
|
223
|
+
}));
|
|
224
|
+
const selected = await searchMultiselect({
|
|
225
|
+
message: 'Which agents do you want to install to?',
|
|
226
|
+
items: otherChoices,
|
|
227
|
+
initialSelected: [],
|
|
228
|
+
lockedSection: {
|
|
229
|
+
title: 'Universal (.agents/skills)',
|
|
230
|
+
items: visibleUniversalAgents.map((a) => ({
|
|
231
|
+
value: a,
|
|
232
|
+
label: agents[a].displayName,
|
|
233
|
+
})),
|
|
234
|
+
hiddenCount: universalAgents.length - visibleUniversalAgents.length,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
if (isCancelled(selected)) {
|
|
238
|
+
p.cancel('Sync cancelled');
|
|
239
|
+
process.exit(0);
|
|
240
|
+
}
|
|
241
|
+
targetAgents = selected;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else if (installedAgents.length === 1 || options.yes) {
|
|
245
|
+
// Ensure universal agents are included
|
|
246
|
+
targetAgents = [...installedAgents];
|
|
247
|
+
for (const ua of universalAgents) {
|
|
248
|
+
if (!targetAgents.includes(ua)) {
|
|
249
|
+
targetAgents.push(ua);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const otherAgents = getNonUniversalAgents().filter((a) => installedAgents.includes(a));
|
|
255
|
+
const otherChoices = otherAgents.map((a) => ({
|
|
256
|
+
value: a,
|
|
257
|
+
label: agents[a].displayName,
|
|
258
|
+
hint: agents[a].skillsDir,
|
|
259
|
+
}));
|
|
260
|
+
const selected = await searchMultiselect({
|
|
261
|
+
message: 'Which agents do you want to install to?',
|
|
262
|
+
items: otherChoices,
|
|
263
|
+
initialSelected: installedAgents.filter((a) => !universalAgents.includes(a)),
|
|
264
|
+
lockedSection: {
|
|
265
|
+
title: 'Universal (.agents/skills)',
|
|
266
|
+
items: visibleUniversalAgents.map((a) => ({
|
|
267
|
+
value: a,
|
|
268
|
+
label: agents[a].displayName,
|
|
269
|
+
})),
|
|
270
|
+
hiddenCount: universalAgents.length - visibleUniversalAgents.length,
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
if (isCancelled(selected)) {
|
|
274
|
+
p.cancel('Sync cancelled');
|
|
275
|
+
process.exit(0);
|
|
276
|
+
}
|
|
277
|
+
targetAgents = selected;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// 4. Build summary
|
|
281
|
+
const summaryLines = [];
|
|
282
|
+
for (const skill of toInstall) {
|
|
283
|
+
const canonicalPath = getCanonicalPath(skill.name, { global: false });
|
|
284
|
+
const shortCanonical = shortenPath(canonicalPath, cwd);
|
|
285
|
+
summaryLines.push(`${pc.cyan(skill.name)} ${pc.dim(`← ${skill.packageName}`)}`);
|
|
286
|
+
summaryLines.push(` ${pc.dim(shortCanonical)}`);
|
|
287
|
+
}
|
|
288
|
+
console.log();
|
|
289
|
+
p.note(summaryLines.join('\n'), 'Sync Summary');
|
|
290
|
+
if (!options.yes) {
|
|
291
|
+
const confirmed = await p.confirm({ message: 'Proceed with sync?' });
|
|
292
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
293
|
+
p.cancel('Sync cancelled');
|
|
294
|
+
process.exit(0);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// 5. Install skills (always project-scoped, always symlink)
|
|
298
|
+
spinner.start('Syncing skills...');
|
|
299
|
+
const results = [];
|
|
300
|
+
for (const skill of toInstall) {
|
|
301
|
+
for (const agent of targetAgents) {
|
|
302
|
+
const result = await installSkillForAgent(skill, agent, {
|
|
303
|
+
global: false,
|
|
304
|
+
cwd,
|
|
305
|
+
mode: 'symlink',
|
|
306
|
+
});
|
|
307
|
+
results.push({
|
|
308
|
+
skill: skill.name,
|
|
309
|
+
packageName: skill.packageName,
|
|
310
|
+
agent: agents[agent].displayName,
|
|
311
|
+
success: result.success,
|
|
312
|
+
path: result.path,
|
|
313
|
+
canonicalPath: result.canonicalPath,
|
|
314
|
+
error: result.error,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
spinner.stop('Sync complete');
|
|
319
|
+
// 6. Update local lock file
|
|
320
|
+
const successful = results.filter((r) => r.success);
|
|
321
|
+
const failed = results.filter((r) => !r.success);
|
|
322
|
+
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
323
|
+
for (const skill of toInstall) {
|
|
324
|
+
if (successfulSkillNames.has(skill.name)) {
|
|
325
|
+
try {
|
|
326
|
+
const computedHash = await computeSkillFolderHash(skill.path);
|
|
327
|
+
await addSkillToLocalLock(skill.name, {
|
|
328
|
+
source: skill.packageName,
|
|
329
|
+
sourceType: 'node_modules',
|
|
330
|
+
computedHash,
|
|
331
|
+
}, cwd);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// Don't fail sync if lock file update fails
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// 7. Display results
|
|
339
|
+
console.log();
|
|
340
|
+
if (successful.length > 0) {
|
|
341
|
+
const bySkill = new Map();
|
|
342
|
+
for (const r of successful) {
|
|
343
|
+
const skillResults = bySkill.get(r.skill) || [];
|
|
344
|
+
skillResults.push(r);
|
|
345
|
+
bySkill.set(r.skill, skillResults);
|
|
346
|
+
}
|
|
347
|
+
const resultLines = [];
|
|
348
|
+
for (const [skillName, skillResults] of bySkill) {
|
|
349
|
+
const firstResult = skillResults[0];
|
|
350
|
+
const pkg = toInstall.find((s) => s.name === skillName)?.packageName;
|
|
351
|
+
if (firstResult.canonicalPath) {
|
|
352
|
+
const shortPath = shortenPath(firstResult.canonicalPath, cwd);
|
|
353
|
+
resultLines.push(`${pc.green('✓')} ${skillName} ${pc.dim(`← ${pkg}`)}`);
|
|
354
|
+
resultLines.push(` ${pc.dim(shortPath)}`);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
resultLines.push(`${pc.green('✓')} ${skillName} ${pc.dim(`← ${pkg}`)}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const skillCount = bySkill.size;
|
|
361
|
+
const title = pc.green(`Synced ${skillCount} skill${skillCount !== 1 ? 's' : ''}`);
|
|
362
|
+
p.note(resultLines.join('\n'), title);
|
|
363
|
+
}
|
|
364
|
+
if (failed.length > 0) {
|
|
365
|
+
console.log();
|
|
366
|
+
p.log.error(pc.red(`Failed to install ${failed.length}`));
|
|
367
|
+
for (const r of failed) {
|
|
368
|
+
p.log.message(` ${pc.red('✗')} ${r.skill} → ${r.agent}: ${pc.dim(r.error)}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// Track telemetry
|
|
372
|
+
track({
|
|
373
|
+
event: 'experimental_sync',
|
|
374
|
+
skillCount: String(toInstall.length),
|
|
375
|
+
successCount: String(successfulSkillNames.size),
|
|
376
|
+
agents: targetAgents.join(','),
|
|
377
|
+
});
|
|
378
|
+
console.log();
|
|
379
|
+
p.outro(pc.green('Done!') + pc.dim(' Review skills before use; they run with full agent permissions.'));
|
|
380
|
+
}
|
|
381
|
+
export function parseSyncOptions(args) {
|
|
382
|
+
const options = {};
|
|
383
|
+
for (let i = 0; i < args.length; i++) {
|
|
384
|
+
const arg = args[i];
|
|
385
|
+
if (arg === '-y' || arg === '--yes') {
|
|
386
|
+
options.yes = true;
|
|
387
|
+
}
|
|
388
|
+
else if (arg === '-f' || arg === '--force') {
|
|
389
|
+
options.force = true;
|
|
390
|
+
}
|
|
391
|
+
else if (arg === '-a' || arg === '--agent') {
|
|
392
|
+
options.agent = options.agent || [];
|
|
393
|
+
i++;
|
|
394
|
+
let nextArg = args[i];
|
|
395
|
+
while (i < args.length && nextArg && !nextArg.startsWith('-')) {
|
|
396
|
+
options.agent.push(nextArg);
|
|
397
|
+
i++;
|
|
398
|
+
nextArg = args[i];
|
|
399
|
+
}
|
|
400
|
+
i--;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return { options };
|
|
404
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const TELEMETRY_URL = 'https://add-skill.vercel.sh/t';
|
|
2
|
+
const AUDIT_URL = 'https://add-skill.vercel.sh/audit';
|
|
3
|
+
let cliVersion = null;
|
|
4
|
+
let detectedAgentName = null;
|
|
5
|
+
/**
|
|
6
|
+
* Set the detected AI agent name for telemetry tracking.
|
|
7
|
+
* Called once during agent detection, then included in all telemetry events.
|
|
8
|
+
*/
|
|
9
|
+
export function setDetectedAgent(agentName) {
|
|
10
|
+
detectedAgentName = agentName;
|
|
11
|
+
}
|
|
12
|
+
function isCI() {
|
|
13
|
+
return !!(process.env.CI ||
|
|
14
|
+
process.env.GITHUB_ACTIONS ||
|
|
15
|
+
process.env.GITLAB_CI ||
|
|
16
|
+
process.env.CIRCLECI ||
|
|
17
|
+
process.env.TRAVIS ||
|
|
18
|
+
process.env.BUILDKITE ||
|
|
19
|
+
process.env.JENKINS_URL ||
|
|
20
|
+
process.env.TEAMCITY_VERSION);
|
|
21
|
+
}
|
|
22
|
+
function isEnabled() {
|
|
23
|
+
return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
|
|
24
|
+
}
|
|
25
|
+
export function setVersion(version) {
|
|
26
|
+
cliVersion = version;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Fetch security audit results for skills from the audit API.
|
|
30
|
+
* Returns null on any error or timeout — never blocks installation.
|
|
31
|
+
*/
|
|
32
|
+
export async function fetchAuditData(source, skillSlugs, timeoutMs = 3000) {
|
|
33
|
+
if (skillSlugs.length === 0)
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
const params = new URLSearchParams({
|
|
37
|
+
source,
|
|
38
|
+
skills: skillSlugs.join(','),
|
|
39
|
+
});
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
42
|
+
const response = await fetch(`${AUDIT_URL}?${params.toString()}`, {
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
});
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
if (!response.ok)
|
|
47
|
+
return null;
|
|
48
|
+
return (await response.json());
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Pending telemetry promises — awaited before CLI exit so we don't lose data,
|
|
55
|
+
// but never block the main workflow.
|
|
56
|
+
const pendingTelemetry = [];
|
|
57
|
+
export function track(data) {
|
|
58
|
+
if (!isEnabled())
|
|
59
|
+
return;
|
|
60
|
+
try {
|
|
61
|
+
const params = new URLSearchParams();
|
|
62
|
+
// Add version
|
|
63
|
+
if (cliVersion) {
|
|
64
|
+
params.set('v', cliVersion);
|
|
65
|
+
}
|
|
66
|
+
// Add CI flag if running in CI
|
|
67
|
+
if (isCI()) {
|
|
68
|
+
params.set('ci', '1');
|
|
69
|
+
}
|
|
70
|
+
// Add detected AI agent name
|
|
71
|
+
if (detectedAgentName) {
|
|
72
|
+
params.set('agent', detectedAgentName);
|
|
73
|
+
}
|
|
74
|
+
// Add event data
|
|
75
|
+
for (const [key, value] of Object.entries(data)) {
|
|
76
|
+
if (value !== undefined && value !== null) {
|
|
77
|
+
params.set(key, String(value));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Fire and forget during the workflow, but track the promise so
|
|
81
|
+
// flushTelemetry() can await it before the process exits.
|
|
82
|
+
const p = fetch(`${TELEMETRY_URL}?${params.toString()}`)
|
|
83
|
+
.catch(() => { })
|
|
84
|
+
.then(() => { });
|
|
85
|
+
pendingTelemetry.push(p);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Silently fail - telemetry should never break the CLI
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Wait for all in-flight telemetry requests to settle.
|
|
93
|
+
* Called once at CLI exit so the process doesn't hang on open sockets
|
|
94
|
+
* but also doesn't drop data by exiting too early.
|
|
95
|
+
*/
|
|
96
|
+
export async function flushTelemetry(timeoutMs = 5000) {
|
|
97
|
+
if (pendingTelemetry.length === 0)
|
|
98
|
+
return;
|
|
99
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
100
|
+
await Promise.race([Promise.all(pendingTelemetry), timeout]);
|
|
101
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { stripTerminalEscapes } from "./sanitize.js";
|
|
4
|
+
// const PROJECT_ROOT = join(import.meta.dirname, '..');
|
|
5
|
+
const CLI_PATH = join(import.meta.dirname, 'cli.ts');
|
|
6
|
+
export function stripAnsi(str) {
|
|
7
|
+
return stripTerminalEscapes(str);
|
|
8
|
+
}
|
|
9
|
+
export function stripLogo(str) {
|
|
10
|
+
return str
|
|
11
|
+
.split('\n')
|
|
12
|
+
.filter((line) => !line.includes('███') && !line.includes('╔') && !line.includes('╚'))
|
|
13
|
+
.join('\n')
|
|
14
|
+
.replace(/^\n+/, '');
|
|
15
|
+
}
|
|
16
|
+
export function hasLogo(str) {
|
|
17
|
+
return str.includes('███') || str.includes('╔') || str.includes('╚');
|
|
18
|
+
}
|
|
19
|
+
export function runCli(args, cwd, env, timeout) {
|
|
20
|
+
try {
|
|
21
|
+
const output = execSync(`node "${CLI_PATH}" ${args.join(' ')}`, {
|
|
22
|
+
encoding: 'utf-8',
|
|
23
|
+
cwd,
|
|
24
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
25
|
+
env: env ? { ...process.env, ...env } : undefined,
|
|
26
|
+
timeout: timeout ?? 30000,
|
|
27
|
+
});
|
|
28
|
+
return { stdout: stripAnsi(output), stderr: '', exitCode: 0 };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return {
|
|
32
|
+
stdout: stripAnsi(error.stdout || ''),
|
|
33
|
+
stderr: stripAnsi(error.stderr || ''),
|
|
34
|
+
exitCode: error.status || 1,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function runCliOutput(args, cwd) {
|
|
39
|
+
const result = runCli(args, cwd);
|
|
40
|
+
return result.stdout || result.stderr;
|
|
41
|
+
}
|
|
42
|
+
export function runCliWithInput(args, input, cwd) {
|
|
43
|
+
try {
|
|
44
|
+
const output = execSync(`node "${CLI_PATH}" ${args.join(' ')}`, {
|
|
45
|
+
encoding: 'utf-8',
|
|
46
|
+
cwd,
|
|
47
|
+
input: input + '\n',
|
|
48
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
49
|
+
});
|
|
50
|
+
return { stdout: stripAnsi(output), stderr: '', exitCode: 0 };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
stdout: stripAnsi(error.stdout || ''),
|
|
55
|
+
stderr: stripAnsi(error.stderr || ''),
|
|
56
|
+
exitCode: error.status || 1,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function formatSourceInput(sourceUrl, ref) {
|
|
2
|
+
if (!ref) {
|
|
3
|
+
return sourceUrl;
|
|
4
|
+
}
|
|
5
|
+
return `${sourceUrl}#${ref}`;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Derive the skill's folder path from a SKILL.md-terminated skillPath.
|
|
9
|
+
* Returns '' when the skill lives at the repo root.
|
|
10
|
+
*/
|
|
11
|
+
function deriveSkillFolder(skillPath) {
|
|
12
|
+
let folder = skillPath;
|
|
13
|
+
if (folder.endsWith('/SKILL.md')) {
|
|
14
|
+
folder = folder.slice(0, -9);
|
|
15
|
+
}
|
|
16
|
+
else if (folder.endsWith('SKILL.md')) {
|
|
17
|
+
folder = folder.slice(0, -8);
|
|
18
|
+
}
|
|
19
|
+
if (folder.endsWith('/')) {
|
|
20
|
+
folder = folder.slice(0, -1);
|
|
21
|
+
}
|
|
22
|
+
return folder;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Whether a skill folder can be safely appended to the given source as a
|
|
26
|
+
* subpath. Only true for sources the source-parser can resolve as a
|
|
27
|
+
* GitHub/GitLab tree URL — owner/repo shorthand or an HTTPS URL on those
|
|
28
|
+
* hosts. Full SSH URLs (`git@host:owner/repo.git`) and generic Git URLs
|
|
29
|
+
* (anything ending in `.git`, or hosts other than github.com/gitlab.com)
|
|
30
|
+
* cannot have a subpath appended without producing an unclonable URL.
|
|
31
|
+
*/
|
|
32
|
+
function supportsAppendedSubpath(source) {
|
|
33
|
+
if (source.startsWith('git@'))
|
|
34
|
+
return false;
|
|
35
|
+
if (source.endsWith('.git'))
|
|
36
|
+
return false;
|
|
37
|
+
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
38
|
+
try {
|
|
39
|
+
const host = new URL(source).hostname;
|
|
40
|
+
return host === 'github.com' || host === 'gitlab.com';
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
function appendFolderAndRef(source, skillPath, ref) {
|
|
49
|
+
if (!supportsAppendedSubpath(source)) {
|
|
50
|
+
return formatSourceInput(source, ref);
|
|
51
|
+
}
|
|
52
|
+
const folder = deriveSkillFolder(skillPath);
|
|
53
|
+
const withFolder = folder ? `${source}/${folder}` : source;
|
|
54
|
+
return ref ? `${withFolder}#${ref}` : withFolder;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build the source argument for `skills add` during update.
|
|
58
|
+
* Uses shorthand form for path-targeted updates to avoid branch/path ambiguity.
|
|
59
|
+
*/
|
|
60
|
+
export function buildUpdateInstallSource(entry) {
|
|
61
|
+
if (!entry.skillPath) {
|
|
62
|
+
return formatSourceInput(entry.sourceUrl, entry.ref);
|
|
63
|
+
}
|
|
64
|
+
return appendFolderAndRef(entry.source, entry.skillPath, entry.ref);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build the source argument for `skills add` during project-level update.
|
|
68
|
+
* Local lock entries don't carry `sourceUrl`, so we fall back to the bare
|
|
69
|
+
* `source` identifier when no `skillPath` is available.
|
|
70
|
+
*/
|
|
71
|
+
export function buildLocalUpdateSource(entry) {
|
|
72
|
+
if (!entry.skillPath) {
|
|
73
|
+
return formatSourceInput(entry.source, entry.ref);
|
|
74
|
+
}
|
|
75
|
+
return appendFolderAndRef(entry.source, entry.skillPath, entry.ref);
|
|
76
|
+
}
|