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