agentinit 1.19.0 → 1.20.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/CHANGELOG.md +25 -0
- package/README.md +46 -4
- package/dist/cli.js +1927 -509
- package/dist/cli.js.map +1 -1
- package/dist/commands/lock.d.ts +3 -0
- package/dist/commands/lock.d.ts.map +1 -0
- package/dist/commands/lock.js +160 -0
- package/dist/commands/lock.js.map +1 -0
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +60 -0
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/rules.d.ts.map +1 -1
- package/dist/commands/rules.js +50 -0
- package/dist/commands/rules.js.map +1 -1
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +299 -20
- package/dist/commands/skills.js.map +1 -1
- package/dist/core/installLock.d.ts +54 -0
- package/dist/core/installLock.d.ts.map +1 -0
- package/dist/core/installLock.js +274 -0
- package/dist/core/installLock.js.map +1 -0
- package/dist/core/skillSecurityScanner.d.ts +28 -0
- package/dist/core/skillSecurityScanner.d.ts.map +1 -0
- package/dist/core/skillSecurityScanner.js +167 -0
- package/dist/core/skillSecurityScanner.js.map +1 -0
- package/dist/core/skillsManager.d.ts +12 -0
- package/dist/core/skillsManager.d.ts.map +1 -1
- package/dist/core/skillsManager.js +373 -14
- package/dist/core/skillsManager.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lockfile.d.ts +68 -0
- package/dist/types/lockfile.d.ts.map +1 -0
- package/dist/types/lockfile.js +2 -0
- package/dist/types/lockfile.js.map +1 -0
- package/dist/types/plugins.d.ts +1 -1
- package/dist/types/plugins.d.ts.map +1 -1
- package/dist/types/skills.d.ts +4 -1
- package/dist/types/skills.d.ts.map +1 -1
- package/dist/utils/lockSource.d.ts +9 -0
- package/dist/utils/lockSource.d.ts.map +1 -0
- package/dist/utils/lockSource.js +59 -0
- package/dist/utils/lockSource.js.map +1 -0
- package/dist/utils/promptUtils.d.ts +13 -2
- package/dist/utils/promptUtils.d.ts.map +1 -1
- package/dist/utils/promptUtils.js +61 -3
- package/dist/utils/promptUtils.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,9 +6,12 @@ import { createHash } from 'crypto';
|
|
|
6
6
|
import { promisify } from 'util';
|
|
7
7
|
import matter from 'gray-matter';
|
|
8
8
|
import { createRelativeSymlink, fileExists, isDirectory, listFiles, readFileIfExists, resolveRealPathOrSelf, } from '../utils/fs.js';
|
|
9
|
+
import { expandTilde } from '../utils/paths.js';
|
|
9
10
|
import { AgentManager } from './agentManager.js';
|
|
10
11
|
import { getConfiguredDefaultMarketplaceId, getMarketplace, getMarketplaceIds } from './marketplaceRegistry.js';
|
|
12
|
+
import { SkillSecurityScanner } from './skillSecurityScanner.js';
|
|
11
13
|
import { SHARED_SKILLS_TARGET_ID } from '../types/skills.js';
|
|
14
|
+
import { InstallLock, hashDirectory, logLockWriteWarning } from './installLock.js';
|
|
12
15
|
const execFileAsync = promisify(execFile);
|
|
13
16
|
const DEFAULT_SKILLS_CATALOG = {
|
|
14
17
|
owner: 'vercel-labs',
|
|
@@ -31,6 +34,7 @@ const SKILL_SEARCH_DIRS = [
|
|
|
31
34
|
export class SkillsManager {
|
|
32
35
|
agentManager;
|
|
33
36
|
preparedSourceContexts = new Map();
|
|
37
|
+
skillScanner = new SkillSecurityScanner();
|
|
34
38
|
constructor(agentManager) {
|
|
35
39
|
this.agentManager = agentManager || new AgentManager();
|
|
36
40
|
}
|
|
@@ -43,12 +47,16 @@ export class SkillsManager {
|
|
|
43
47
|
return { type: 'local', path: source };
|
|
44
48
|
}
|
|
45
49
|
// Full GitHub URL
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
return
|
|
50
|
+
const httpSource = this.parseHttpRepositorySource(source);
|
|
51
|
+
if (httpSource) {
|
|
52
|
+
return httpSource;
|
|
49
53
|
}
|
|
50
54
|
// Full git URL
|
|
51
|
-
|
|
55
|
+
const sshSource = this.parseSshRepositorySource(source);
|
|
56
|
+
if (sshSource) {
|
|
57
|
+
return sshSource;
|
|
58
|
+
}
|
|
59
|
+
if (source.endsWith('.git')) {
|
|
52
60
|
return { type: 'github', url: source };
|
|
53
61
|
}
|
|
54
62
|
if (options?.from) {
|
|
@@ -61,6 +69,14 @@ export class SkillsManager {
|
|
|
61
69
|
pluginName: source,
|
|
62
70
|
};
|
|
63
71
|
}
|
|
72
|
+
const gitLabShorthandSource = this.parseGitLabShorthandSource(source);
|
|
73
|
+
if (gitLabShorthandSource) {
|
|
74
|
+
return gitLabShorthandSource;
|
|
75
|
+
}
|
|
76
|
+
const bitbucketShorthandSource = this.parseBitbucketShorthandSource(source);
|
|
77
|
+
if (bitbucketShorthandSource) {
|
|
78
|
+
return bitbucketShorthandSource;
|
|
79
|
+
}
|
|
64
80
|
const githubShorthandSource = this.parseGitHubShorthandSource(source);
|
|
65
81
|
if (githubShorthandSource?.subpath) {
|
|
66
82
|
return githubShorthandSource;
|
|
@@ -161,6 +177,110 @@ export class SkillsManager {
|
|
|
161
177
|
return null;
|
|
162
178
|
}
|
|
163
179
|
}
|
|
180
|
+
parseGitLabHttpSource(source) {
|
|
181
|
+
if (!source.startsWith('https://gitlab.com/') && !source.startsWith('http://gitlab.com/')) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const parsedUrl = new URL(source);
|
|
186
|
+
const segments = parsedUrl.pathname.replace(/\/+$/, '').split('/').filter(Boolean);
|
|
187
|
+
const dashIndex = segments.indexOf('-');
|
|
188
|
+
const repoBoundary = dashIndex >= 0 ? dashIndex : segments.length;
|
|
189
|
+
if (repoBoundary < 2) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const repo = segments[repoBoundary - 1];
|
|
193
|
+
const owner = segments.slice(0, repoBoundary - 1).join('/');
|
|
194
|
+
if (!owner || !repo) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
let subpath;
|
|
198
|
+
if (dashIndex >= 0) {
|
|
199
|
+
const marker = segments[dashIndex + 1];
|
|
200
|
+
if ((marker === 'tree' || marker === 'blob') && segments.length > dashIndex + 3) {
|
|
201
|
+
subpath = segments.slice(dashIndex + 3).join('/');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
type: 'gitlab',
|
|
206
|
+
url: `https://gitlab.com/${owner}/${repo}.git`,
|
|
207
|
+
owner,
|
|
208
|
+
repo,
|
|
209
|
+
...(subpath ? { subpath } : {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
parseBitbucketHttpSource(source) {
|
|
217
|
+
if (!source.startsWith('https://bitbucket.org/') && !source.startsWith('http://bitbucket.org/')) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const parsedUrl = new URL(source);
|
|
222
|
+
const segments = parsedUrl.pathname.replace(/\/+$/, '').split('/').filter(Boolean);
|
|
223
|
+
if (segments.length < 2) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const [owner, repo, marker, _commitish, ...rest] = segments;
|
|
227
|
+
if (!owner || !repo) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
let subpath;
|
|
231
|
+
if (marker === 'src' && rest.length > 0) {
|
|
232
|
+
subpath = rest.join('/');
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
type: 'bitbucket',
|
|
236
|
+
url: `https://bitbucket.org/${owner}/${repo}.git`,
|
|
237
|
+
owner,
|
|
238
|
+
repo,
|
|
239
|
+
...(subpath ? { subpath } : {}),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
parseHttpRepositorySource(source) {
|
|
247
|
+
return this.parseGitHubHttpSource(source)
|
|
248
|
+
|| this.parseGitLabHttpSource(source)
|
|
249
|
+
|| this.parseBitbucketHttpSource(source);
|
|
250
|
+
}
|
|
251
|
+
parseSshRepositorySource(source) {
|
|
252
|
+
const githubMatch = source.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
253
|
+
if (githubMatch) {
|
|
254
|
+
const [, owner, repo] = githubMatch;
|
|
255
|
+
return {
|
|
256
|
+
type: 'github',
|
|
257
|
+
url: `git@github.com:${owner}/${repo}.git`,
|
|
258
|
+
owner,
|
|
259
|
+
repo,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const gitlabMatch = source.match(/^git@gitlab\.com:(.+)\/([^/]+?)(?:\.git)?$/);
|
|
263
|
+
if (gitlabMatch) {
|
|
264
|
+
const [, owner, repo] = gitlabMatch;
|
|
265
|
+
return {
|
|
266
|
+
type: 'gitlab',
|
|
267
|
+
url: `git@gitlab.com:${owner}/${repo}.git`,
|
|
268
|
+
owner,
|
|
269
|
+
repo,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const bitbucketMatch = source.match(/^git@bitbucket\.org:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
273
|
+
if (bitbucketMatch) {
|
|
274
|
+
const [, owner, repo] = bitbucketMatch;
|
|
275
|
+
return {
|
|
276
|
+
type: 'bitbucket',
|
|
277
|
+
url: `git@bitbucket.org:${owner}/${repo}.git`,
|
|
278
|
+
owner,
|
|
279
|
+
repo,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
164
284
|
parseGitHubShorthandSource(source) {
|
|
165
285
|
const githubShorthandMatch = source.match(/^([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)(?:\/(.+))?$/);
|
|
166
286
|
if (!githubShorthandMatch) {
|
|
@@ -175,6 +295,58 @@ export class SkillsManager {
|
|
|
175
295
|
...(subpath ? { subpath } : {}),
|
|
176
296
|
};
|
|
177
297
|
}
|
|
298
|
+
parseGitLabShorthandSource(source) {
|
|
299
|
+
const normalized = source.startsWith('gitlab:')
|
|
300
|
+
? source.slice('gitlab:'.length)
|
|
301
|
+
: source.startsWith('gitlab.com/')
|
|
302
|
+
? source.slice('gitlab.com/'.length)
|
|
303
|
+
: null;
|
|
304
|
+
if (!normalized) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
const [repoSpec = normalized, subpathSpec] = normalized.split('//', 2);
|
|
308
|
+
const segments = repoSpec.split('/').filter(Boolean);
|
|
309
|
+
if (segments.length < 2) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const repo = segments[segments.length - 1];
|
|
313
|
+
const owner = segments.slice(0, segments.length - 1).join('/');
|
|
314
|
+
if (!owner || !repo) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
type: 'gitlab',
|
|
319
|
+
url: `https://gitlab.com/${owner}/${repo}.git`,
|
|
320
|
+
owner,
|
|
321
|
+
repo,
|
|
322
|
+
...(subpathSpec ? { subpath: subpathSpec } : {}),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
parseBitbucketShorthandSource(source) {
|
|
326
|
+
const normalized = source.startsWith('bitbucket:')
|
|
327
|
+
? source.slice('bitbucket:'.length)
|
|
328
|
+
: source.startsWith('bitbucket.org/')
|
|
329
|
+
? source.slice('bitbucket.org/'.length)
|
|
330
|
+
: null;
|
|
331
|
+
if (!normalized) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
335
|
+
if (segments.length < 2) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
const [owner, repo, ...rest] = segments;
|
|
339
|
+
if (!owner || !repo) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
type: 'bitbucket',
|
|
344
|
+
url: `https://bitbucket.org/${owner}/${repo}.git`,
|
|
345
|
+
owner,
|
|
346
|
+
repo,
|
|
347
|
+
...(rest.length > 0 ? { subpath: rest.join('/') } : {}),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
178
350
|
/**
|
|
179
351
|
* Parse a SKILL.md file and extract name + description from frontmatter
|
|
180
352
|
*/
|
|
@@ -322,7 +494,7 @@ export class SkillsManager {
|
|
|
322
494
|
}
|
|
323
495
|
async resolveDiscoveryRoot(repoPath, source, sourceLabel) {
|
|
324
496
|
const resolvedRepoPath = resolve(repoPath);
|
|
325
|
-
if (source.type !== 'github' || !source.subpath) {
|
|
497
|
+
if ((source.type !== 'github' && source.type !== 'gitlab' && source.type !== 'bitbucket') || !source.subpath) {
|
|
326
498
|
return resolvedRepoPath;
|
|
327
499
|
}
|
|
328
500
|
const discoveryRoot = resolve(resolvedRepoPath, source.subpath);
|
|
@@ -367,7 +539,7 @@ export class SkillsManager {
|
|
|
367
539
|
};
|
|
368
540
|
}
|
|
369
541
|
let repoPath;
|
|
370
|
-
if (resolved.type === 'github') {
|
|
542
|
+
if (resolved.type === 'github' || resolved.type === 'gitlab' || resolved.type === 'bitbucket') {
|
|
371
543
|
if (!resolved.url) {
|
|
372
544
|
throw new Error(`Invalid source: ${source}`);
|
|
373
545
|
}
|
|
@@ -543,7 +715,7 @@ export class SkillsManager {
|
|
|
543
715
|
...(options.global !== undefined ? { global: options.global } : {}),
|
|
544
716
|
...(options.copy !== undefined ? { copy: options.copy } : {}),
|
|
545
717
|
});
|
|
546
|
-
return this.
|
|
718
|
+
return this.compareSkillInstallStatus(skill, plan);
|
|
547
719
|
}
|
|
548
720
|
async installSkillForAgent(skillPath, skillName, agent, projectPath, options = {}) {
|
|
549
721
|
const plan = await this.getInstallPlan(skillName, agent, projectPath, options);
|
|
@@ -625,6 +797,76 @@ export class SkillsManager {
|
|
|
625
797
|
mode: 'symlink',
|
|
626
798
|
};
|
|
627
799
|
}
|
|
800
|
+
normalizeSkillPrefix(prefix) {
|
|
801
|
+
const normalized = prefix?.trim() ?? '';
|
|
802
|
+
if (normalized.includes('/') || normalized.includes('\\')) {
|
|
803
|
+
throw new Error(`Invalid skill prefix: ${prefix}`);
|
|
804
|
+
}
|
|
805
|
+
return normalized;
|
|
806
|
+
}
|
|
807
|
+
withSkillPrefix(skillName, prefix) {
|
|
808
|
+
const normalizedPrefix = this.normalizeSkillPrefix(prefix);
|
|
809
|
+
return normalizedPrefix ? `${normalizedPrefix}${skillName}` : skillName;
|
|
810
|
+
}
|
|
811
|
+
async rewriteSkillFileName(filePath, skillName) {
|
|
812
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
813
|
+
const parsed = matter(content);
|
|
814
|
+
const nextContent = matter.stringify(parsed.content, {
|
|
815
|
+
...parsed.data,
|
|
816
|
+
name: skillName,
|
|
817
|
+
});
|
|
818
|
+
await fs.writeFile(filePath, nextContent, 'utf8');
|
|
819
|
+
}
|
|
820
|
+
async applyPrefixToSkills(skills, prefix) {
|
|
821
|
+
const normalizedPrefix = this.normalizeSkillPrefix(prefix);
|
|
822
|
+
if (!normalizedPrefix) {
|
|
823
|
+
return { skills, cleanup: async () => { } };
|
|
824
|
+
}
|
|
825
|
+
const tempDirs = [];
|
|
826
|
+
try {
|
|
827
|
+
const prefixedSkills = await Promise.all(skills.map(async (skill) => {
|
|
828
|
+
const name = this.withSkillPrefix(skill.name, normalizedPrefix);
|
|
829
|
+
if (skill.generatedContent) {
|
|
830
|
+
return {
|
|
831
|
+
...skill,
|
|
832
|
+
name,
|
|
833
|
+
generatedContent: matter.stringify(matter(skill.generatedContent).content, {
|
|
834
|
+
...matter(skill.generatedContent).data,
|
|
835
|
+
name,
|
|
836
|
+
}),
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
const tempRoot = await fs.mkdtemp(join(tmpdir(), 'agentinit-prefixed-skill-'));
|
|
840
|
+
const tempSkillPath = join(tempRoot, basename(skill.path));
|
|
841
|
+
tempDirs.push(tempRoot);
|
|
842
|
+
await this.copyDir(skill.path, tempSkillPath);
|
|
843
|
+
const skillMdPath = join(tempSkillPath, 'SKILL.md');
|
|
844
|
+
const skillMdPathLower = join(tempSkillPath, 'skill.md');
|
|
845
|
+
const skillFile = (await fileExists(skillMdPath)) ? skillMdPath
|
|
846
|
+
: (await fileExists(skillMdPathLower)) ? skillMdPathLower
|
|
847
|
+
: null;
|
|
848
|
+
if (!skillFile) {
|
|
849
|
+
throw new Error(`Skill "${skill.name}" is missing SKILL.md`);
|
|
850
|
+
}
|
|
851
|
+
await this.rewriteSkillFileName(skillFile, name);
|
|
852
|
+
return {
|
|
853
|
+
...skill,
|
|
854
|
+
name,
|
|
855
|
+
path: tempSkillPath,
|
|
856
|
+
};
|
|
857
|
+
}));
|
|
858
|
+
return {
|
|
859
|
+
skills: prefixedSkills,
|
|
860
|
+
cleanup: async () => {
|
|
861
|
+
await Promise.all(tempDirs.map(dir => fs.rm(dir, { recursive: true, force: true }).catch(() => { })));
|
|
862
|
+
},
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
catch (error) {
|
|
866
|
+
await Promise.all(tempDirs.map(dir => fs.rm(dir, { recursive: true, force: true }).catch(() => { })));
|
|
867
|
+
throw error;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
628
870
|
normalizeSkillName(skillName) {
|
|
629
871
|
const normalized = skillName.trim();
|
|
630
872
|
if (!normalized) {
|
|
@@ -693,6 +935,23 @@ export class SkillsManager {
|
|
|
693
935
|
return 'new';
|
|
694
936
|
return existing === incoming ? 'unchanged' : 'changed';
|
|
695
937
|
}
|
|
938
|
+
async compareSkillInstallStatus(skill, plan) {
|
|
939
|
+
const incoming = await this.getNewSkillSnapshot(skill);
|
|
940
|
+
if (incoming === null)
|
|
941
|
+
return 'new';
|
|
942
|
+
const existingAtTarget = await this.readExistingSkillSnapshot(plan.path);
|
|
943
|
+
if (existingAtTarget !== null) {
|
|
944
|
+
return existingAtTarget === incoming ? 'unchanged' : 'changed';
|
|
945
|
+
}
|
|
946
|
+
if (!plan.canonicalPath || plan.canonicalPath === plan.path) {
|
|
947
|
+
return 'new';
|
|
948
|
+
}
|
|
949
|
+
const existingAtCanonical = await this.readExistingSkillSnapshot(plan.canonicalPath);
|
|
950
|
+
if (existingAtCanonical === null) {
|
|
951
|
+
return 'new';
|
|
952
|
+
}
|
|
953
|
+
return existingAtCanonical === incoming ? 'new' : 'changed';
|
|
954
|
+
}
|
|
696
955
|
async cleanAndCreateDirectory(path) {
|
|
697
956
|
await fs.rm(path, { recursive: true, force: true }).catch(() => { });
|
|
698
957
|
await fs.mkdir(path, { recursive: true });
|
|
@@ -713,11 +972,13 @@ export class SkillsManager {
|
|
|
713
972
|
* Add skills from a source (GitHub repo or local path)
|
|
714
973
|
*/
|
|
715
974
|
async addFromSource(source, projectPath, options = {}) {
|
|
975
|
+
const normalizedPrefix = this.normalizeSkillPrefix(options.prefix);
|
|
716
976
|
const context = this.takePreparedSourceContext(source, projectPath, options.from)
|
|
717
977
|
|| await this.loadDiscoveredSkillsContext(source, projectPath, {
|
|
718
978
|
...(options.from !== undefined ? { from: options.from } : {}),
|
|
719
979
|
...(options.pluginName !== undefined ? { pluginName: options.pluginName } : {}),
|
|
720
980
|
});
|
|
981
|
+
let prefixedCleanup = async () => { };
|
|
721
982
|
try {
|
|
722
983
|
let skills = context.skills;
|
|
723
984
|
if (skills.length === 0) {
|
|
@@ -725,7 +986,37 @@ export class SkillsManager {
|
|
|
725
986
|
}
|
|
726
987
|
if (options.skills && options.skills.length > 0) {
|
|
727
988
|
const names = new Set(options.skills.map(skill => skill.toLowerCase()));
|
|
728
|
-
skills = skills.filter(skill => names.has(skill.name.toLowerCase())
|
|
989
|
+
skills = skills.filter(skill => names.has(skill.name.toLowerCase())
|
|
990
|
+
|| names.has(this.withSkillPrefix(skill.name, normalizedPrefix).toLowerCase()));
|
|
991
|
+
}
|
|
992
|
+
const prefixed = await this.applyPrefixToSkills(skills, normalizedPrefix);
|
|
993
|
+
skills = prefixed.skills;
|
|
994
|
+
prefixedCleanup = prefixed.cleanup;
|
|
995
|
+
const result = { installed: [], updated: [], unchanged: [], skipped: [], warnings: [...context.warnings] };
|
|
996
|
+
if (options.scan !== false) {
|
|
997
|
+
const scannedWarnings = new Set();
|
|
998
|
+
const scannableSkills = [];
|
|
999
|
+
for (const skill of skills) {
|
|
1000
|
+
const scan = await this.skillScanner.scanSkill(skill);
|
|
1001
|
+
if (scan.findings.length === 0) {
|
|
1002
|
+
scannableSkills.push(skill);
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
if (scan.blocked && !options.allowRisky) {
|
|
1006
|
+
result.skipped.push({
|
|
1007
|
+
skill,
|
|
1008
|
+
reason: this.skillScanner.formatBlockingReason(scan),
|
|
1009
|
+
});
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
const summary = this.skillScanner.formatShortSummary(scan);
|
|
1013
|
+
scannedWarnings.add(scan.blocked
|
|
1014
|
+
? `Proceeding with "${skill.name}" despite high-risk findings: ${summary}`
|
|
1015
|
+
: `Security warnings for "${skill.name}": ${summary}`);
|
|
1016
|
+
scannableSkills.push(skill);
|
|
1017
|
+
}
|
|
1018
|
+
skills = scannableSkills;
|
|
1019
|
+
result.warnings.push(...scannedWarnings);
|
|
729
1020
|
}
|
|
730
1021
|
const installToSharedStore = options.agents?.includes(SHARED_SKILLS_TARGET_ID) ?? false;
|
|
731
1022
|
const agents = await this.getTargetAgents(projectPath, options);
|
|
@@ -734,13 +1025,15 @@ export class SkillsManager {
|
|
|
734
1025
|
installed: [],
|
|
735
1026
|
updated: [],
|
|
736
1027
|
unchanged: [],
|
|
737
|
-
skipped:
|
|
738
|
-
|
|
1028
|
+
skipped: [
|
|
1029
|
+
...result.skipped,
|
|
1030
|
+
...skills.map(skill => ({ skill, reason: 'No target agents found' })),
|
|
1031
|
+
],
|
|
1032
|
+
warnings: result.warnings,
|
|
739
1033
|
};
|
|
740
1034
|
}
|
|
741
|
-
const result = { installed: [], updated: [], unchanged: [], skipped: [], warnings: context.warnings };
|
|
742
1035
|
const installableAgents = [];
|
|
743
|
-
// Cache comparison results by
|
|
1036
|
+
// Cache comparison results by install path to avoid re-comparing the same target
|
|
744
1037
|
const comparisonCache = new Map();
|
|
745
1038
|
// Collect pending updates to prompt user once for all of them
|
|
746
1039
|
const pendingUpdates = [];
|
|
@@ -795,10 +1088,10 @@ export class SkillsManager {
|
|
|
795
1088
|
...(options.copy !== undefined ? { copy: options.copy } : {}),
|
|
796
1089
|
};
|
|
797
1090
|
const plan = await this.getInstallPlan(skill.name, agent, projectPath, installOptions);
|
|
798
|
-
const comparisonPath = plan.
|
|
1091
|
+
const comparisonPath = plan.path;
|
|
799
1092
|
let comparison = comparisonCache.get(comparisonPath);
|
|
800
1093
|
if (comparison === undefined) {
|
|
801
|
-
comparison = await this.
|
|
1094
|
+
comparison = await this.compareSkillInstallStatus(skill, plan);
|
|
802
1095
|
comparisonCache.set(comparisonPath, comparison);
|
|
803
1096
|
}
|
|
804
1097
|
if (comparison === 'unchanged') {
|
|
@@ -857,9 +1150,50 @@ export class SkillsManager {
|
|
|
857
1150
|
}
|
|
858
1151
|
}
|
|
859
1152
|
}
|
|
1153
|
+
// Record to global install lock
|
|
1154
|
+
try {
|
|
1155
|
+
const lock = new InstallLock();
|
|
1156
|
+
const fromOption = options.from;
|
|
1157
|
+
const { source: resolvedSource } = this.resolveSourceRequest(source, ...(fromOption ? [{ from: fromOption }] : []));
|
|
1158
|
+
const lockSource = {
|
|
1159
|
+
type: resolvedSource.type,
|
|
1160
|
+
...(resolvedSource.marketplace ? { marketplace: resolvedSource.marketplace } : {}),
|
|
1161
|
+
...(resolvedSource.pluginName ? { pluginName: resolvedSource.pluginName } : {}),
|
|
1162
|
+
...(normalizedPrefix ? { prefix: normalizedPrefix } : {}),
|
|
1163
|
+
...(resolvedSource.url ? { url: resolvedSource.url } : {}),
|
|
1164
|
+
...(resolvedSource.path ? { path: resolve(projectPath, expandTilde(resolvedSource.path)) } : {}),
|
|
1165
|
+
...(resolvedSource.owner ? { owner: resolvedSource.owner } : {}),
|
|
1166
|
+
...(resolvedSource.repo ? { repo: resolvedSource.repo } : {}),
|
|
1167
|
+
...(resolvedSource.subpath ? { subpath: resolvedSource.subpath } : {}),
|
|
1168
|
+
};
|
|
1169
|
+
const recordEntries = async (entries, action) => {
|
|
1170
|
+
for (const entry of entries) {
|
|
1171
|
+
const hashPath = entry.canonicalPath || entry.path;
|
|
1172
|
+
const contentHash = await hashDirectory(hashPath);
|
|
1173
|
+
await lock.recordSkill({
|
|
1174
|
+
action,
|
|
1175
|
+
name: entry.skill.name,
|
|
1176
|
+
projectPath: resolve(projectPath),
|
|
1177
|
+
agents: [entry.agent],
|
|
1178
|
+
scope: options.global ? 'global' : 'project',
|
|
1179
|
+
source: lockSource,
|
|
1180
|
+
installPath: entry.path,
|
|
1181
|
+
mode: entry.mode,
|
|
1182
|
+
...(entry.canonicalPath ? { canonicalPath: entry.canonicalPath } : {}),
|
|
1183
|
+
...(contentHash ? { contentHash } : {}),
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
await recordEntries(result.installed, 'install');
|
|
1188
|
+
await recordEntries(result.updated, 'update');
|
|
1189
|
+
}
|
|
1190
|
+
catch (error) {
|
|
1191
|
+
logLockWriteWarning('Skills changed successfully', error);
|
|
1192
|
+
}
|
|
860
1193
|
return result;
|
|
861
1194
|
}
|
|
862
1195
|
finally {
|
|
1196
|
+
await prefixedCleanup();
|
|
863
1197
|
await context.cleanup();
|
|
864
1198
|
}
|
|
865
1199
|
}
|
|
@@ -1060,6 +1394,31 @@ export class SkillsManager {
|
|
|
1060
1394
|
notFound.push(name);
|
|
1061
1395
|
}
|
|
1062
1396
|
}
|
|
1397
|
+
// Record removals to global install lock
|
|
1398
|
+
try {
|
|
1399
|
+
if (targetedEntries.length > 0) {
|
|
1400
|
+
const lock = new InstallLock();
|
|
1401
|
+
for (const entry of targetedEntries) {
|
|
1402
|
+
const entryKey = `${entry.agent}:${entry.name}`;
|
|
1403
|
+
if (!removed.includes(entryKey))
|
|
1404
|
+
continue;
|
|
1405
|
+
await lock.recordSkill({
|
|
1406
|
+
action: 'remove',
|
|
1407
|
+
name: entry.name,
|
|
1408
|
+
projectPath: resolve(projectPath),
|
|
1409
|
+
agents: [entry.agent],
|
|
1410
|
+
scope: entry.scope,
|
|
1411
|
+
source: { type: 'local', path: entry.path },
|
|
1412
|
+
installPath: entry.path,
|
|
1413
|
+
mode: entry.mode,
|
|
1414
|
+
...(entry.canonicalPath ? { canonicalPath: entry.canonicalPath } : {}),
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
catch (error) {
|
|
1420
|
+
logLockWriteWarning('Skills removed successfully', error);
|
|
1421
|
+
}
|
|
1063
1422
|
return { removed, notFound, skipped };
|
|
1064
1423
|
}
|
|
1065
1424
|
}
|