mymskills 1.0.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.
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+
3
+ const nodeVersion = parseInt(process.versions.node.split('.')[0], 10);
4
+ if (nodeVersion < 18) {
5
+ console.error(`mymskills requires Node.js 18 or later. You are running Node.js ${process.versions.node}.`);
6
+ process.exit(1);
7
+ }
8
+
9
+ import { createProgram } from '../lib/cli.js';
10
+
11
+ const program = createProgram();
12
+ program.parseAsync(process.argv).catch((error) => {
13
+ process.stderr.write(`\nError: ${error.message}\n`);
14
+ process.exit(1);
15
+ });
package/lib/cli.js ADDED
@@ -0,0 +1,33 @@
1
+ import { Command } from 'commander';
2
+ import { VERSION } from './constants.js';
3
+ import { installCommand } from './commands/install.js';
4
+ import { updateCommand } from './commands/update.js';
5
+ import { removeCommand } from './commands/remove.js';
6
+ import { listCommand } from './commands/list.js';
7
+
8
+ export function createProgram() {
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('mymskills')
13
+ .description('Interactive installer for mymediset AI skills')
14
+ .version(VERSION, '-v, --version')
15
+ .action(installCommand);
16
+
17
+ program
18
+ .command('update')
19
+ .description('Update installed skills to the latest version')
20
+ .action(updateCommand);
21
+
22
+ program
23
+ .command('remove')
24
+ .description('Remove installed skills')
25
+ .action(removeCommand);
26
+
27
+ program
28
+ .command('list')
29
+ .description('List installed and available skills')
30
+ .action(listCommand);
31
+
32
+ return program;
33
+ }
@@ -0,0 +1,116 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import boxen from 'boxen';
4
+ import { showBanner } from '../ui/banner.js';
5
+ import { promptSkillSelection, promptPlatform, promptScope, promptMode, promptConfirm } from '../ui/prompts.js';
6
+ import { fetchSkillSource } from '../core/fetcher.js';
7
+ import { buildCatalog } from '../core/registry.js';
8
+ import { installSkill } from '../core/installer.js';
9
+ import { getSkillsDir } from '../core/paths.js';
10
+ import { PLATFORMS } from '../constants.js';
11
+ import path from 'node:path';
12
+
13
+ export async function installCommand() {
14
+ showBanner();
15
+
16
+ const { sourceDir, catalog } = await fetchCatalog();
17
+
18
+ const selectedNames = await promptSkillSelection(catalog);
19
+ const platforms = await promptPlatform();
20
+ const scope = await promptScope();
21
+ const mode = await promptMode();
22
+
23
+ const summary = buildSummary(selectedNames, platforms, scope, mode);
24
+ console.log(summary);
25
+
26
+ const proceed = await promptConfirm('Proceed with installation?');
27
+ if (!proceed) {
28
+ console.log(chalk.yellow('\nInstallation cancelled.'));
29
+ return;
30
+ }
31
+
32
+ const results = executeInstalls(sourceDir, selectedNames, platforms, scope, mode);
33
+
34
+ if (results.length > 0) {
35
+ printSuccess(results, scope);
36
+ }
37
+ }
38
+
39
+ async function fetchCatalog() {
40
+ const spinner = ora('Fetching skill catalog...').start();
41
+ let sourceDir;
42
+ try {
43
+ sourceDir = await fetchSkillSource(spinner);
44
+ } catch (error) {
45
+ spinner.fail(error.message);
46
+ process.exit(1);
47
+ }
48
+
49
+ const catalog = buildCatalog(path.join(sourceDir, 'skills'));
50
+ if (catalog.length === 0) {
51
+ spinner.fail('No skills found in the library.');
52
+ process.exit(1);
53
+ }
54
+ spinner.succeed(`Found ${catalog.length} skills`);
55
+ return { sourceDir, catalog };
56
+ }
57
+
58
+ function executeInstalls(sourceDir, selectedNames, platforms, scope, mode) {
59
+ console.log('');
60
+ const results = [];
61
+
62
+ for (const platformKey of platforms) {
63
+ const targetDir = getSkillsDir(platformKey, scope);
64
+
65
+ for (const skillDirName of selectedNames) {
66
+ const skillSpinner = ora(`Installing ${skillDirName} to ${PLATFORMS[platformKey].name}...`).start();
67
+ try {
68
+ const result = installSkill(sourceDir, targetDir, skillDirName, mode);
69
+ results.push({ ...result, platform: PLATFORMS[platformKey].name });
70
+ skillSpinner.succeed(`${skillDirName} ${chalk.dim(`(${result.mode})`)}`);
71
+ } catch (error) {
72
+ skillSpinner.fail(`${skillDirName}: ${error.message}`);
73
+ }
74
+ }
75
+ }
76
+
77
+ return results;
78
+ }
79
+
80
+ function buildSummary(skills, platforms, scope, mode) {
81
+ const lines = [
82
+ chalk.bold('Installation Summary'),
83
+ '',
84
+ ` Skills: ${skills.map(s => chalk.cyan(s)).join(', ')}`,
85
+ ` Platform: ${platforms.map(p => PLATFORMS[p].name).join(', ')}`,
86
+ ` Scope: ${scope}`,
87
+ ` Mode: ${mode}`,
88
+ ];
89
+
90
+ return boxen(lines.join('\n'), {
91
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
92
+ margin: { top: 1, bottom: 0, left: 0, right: 0 },
93
+ borderStyle: 'round',
94
+ borderColor: 'cyan',
95
+ });
96
+ }
97
+
98
+ function printSuccess(results, scope) {
99
+ console.log('');
100
+ console.log(chalk.green.bold(' Installation complete!'));
101
+ console.log('');
102
+
103
+ for (const r of results) {
104
+ console.log(` ${chalk.green('\u2713')} ${r.name} \u2192 ${r.platform} ${chalk.dim(`(${r.mode})`)}`);
105
+ }
106
+
107
+ console.log('');
108
+ if (scope === 'project') {
109
+ console.log(chalk.dim(' Start a coding session in this directory \u2014 skills activate automatically.'));
110
+ } else {
111
+ console.log(chalk.dim(' Skills installed globally \u2014 available in all projects.'));
112
+ }
113
+
114
+ console.log(chalk.dim(' Run `npx mymskills update` to pull the latest skill content.'));
115
+ console.log('');
116
+ }
@@ -0,0 +1,55 @@
1
+ import chalk from 'chalk';
2
+ import { showMiniBanner } from '../ui/banner.js';
3
+ import { detectInstalledSkills } from '../core/detector.js';
4
+ import { buildCatalog } from '../core/registry.js';
5
+ import { getCacheDir } from '../core/paths.js';
6
+ import path from 'node:path';
7
+ import fs from 'node:fs';
8
+
9
+ export async function listCommand() {
10
+ showMiniBanner();
11
+
12
+ const installed = detectInstalledSkills({ prefix: 'mymediset-' });
13
+
14
+ if (installed.length > 0) {
15
+ console.log(chalk.bold(' Installed Skills'));
16
+ console.log(chalk.dim(' ' + '\u2500'.repeat(60)));
17
+
18
+ for (const skill of installed) {
19
+ console.log(
20
+ ` ${chalk.green('\u2713')} ${chalk.bold(skill.name)} ` +
21
+ `${chalk.dim(skill.platformName)} \u00b7 ${chalk.dim(skill.scope)} \u00b7 ${chalk.dim(skill.mode)}`
22
+ );
23
+ }
24
+ console.log('');
25
+ } else {
26
+ console.log(chalk.dim(' No skills installed.\n'));
27
+ }
28
+
29
+ const cacheDir = getCacheDir();
30
+ const skillsDir = path.join(cacheDir, 'skills');
31
+
32
+ if (fs.existsSync(skillsDir)) {
33
+ const catalog = buildCatalog(skillsDir);
34
+ const installedNames = new Set(installed.map(s => s.name));
35
+ const available = catalog
36
+ .filter(s => s.dirName.startsWith('mymediset-'))
37
+ .filter(s => !installedNames.has(s.dirName));
38
+
39
+ if (available.length > 0) {
40
+ console.log(chalk.bold(' Available Skills'));
41
+ console.log(chalk.dim(' ' + '\u2500'.repeat(60)));
42
+
43
+ for (const skill of available) {
44
+ console.log(
45
+ ` ${chalk.dim('\u25cb')} ${skill.name} ` +
46
+ chalk.dim(`${skill.refCount} references`)
47
+ );
48
+ }
49
+ console.log('');
50
+ console.log(chalk.dim(' Run `npx mymskills` to install.\n'));
51
+ }
52
+ } else {
53
+ console.log(chalk.dim(' Run `npx mymskills` to fetch the skill catalog.\n'));
54
+ }
55
+ }
@@ -0,0 +1,43 @@
1
+ import chalk from 'chalk';
2
+ import { showMiniBanner } from '../ui/banner.js';
3
+ import { detectInstalledSkills } from '../core/detector.js';
4
+ import { removeSkill } from '../core/installer.js';
5
+ import { promptRemoveSelection, promptConfirm } from '../ui/prompts.js';
6
+
7
+ export async function removeCommand() {
8
+ showMiniBanner();
9
+
10
+ const installed = detectInstalledSkills({ prefix: 'mymediset-' });
11
+ if (installed.length === 0) {
12
+ console.log(chalk.yellow(' No mymediset skills installed.\n'));
13
+ return;
14
+ }
15
+
16
+ const toRemove = await promptRemoveSelection(installed);
17
+ if (toRemove.length === 0) return;
18
+
19
+ console.log('');
20
+ const proceed = await promptConfirm(
21
+ `Remove ${toRemove.length} skill(s)? This cannot be undone.`
22
+ );
23
+ if (!proceed) {
24
+ console.log(chalk.yellow('\n Removal cancelled.\n'));
25
+ return;
26
+ }
27
+
28
+ console.log('');
29
+ let removed = 0;
30
+
31
+ for (const skill of toRemove) {
32
+ const success = removeSkill(skill.path);
33
+ if (success) {
34
+ console.log(` ${chalk.red('\u2717')} Removed ${skill.name} ${chalk.dim(`from ${skill.platformName} (${skill.scope})`)}`);
35
+ removed++;
36
+ } else {
37
+ console.log(` ${chalk.yellow('!')} ${skill.name} not found at ${skill.path}`);
38
+ }
39
+ }
40
+
41
+ console.log('');
42
+ console.log(chalk.green(` ${removed} skill(s) removed.\n`));
43
+ }
@@ -0,0 +1,65 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import { showMiniBanner } from '../ui/banner.js';
4
+ import { fetchSkillSource } from '../core/fetcher.js';
5
+ import { detectInstalledSkills } from '../core/detector.js';
6
+ import { installSkill } from '../core/installer.js';
7
+ import path from 'node:path';
8
+
9
+ export async function updateCommand() {
10
+ showMiniBanner();
11
+
12
+ const installed = detectInstalledSkills({ prefix: 'mymediset-' });
13
+ if (installed.length === 0) {
14
+ console.log(chalk.yellow(' No mymediset skills installed. Run `npx mymskills` to install.\n'));
15
+ return;
16
+ }
17
+
18
+ console.log(` Found ${installed.length} installed skill(s)\n`);
19
+
20
+ const spinner = ora('Pulling latest skill library...').start();
21
+ let sourceDir;
22
+ try {
23
+ sourceDir = await fetchSkillSource(spinner);
24
+ spinner.succeed('Skill library updated');
25
+ } catch (error) {
26
+ spinner.fail(error.message);
27
+ process.exit(1);
28
+ }
29
+
30
+ const { updated, skipped } = applyUpdates(installed, sourceDir);
31
+
32
+ console.log('');
33
+ if (updated > 0) {
34
+ console.log(chalk.green(` ${updated} skill(s) updated.`));
35
+ }
36
+ if (skipped > 0) {
37
+ console.log(chalk.dim(` ${skipped} symlinked skill(s) already up to date.`));
38
+ }
39
+ console.log('');
40
+ }
41
+
42
+ function applyUpdates(installed, sourceDir) {
43
+ let updated = 0;
44
+ let skipped = 0;
45
+
46
+ for (const skill of installed) {
47
+ if (skill.mode === 'symlink') {
48
+ console.log(` ${chalk.green('\u2713')} ${skill.name} ${chalk.dim('(symlink \u2014 already points to latest)')}`);
49
+ skipped++;
50
+ continue;
51
+ }
52
+
53
+ const skillSpinner = ora(`Updating ${skill.name}...`).start();
54
+ try {
55
+ const targetDir = path.dirname(skill.path);
56
+ installSkill(sourceDir, targetDir, skill.name, 'copy');
57
+ skillSpinner.succeed(`${skill.name} ${chalk.dim('(re-copied)')}`);
58
+ updated++;
59
+ } catch (error) {
60
+ skillSpinner.fail(`${skill.name}: ${error.message}`);
61
+ }
62
+ }
63
+
64
+ return { updated, skipped };
65
+ }
@@ -0,0 +1,21 @@
1
+ import { createRequire } from 'node:module';
2
+ import { fileURLToPath } from 'node:url';
3
+ import path from 'node:path';
4
+
5
+ const require = createRequire(import.meta.url);
6
+ const pkg = require('../package.json');
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ export const VERSION = pkg.version;
12
+ export const GH_REPO = 'BITASIA/mym-sap-skill';
13
+ export const GH_CLONE_URL = `https://github.com/${GH_REPO}.git`;
14
+ export const CACHE_DIR_NAME = '.mym-sap-skills';
15
+ export const INSTALLER_ROOT = path.resolve(__dirname, '..');
16
+ export const SKILL_PREFIX = 'mymediset-';
17
+
18
+ export const PLATFORMS = {
19
+ claude: { name: 'Claude Code', dir: '.claude/skills' },
20
+ cursor: { name: 'Cursor', dir: '.cursor/skills' },
21
+ };
@@ -0,0 +1,52 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getAllScanLocations } from './paths.js';
4
+ import { PLATFORMS } from '../constants.js';
5
+
6
+ export function detectInstalledSkills({ prefix } = {}) {
7
+ const locations = getAllScanLocations();
8
+ const installed = [];
9
+
10
+ for (const loc of locations) {
11
+ if (!fs.existsSync(loc.path)) continue;
12
+
13
+ let entries;
14
+ try {
15
+ entries = fs.readdirSync(loc.path, { withFileTypes: true });
16
+ } catch {
17
+ continue;
18
+ }
19
+
20
+ for (const entry of entries) {
21
+ const fullPath = path.join(loc.path, entry.name);
22
+ const skillMdPath = path.join(fullPath, 'SKILL.md');
23
+ const isLink = isSymlink(fullPath);
24
+
25
+ const hasSkillMd = isLink
26
+ ? fs.existsSync(skillMdPath)
27
+ : entry.isDirectory() && fs.existsSync(skillMdPath);
28
+
29
+ if (!hasSkillMd) continue;
30
+ if (prefix && !entry.name.startsWith(prefix)) continue;
31
+
32
+ installed.push({
33
+ name: entry.name,
34
+ path: fullPath,
35
+ scope: loc.scope,
36
+ platform: loc.platform,
37
+ platformName: PLATFORMS[loc.platform].name,
38
+ mode: isLink ? 'symlink' : 'copy',
39
+ });
40
+ }
41
+ }
42
+
43
+ return installed;
44
+ }
45
+
46
+ function isSymlink(filePath) {
47
+ try {
48
+ return fs.lstatSync(filePath).isSymbolicLink();
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
@@ -0,0 +1,72 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { GH_CLONE_URL } from '../constants.js';
5
+ import { getCacheDir } from './paths.js';
6
+
7
+ function gitAvailable() {
8
+ try {
9
+ execFileSync('git', ['--version'], { stdio: 'ignore' });
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ function isInsideRepo() {
17
+ const installerRoot = path.resolve(new URL('..', import.meta.url).pathname);
18
+ const repoRoot = path.resolve(installerRoot, '..');
19
+ const skillsDir = path.join(repoRoot, 'skills');
20
+ const readme = path.join(repoRoot, 'README.md');
21
+
22
+ return fs.existsSync(skillsDir) && fs.existsSync(readme) ? repoRoot : null;
23
+ }
24
+
25
+ export async function fetchSkillSource(spinner) {
26
+ const localRepo = isInsideRepo();
27
+ if (localRepo) {
28
+ spinner.text = 'Using local skill library...';
29
+ if (fs.existsSync(path.join(localRepo, '.git'))) {
30
+ try {
31
+ execFileSync('git', ['pull', '--ff-only', '--quiet'], { cwd: localRepo, stdio: 'ignore' });
32
+ spinner.text = 'Local skill library updated';
33
+ } catch {
34
+ spinner.text = 'Using cached local version';
35
+ }
36
+ }
37
+ return localRepo;
38
+ }
39
+
40
+ const cacheDir = getCacheDir();
41
+
42
+ if (fs.existsSync(path.join(cacheDir, '.git'))) {
43
+ spinner.text = 'Updating skill library...';
44
+ try {
45
+ execFileSync('git', ['pull', '--ff-only', '--quiet'], { cwd: cacheDir, stdio: 'ignore' });
46
+ } catch {
47
+ spinner.text = 'Using cached version';
48
+ }
49
+ return cacheDir;
50
+ }
51
+
52
+ if (!gitAvailable()) {
53
+ throw new Error(
54
+ 'git is required but not found.\n' +
55
+ 'Install git and ensure you have access to the mymediset skill repository.\n' +
56
+ `Alternatively, clone manually: git clone ${GH_CLONE_URL} ${cacheDir}`
57
+ );
58
+ }
59
+
60
+ spinner.text = 'Downloading skill library...';
61
+ try {
62
+ execFileSync('git', ['clone', '--depth', '1', GH_CLONE_URL, cacheDir], { stdio: 'ignore' });
63
+ } catch {
64
+ throw new Error(
65
+ 'Failed to clone the skill repository.\n' +
66
+ 'Ensure you have access to the repository and your git credentials are configured.\n' +
67
+ `Repository: ${GH_CLONE_URL}`
68
+ );
69
+ }
70
+
71
+ return cacheDir;
72
+ }
@@ -0,0 +1,90 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { PLATFORMS } from '../constants.js';
5
+
6
+ const VALID_SKILL_NAME = /^[a-zA-Z0-9_-]+$/;
7
+
8
+ function validateSkillName(name) {
9
+ if (!VALID_SKILL_NAME.test(name)) {
10
+ throw new Error(`Invalid skill name: "${name}" — names may only contain letters, numbers, hyphens, and underscores`);
11
+ }
12
+ }
13
+
14
+ function assertPathContained(child, parent) {
15
+ const resolvedChild = path.resolve(child);
16
+ const resolvedParent = path.resolve(parent);
17
+ if (!resolvedChild.startsWith(resolvedParent + path.sep)) {
18
+ throw new Error(`Path traversal detected: "${child}" escapes expected directory "${parent}"`);
19
+ }
20
+ }
21
+
22
+ function getValidSkillParents() {
23
+ const home = os.homedir();
24
+ const parents = [];
25
+ for (const platform of Object.values(PLATFORMS)) {
26
+ parents.push(path.join(home, platform.dir));
27
+ parents.push(path.join(process.cwd(), platform.dir));
28
+ }
29
+ return parents;
30
+ }
31
+
32
+ export function installSkill(sourceDir, targetDir, skillDirName, mode) {
33
+ validateSkillName(skillDirName);
34
+
35
+ const source = path.resolve(sourceDir, 'skills', skillDirName);
36
+ if (!fs.existsSync(source)) {
37
+ throw new Error(`Skill source not found: ${source}`);
38
+ }
39
+
40
+ const target = path.join(targetDir, skillDirName);
41
+ assertPathContained(target, targetDir);
42
+
43
+ fs.mkdirSync(targetDir, { recursive: true });
44
+
45
+ if (fs.existsSync(target)) {
46
+ const stat = fs.lstatSync(target);
47
+ if (stat.isSymbolicLink()) {
48
+ fs.unlinkSync(target);
49
+ } else {
50
+ fs.rmSync(target, { recursive: true });
51
+ }
52
+ }
53
+
54
+ const effectiveMode = resolveMode(mode);
55
+
56
+ if (effectiveMode === 'symlink') {
57
+ fs.symlinkSync(source, target, 'dir');
58
+ } else {
59
+ fs.cpSync(source, target, { recursive: true });
60
+ }
61
+
62
+ return { name: skillDirName, target, mode: effectiveMode };
63
+ }
64
+
65
+ function resolveMode(requestedMode) {
66
+ if (requestedMode === 'copy') return 'copy';
67
+ if (os.platform() === 'win32') return 'copy';
68
+ return 'symlink';
69
+ }
70
+
71
+ export function removeSkill(skillPath) {
72
+ if (!fs.existsSync(skillPath)) return false;
73
+
74
+ const normalized = path.resolve(skillPath);
75
+ const validParents = getValidSkillParents();
76
+ const isInside = validParents.some(
77
+ (parent) => normalized.startsWith(parent + path.sep)
78
+ );
79
+ if (!isInside) {
80
+ throw new Error(`Refusing to remove path outside skills directories: ${skillPath}`);
81
+ }
82
+
83
+ const stat = fs.lstatSync(skillPath);
84
+ if (stat.isSymbolicLink()) {
85
+ fs.unlinkSync(skillPath);
86
+ } else {
87
+ fs.rmSync(skillPath, { recursive: true });
88
+ }
89
+ return true;
90
+ }
@@ -0,0 +1,29 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { PLATFORMS, CACHE_DIR_NAME } from '../constants.js';
4
+
5
+ export function getCacheDir() {
6
+ return path.join(os.homedir(), CACHE_DIR_NAME);
7
+ }
8
+
9
+ export function getSkillsDir(platform, scope) {
10
+ const base = scope === 'global' ? os.homedir() : process.cwd();
11
+ return path.join(base, PLATFORMS[platform].dir);
12
+ }
13
+
14
+ export function getAllScanLocations() {
15
+ const locations = [];
16
+ for (const [platformKey, platformConfig] of Object.entries(PLATFORMS)) {
17
+ locations.push({
18
+ path: path.join(os.homedir(), platformConfig.dir),
19
+ scope: 'global',
20
+ platform: platformKey,
21
+ });
22
+ locations.push({
23
+ path: path.join(process.cwd(), platformConfig.dir),
24
+ scope: 'project',
25
+ platform: platformKey,
26
+ });
27
+ }
28
+ return locations;
29
+ }
@@ -0,0 +1,78 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export function parseSkillFrontmatter(skillMdPath) {
5
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
6
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
7
+ if (!match) return null;
8
+
9
+ const frontmatter = match[1];
10
+ const name = extractField(frontmatter, 'name');
11
+ const description = extractDescription(frontmatter);
12
+
13
+ return { name, description };
14
+ }
15
+
16
+ function extractField(text, field) {
17
+ const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
18
+ const match = text.match(new RegExp(`^${escaped}:\\s*(.+)$`, 'm'));
19
+ return match ? match[1].trim().replace(/^["']|["']$/g, '') : '';
20
+ }
21
+
22
+ function extractDescription(text) {
23
+ const lines = text.split('\n');
24
+ let capturing = false;
25
+ const parts = [];
26
+
27
+ for (const line of lines) {
28
+ if (line.match(/^description:/)) {
29
+ const inline = line.replace(/^description:\s*>?\s*/, '').trim();
30
+ if (inline && !inline.startsWith('>')) {
31
+ return inline.replace(/^["']|["']$/g, '');
32
+ }
33
+ capturing = true;
34
+ continue;
35
+ }
36
+ if (capturing) {
37
+ if (line.match(/^\S/)) break;
38
+ parts.push(line.trim());
39
+ }
40
+ }
41
+
42
+ return parts.join(' ').trim().replace(/^["']|["']$/g, '');
43
+ }
44
+
45
+ export function buildCatalog(skillsDir) {
46
+ if (!fs.existsSync(skillsDir)) return [];
47
+
48
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
49
+ const catalog = [];
50
+
51
+ for (const entry of entries) {
52
+ if (!entry.isDirectory()) continue;
53
+
54
+ const skillPath = path.join(skillsDir, entry.name);
55
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
56
+
57
+ if (!fs.existsSync(skillMdPath)) continue;
58
+
59
+ const meta = parseSkillFrontmatter(skillMdPath);
60
+ if (!meta) continue;
61
+
62
+ const refsDir = path.join(skillPath, 'references');
63
+ let refCount = 0;
64
+ if (fs.existsSync(refsDir)) {
65
+ refCount = fs.readdirSync(refsDir).filter(f => f.endsWith('.md')).length;
66
+ }
67
+
68
+ catalog.push({
69
+ name: meta.name || entry.name,
70
+ dirName: entry.name,
71
+ description: meta.description,
72
+ refCount,
73
+ path: skillPath,
74
+ });
75
+ }
76
+
77
+ return catalog.sort((a, b) => a.name.localeCompare(b.name));
78
+ }
@@ -0,0 +1,36 @@
1
+ import figlet from 'figlet';
2
+ import gradient from 'gradient-string';
3
+ import boxen from 'boxen';
4
+ import chalk from 'chalk';
5
+ import { VERSION } from '../constants.js';
6
+
7
+ const COOL_GRADIENT = gradient(['#00b4d8', '#0077b6', '#6c63ff', '#e040fb']);
8
+
9
+ export function showBanner() {
10
+ const ascii = figlet.textSync('mymskills', {
11
+ font: 'Small',
12
+ horizontalLayout: 'default',
13
+ });
14
+
15
+ const banner = COOL_GRADIENT(ascii);
16
+
17
+ const tagline = chalk.dim('Interactive installer for mymediset AI skills');
18
+ const version = chalk.dim(`v${VERSION}`);
19
+
20
+ const content = `${banner}\n\n ${tagline}\n ${version}`;
21
+
22
+ console.log('');
23
+ console.log(
24
+ boxen(content, {
25
+ padding: 1,
26
+ margin: { top: 0, bottom: 1, left: 2, right: 2 },
27
+ borderStyle: 'round',
28
+ borderColor: '#0077b6',
29
+ })
30
+ );
31
+ }
32
+
33
+ export function showMiniBanner() {
34
+ const title = COOL_GRADIENT('mymskills');
35
+ console.log(`\n ${title} ${chalk.dim(`v${VERSION}`)}\n`);
36
+ }
@@ -0,0 +1,85 @@
1
+ import { checkbox, select, confirm } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+ import { PLATFORMS } from '../constants.js';
4
+
5
+ export async function promptSkillSelection(catalog) {
6
+ const choices = catalog.map(skill => ({
7
+ name: `${chalk.bold(skill.name)} ${chalk.dim(`(${skill.refCount} references)`)}`,
8
+ value: skill.dirName,
9
+ description: truncate(skill.description, 80),
10
+ }));
11
+
12
+ return checkbox({
13
+ message: 'Select skills to install:',
14
+ choices,
15
+ required: true,
16
+ instructions: chalk.dim(' (space to select, enter to confirm)'),
17
+ });
18
+ }
19
+
20
+ export async function promptPlatform() {
21
+ return select({
22
+ message: 'Target platform:',
23
+ choices: [
24
+ { name: 'Claude Code', value: ['claude'] },
25
+ { name: 'Cursor', value: ['cursor'] },
26
+ { name: 'Both', value: ['claude', 'cursor'] },
27
+ ],
28
+ });
29
+ }
30
+
31
+ export async function promptScope() {
32
+ return select({
33
+ message: 'Installation scope:',
34
+ choices: [
35
+ {
36
+ name: `Project ${chalk.dim('(./<platform>/skills/ in current directory)')}`,
37
+ value: 'project',
38
+ },
39
+ {
40
+ name: `Global ${chalk.dim('(~/<platform>/skills/ for all projects)')}`,
41
+ value: 'global',
42
+ },
43
+ ],
44
+ });
45
+ }
46
+
47
+ export async function promptMode() {
48
+ return select({
49
+ message: 'Installation mode:',
50
+ choices: [
51
+ {
52
+ name: `Symlink ${chalk.dim('(recommended — auto-updates with mymskills update)')}`,
53
+ value: 'symlink',
54
+ },
55
+ {
56
+ name: `Copy ${chalk.dim('(independent copy, manual updates)')}`,
57
+ value: 'copy',
58
+ },
59
+ ],
60
+ });
61
+ }
62
+
63
+ export async function promptConfirm(message) {
64
+ return confirm({ message, default: true });
65
+ }
66
+
67
+ export async function promptRemoveSelection(installed) {
68
+ const choices = installed.map(skill => ({
69
+ name: `${chalk.bold(skill.name)} ${chalk.dim(`(${skill.platformName}, ${skill.scope}, ${skill.mode})`)}`,
70
+ value: skill,
71
+ description: skill.path,
72
+ }));
73
+
74
+ return checkbox({
75
+ message: 'Select skills to remove:',
76
+ choices,
77
+ required: true,
78
+ instructions: chalk.dim(' (space to select, enter to confirm)'),
79
+ });
80
+ }
81
+
82
+ function truncate(str, max) {
83
+ if (!str) return '';
84
+ return str.length > max ? str.slice(0, max - 1) + '\u2026' : str;
85
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "mymskills",
3
+ "version": "1.0.0",
4
+ "description": "Interactive installer for mymediset AI skills — install domain expertise into Claude Code, Cursor, and other AI coding assistants",
5
+ "type": "module",
6
+ "bin": {
7
+ "mymskills": "./bin/mymskills.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "lib/"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "keywords": [
17
+ "mymediset",
18
+ "ai-skills",
19
+ "claude-code",
20
+ "cursor",
21
+ "sap",
22
+ "medical-equipment"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/BITASIA/mym-sap-skill"
27
+ },
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@inquirer/prompts": "^7.0.0",
31
+ "boxen": "^8.0.0",
32
+ "chalk": "^5.3.0",
33
+ "commander": "^12.0.0",
34
+ "figlet": "^1.8.0",
35
+ "gradient-string": "^3.0.0",
36
+ "ora": "^8.0.0"
37
+ }
38
+ }