@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.
Files changed (110) hide show
  1. package/dist/bin/adg.js +703 -0
  2. package/dist/src/adapters/anthropic.js +54 -0
  3. package/dist/src/adapters/index.js +10 -0
  4. package/dist/src/adapters/openai.js +30 -0
  5. package/dist/src/adapters/reverse.js +53 -0
  6. package/dist/src/agents/claude.js +118 -0
  7. package/dist/src/agents/codex.js +61 -0
  8. package/{src/agents/index.ts → dist/src/agents/index.js} +6 -8
  9. package/dist/src/agents/registry.js +24 -0
  10. package/dist/src/agents/types.js +1 -0
  11. package/dist/src/commands/adapt.js +26 -0
  12. package/dist/src/commands/import.js +51 -0
  13. package/dist/src/commands/init.js +104 -0
  14. package/dist/src/commands/install.js +257 -0
  15. package/dist/src/commands/link.js +34 -0
  16. package/dist/src/commands/list.js +19 -0
  17. package/dist/src/commands/marketplace.js +124 -0
  18. package/dist/src/commands/migrate.js +60 -0
  19. package/dist/src/commands/multiselect-skills.js +103 -0
  20. package/dist/src/commands/remove.js +102 -0
  21. package/dist/src/commands/select-agents.js +40 -0
  22. package/dist/src/commands/select-components.js +61 -0
  23. package/dist/src/commands/select-plugins.js +25 -0
  24. package/dist/src/commands/select-scope.js +20 -0
  25. package/dist/src/commands/update.js +50 -0
  26. package/dist/src/commands/validate.js +50 -0
  27. package/dist/src/components.js +90 -0
  28. package/dist/src/deps.js +46 -0
  29. package/dist/src/fsutil.js +32 -0
  30. package/dist/src/hash.js +51 -0
  31. package/dist/src/lock.js +51 -0
  32. package/dist/src/manifest.js +110 -0
  33. package/dist/src/marketplace.js +39 -0
  34. package/{src/package.ts → dist/src/package.js} +37 -42
  35. package/{src/paths.ts → dist/src/paths.js} +54 -60
  36. package/dist/src/semver.js +55 -0
  37. package/dist/src/skills.js +79 -0
  38. package/dist/src/sources.js +122 -0
  39. package/dist/src/types.js +19 -0
  40. package/dist/vendor/skills/package.json +143 -0
  41. package/dist/vendor/skills/src/add.js +1663 -0
  42. package/dist/vendor/skills/src/agents.js +729 -0
  43. package/dist/vendor/skills/src/blob.js +436 -0
  44. package/dist/vendor/skills/src/cli.js +340 -0
  45. package/dist/vendor/skills/src/constants.js +3 -0
  46. package/dist/vendor/skills/src/detect-agent.js +56 -0
  47. package/dist/vendor/skills/src/find.js +294 -0
  48. package/dist/vendor/skills/src/frontmatter.js +13 -0
  49. package/dist/vendor/skills/src/git-tree.js +32 -0
  50. package/dist/vendor/skills/src/git.js +235 -0
  51. package/dist/vendor/skills/src/install.js +75 -0
  52. package/dist/vendor/skills/src/installer.js +924 -0
  53. package/dist/vendor/skills/src/list.js +201 -0
  54. package/dist/vendor/skills/src/local-lock.js +109 -0
  55. package/dist/vendor/skills/src/plugin-manifest.js +152 -0
  56. package/dist/vendor/skills/src/prompts/search-multiselect.js +312 -0
  57. package/dist/vendor/skills/src/providers/index.js +4 -0
  58. package/dist/vendor/skills/src/providers/registry.js +42 -0
  59. package/dist/vendor/skills/src/providers/types.js +1 -0
  60. package/dist/vendor/skills/src/providers/wellknown.js +625 -0
  61. package/dist/vendor/skills/src/remove.js +263 -0
  62. package/dist/vendor/skills/src/sanitize.js +57 -0
  63. package/dist/vendor/skills/src/self-cli.js +15 -0
  64. package/dist/vendor/skills/src/skill-lock.js +237 -0
  65. package/dist/vendor/skills/src/skills.js +264 -0
  66. package/dist/vendor/skills/src/source-parser.js +367 -0
  67. package/dist/vendor/skills/src/sync.js +404 -0
  68. package/dist/vendor/skills/src/telemetry.js +101 -0
  69. package/dist/vendor/skills/src/test-utils.js +59 -0
  70. package/dist/vendor/skills/src/types.js +1 -0
  71. package/dist/vendor/skills/src/update-source.js +76 -0
  72. package/dist/vendor/skills/src/update.js +590 -0
  73. package/dist/vendor/skills/src/use.js +505 -0
  74. package/package.json +15 -7
  75. package/bin/adg.ts +0 -758
  76. package/src/adapters/anthropic.ts +0 -54
  77. package/src/adapters/index.ts +0 -24
  78. package/src/adapters/openai.ts +0 -37
  79. package/src/adapters/reverse.ts +0 -60
  80. package/src/agents/claude.ts +0 -124
  81. package/src/agents/codex.ts +0 -67
  82. package/src/agents/registry.ts +0 -30
  83. package/src/agents/types.ts +0 -47
  84. package/src/commands/adapt.ts +0 -36
  85. package/src/commands/import.ts +0 -69
  86. package/src/commands/init.ts +0 -146
  87. package/src/commands/install.ts +0 -411
  88. package/src/commands/link.ts +0 -61
  89. package/src/commands/list.ts +0 -28
  90. package/src/commands/marketplace.ts +0 -198
  91. package/src/commands/migrate.ts +0 -84
  92. package/src/commands/multiselect-skills.ts +0 -137
  93. package/src/commands/remove.ts +0 -136
  94. package/src/commands/select-agents.ts +0 -45
  95. package/src/commands/select-components.ts +0 -66
  96. package/src/commands/select-plugins.ts +0 -28
  97. package/src/commands/select-scope.ts +0 -21
  98. package/src/commands/update.ts +0 -85
  99. package/src/commands/validate.ts +0 -57
  100. package/src/components.ts +0 -90
  101. package/src/deps.ts +0 -64
  102. package/src/fsutil.ts +0 -38
  103. package/src/hash.ts +0 -61
  104. package/src/lock.ts +0 -57
  105. package/src/manifest.ts +0 -113
  106. package/src/marketplace.ts +0 -41
  107. package/src/semver.ts +0 -67
  108. package/src/skills.ts +0 -88
  109. package/src/sources.ts +0 -159
  110. 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
+ }