ma-agents 1.2.0 → 1.4.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/lib/installer.js CHANGED
@@ -1,11 +1,72 @@
1
1
  const fs = require('fs-extra');
2
2
  const path = require('path');
3
3
  const chalk = require('chalk');
4
+ const prompts = require('prompts');
4
5
  const { getAgent, getAllAgents } = require('./agents');
5
6
 
7
+ const MANIFEST_FILE = '.ma-agents.json';
8
+ const MANIFEST_VERSION = '1.0.0';
9
+
10
+ function getPackageVersion() {
11
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
12
+ return pkg.version;
13
+ }
14
+
15
+ // --- Manifest functions ---
16
+
17
+ function readManifest(installPath) {
18
+ const manifestPath = path.join(installPath, MANIFEST_FILE);
19
+ if (!fs.existsSync(manifestPath)) {
20
+ return null;
21
+ }
22
+ try {
23
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function writeManifest(installPath, manifest) {
30
+ const manifestPath = path.join(installPath, MANIFEST_FILE);
31
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
32
+ }
33
+
34
+ function ensureManifest(installPath, agentId, scope) {
35
+ let manifest = readManifest(installPath);
36
+ if (!manifest) {
37
+ manifest = {
38
+ manifestVersion: MANIFEST_VERSION,
39
+ agent: agentId,
40
+ scope: scope,
41
+ skills: {}
42
+ };
43
+ }
44
+ return manifest;
45
+ }
46
+
47
+ function getInstalledSkillInfo(installPath, skillId) {
48
+ const manifest = readManifest(installPath);
49
+ if (!manifest || !manifest.skills || !manifest.skills[skillId]) {
50
+ return null;
51
+ }
52
+ return manifest.skills[skillId];
53
+ }
54
+
6
55
  /**
7
- * Get all available skills
56
+ * Compare two semver strings. Returns -1, 0, or 1.
8
57
  */
58
+ function compareSemver(a, b) {
59
+ const pa = (a || '0.0.0').split('.').map(Number);
60
+ const pb = (b || '0.0.0').split('.').map(Number);
61
+ for (let i = 0; i < 3; i++) {
62
+ if (pa[i] > pb[i]) return 1;
63
+ if (pa[i] < pb[i]) return -1;
64
+ }
65
+ return 0;
66
+ }
67
+
68
+ // --- Skill listing ---
69
+
9
70
  function listSkills() {
10
71
  const skillsDir = path.join(__dirname, '..', 'skills');
11
72
 
@@ -32,17 +93,82 @@ function listSkills() {
32
93
  }).filter(Boolean);
33
94
  }
34
95
 
35
- /**
36
- * Get all supported agents
37
- */
38
96
  function listAgents() {
39
97
  return getAllAgents();
40
98
  }
41
99
 
42
- /**
43
- * Install a skill for specified agents
44
- */
45
- async function installSkill(skillId, agentIds, customPath = '') {
100
+ // --- Core install logic (no prompts) ---
101
+
102
+ async function performInstall(skillId, skill, agent, installPath) {
103
+ const skillSourceDir = path.join(__dirname, '..', 'skills', skillId);
104
+ let sourceFile = path.join(skillSourceDir, `${agent.template}${agent.fileExtension}`);
105
+
106
+ if (!fs.existsSync(sourceFile)) {
107
+ sourceFile = path.join(skillSourceDir, `generic${agent.fileExtension}`);
108
+ }
109
+
110
+ if (!fs.existsSync(sourceFile)) {
111
+ const skillMdPath = path.join(skillSourceDir, 'SKILL.md');
112
+ if (fs.existsSync(skillMdPath)) {
113
+ sourceFile = skillMdPath;
114
+ }
115
+ }
116
+
117
+ if (!fs.existsSync(sourceFile)) {
118
+ console.log(chalk.yellow(` Warning: No template found for ${agent.name}, skipping`));
119
+ return false;
120
+ }
121
+
122
+ const skillDir = path.join(installPath, skillId);
123
+ await fs.ensureDir(skillDir);
124
+
125
+ let content = await fs.readFile(sourceFile, 'utf-8');
126
+
127
+ // Strip any existing YAML frontmatter from the source
128
+ const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
129
+ content = content.replace(frontmatterRegex, '');
130
+
131
+ // Inject YAML frontmatter from skill.json (single source of truth)
132
+ const frontmatter = [
133
+ '---',
134
+ `name: ${skill.name}`,
135
+ `description: ${skill.description}`,
136
+ '---',
137
+ ''
138
+ ].join('\n');
139
+ content = frontmatter + content;
140
+
141
+ const targetFile = path.join(skillDir, 'SKILL.md');
142
+ await fs.writeFile(targetFile, content, 'utf-8');
143
+ console.log(chalk.green(` + Installed to ${targetFile}`));
144
+
145
+ // Copy bundled resources
146
+ const resourceMap = agent.resourceMap || {};
147
+ const resourceDirs = ['scripts', 'references', 'assets', 'examples', 'hooks', 'docs', 'templates'];
148
+ for (const dir of resourceDirs) {
149
+ const resourceSource = path.join(skillSourceDir, dir);
150
+ if (fs.existsSync(resourceSource)) {
151
+ const targetDirName = resourceMap[dir] || dir;
152
+ const resourceTarget = path.join(skillDir, targetDirName);
153
+ await fs.copy(resourceSource, resourceTarget);
154
+ console.log(chalk.green(` + Copied ${dir}/ → ${targetDirName}/`));
155
+ }
156
+ }
157
+
158
+ // Copy template.md if it exists
159
+ const templateSource = path.join(skillSourceDir, 'template.md');
160
+ if (fs.existsSync(templateSource)) {
161
+ await fs.copy(templateSource, path.join(skillDir, 'template.md'));
162
+ console.log(chalk.green(` + Copied template.md`));
163
+ }
164
+
165
+ return true;
166
+ }
167
+
168
+ // --- Install with upgrade detection ---
169
+
170
+ async function installSkill(skillId, agentIds, customPath = '', scope = 'project', options = {}) {
171
+ const { force = false } = options;
46
172
  const skills = listSkills();
47
173
  const skill = skills.find(s => s.id === skillId);
48
174
 
@@ -50,7 +176,7 @@ async function installSkill(skillId, agentIds, customPath = '') {
50
176
  throw new Error(`Skill '${skillId}' not found. Run "list" to see available skills.`);
51
177
  }
52
178
 
53
- console.log(chalk.cyan(`\nInstalling skill: ${skill.name}`));
179
+ console.log(chalk.cyan(`\nInstalling skill: ${skill.name} v${skill.version}`));
54
180
 
55
181
  for (const agentId of agentIds) {
56
182
  const agent = getAgent(agentId);
@@ -60,61 +186,202 @@ async function installSkill(skillId, agentIds, customPath = '') {
60
186
  continue;
61
187
  }
62
188
 
63
- console.log(chalk.gray(` Installing for ${agent.name}...`));
64
-
65
189
  try {
66
- // Determine installation path
67
- const installPath = customPath || agent.getSkillsPath();
68
-
69
- // Ensure the skills directory exists
190
+ const installPath = customPath || (scope === 'global' ? agent.getGlobalPath() : agent.getProjectPath());
70
191
  await fs.ensureDir(installPath);
71
192
 
72
- // Get the skill source file based on agent template
73
- const skillSourceDir = path.join(__dirname, '..', 'skills', skillId);
74
- let sourceFile = path.join(skillSourceDir, `${agent.template}${agent.fileExtension}`);
193
+ const installed = getInstalledSkillInfo(installPath, skillId);
75
194
 
76
- // Fallback to generic template if specific template doesn't exist
77
- if (!fs.existsSync(sourceFile)) {
78
- sourceFile = path.join(skillSourceDir, `generic${agent.fileExtension}`);
79
- }
195
+ if (installed && !force) {
196
+ const cmp = compareSemver(skill.version, installed.version);
197
+
198
+ let action;
199
+ if (cmp > 0) {
200
+ // Upgrade available
201
+ console.log(chalk.yellow(` ${skill.name} v${installed.version} → v${skill.version} update available for ${agent.name}`));
202
+ const { choice } = await prompts({
203
+ type: 'select',
204
+ name: 'choice',
205
+ message: 'What would you like to do?',
206
+ choices: [
207
+ { title: 'Update (recommended)', value: 'update' },
208
+ { title: 'Skip (keep current)', value: 'skip' },
209
+ { title: 'Clean reinstall', value: 'reinstall' },
210
+ { title: 'Remove (uninstall)', value: 'remove' }
211
+ ]
212
+ });
213
+ action = choice;
214
+ } else if (cmp === 0) {
215
+ // Same version
216
+ console.log(chalk.gray(` ${skill.name} v${installed.version} already installed for ${agent.name}`));
217
+ const { choice } = await prompts({
218
+ type: 'select',
219
+ name: 'choice',
220
+ message: 'What would you like to do?',
221
+ choices: [
222
+ { title: 'Skip (keep current)', value: 'skip' },
223
+ { title: 'Clean reinstall', value: 'reinstall' },
224
+ { title: 'Remove (uninstall)', value: 'remove' }
225
+ ]
226
+ });
227
+ action = choice;
228
+ } else {
229
+ // Downgrade
230
+ console.log(chalk.yellow(` ${skill.name} v${installed.version} installed, package has v${skill.version} for ${agent.name}`));
231
+ const { choice } = await prompts({
232
+ type: 'select',
233
+ name: 'choice',
234
+ message: 'What would you like to do?',
235
+ choices: [
236
+ { title: 'Skip (keep current)', value: 'skip' },
237
+ { title: `Downgrade to v${skill.version}`, value: 'update' },
238
+ { title: 'Remove (uninstall)', value: 'remove' }
239
+ ]
240
+ });
241
+ action = choice;
242
+ }
243
+
244
+ if (!action || action === 'skip') {
245
+ console.log(chalk.gray(` Skipped`));
246
+ continue;
247
+ }
248
+
249
+ if (action === 'remove') {
250
+ await performUninstall(skillId, installPath);
251
+ const manifest = ensureManifest(installPath, agentId, scope);
252
+ delete manifest.skills[skillId];
253
+ writeManifest(installPath, manifest);
254
+ console.log(chalk.green(` - Removed ${skill.name} from ${agent.name}`));
255
+ continue;
256
+ }
80
257
 
81
- // Fallback to SKILL.md if no agent-specific or generic template exists
82
- if (!fs.existsSync(sourceFile)) {
83
- const skillMdPath = path.join(skillSourceDir, 'SKILL.md');
84
- if (fs.existsSync(skillMdPath)) {
85
- sourceFile = skillMdPath;
258
+ if (action === 'reinstall') {
259
+ await performUninstall(skillId, installPath);
86
260
  }
261
+
262
+ // action === 'update' or 'reinstall' → proceed to install below
87
263
  }
88
264
 
89
- if (!fs.existsSync(sourceFile)) {
90
- console.log(chalk.yellow(` Warning: No template found for ${agent.name}, skipping`));
91
- continue;
265
+ if (!installed || force) {
266
+ console.log(chalk.gray(` Installing for ${agent.name}...`));
267
+ }
268
+
269
+ const success = await performInstall(skillId, skill, agent, installPath);
270
+
271
+ if (success) {
272
+ // Update manifest
273
+ const manifest = ensureManifest(installPath, agentId, scope);
274
+ const now = new Date().toISOString();
275
+ const existing = manifest.skills[skillId];
276
+ manifest.skills[skillId] = {
277
+ version: skill.version,
278
+ installedAt: existing ? existing.installedAt : now,
279
+ updatedAt: now,
280
+ installerVersion: getPackageVersion(),
281
+ agentVersion: agent.version
282
+ };
283
+ writeManifest(installPath, manifest);
92
284
  }
285
+ } catch (error) {
286
+ console.log(chalk.red(` x Failed: ${error.message}`));
287
+ }
288
+ }
289
+ }
290
+
291
+ // --- Uninstall ---
292
+
293
+ async function performUninstall(skillId, installPath) {
294
+ const skillDir = path.join(installPath, skillId);
295
+ if (fs.existsSync(skillDir)) {
296
+ await fs.remove(skillDir);
297
+ }
298
+ }
299
+
300
+ async function uninstallSkill(skillId, agentIds, customPath = '', scope = 'project') {
301
+ console.log(chalk.cyan(`\nUninstalling skill: ${skillId}`));
302
+
303
+ for (const agentId of agentIds) {
304
+ const agent = getAgent(agentId);
93
305
 
94
- // Copy the skill file
95
- const targetFile = path.join(installPath, `${skillId}${agent.fileExtension}`);
96
- await fs.copy(sourceFile, targetFile);
97
- console.log(chalk.green(` + Installed to ${targetFile}`));
98
-
99
- // Copy bundled resources (scripts/, references/, assets/) if they exist
100
- const resourceDirs = ['scripts', 'references', 'assets'];
101
- for (const dir of resourceDirs) {
102
- const resourceSource = path.join(skillSourceDir, dir);
103
- if (fs.existsSync(resourceSource)) {
104
- const resourceTarget = path.join(installPath, skillId, dir);
105
- await fs.ensureDir(path.join(installPath, skillId));
106
- await fs.copy(resourceSource, resourceTarget);
107
- console.log(chalk.green(` + Copied ${dir}/ resources`));
306
+ if (!agent) {
307
+ console.log(chalk.yellow(` Warning: Skipping unknown agent: ${agentId}`));
308
+ continue;
309
+ }
310
+
311
+ try {
312
+ const installPath = customPath || (scope === 'global' ? agent.getGlobalPath() : agent.getProjectPath());
313
+ const installed = getInstalledSkillInfo(installPath, skillId);
314
+
315
+ if (!installed) {
316
+ const skillDir = path.join(installPath, skillId);
317
+ if (fs.existsSync(skillDir)) {
318
+ await performUninstall(skillId, installPath);
319
+ console.log(chalk.green(` - Removed ${skillId} from ${agent.name} (legacy install)`));
320
+ } else {
321
+ console.log(chalk.gray(` ${skillId} is not installed for ${agent.name}`));
108
322
  }
323
+ continue;
109
324
  }
325
+
326
+ await performUninstall(skillId, installPath);
327
+
328
+ const manifest = ensureManifest(installPath, agentId, scope);
329
+ delete manifest.skills[skillId];
330
+ writeManifest(installPath, manifest);
331
+
332
+ console.log(chalk.green(` - Removed ${skillId} v${installed.version} from ${agent.name}`));
110
333
  } catch (error) {
111
334
  console.log(chalk.red(` x Failed: ${error.message}`));
112
335
  }
113
336
  }
114
337
  }
115
338
 
339
+ // --- Status ---
340
+
341
+ function getStatus(agentIds, customPath = '', scope = 'project') {
342
+ const results = [];
343
+
344
+ const targetAgents = agentIds && agentIds.length > 0
345
+ ? agentIds.map(id => getAgent(id)).filter(Boolean)
346
+ : getAllAgents();
347
+
348
+ for (const agent of targetAgents) {
349
+ const projectPath = agent.getProjectPath();
350
+ const globalPath = agent.getGlobalPath();
351
+
352
+ // Check both scopes unless custom path is specified
353
+ const pathsToCheck = customPath
354
+ ? [{ path: customPath, scope: 'custom' }]
355
+ : [
356
+ { path: projectPath, scope: 'project' },
357
+ { path: globalPath, scope: 'global' }
358
+ ];
359
+
360
+ for (const { path: checkPath, scope: checkScope } of pathsToCheck) {
361
+ const manifest = readManifest(checkPath);
362
+ if (!manifest || !manifest.skills || Object.keys(manifest.skills).length === 0) {
363
+ continue;
364
+ }
365
+
366
+ results.push({
367
+ agent: agent,
368
+ installPath: checkPath,
369
+ scope: checkScope,
370
+ skills: manifest.skills
371
+ });
372
+ }
373
+ }
374
+
375
+ return results;
376
+ }
377
+
116
378
  module.exports = {
117
379
  listSkills,
118
380
  listAgents,
119
- installSkill
381
+ installSkill,
382
+ uninstallSkill,
383
+ getStatus,
384
+ readManifest,
385
+ getInstalledSkillInfo,
386
+ compareSemver
120
387
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ma-agents",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "NPX tool to install skills for AI coding agents (Claude Code, Gemini, Copilot, Kilocode, Cline, Cursor)",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,39 @@
1
+ # Code Review
2
+
3
+ Perform comprehensive code reviews following industry best practices.
4
+
5
+ ## What to Review
6
+
7
+ 1. **Code Quality**: Readability, naming conventions, structure
8
+ 2. **Best Practices**: Language-specific patterns, error handling, performance
9
+ 3. **Security**: Common vulnerabilities (SQL injection, XSS, CSRF, etc.)
10
+ 4. **Testing**: Coverage, edge cases, test quality
11
+ 5. **Documentation**: Comments, API docs, clarity
12
+
13
+ ## Review Process
14
+
15
+ - Analyze code for bugs and logical errors
16
+ - Check adherence to coding standards
17
+ - Identify security vulnerabilities
18
+ - Suggest refactoring opportunities
19
+ - Assess test coverage and documentation
20
+
21
+ ## Output Format
22
+
23
+ ```
24
+ ## Code Review Summary
25
+
26
+ ### Strengths
27
+ - [Positive aspects]
28
+
29
+ ### Issues Found
30
+ - **[High/Medium/Low]** [Issue description]
31
+ - Location: [file:line]
32
+ - Fix: [recommendation]
33
+
34
+ ### Suggestions
35
+ - [Improvements]
36
+
37
+ ### Overall Assessment
38
+ [Summary and rating]
39
+ ```
@@ -0,0 +1,75 @@
1
+ # Commit Message Generator
2
+
3
+ Generate meaningful commit messages following Conventional Commits specification.
4
+
5
+ ## Format
6
+
7
+ ```
8
+ <type>(<scope>): <subject>
9
+
10
+ <body>
11
+
12
+ <footer>
13
+ ```
14
+
15
+ ## Types
16
+
17
+ - `feat`: New feature
18
+ - `fix`: Bug fix
19
+ - `docs`: Documentation changes
20
+ - `style`: Code style/formatting
21
+ - `refactor`: Code refactoring
22
+ - `test`: Adding/updating tests
23
+ - `chore`: Maintenance tasks
24
+ - `perf`: Performance improvements
25
+ - `ci`: CI/CD changes
26
+ - `build`: Build system changes
27
+ - `revert`: Revert previous commit
28
+
29
+ ## Guidelines
30
+
31
+ 1. **Subject line** (max 50 chars):
32
+ - Use imperative mood ("Add" not "Added")
33
+ - Don't capitalize first letter
34
+ - No period at the end
35
+
36
+ 2. **Body** (optional):
37
+ - Explain what and why, not how
38
+ - Wrap at 72 characters
39
+
40
+ 3. **Footer** (optional):
41
+ - Breaking changes: `BREAKING CHANGE: description`
42
+ - Issue references: `Fixes #123`
43
+
44
+ ## Examples
45
+
46
+ ```
47
+ feat(auth): add JWT token refresh mechanism
48
+
49
+ Implement automatic token refresh to improve user experience
50
+ and reduce re-authentication prompts.
51
+
52
+ Fixes #456
53
+ ```
54
+
55
+ ```
56
+ fix(api): resolve memory leak in user service
57
+
58
+ The user cache was not being cleared properly, causing
59
+ memory to grow over time.
60
+ ```
61
+
62
+ ```
63
+ docs: update installation instructions
64
+
65
+ Add steps for Windows users and clarify dependency requirements.
66
+ ```
67
+
68
+ ## Process
69
+
70
+ 1. Analyze the code changes
71
+ 2. Determine the type of change
72
+ 3. Identify the scope (component/module affected)
73
+ 4. Write clear, concise subject
74
+ 5. Add body if changes need explanation
75
+ 6. Add footer for breaking changes or issue refs
@@ -1,8 +1,3 @@
1
- ---
2
- name: create-hardened-docker
3
- description: Creates production-ready hardened Docker configurations (Dockerfile, docker-compose.yml, nginx.conf, .dockerignore) following CIS Docker Benchmark, OWASP, and NIST SP 800-190 standards. Includes multi-stage builds, non-root execution, read-only filesystems, capability dropping, and comprehensive security hardening.
4
- ---
5
-
6
1
  # Create Hardened Docker
7
2
 
8
3
  ## Overview
@@ -1,8 +1,3 @@
1
- ---
2
- name: git-workflow-skill
3
- description: Mandatory feature branch workflow using git worktrees for parallel multi-agent development. Use this skill BEFORE making ANY change to files in a Git repository that are not ignored by .gitignore. Enforces isolated worktrees per feature, conventional commits, and PR-based merging. Supports multiple AI agents working simultaneously on different features without conflicts.
4
- ---
5
-
6
1
  # Git Workflow (Worktree-Based)
7
2
 
8
3
  Multi-agent parallel development using git worktrees. Each agent gets an isolated working directory — no branch switching, no conflicts between agents.
@@ -1,7 +1,3 @@
1
- ---
2
- name: js-ts-security-skill
3
- description: Verify the security of JavaScript and TypeScript codebases against OWASP Top 10 2025 standards.
4
- ---
5
1
 
6
2
  # JS/TS Security Skill
7
3