@silverassist/agents-toolkit 2.3.0 → 2.5.0
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 +2 -2
- package/README.md +166 -33
- package/bin/cli.js +511 -16
- package/package.json +1 -1
- package/src/index.js +49 -9
- package/templates/agents/AGENTS.md +33 -5
- package/templates/shared/hooks/lint-format.json +1 -0
- package/templates/shared/hooks/validate-tsx.json +1 -0
- package/templates/shared/instructions/caching.instructions.md +121 -0
- package/templates/shared/instructions/seo-ai-optimization.instructions.md +272 -0
- package/templates/shared/prompts/_partials/release-node.md +58 -0
- package/templates/shared/prompts/_partials/release-wordpress.md +56 -0
- package/templates/shared/prompts/_partials/validations.md +1 -0
- package/templates/shared/prompts/create-github-pr.prompt.md +147 -0
- package/templates/shared/prompts/create-plan.prompt.md +8 -0
- package/templates/shared/prompts/finalize-github-pr.prompt.md +129 -0
- package/templates/shared/prompts/prepare-github-release.prompt.md +134 -0
- package/templates/shared/prompts/review-code.prompt.md +12 -0
- package/templates/shared/prompts/work-github-issue.prompt.md +1 -1
- package/templates/shared/skills/README.md +25 -3
- package/templates/shared/skills/nextjs-caching/SKILL.md +184 -0
- package/templates/shared/prompts/prepare-release.prompt.md +0 -74
package/bin/cli.js
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
|
+
import crypto from 'node:crypto';
|
|
10
11
|
import { fileURLToPath } from 'url';
|
|
12
|
+
import { VERSION } from '../src/index.js';
|
|
11
13
|
|
|
12
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
15
|
const __dirname = path.dirname(__filename);
|
|
@@ -49,7 +51,7 @@ const DEFAULT_CONFIG = {
|
|
|
49
51
|
*/
|
|
50
52
|
const FILE_CATEGORIES = {
|
|
51
53
|
instructions: {
|
|
52
|
-
react: ['css-styling', 'react-components', 'server-actions', 'tests', 'typescript'],
|
|
54
|
+
react: ['caching', 'css-styling', 'react-components', 'seo-ai-optimization', 'server-actions', 'tests', 'typescript'],
|
|
53
55
|
wordpress: ['php-standards', 'wordpress-plugin-architecture', 'testing-standards'],
|
|
54
56
|
universal: ['documentation-language', 'github-workflow'],
|
|
55
57
|
},
|
|
@@ -59,18 +61,21 @@ const FILE_CATEGORIES = {
|
|
|
59
61
|
universal: [
|
|
60
62
|
'analyze-ticket', 'work-ticket', 'analyze-github-issue', 'work-github-issue',
|
|
61
63
|
'create-plan', 'create-pr', 'prepare-pr', 'finalize-pr',
|
|
62
|
-
'
|
|
64
|
+
'create-github-pr', 'finalize-github-pr',
|
|
65
|
+
'review-code', 'fix-issues', 'add-tests', 'prepare-github-release',
|
|
63
66
|
],
|
|
64
67
|
jira: ['analyze-ticket', 'work-ticket', 'create-pr', 'finalize-pr'],
|
|
65
|
-
github: ['analyze-github-issue', 'work-github-issue'],
|
|
68
|
+
github: ['analyze-github-issue', 'work-github-issue', 'create-github-pr', 'finalize-github-pr'],
|
|
66
69
|
},
|
|
67
70
|
partials: {
|
|
71
|
+
react: ['release-node'],
|
|
72
|
+
wordpress: ['release-wordpress'],
|
|
68
73
|
jira: ['jira-integration'],
|
|
69
74
|
github: ['github-integration'],
|
|
70
75
|
universal: ['git-operations', 'pr-template', 'validations', 'documentation'],
|
|
71
76
|
},
|
|
72
77
|
skills: {
|
|
73
|
-
react: ['component-architecture', 'testing-patterns'],
|
|
78
|
+
react: ['component-architecture', 'nextjs-caching', 'testing-patterns'],
|
|
74
79
|
wordpress: ['create-component', 'plugin-creation', 'quality-checks', 'testing'],
|
|
75
80
|
universal: ['domain-driven-design', 'release-management'],
|
|
76
81
|
},
|
|
@@ -179,6 +184,242 @@ function getClaudeTargetDir(global = false) {
|
|
|
179
184
|
return path.join(process.cwd(), '.claude');
|
|
180
185
|
}
|
|
181
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Get the canonical skills directory following the `npx skills` standard.
|
|
189
|
+
* Skills live here once (single source of truth) and each agent's skills
|
|
190
|
+
* directory symlinks to it.
|
|
191
|
+
* @param {boolean} global - Use user-level ~/.agents/skills/
|
|
192
|
+
* @returns {string} Path to .agents/skills directory
|
|
193
|
+
*/
|
|
194
|
+
function getAgentsSkillsDir(global = false) {
|
|
195
|
+
const base = global ? getHomeDir() : process.cwd();
|
|
196
|
+
return path.join(base, '.agents', 'skills');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Path to the project-level lockfile. */
|
|
200
|
+
const LOCKFILE_NAME = 'agents-toolkit-lock.json';
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Compute a hex-encoded SHA-256 hash of a skill's SKILL.md content.
|
|
204
|
+
* Matches the algorithm used by `npx skills` (Vercel).
|
|
205
|
+
* @param {string} skillDir - Absolute path to the skill directory
|
|
206
|
+
* @returns {string|null} Hex hash string, or null if SKILL.md is missing
|
|
207
|
+
*/
|
|
208
|
+
function computeSkillHash(skillDir) {
|
|
209
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
210
|
+
if (!fs.existsSync(skillMdPath)) return null;
|
|
211
|
+
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
212
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Read and parse the project lockfile.
|
|
217
|
+
* @param {string} [cwd] - Directory to look in (defaults to process.cwd())
|
|
218
|
+
* @returns {Object|null} Parsed lockfile, or null if absent or invalid
|
|
219
|
+
*/
|
|
220
|
+
function readLockfile(cwd = process.cwd()) {
|
|
221
|
+
const lockPath = path.join(cwd, LOCKFILE_NAME);
|
|
222
|
+
if (!fs.existsSync(lockPath)) return null;
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Write the project lockfile, merging new skills with any existing entries.
|
|
232
|
+
* This preserves skills installed by a previous `install --target` run so that
|
|
233
|
+
* running `install` followed by `install --claude` does not lose the first
|
|
234
|
+
* target's entries in the lockfile.
|
|
235
|
+
* @param {Object} params
|
|
236
|
+
* @param {Record<string, {computedHash: string|null, agents: string[]}>} params.skills - Newly installed skills map
|
|
237
|
+
* @param {{ stack: string, tracker: string }} params.config - Active install config
|
|
238
|
+
* @param {string} params.packageVersion - Current package version
|
|
239
|
+
* @param {string} [params.cwd] - Directory to write to (defaults to process.cwd())
|
|
240
|
+
*/
|
|
241
|
+
function writeLockfile({ skills, config, packageVersion, cwd = process.cwd() }) {
|
|
242
|
+
const lockPath = path.join(cwd, LOCKFILE_NAME);
|
|
243
|
+
|
|
244
|
+
// Merge with any existing lockfile so successive multi-target installs
|
|
245
|
+
// (e.g. `install` then `install --claude`) accumulate entries.
|
|
246
|
+
const existing = readLockfile(cwd);
|
|
247
|
+
const mergedSkills = Object.assign({}, existing?.skills ?? {});
|
|
248
|
+
|
|
249
|
+
for (const [name, meta] of Object.entries(skills)) {
|
|
250
|
+
const prev = mergedSkills[name];
|
|
251
|
+
// Merge agents arrays: union of previous and new agent dirs.
|
|
252
|
+
const prevAgents = prev?.agents ?? [];
|
|
253
|
+
const allAgents = Array.from(new Set([...prevAgents, ...meta.agents]));
|
|
254
|
+
mergedSkills[name] = {
|
|
255
|
+
source: '@silverassist/agents-toolkit',
|
|
256
|
+
packageVersion,
|
|
257
|
+
computedHash: meta.computedHash,
|
|
258
|
+
agents: allAgents,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const lockfile = {
|
|
263
|
+
version: 1,
|
|
264
|
+
packageVersion,
|
|
265
|
+
config,
|
|
266
|
+
skills: mergedSkills,
|
|
267
|
+
};
|
|
268
|
+
fs.writeFileSync(lockPath, JSON.stringify(lockfile, null, 2) + '\n', 'utf-8');
|
|
269
|
+
success(`Wrote ${LOCKFILE_NAME}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Append skills-managed entries to .gitignore if not already present.
|
|
274
|
+
* Never runs during dry-run or global installs.
|
|
275
|
+
* @param {string} cwd - Project root directory
|
|
276
|
+
*/
|
|
277
|
+
function appendSkillsToGitignore(cwd) {
|
|
278
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
279
|
+
const block = [
|
|
280
|
+
'',
|
|
281
|
+
'# agents-toolkit managed — regenerate with: npx @silverassist/agents-toolkit restore',
|
|
282
|
+
'.agents/skills/',
|
|
283
|
+
'.github/skills/',
|
|
284
|
+
'.claude/skills/',
|
|
285
|
+
].join('\n');
|
|
286
|
+
|
|
287
|
+
let existing = '';
|
|
288
|
+
if (fs.existsSync(gitignorePath)) {
|
|
289
|
+
existing = fs.readFileSync(gitignorePath, 'utf-8');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Only append if none of the three managed paths are already present.
|
|
293
|
+
if (
|
|
294
|
+
existing.includes('.agents/skills/') ||
|
|
295
|
+
existing.includes('.github/skills/') ||
|
|
296
|
+
existing.includes('.claude/skills/')
|
|
297
|
+
) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
fs.writeFileSync(gitignorePath, existing + block + '\n', 'utf-8');
|
|
302
|
+
info('Updated .gitignore with agents-toolkit managed paths');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Link a single skill from the canonical store into an agent's skills
|
|
307
|
+
* directory, following the `npx skills` standard (symlink with copy fallback).
|
|
308
|
+
* @param {string} canonicalSkillDir - Absolute path to the canonical skill folder
|
|
309
|
+
* @param {string} agentSkillLinkPath - Absolute path where the agent expects the skill
|
|
310
|
+
* @param {Object} options - Link options
|
|
311
|
+
* @param {boolean} [options.dryRun] - Only report what would happen
|
|
312
|
+
* @param {boolean} [options.force] - Replace an existing non-matching entry
|
|
313
|
+
* @param {boolean} [options.copy] - Force a copy instead of a symlink
|
|
314
|
+
* @returns {{ written: number, planned: number }} Change counters
|
|
315
|
+
*/
|
|
316
|
+
function linkSkill(canonicalSkillDir, agentSkillLinkPath, options = {}) {
|
|
317
|
+
const { dryRun = false, force = false, copy = false } = options;
|
|
318
|
+
const totals = { written: 0, planned: 0 };
|
|
319
|
+
|
|
320
|
+
const relTarget = path.relative(path.dirname(agentSkillLinkPath), canonicalSkillDir);
|
|
321
|
+
const rel = (p) => path.relative(process.cwd(), p);
|
|
322
|
+
|
|
323
|
+
// Inspect any existing entry at the link path.
|
|
324
|
+
let existing = null;
|
|
325
|
+
try {
|
|
326
|
+
existing = fs.lstatSync(agentSkillLinkPath);
|
|
327
|
+
} catch {
|
|
328
|
+
existing = null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (existing) {
|
|
332
|
+
// Already a symlink pointing at our canonical store — nothing to do.
|
|
333
|
+
if (existing.isSymbolicLink()) {
|
|
334
|
+
const current = fs.readlinkSync(agentSkillLinkPath);
|
|
335
|
+
const resolved = path.resolve(path.dirname(agentSkillLinkPath), current);
|
|
336
|
+
if (resolved === path.resolve(canonicalSkillDir) && !copy) {
|
|
337
|
+
return totals;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (!force) {
|
|
341
|
+
warn(`Skipping existing skill: ${rel(agentSkillLinkPath)}`);
|
|
342
|
+
return totals;
|
|
343
|
+
}
|
|
344
|
+
if (!dryRun) {
|
|
345
|
+
fs.rmSync(agentSkillLinkPath, { recursive: true, force: true });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
totals.planned++;
|
|
350
|
+
|
|
351
|
+
if (dryRun) {
|
|
352
|
+
info(copy ? `Would copy skill: ${rel(agentSkillLinkPath)}` : `Would link: ${rel(agentSkillLinkPath)} -> ${relTarget}`);
|
|
353
|
+
return totals;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fs.mkdirSync(path.dirname(agentSkillLinkPath), { recursive: true });
|
|
357
|
+
|
|
358
|
+
const doCopy = () => {
|
|
359
|
+
copyDir(canonicalSkillDir, agentSkillLinkPath, { force: true });
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
if (copy) {
|
|
363
|
+
doCopy();
|
|
364
|
+
} else {
|
|
365
|
+
try {
|
|
366
|
+
fs.symlinkSync(relTarget, agentSkillLinkPath, 'dir');
|
|
367
|
+
} catch {
|
|
368
|
+
// Symlinks unsupported (e.g. Windows without developer mode) — copy instead.
|
|
369
|
+
doCopy();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
totals.written++;
|
|
374
|
+
return totals;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Install skills following the `npx skills` standard: copy each skill once
|
|
379
|
+
* into the canonical .agents/skills store, then symlink the agent's skills
|
|
380
|
+
* directory entries to that store.
|
|
381
|
+
* @param {Object} params - Install parameters
|
|
382
|
+
* @param {boolean} params.isGlobal - Install at user level
|
|
383
|
+
* @param {string} params.agentSkillsDir - Agent skills dir (.claude/skills or .github/skills)
|
|
384
|
+
* @param {boolean} [params.force] - Overwrite existing files/links
|
|
385
|
+
* @param {boolean} [params.dryRun] - Only report planned changes
|
|
386
|
+
* @param {boolean} [params.copy] - Copy instead of symlink
|
|
387
|
+
* @param {(name: string) => boolean} [params.dirFilter] - Skill folder filter
|
|
388
|
+
* @returns {{ written: number, planned: number, installedSkills: Record<string, { canonicalDir: string }> }} Aggregated change counters and installed skill metadata
|
|
389
|
+
*/
|
|
390
|
+
function installSkillsStandard({ isGlobal, agentSkillsDir, force = false, dryRun = false, copy = false, dirFilter = null }) {
|
|
391
|
+
const totals = { written: 0, planned: 0, installedSkills: {} };
|
|
392
|
+
const skillsSrc = path.join(TEMPLATES_DIR, 'shared', 'skills');
|
|
393
|
+
const canonicalDir = getAgentsSkillsDir(isGlobal);
|
|
394
|
+
|
|
395
|
+
if (!fs.existsSync(skillsSrc)) {
|
|
396
|
+
return totals;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 1. Populate the canonical store once (filtered by stack).
|
|
400
|
+
const canonicalResult = copyDir(skillsSrc, canonicalDir, { force, dryRun, dirFilter });
|
|
401
|
+
totals.written += canonicalResult.written;
|
|
402
|
+
totals.planned += canonicalResult.planned;
|
|
403
|
+
|
|
404
|
+
// 2. Symlink each included skill folder into the agent's skills directory.
|
|
405
|
+
const entries = fs.readdirSync(skillsSrc, { withFileTypes: true });
|
|
406
|
+
for (const entry of entries) {
|
|
407
|
+
if (!entry.isDirectory()) continue;
|
|
408
|
+
if (dirFilter && !dirFilter(entry.name)) continue;
|
|
409
|
+
|
|
410
|
+
const canonicalSkillDir = path.join(canonicalDir, entry.name);
|
|
411
|
+
const agentSkillLinkPath = path.join(agentSkillsDir, entry.name);
|
|
412
|
+
const linkResult = linkSkill(canonicalSkillDir, agentSkillLinkPath, { dryRun, force, copy });
|
|
413
|
+
totals.written += linkResult.written;
|
|
414
|
+
totals.planned += linkResult.planned;
|
|
415
|
+
|
|
416
|
+
// Track for lockfile even during dry-run (so callers know what would be installed).
|
|
417
|
+
totals.installedSkills[entry.name] = { canonicalDir: canonicalSkillDir };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return totals;
|
|
421
|
+
}
|
|
422
|
+
|
|
182
423
|
/**
|
|
183
424
|
* Strip GitHub Copilot frontmatter from a prompt file
|
|
184
425
|
* Removes the ---\nagent: ...\ndescription: ...\n--- block
|
|
@@ -303,7 +544,38 @@ function getChangeCount(result, dryRun) {
|
|
|
303
544
|
return dryRun ? result.planned : result.written;
|
|
304
545
|
}
|
|
305
546
|
|
|
306
|
-
|
|
547
|
+
/**
|
|
548
|
+
* Finalize installed hook configs so Copilot can resolve the script command.
|
|
549
|
+
* Copilot runs a hook `command` from the workspace root by default, not from
|
|
550
|
+
* the hooks directory, so the relative `scripts/<name>.sh` command needs a
|
|
551
|
+
* `cwd` pointing at the actual hooks dir. Also ensures the required
|
|
552
|
+
* `version: 1` field is present.
|
|
553
|
+
* @param {string} hooksDest - Absolute path to the installed hooks directory
|
|
554
|
+
* @param {boolean} isGlobal - Whether this is a global (~/.copilot) install
|
|
555
|
+
*/
|
|
556
|
+
function finalizeHookConfigs(hooksDest, isGlobal) {
|
|
557
|
+
// Global installs have no workspace anchor → use the absolute hooks path.
|
|
558
|
+
// Project installs use a path relative to the workspace root so the config
|
|
559
|
+
// stays portable/committable across machines and teammates.
|
|
560
|
+
const cwd = isGlobal
|
|
561
|
+
? hooksDest
|
|
562
|
+
: path.relative(process.cwd(), hooksDest).split(path.sep).join('/');
|
|
563
|
+
|
|
564
|
+
const jsonFiles = fs.readdirSync(hooksDest).filter((f) => f.endsWith('.json'));
|
|
565
|
+
for (const file of jsonFiles) {
|
|
566
|
+
const filePath = path.join(hooksDest, file);
|
|
567
|
+
const config = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
568
|
+
config.version = 1;
|
|
569
|
+
for (const events of Object.values(config.hooks || {})) {
|
|
570
|
+
for (const entry of events) {
|
|
571
|
+
entry.cwd = cwd;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function installHooks({ targetDir, force = false, dryRun = false, global: isGlobal = false }) {
|
|
307
579
|
const hooksSrc = path.join(TEMPLATES_DIR, 'shared', 'hooks');
|
|
308
580
|
const hooksDest = path.join(targetDir, 'hooks');
|
|
309
581
|
|
|
@@ -315,7 +587,7 @@ function installHooks({ targetDir, force = false, dryRun = false }) {
|
|
|
315
587
|
// Copy hook JSON configs and scripts
|
|
316
588
|
const result = copyDir(hooksSrc, hooksDest, { force, dryRun });
|
|
317
589
|
|
|
318
|
-
// Make scripts executable (non-dry-run only)
|
|
590
|
+
// Make scripts executable and finalize configs (non-dry-run only)
|
|
319
591
|
if (!dryRun) {
|
|
320
592
|
const scriptsDir = path.join(hooksDest, 'scripts');
|
|
321
593
|
if (fs.existsSync(scriptsDir)) {
|
|
@@ -325,6 +597,9 @@ function installHooks({ targetDir, force = false, dryRun = false }) {
|
|
|
325
597
|
fs.chmodSync(scriptPath, 0o755);
|
|
326
598
|
}
|
|
327
599
|
}
|
|
600
|
+
if (fs.existsSync(hooksDest)) {
|
|
601
|
+
finalizeHookConfigs(hooksDest, isGlobal);
|
|
602
|
+
}
|
|
328
603
|
}
|
|
329
604
|
|
|
330
605
|
if (!dryRun && result.written > 0) {
|
|
@@ -472,6 +747,7 @@ function installGitBasedTarget(options = {}, target = 'copilot') {
|
|
|
472
747
|
force = false,
|
|
473
748
|
append = false,
|
|
474
749
|
dryRun = false,
|
|
750
|
+
copy = false,
|
|
475
751
|
global: isGlobal = false,
|
|
476
752
|
filters = { stack: 'all', tracker: 'all' },
|
|
477
753
|
} = options;
|
|
@@ -479,6 +755,8 @@ function installGitBasedTarget(options = {}, target = 'copilot') {
|
|
|
479
755
|
const targetDir = getTargetDir(isGlobal);
|
|
480
756
|
const scope = getInstallScope(options);
|
|
481
757
|
let totalChanges = 0;
|
|
758
|
+
/** @type {Record<string, { canonicalDir: string, agents: string[] }>} */
|
|
759
|
+
const installedSkillsMap = {};
|
|
482
760
|
|
|
483
761
|
const makeFilter = (category) => (name) => {
|
|
484
762
|
const basename = name.replace(/\.(prompt\.md|instructions\.md|md)$/, '');
|
|
@@ -519,18 +797,32 @@ function installGitBasedTarget(options = {}, target = 'copilot') {
|
|
|
519
797
|
}
|
|
520
798
|
|
|
521
799
|
if (scope.shouldInstallSkills) {
|
|
522
|
-
info('Installing skills...');
|
|
523
|
-
const result =
|
|
800
|
+
info('Installing skills (npx skills standard)...');
|
|
801
|
+
const result = installSkillsStandard({
|
|
802
|
+
isGlobal,
|
|
803
|
+
agentSkillsDir: path.join(targetDir, 'skills'),
|
|
804
|
+
force,
|
|
805
|
+
dryRun,
|
|
806
|
+
copy,
|
|
807
|
+
dirFilter: makeFilter('skills'),
|
|
808
|
+
});
|
|
524
809
|
totalChanges += getChangeCount(result, dryRun);
|
|
810
|
+
// Merge installed skills for lockfile (agent dir = .github/skills or ~/.copilot/skills).
|
|
811
|
+
for (const [name, meta] of Object.entries(result.installedSkills)) {
|
|
812
|
+
if (!installedSkillsMap[name]) {
|
|
813
|
+
installedSkillsMap[name] = { canonicalDir: meta.canonicalDir, agents: [] };
|
|
814
|
+
}
|
|
815
|
+
installedSkillsMap[name].agents.push(path.relative(process.cwd(), path.join(targetDir, 'skills')));
|
|
816
|
+
}
|
|
525
817
|
|
|
526
818
|
if (!dryRun && result.written > 0) {
|
|
527
|
-
success(`Installed ${result.written} skill files`);
|
|
819
|
+
success(`Installed ${result.written} skill files/links`);
|
|
528
820
|
}
|
|
529
821
|
}
|
|
530
822
|
|
|
531
823
|
if (scope.shouldInstallHooks) {
|
|
532
824
|
info('Installing hooks...');
|
|
533
|
-
const hooksResult = installHooks({ targetDir, force, dryRun });
|
|
825
|
+
const hooksResult = installHooks({ targetDir, force, dryRun, global: isGlobal });
|
|
534
826
|
totalChanges += getChangeCount(hooksResult, dryRun);
|
|
535
827
|
}
|
|
536
828
|
|
|
@@ -550,6 +842,19 @@ function installGitBasedTarget(options = {}, target = 'copilot') {
|
|
|
550
842
|
totalChanges += getChangeCount(agentsResult, dryRun);
|
|
551
843
|
}
|
|
552
844
|
|
|
845
|
+
// Write lockfile and update .gitignore for project (non-global, non-dry-run) installs.
|
|
846
|
+
if (!isGlobal && !dryRun && scope.shouldInstallSkills && Object.keys(installedSkillsMap).length > 0) {
|
|
847
|
+
const skillsForLock = {};
|
|
848
|
+
for (const [name, meta] of Object.entries(installedSkillsMap)) {
|
|
849
|
+
skillsForLock[name] = {
|
|
850
|
+
computedHash: computeSkillHash(meta.canonicalDir),
|
|
851
|
+
agents: meta.agents,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
writeLockfile({ skills: skillsForLock, config: filters, packageVersion: VERSION });
|
|
855
|
+
appendSkillsToGitignore(process.cwd());
|
|
856
|
+
}
|
|
857
|
+
|
|
553
858
|
console.log('');
|
|
554
859
|
if (dryRun) {
|
|
555
860
|
info(`Dry run complete. ${totalChanges} files would be installed.`);
|
|
@@ -598,11 +903,13 @@ function installCodex(options = {}) {
|
|
|
598
903
|
* @param {Object} options - Install options
|
|
599
904
|
*/
|
|
600
905
|
function installClaude(options = {}) {
|
|
601
|
-
const { force = false, dryRun = false, global: isGlobal = false, filters = { stack: 'all', tracker: 'all' } } = options;
|
|
906
|
+
const { force = false, dryRun = false, copy = false, global: isGlobal = false, filters = { stack: 'all', tracker: 'all' } } = options;
|
|
602
907
|
const scope = getInstallScope(options);
|
|
603
908
|
const claudeDir = getClaudeTargetDir(isGlobal);
|
|
604
909
|
const githubDir = getTargetDir(isGlobal);
|
|
605
910
|
let totalChanges = 0;
|
|
911
|
+
/** @type {Record<string, { canonicalDir: string, agents: string[] }>} */
|
|
912
|
+
const installedSkillsMap = {};
|
|
606
913
|
|
|
607
914
|
const makeFilter = (category) => (name) => {
|
|
608
915
|
const basename = name.replace(/\.(prompt\.md|instructions\.md|md)$/, '');
|
|
@@ -646,12 +953,25 @@ function installClaude(options = {}) {
|
|
|
646
953
|
}
|
|
647
954
|
|
|
648
955
|
if (scope.shouldInstallSkills) {
|
|
649
|
-
info('Installing skills...');
|
|
650
|
-
const result =
|
|
956
|
+
info('Installing skills (npx skills standard)...');
|
|
957
|
+
const result = installSkillsStandard({
|
|
958
|
+
isGlobal,
|
|
959
|
+
agentSkillsDir: path.join(claudeDir, 'skills'),
|
|
960
|
+
force,
|
|
961
|
+
dryRun,
|
|
962
|
+
copy,
|
|
963
|
+
dirFilter: makeFilter('skills'),
|
|
964
|
+
});
|
|
651
965
|
totalChanges += getChangeCount(result, dryRun);
|
|
966
|
+
for (const [name, meta] of Object.entries(result.installedSkills)) {
|
|
967
|
+
if (!installedSkillsMap[name]) {
|
|
968
|
+
installedSkillsMap[name] = { canonicalDir: meta.canonicalDir, agents: [] };
|
|
969
|
+
}
|
|
970
|
+
installedSkillsMap[name].agents.push(path.relative(process.cwd(), path.join(claudeDir, 'skills')));
|
|
971
|
+
}
|
|
652
972
|
|
|
653
973
|
if (!dryRun && result.written > 0) {
|
|
654
|
-
success(`Installed ${result.written} skill files
|
|
974
|
+
success(`Installed ${result.written} skill files/links to .claude/skills/`);
|
|
655
975
|
}
|
|
656
976
|
}
|
|
657
977
|
|
|
@@ -678,6 +998,19 @@ function installClaude(options = {}) {
|
|
|
678
998
|
const configResult = ensureConfigFile({ dryRun, global: isGlobal });
|
|
679
999
|
totalChanges += getChangeCount(configResult, dryRun);
|
|
680
1000
|
|
|
1001
|
+
// Write lockfile and update .gitignore for project (non-global, non-dry-run) installs.
|
|
1002
|
+
if (!isGlobal && !dryRun && scope.shouldInstallSkills && Object.keys(installedSkillsMap).length > 0) {
|
|
1003
|
+
const skillsForLock = {};
|
|
1004
|
+
for (const [name, meta] of Object.entries(installedSkillsMap)) {
|
|
1005
|
+
skillsForLock[name] = {
|
|
1006
|
+
computedHash: computeSkillHash(meta.canonicalDir),
|
|
1007
|
+
agents: meta.agents,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
writeLockfile({ skills: skillsForLock, config: filters, packageVersion: VERSION });
|
|
1011
|
+
appendSkillsToGitignore(process.cwd());
|
|
1012
|
+
}
|
|
1013
|
+
|
|
681
1014
|
console.log('');
|
|
682
1015
|
if (dryRun) {
|
|
683
1016
|
info(`Dry run complete. ${totalChanges} files would be installed.`);
|
|
@@ -700,6 +1033,158 @@ function installClaude(options = {}) {
|
|
|
700
1033
|
console.log('');
|
|
701
1034
|
}
|
|
702
1035
|
|
|
1036
|
+
/**
|
|
1037
|
+
* Restore skills from the lockfile.
|
|
1038
|
+
* Reads agents-toolkit-lock.json, reinstalls all skills, and verifies hashes.
|
|
1039
|
+
* Restore always overwrites existing skill files — that is its purpose.
|
|
1040
|
+
* @param {Object} [options]
|
|
1041
|
+
* @param {boolean} [options.dryRun] - Only report planned changes
|
|
1042
|
+
* @param {boolean} [options.copy] - Copy instead of symlink
|
|
1043
|
+
*/
|
|
1044
|
+
function restore(options = {}) {
|
|
1045
|
+
const { dryRun = false, copy = false } = options;
|
|
1046
|
+
log('\n🔄 Agents Toolkit Restore\n', 'bright');
|
|
1047
|
+
|
|
1048
|
+
const lockfile = readLockfile();
|
|
1049
|
+
if (!lockfile) {
|
|
1050
|
+
error(`No ${LOCKFILE_NAME} found. Run "install" first to generate it.`);
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (lockfile.packageVersion !== VERSION) {
|
|
1055
|
+
warn(`Lockfile was created with v${lockfile.packageVersion}, current package is v${VERSION}.`);
|
|
1056
|
+
warn('Run "update" to refresh the lockfile for the current version.');
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (dryRun) {
|
|
1060
|
+
info('Dry run mode - no files will be restored\n');
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const { stack = 'all', tracker = 'all' } = lockfile.config || {};
|
|
1064
|
+
const filters = { stack, tracker };
|
|
1065
|
+
const makeFilter = (category) => (name) => {
|
|
1066
|
+
const basename = name.replace(/\.(prompt\.md|instructions\.md|md)$/, '');
|
|
1067
|
+
return shouldIncludeFile(basename, category, filters);
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
// Determine which agent dirs were recorded in the lockfile.
|
|
1071
|
+
const agentDirs = new Set();
|
|
1072
|
+
for (const meta of Object.values(lockfile.skills || {})) {
|
|
1073
|
+
for (const agentDir of (meta.agents || [])) {
|
|
1074
|
+
agentDirs.add(agentDir);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Reinstall into each unique agent skills dir.
|
|
1079
|
+
let totalRestored = 0;
|
|
1080
|
+
for (const agentDir of agentDirs) {
|
|
1081
|
+
const agentSkillsDir = path.join(process.cwd(), agentDir);
|
|
1082
|
+
const result = installSkillsStandard({
|
|
1083
|
+
isGlobal: false,
|
|
1084
|
+
agentSkillsDir,
|
|
1085
|
+
force: true, // restore always overwrites — that is its purpose
|
|
1086
|
+
dryRun,
|
|
1087
|
+
copy,
|
|
1088
|
+
dirFilter: makeFilter('skills'),
|
|
1089
|
+
});
|
|
1090
|
+
totalRestored += dryRun ? result.planned : result.written;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (dryRun) {
|
|
1094
|
+
info(`Dry run complete. ${totalRestored} files would be restored.`);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Verify hashes match the lockfile.
|
|
1099
|
+
const canonicalDir = getAgentsSkillsDir(false);
|
|
1100
|
+
let allMatch = true;
|
|
1101
|
+
for (const [name, meta] of Object.entries(lockfile.skills || {})) {
|
|
1102
|
+
const canonicalSkillDir = path.join(canonicalDir, name);
|
|
1103
|
+
const hash = computeSkillHash(canonicalSkillDir);
|
|
1104
|
+
if (hash !== meta.computedHash) {
|
|
1105
|
+
warn(`Hash mismatch for skill "${name}" — expected ${meta.computedHash?.slice(0, 12)}… got ${hash?.slice(0, 12)}…`);
|
|
1106
|
+
allMatch = false;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
console.log('');
|
|
1111
|
+
if (allMatch) {
|
|
1112
|
+
success(`Restored ${Object.keys(lockfile.skills || {}).length} skills successfully.`);
|
|
1113
|
+
} else {
|
|
1114
|
+
warn(`Restored with hash mismatches. Run "update" to refresh the lockfile.`);
|
|
1115
|
+
}
|
|
1116
|
+
console.log('');
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Show the status of installed skills relative to the lockfile.
|
|
1121
|
+
* Exits with code 1 if any skill is missing or has a hash mismatch.
|
|
1122
|
+
*/
|
|
1123
|
+
function status() {
|
|
1124
|
+
log('\n📊 Agents Toolkit Status\n', 'bright');
|
|
1125
|
+
|
|
1126
|
+
const lockfile = readLockfile();
|
|
1127
|
+
if (!lockfile) {
|
|
1128
|
+
error(`No ${LOCKFILE_NAME} found. Run "install" first to generate it.`);
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const canonicalDir = getAgentsSkillsDir(false);
|
|
1133
|
+
const skills = lockfile.skills || {};
|
|
1134
|
+
let hasIssues = false;
|
|
1135
|
+
|
|
1136
|
+
if (Object.keys(skills).length === 0) {
|
|
1137
|
+
info('No skills recorded in lockfile.');
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (lockfile.packageVersion !== VERSION) {
|
|
1142
|
+
warn(`Lockfile package version: v${lockfile.packageVersion} — current: v${VERSION}`);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
console.log('');
|
|
1146
|
+
const COL_NAME = 28;
|
|
1147
|
+
const COL_STATUS = 14;
|
|
1148
|
+
const header = `${'Skill'.padEnd(COL_NAME)} ${'Status'.padEnd(COL_STATUS)} Hash`;
|
|
1149
|
+
log(header, 'cyan');
|
|
1150
|
+
log('─'.repeat(header.length), 'cyan');
|
|
1151
|
+
|
|
1152
|
+
for (const [name, meta] of Object.entries(skills)) {
|
|
1153
|
+
const canonicalSkillDir = path.join(canonicalDir, name);
|
|
1154
|
+
const hash = computeSkillHash(canonicalSkillDir);
|
|
1155
|
+
|
|
1156
|
+
let statusLabel;
|
|
1157
|
+
let statusColor;
|
|
1158
|
+
|
|
1159
|
+
if (hash === null) {
|
|
1160
|
+
statusLabel = 'missing';
|
|
1161
|
+
statusColor = 'red';
|
|
1162
|
+
hasIssues = true;
|
|
1163
|
+
} else if (hash !== meta.computedHash) {
|
|
1164
|
+
statusLabel = 'modified';
|
|
1165
|
+
statusColor = 'yellow';
|
|
1166
|
+
hasIssues = true;
|
|
1167
|
+
} else {
|
|
1168
|
+
statusLabel = 'up-to-date';
|
|
1169
|
+
statusColor = 'green';
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const hashDisplay = hash ? hash.slice(0, 12) + '…' : '—';
|
|
1173
|
+
const line = `${name.padEnd(COL_NAME)} ${statusLabel.padEnd(COL_STATUS)} ${hashDisplay}`;
|
|
1174
|
+
log(line, statusColor);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
console.log('');
|
|
1178
|
+
|
|
1179
|
+
if (hasIssues) {
|
|
1180
|
+
warn('Some skills are out of sync. Run "restore" or "update" to fix.');
|
|
1181
|
+
process.exit(1);
|
|
1182
|
+
} else {
|
|
1183
|
+
success(`All ${Object.keys(skills).length} skills are up-to-date.`);
|
|
1184
|
+
}
|
|
1185
|
+
console.log('');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
703
1188
|
/**
|
|
704
1189
|
* List available prompts
|
|
705
1190
|
*/
|
|
@@ -718,7 +1203,7 @@ function list() {
|
|
|
718
1203
|
.map(f => f.replace('.prompt.md', ''));
|
|
719
1204
|
|
|
720
1205
|
log('Workflow Prompts:', 'cyan');
|
|
721
|
-
const workflowPrompts = ['analyze-ticket', 'create-plan', 'work-ticket', 'prepare-pr', 'create-pr', 'finalize-pr'];
|
|
1206
|
+
const workflowPrompts = ['analyze-ticket', 'create-plan', 'work-ticket', 'prepare-pr', 'create-pr', 'finalize-pr', 'analyze-github-issue', 'work-github-issue', 'create-github-pr', 'finalize-github-pr'];
|
|
722
1207
|
workflowPrompts.forEach((p, i) => {
|
|
723
1208
|
if (prompts.includes(p)) {
|
|
724
1209
|
console.log(` ${i + 1}. ${p}`);
|
|
@@ -777,8 +1262,10 @@ function showHelp() {
|
|
|
777
1262
|
|
|
778
1263
|
log('Commands:', 'cyan');
|
|
779
1264
|
console.log(' install Install prompts (default target: copilot)');
|
|
1265
|
+
console.log(' restore Restore skills from agents-toolkit-lock.json');
|
|
1266
|
+
console.log(' status Check if installed skills match the lockfile');
|
|
1267
|
+
console.log(' update Update existing prompts and refresh the lockfile');
|
|
780
1268
|
console.log(' list List available prompts');
|
|
781
|
-
console.log(' update Update existing prompts (alias for install --force)');
|
|
782
1269
|
console.log(' help Show this help message');
|
|
783
1270
|
|
|
784
1271
|
console.log('');
|
|
@@ -796,6 +1283,7 @@ function showHelp() {
|
|
|
796
1283
|
console.log(' --partials-only Only install partials');
|
|
797
1284
|
console.log(' --skills-only Only install skills');
|
|
798
1285
|
console.log(' --hooks-only Only install hooks (PostToolUse validation scripts)');
|
|
1286
|
+
console.log(' --copy Copy skills instead of symlinking to .agents/skills/');
|
|
799
1287
|
console.log(' --dry-run Show what would be installed');
|
|
800
1288
|
|
|
801
1289
|
console.log('');
|
|
@@ -874,6 +1362,7 @@ function parseArgs() {
|
|
|
874
1362
|
instructionsOnly: flags.includes('--instructions-only'),
|
|
875
1363
|
hooksOnly: flags.includes('--hooks-only'),
|
|
876
1364
|
dryRun: flags.includes('--dry-run'),
|
|
1365
|
+
copy: flags.includes('--copy'),
|
|
877
1366
|
claude: flags.includes('--claude'),
|
|
878
1367
|
codex: flags.includes('--codex'),
|
|
879
1368
|
append: flags.includes('--append'),
|
|
@@ -1017,6 +1506,12 @@ function main() {
|
|
|
1017
1506
|
install({ ...options, filters });
|
|
1018
1507
|
}
|
|
1019
1508
|
break;
|
|
1509
|
+
case 'restore':
|
|
1510
|
+
restore(options);
|
|
1511
|
+
break;
|
|
1512
|
+
case 'status':
|
|
1513
|
+
status();
|
|
1514
|
+
break;
|
|
1020
1515
|
case 'update':
|
|
1021
1516
|
if (target === 'claude') {
|
|
1022
1517
|
installClaude({ ...options, force: true, filters });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silverassist/agents-toolkit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Reusable AI agent prompts for development workflows with Jira integration — supports GitHub Copilot, Claude Code, and Codex",
|
|
5
5
|
"author": "Santiago Ramirez",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|