@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,590 @@
1
+ import { spawnSync } from 'child_process';
2
+ import { existsSync, readdirSync } from 'fs';
3
+ import { join, dirname, relative, sep } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import * as p from '@clack/prompts';
6
+ import pc from 'picocolors';
7
+ import { readSkillLock, writeSkillLock, getGitHubToken } from "./skill-lock.js";
8
+ import { computeSkillFolderHash, readLocalLock } from "./local-lock.js";
9
+ import { formatSourceInput, buildUpdateInstallSource, buildLocalUpdateSource, } from "./update-source.js";
10
+ import { cloneRepo, cleanupTempDir } from "./git.js";
11
+ import { discoverSkills } from "./skills.js";
12
+ import { fetchRepoTree, findSkillMdPaths, getSkillFolderHashFromTree } from "./blob.js";
13
+ import { removeCommand } from "./remove.js";
14
+ import { sanitizeMetadata } from "./sanitize.js";
15
+ import { track } from "./telemetry.js";
16
+ import { agents, isUniversalAgent } from "./agents.js";
17
+ import { selfCliArgv } from "./self-cli.js";
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ /**
20
+ * ADG patch: upstream re-invokes its *built* `bin/cli.mjs` to perform an update,
21
+ * which a source-only vendoring (we copy `src/` only) does not ship — every
22
+ * update then failed with "CLI entrypoint not found". Point self-invocation at
23
+ * our actual TS entry, run via Node's type-stripping (Node >= 22.6), exactly how
24
+ * `adg skills` launches the CLI. See vendor/skills/PROVENANCE.md.
25
+ */
26
+ const SELF_CLI_ENTRY = join(__dirname, 'cli.ts');
27
+ const RESET = '\x1b[0m';
28
+ const BOLD = '\x1b[1m';
29
+ const DIM = '\x1b[38;5;102m';
30
+ const TEXT = '\x1b[38;5;145m';
31
+ export function parseUpdateOptions(args) {
32
+ const options = {};
33
+ const positional = [];
34
+ for (const arg of args) {
35
+ if (arg === '-g' || arg === '--global') {
36
+ options.global = true;
37
+ }
38
+ else if (arg === '-p' || arg === '--project') {
39
+ options.project = true;
40
+ }
41
+ else if (arg === '-y' || arg === '--yes') {
42
+ options.yes = true;
43
+ }
44
+ else if (!arg.startsWith('-')) {
45
+ positional.push(arg);
46
+ }
47
+ }
48
+ if (positional.length > 0) {
49
+ options.skills = positional;
50
+ }
51
+ return options;
52
+ }
53
+ /**
54
+ * Check whether the current working directory has project-level skills.
55
+ * Returns true if either:
56
+ * - skills-lock.json exists in cwd, OR
57
+ * - .agents/skills/ contains at least one subdirectory with a SKILL.md
58
+ */
59
+ export function hasProjectSkills(cwd) {
60
+ const dir = cwd || process.cwd();
61
+ // Check 1: skills-lock.json exists
62
+ const lockPath = join(dir, 'skills-lock.json');
63
+ if (existsSync(lockPath)) {
64
+ return true;
65
+ }
66
+ // Check 2: .agents/skills/ has at least one skill
67
+ const skillsDir = join(dir, '.agents', 'skills');
68
+ try {
69
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
70
+ for (const entry of entries) {
71
+ if (entry.isDirectory()) {
72
+ const skillMd = join(skillsDir, entry.name, 'SKILL.md');
73
+ if (existsSync(skillMd)) {
74
+ return true;
75
+ }
76
+ }
77
+ }
78
+ }
79
+ catch {
80
+ // Directory doesn't exist
81
+ }
82
+ return false;
83
+ }
84
+ /**
85
+ * Determine the update/check scope via interactive prompt or auto-detection.
86
+ */
87
+ export async function resolveUpdateScope(options) {
88
+ if (options.skills && options.skills.length > 0) {
89
+ if (options.global)
90
+ return 'global';
91
+ if (options.project)
92
+ return 'project';
93
+ return 'both';
94
+ }
95
+ if (options.global && options.project) {
96
+ return 'both';
97
+ }
98
+ if (options.global) {
99
+ return 'global';
100
+ }
101
+ if (options.project) {
102
+ return 'project';
103
+ }
104
+ if (options.yes || !process.stdin.isTTY) {
105
+ return hasProjectSkills() ? 'project' : 'global';
106
+ }
107
+ const scope = await p.select({
108
+ message: 'Update scope',
109
+ options: [
110
+ {
111
+ value: 'project',
112
+ label: 'Project',
113
+ hint: 'Update skills in current directory',
114
+ },
115
+ {
116
+ value: 'global',
117
+ label: 'Global',
118
+ hint: 'Update skills in home directory',
119
+ },
120
+ {
121
+ value: 'both',
122
+ label: 'Both',
123
+ hint: 'Update all skills',
124
+ },
125
+ ],
126
+ });
127
+ if (p.isCancel(scope)) {
128
+ p.cancel('Cancelled');
129
+ process.exit(0);
130
+ }
131
+ return scope;
132
+ }
133
+ export function matchesSkillFilter(name, filter) {
134
+ if (!filter || filter.length === 0)
135
+ return true;
136
+ const lower = name.toLowerCase();
137
+ return filter.some((f) => f.toLowerCase() === lower);
138
+ }
139
+ export function getSkipReason(entry) {
140
+ if (entry.sourceType === 'local') {
141
+ return 'Local path';
142
+ }
143
+ if (entry.sourceType === 'git') {
144
+ return 'Git URL';
145
+ }
146
+ if (entry.sourceType === 'well-known') {
147
+ return 'Well-known skill';
148
+ }
149
+ if (!entry.skillFolderHash) {
150
+ return 'Private or deleted repo';
151
+ }
152
+ if (!entry.skillPath) {
153
+ return 'No skill path recorded';
154
+ }
155
+ return 'No version tracking';
156
+ }
157
+ export function getInstallSource(skill) {
158
+ let url = skill.sourceUrl;
159
+ if (skill.sourceType === 'well-known') {
160
+ const idx = url.indexOf('/.well-known/');
161
+ if (idx !== -1) {
162
+ url = url.slice(0, idx);
163
+ }
164
+ }
165
+ return formatSourceInput(url, skill.ref);
166
+ }
167
+ export function printSkippedSkills(skipped) {
168
+ if (skipped.length === 0)
169
+ return;
170
+ console.log();
171
+ console.log(`${DIM}${skipped.length} skill(s) cannot be checked automatically:${RESET}`);
172
+ const grouped = new Map();
173
+ for (const skill of skipped) {
174
+ const source = getInstallSource(skill);
175
+ const existing = grouped.get(source) || [];
176
+ existing.push(skill);
177
+ grouped.set(source, existing);
178
+ }
179
+ for (const [source, skills] of grouped) {
180
+ if (skills.length === 1) {
181
+ const skill = skills[0];
182
+ console.log(` ${TEXT}•${RESET} ${sanitizeMetadata(skill.name)} ${DIM}(${skill.reason})${RESET}`);
183
+ }
184
+ else {
185
+ const reason = skills[0].reason;
186
+ const names = skills.map((s) => sanitizeMetadata(s.name)).join(', ');
187
+ console.log(` ${TEXT}•${RESET} ${names} ${DIM}(${reason})${RESET}`);
188
+ }
189
+ console.log(` ${DIM}To update: ${TEXT}npx skills add ${source} -g -y${RESET}`);
190
+ }
191
+ }
192
+ export async function getProjectSkillsForUpdate(skillFilter) {
193
+ const localLock = await readLocalLock();
194
+ const skills = [];
195
+ for (const [name, entry] of Object.entries(localLock.skills)) {
196
+ if (!matchesSkillFilter(name, skillFilter))
197
+ continue;
198
+ if (entry.sourceType === 'node_modules' || entry.sourceType === 'local') {
199
+ continue;
200
+ }
201
+ skills.push({ name, source: entry.source, entry });
202
+ }
203
+ return skills;
204
+ }
205
+ export async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills, isGlobal, options, discoveredPaths) {
206
+ const deletedSkills = allLockedForSource.filter((name) => {
207
+ const entry = lockSkills[name];
208
+ if (!entry?.skillPath)
209
+ return false;
210
+ return !discoveredPaths.includes(entry.skillPath);
211
+ });
212
+ if (deletedSkills.length > 0) {
213
+ console.log();
214
+ console.log(`${DIM}Warning:${RESET} The following skills from ${DIM}${source}${RESET} appear to have been deleted upstream:`);
215
+ for (const s of deletedSkills) {
216
+ console.log(` ${DIM}•${RESET} ${s}`);
217
+ }
218
+ const isNonInteractive = options.yes || !process.stdin.isTTY;
219
+ if (isNonInteractive) {
220
+ console.log(`${DIM}Skipping deletion in non-interactive mode.${RESET}`);
221
+ }
222
+ else {
223
+ const confirmed = await p.confirm({
224
+ message: `Would you like to remove the local copies of these deleted skills?`,
225
+ });
226
+ if (confirmed && !p.isCancel(confirmed)) {
227
+ for (const s of deletedSkills) {
228
+ console.log(`${DIM}Removing${RESET} ${s}...`);
229
+ await removeCommand([s], { yes: true, global: isGlobal });
230
+ }
231
+ }
232
+ }
233
+ }
234
+ return deletedSkills;
235
+ }
236
+ export async function updateGlobalSkills(options = {}) {
237
+ const lock = await readSkillLock();
238
+ const skillNames = Object.keys(lock.skills);
239
+ let successCount = 0;
240
+ let failCount = 0;
241
+ if (skillNames.length === 0) {
242
+ if (!options.skills) {
243
+ console.log(`${DIM}No global skills tracked in lock file.${RESET}`);
244
+ console.log(`${DIM}Install skills with${RESET} ${TEXT}npx skills add <package> -g${RESET}`);
245
+ }
246
+ return { successCount, failCount, checkedCount: 0 };
247
+ }
248
+ const updates = [];
249
+ const skipped = [];
250
+ const checkable = [];
251
+ // ADG patch: sources whose tree/clone could not be checked (e.g. a private
252
+ // repo with no usable token). Tracked so we don't falsely report "up to date".
253
+ const failedSources = [];
254
+ // ADG patch: set when we normalize a legacy non-tree-SHA hash (see below).
255
+ let healed = false;
256
+ for (const skillName of skillNames) {
257
+ if (!matchesSkillFilter(skillName, options.skills))
258
+ continue;
259
+ const entry = lock.skills[skillName];
260
+ if (!entry)
261
+ continue;
262
+ if (!entry.skillFolderHash || !entry.skillPath) {
263
+ skipped.push({
264
+ name: skillName,
265
+ reason: getSkipReason(entry),
266
+ sourceUrl: entry.sourceUrl,
267
+ sourceType: entry.sourceType,
268
+ ref: entry.ref,
269
+ });
270
+ continue;
271
+ }
272
+ checkable.push({ name: skillName, entry });
273
+ }
274
+ const bySource = new Map();
275
+ for (const item of checkable) {
276
+ const source = item.entry.source;
277
+ const existing = bySource.get(source) || [];
278
+ existing.push(item);
279
+ bySource.set(source, existing);
280
+ }
281
+ for (const [source, itemsForSource] of bySource) {
282
+ const firstEntry = itemsForSource[0].entry;
283
+ const sourceUrl = firstEntry.sourceUrl || firstEntry.source;
284
+ let tempDir = null;
285
+ process.stdout.write(`\r${DIM}Checking skills from source: ${source}${RESET}\x1b[K\n`);
286
+ try {
287
+ const isGitHubSource = firstEntry.sourceType === 'github';
288
+ if (isGitHubSource) {
289
+ const tree = await fetchRepoTree(source, firstEntry.ref, getGitHubToken);
290
+ if (!tree) {
291
+ // ADG patch: a private repo returns 404 to anon and fetchRepoTree now
292
+ // retries with a token; reaching here means even that failed (no token
293
+ // or no access). Record it so the summary is honest.
294
+ console.log(` ${DIM}✗ Could not check ${source} — private repo or no GitHub access.${RESET}`);
295
+ console.log(` ${DIM}Set ${RESET}${TEXT}GITHUB_TOKEN${RESET}${DIM} or run ${RESET}${TEXT}gh auth login${RESET}${DIM} with access to it.${RESET}`);
296
+ failedSources.push(source);
297
+ continue;
298
+ }
299
+ const discoveredPaths = findSkillMdPaths(tree);
300
+ const allLockedForSource = Object.entries(lock.skills)
301
+ .filter(([_, entry]) => entry.source === source)
302
+ .map(([name, _]) => name);
303
+ const deletedSkills = await checkAndPromptForDeletions(source, allLockedForSource, lock.skills, true, options, discoveredPaths);
304
+ const deletedSkillSet = new Set(deletedSkills);
305
+ for (const { name: skillName, entry } of itemsForSource) {
306
+ if (deletedSkillSet.has(skillName))
307
+ continue;
308
+ const latestHash = getSkillFolderHashFromTree(tree, entry.skillPath);
309
+ if (!latestHash)
310
+ continue;
311
+ // ADG patch (self-heal): a github entry whose stored hash isn't a
312
+ // 40-hex git tree SHA was written by an older clone-fallback that used
313
+ // a sha256 content hash — it can never match `latestHash`, so it would
314
+ // be re-flagged on every update. We can't tell whether the content
315
+ // truly changed, so normalize the lock to the current tree SHA and do
316
+ // NOT flag a spurious update. `add.ts` now always stores a tree SHA, so
317
+ // this only ever fires for pre-fix entries.
318
+ if (!/^[0-9a-f]{40}$/.test(entry.skillFolderHash)) {
319
+ entry.skillFolderHash = latestHash;
320
+ healed = true;
321
+ continue;
322
+ }
323
+ if (latestHash !== entry.skillFolderHash) {
324
+ updates.push({ name: skillName, source, entry });
325
+ }
326
+ }
327
+ continue;
328
+ }
329
+ tempDir = await cloneRepo(sourceUrl, firstEntry.ref);
330
+ const discoveredPaths = (await discoverSkills(tempDir)).map((skill) => {
331
+ return join(relative(tempDir, skill.path), 'SKILL.md').split(sep).join('/');
332
+ });
333
+ const allLockedForSource = Object.entries(lock.skills)
334
+ .filter(([_, entry]) => entry.source === source)
335
+ .map(([name, _]) => name);
336
+ const deletedSkills = await checkAndPromptForDeletions(source, allLockedForSource, lock.skills, true, options, discoveredPaths);
337
+ const deletedSkillSet = new Set(deletedSkills);
338
+ for (const { name: skillName, entry } of itemsForSource) {
339
+ if (deletedSkillSet.has(skillName))
340
+ continue;
341
+ const skillPath = entry.skillPath;
342
+ if (!discoveredPaths.includes(skillPath))
343
+ continue;
344
+ const latestHash = await computeSkillFolderHash(join(tempDir, dirname(skillPath)));
345
+ if (latestHash && latestHash !== entry.skillFolderHash) {
346
+ updates.push({ name: skillName, source, entry });
347
+ }
348
+ }
349
+ }
350
+ catch (error) {
351
+ console.log(` ${DIM}✗ Failed to check skills from ${source}${RESET}`);
352
+ failedSources.push(source); // ADG patch: surface in the summary
353
+ }
354
+ finally {
355
+ if (tempDir)
356
+ await cleanupTempDir(tempDir);
357
+ }
358
+ }
359
+ if (checkable.length > 0) {
360
+ process.stdout.write('\r\x1b[K');
361
+ }
362
+ // ADG patch: persist any self-healed (normalized) hashes once.
363
+ if (healed) {
364
+ try {
365
+ await writeSkillLock(lock);
366
+ }
367
+ catch {
368
+ // A failed heal-write is non-fatal: next run simply normalizes again.
369
+ }
370
+ }
371
+ const checkedCount = checkable.length + skipped.length;
372
+ if (checkable.length === 0 && skipped.length === 0) {
373
+ if (!options.skills) {
374
+ console.log(`${DIM}No global skills to check.${RESET}`);
375
+ }
376
+ return { successCount, failCount, checkedCount: 0 };
377
+ }
378
+ if (checkable.length === 0 && skipped.length > 0) {
379
+ printSkippedSkills(skipped);
380
+ return { successCount, failCount, checkedCount };
381
+ }
382
+ if (updates.length === 0) {
383
+ // ADG patch: don't claim everything is current if some sources couldn't be
384
+ // checked — that's how a private-repo fetch failure used to be hidden.
385
+ if (failedSources.length > 0) {
386
+ const n = failedSources.length;
387
+ console.log(`${TEXT}✓ Checked global skills; ${n} source${n !== 1 ? 's' : ''} could not be verified (see above)${RESET}`);
388
+ return { successCount, failCount: failCount + n, checkedCount };
389
+ }
390
+ console.log(`${TEXT}✓ All global skills are up to date${RESET}`);
391
+ return { successCount, failCount, checkedCount };
392
+ }
393
+ console.log(`${TEXT}Found ${updates.length} global update(s)${RESET}`);
394
+ console.log();
395
+ for (const update of updates) {
396
+ const safeName = sanitizeMetadata(update.name);
397
+ console.log(`${TEXT}Updating ${safeName}...${RESET}`);
398
+ const installUrl = buildUpdateInstallSource(update.entry);
399
+ const cliEntry = SELF_CLI_ENTRY;
400
+ if (!existsSync(cliEntry)) {
401
+ failCount++;
402
+ console.log(` ${DIM}✗ Failed to update ${safeName}: CLI entrypoint not found at ${cliEntry}${RESET}`);
403
+ continue;
404
+ }
405
+ const result = spawnSync(process.execPath, selfCliArgv(cliEntry, ['add', installUrl, '-g', '-y']), {
406
+ stdio: ['inherit', 'pipe', 'pipe'],
407
+ encoding: 'utf-8',
408
+ shell: process.platform === 'win32',
409
+ });
410
+ if (result.status === 0) {
411
+ successCount++;
412
+ console.log(` ${TEXT}✓${RESET} Updated ${safeName}`);
413
+ }
414
+ else {
415
+ failCount++;
416
+ console.log(` ${DIM}✗ Failed to update ${safeName}${RESET}`);
417
+ }
418
+ }
419
+ printSkippedSkills(skipped);
420
+ return { successCount, failCount, checkedCount };
421
+ }
422
+ export async function updateProjectSkills(options = {}) {
423
+ const projectSkills = await getProjectSkillsForUpdate(options.skills);
424
+ let successCount = 0;
425
+ let failCount = 0;
426
+ if (projectSkills.length === 0) {
427
+ if (!options.skills) {
428
+ console.log(`${DIM}No project skills to update.${RESET}`);
429
+ console.log(`${DIM}Install project skills with${RESET} ${TEXT}npx skills add <package>${RESET}`);
430
+ }
431
+ return { successCount, failCount, foundCount: 0 };
432
+ }
433
+ const updatable = projectSkills.filter((s) => s.entry.skillPath);
434
+ const legacy = projectSkills.filter((s) => !s.entry.skillPath);
435
+ if (updatable.length === 0) {
436
+ console.log(`${DIM}No project skills can be updated in place.${RESET}`);
437
+ printLegacyProjectSkills(legacy);
438
+ return { successCount, failCount, foundCount: projectSkills.length };
439
+ }
440
+ const cwd = process.cwd();
441
+ const targetAgentNames = [];
442
+ let hasUniversal = false;
443
+ for (const [type, config] of Object.entries(agents)) {
444
+ if (isUniversalAgent(type)) {
445
+ if (!hasUniversal && existsSync(join(cwd, '.agents'))) {
446
+ hasUniversal = true;
447
+ }
448
+ }
449
+ else {
450
+ const agentRoot = config.skillsDir.split('/')[0];
451
+ if (existsSync(join(cwd, agentRoot))) {
452
+ targetAgentNames.push(config.displayName);
453
+ }
454
+ }
455
+ }
456
+ const targetParts = [];
457
+ if (hasUniversal)
458
+ targetParts.push('Universal');
459
+ targetParts.push(...targetAgentNames);
460
+ if (targetParts.length > 0) {
461
+ console.log(`${TEXT}Updating for: ${targetParts.join(', ')}${RESET}`);
462
+ }
463
+ console.log(`${TEXT}Refreshing ${updatable.length} skill(s)...${RESET}`);
464
+ console.log();
465
+ const bySource = new Map();
466
+ for (const skill of updatable) {
467
+ const source = skill.entry.source;
468
+ const existing = bySource.get(source) || [];
469
+ existing.push(skill);
470
+ bySource.set(source, existing);
471
+ }
472
+ const localLock = await readLocalLock();
473
+ const cliEntry = SELF_CLI_ENTRY;
474
+ if (!existsSync(cliEntry)) {
475
+ console.log(`${DIM}✗ CLI entrypoint not found at ${cliEntry}${RESET}`);
476
+ return { successCount, failCount: updatable.length, foundCount: projectSkills.length };
477
+ }
478
+ for (const [source, skillsForSource] of bySource) {
479
+ const firstEntry = skillsForSource[0].entry;
480
+ const sourceUrl = firstEntry.source;
481
+ const ref = firstEntry.ref;
482
+ const allLockedForSource = Object.entries(localLock.skills)
483
+ .filter(([_, entry]) => entry.source === source)
484
+ .map(([name, _]) => name);
485
+ let tempDir = null;
486
+ let deletedSkills = [];
487
+ try {
488
+ tempDir = await cloneRepo(sourceUrl, ref);
489
+ const discovered = await discoverSkills(tempDir);
490
+ const discoveredPaths = discovered.map((s) => {
491
+ const relPath = relative(tempDir, s.path);
492
+ return join(relPath, 'SKILL.md').split(sep).join('/');
493
+ });
494
+ deletedSkills = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredPaths);
495
+ }
496
+ catch (error) {
497
+ console.log(`${DIM}✗ Failed to check for deleted skills from ${source}${RESET}`);
498
+ }
499
+ finally {
500
+ if (tempDir) {
501
+ await cleanupTempDir(tempDir);
502
+ }
503
+ }
504
+ const remainingSkills = skillsForSource.filter((s) => !deletedSkills.includes(s.name));
505
+ for (const skill of remainingSkills) {
506
+ const safeName = sanitizeMetadata(skill.name);
507
+ console.log(`${TEXT}Updating ${safeName}...${RESET}`);
508
+ const installUrl = formatSourceInput(skill.entry.source, skill.entry.ref);
509
+ const result = spawnSync(process.execPath, selfCliArgv(cliEntry, ['add', installUrl, '--skill', skill.name, '-y']), {
510
+ stdio: ['inherit', 'pipe', 'pipe'],
511
+ encoding: 'utf-8',
512
+ shell: process.platform === 'win32',
513
+ });
514
+ if (result.status === 0) {
515
+ successCount++;
516
+ console.log(` ${TEXT}✓${RESET} Updated ${safeName}`);
517
+ }
518
+ else {
519
+ failCount++;
520
+ console.log(` ${DIM}✗ Failed to update ${safeName}${RESET}`);
521
+ }
522
+ }
523
+ }
524
+ printLegacyProjectSkills(legacy);
525
+ return { successCount, failCount, foundCount: projectSkills.length };
526
+ }
527
+ export function printLegacyProjectSkills(legacy) {
528
+ if (legacy.length === 0)
529
+ return;
530
+ console.log();
531
+ console.log(`${DIM}${legacy.length} project skill(s) cannot be updated automatically (installed before skillPath tracking):${RESET}`);
532
+ for (const skill of legacy) {
533
+ const reinstall = formatSourceInput(skill.entry.source, skill.entry.ref);
534
+ console.log(` ${TEXT}•${RESET} ${sanitizeMetadata(skill.name)}`);
535
+ console.log(` ${DIM}To refresh: ${TEXT}npx skills add ${reinstall} -y${RESET}`);
536
+ }
537
+ }
538
+ export async function runUpdate(args = []) {
539
+ const options = parseUpdateOptions(args);
540
+ const scope = await resolveUpdateScope(options);
541
+ if (options.skills) {
542
+ console.log(`${TEXT}Updating ${options.skills.join(', ')}...${RESET}`);
543
+ }
544
+ else {
545
+ console.log(`${TEXT}Checking for skill updates...${RESET}`);
546
+ }
547
+ console.log();
548
+ let totalSuccess = 0;
549
+ let totalFail = 0;
550
+ let totalFound = 0;
551
+ if (scope === 'global' || scope === 'both') {
552
+ if (scope === 'both' && !options.skills) {
553
+ console.log(`${BOLD}Global Skills${RESET}`);
554
+ }
555
+ const { successCount, failCount, checkedCount } = await updateGlobalSkills(options);
556
+ totalSuccess += successCount;
557
+ totalFail += failCount;
558
+ totalFound += checkedCount;
559
+ if (scope === 'both' && !options.skills) {
560
+ console.log();
561
+ }
562
+ }
563
+ if (scope === 'project' || scope === 'both') {
564
+ if (scope === 'both' && !options.skills) {
565
+ console.log(`${BOLD}Project Skills${RESET}`);
566
+ }
567
+ const { successCount, failCount, foundCount } = await updateProjectSkills(options);
568
+ totalSuccess += successCount;
569
+ totalFail += failCount;
570
+ totalFound += foundCount;
571
+ }
572
+ if (options.skills && totalFound === 0) {
573
+ console.log(`${DIM}No installed skills found matching: ${options.skills.join(', ')}${RESET}`);
574
+ }
575
+ console.log();
576
+ if (totalSuccess > 0) {
577
+ console.log(`${TEXT}✓ Updated ${totalSuccess} skill(s)${RESET}`);
578
+ }
579
+ if (totalFail > 0) {
580
+ console.log(`${DIM}Failed to update ${totalFail} skill(s)${RESET}`);
581
+ }
582
+ track({
583
+ event: 'update',
584
+ scope,
585
+ skillCount: String(totalSuccess + totalFail),
586
+ successCount: String(totalSuccess),
587
+ failCount: String(totalFail),
588
+ });
589
+ console.log();
590
+ }