skiller 0.8.1 → 0.9.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.
@@ -3,6 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.run = run;
4
4
  const handlers_1 = require("./handlers");
5
5
  const index_1 = require("../agents/index");
6
+ function skillsArgsBuilder(y) {
7
+ return y
8
+ .option('project-root', {
9
+ type: 'string',
10
+ description: 'Project root directory',
11
+ default: process.cwd(),
12
+ })
13
+ .positional('args', {
14
+ type: 'string',
15
+ array: true,
16
+ description: 'Arguments passed through to the local skills CLI',
17
+ });
18
+ }
6
19
  /**
7
20
  * Sets up and parses CLI commands.
8
21
  */
@@ -74,6 +87,16 @@ async function run() {
74
87
  description: 'Enable/disable skills support (experimental, default: enabled)',
75
88
  });
76
89
  }, handlers_1.applyHandler)
90
+ .command('add [args..]', 'Run the local skills CLI add command', skillsArgsBuilder, handlers_1.addHandler)
91
+ .command('remove [args..]', 'Run the local skills CLI remove command', skillsArgsBuilder, handlers_1.removeHandler)
92
+ .command('list [args..]', 'Run the local skills CLI list command', skillsArgsBuilder, handlers_1.listHandler)
93
+ .command('find [args..]', 'Run the local skills CLI find command', skillsArgsBuilder, handlers_1.findHandler)
94
+ .command('check [args..]', 'Run the local skills CLI check command', skillsArgsBuilder, handlers_1.checkHandler)
95
+ .command('update [args..]', 'Run the local skills CLI update command', skillsArgsBuilder, handlers_1.updateHandler)
96
+ .command('skills <subcommand> [args..]', 'Pass through an arbitrary command to the local skills CLI', (y) => skillsArgsBuilder(y).positional('subcommand', {
97
+ type: 'string',
98
+ description: 'The local skills CLI subcommand to run',
99
+ }), handlers_1.skillsHandler)
77
100
  .command('init', 'Scaffold a .claude directory with default files', (y) => {
78
101
  return y
79
102
  .option('project-root', {
@@ -87,6 +110,42 @@ async function run() {
87
110
  default: false,
88
111
  });
89
112
  }, handlers_1.initHandler)
113
+ .command('migrate claude-plugins', 'Plan or execute a one-shot migration from legacy Claude plugins to skills installs', (y) => {
114
+ return y
115
+ .option('project-root', {
116
+ type: 'string',
117
+ description: 'Project root directory',
118
+ default: process.cwd(),
119
+ })
120
+ .option('execute', {
121
+ type: 'boolean',
122
+ description: 'Actually install the resolved repos through the local skills CLI',
123
+ default: false,
124
+ });
125
+ }, handlers_1.migrateClaudePluginsHandler)
126
+ .command('migrate rules-to-skills [rules..]', 'Detect local .agents/rules .mdc files that already exist on skills.sh and optionally replace them', (y) => {
127
+ return y
128
+ .option('project-root', {
129
+ type: 'string',
130
+ description: 'Project root directory',
131
+ default: process.cwd(),
132
+ })
133
+ .option('execute', {
134
+ type: 'boolean',
135
+ description: 'Actually replace selected local rules after detection',
136
+ default: false,
137
+ })
138
+ .option('yes', {
139
+ type: 'boolean',
140
+ description: 'Auto-replace only unambiguous exact matches without prompting',
141
+ default: false,
142
+ })
143
+ .positional('rules', {
144
+ type: 'string',
145
+ array: true,
146
+ description: 'Specific rule names or .mdc files to check (default: all .agents/rules/*.mdc)',
147
+ });
148
+ }, handlers_1.migrateRulesToSkillsHandler)
90
149
  .command('revert', 'Revert skiller configurations from supported AI agents', (y) => {
91
150
  return y
92
151
  .option('project-root', {
@@ -120,7 +179,7 @@ async function run() {
120
179
  })
121
180
  .option('local-only', {
122
181
  type: 'boolean',
123
- description: 'Only search for local .claude directories, ignore global config',
182
+ description: 'Only search for local .agents directories, ignore global config',
124
183
  default: false,
125
184
  });
126
185
  }, handlers_1.revertHandler)
@@ -35,6 +35,15 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.applyHandler = applyHandler;
37
37
  exports.initHandler = initHandler;
38
+ exports.migrateClaudePluginsHandler = migrateClaudePluginsHandler;
39
+ exports.migrateRulesToSkillsHandler = migrateRulesToSkillsHandler;
40
+ exports.addHandler = addHandler;
41
+ exports.removeHandler = removeHandler;
42
+ exports.listHandler = listHandler;
43
+ exports.findHandler = findHandler;
44
+ exports.checkHandler = checkHandler;
45
+ exports.updateHandler = updateHandler;
46
+ exports.skillsHandler = skillsHandler;
38
47
  exports.revertHandler = revertHandler;
39
48
  const lib_1 = require("../lib");
40
49
  const revert_1 = require("../revert");
@@ -43,7 +52,107 @@ const os = __importStar(require("os"));
43
52
  const fs = __importStar(require("fs/promises"));
44
53
  const constants_1 = require("../constants");
45
54
  const ConfigLoader_1 = require("../core/ConfigLoader");
55
+ const ClaudePluginMigration_1 = require("../core/ClaudePluginMigration");
56
+ const RulesToSkillsMigration_1 = require("../core/RulesToSkillsMigration");
46
57
  const agents_1 = require("../agents");
58
+ const skills_cli_1 = require("./skills-cli");
59
+ const project_paths_1 = require("../core/project-paths");
60
+ const readline = __importStar(require("readline/promises"));
61
+ async function executeSkillsWrapper(projectRoot, args) {
62
+ try {
63
+ await (0, skills_cli_1.runSkillsCli)(projectRoot, args);
64
+ }
65
+ catch (err) {
66
+ const message = err instanceof Error ? err.message : String(err);
67
+ console.error(`${constants_1.ERROR_PREFIX} ${message}`);
68
+ process.exit(1);
69
+ }
70
+ }
71
+ function buildClaudePluginMigrationArgs(source) {
72
+ return ['add', source, '--agent', 'universal', '--skill', '*', '-y'];
73
+ }
74
+ function formatInstalls(count) {
75
+ if (!count || count <= 0)
76
+ return '0 installs';
77
+ if (count >= 1000000) {
78
+ return `${(count / 1000000).toFixed(1).replace(/\.0$/, '')}M installs`;
79
+ }
80
+ if (count >= 1000) {
81
+ return `${(count / 1000).toFixed(1).replace(/\.0$/, '')}K installs`;
82
+ }
83
+ return `${count} install${count === 1 ? '' : 's'}`;
84
+ }
85
+ function formatMatch(match) {
86
+ const source = match.source || match.slug;
87
+ return `${source}@${match.name} (${formatInstalls(match.installs)})`;
88
+ }
89
+ async function promptLine(message) {
90
+ const rl = readline.createInterface({
91
+ input: process.stdin,
92
+ output: process.stdout,
93
+ });
94
+ try {
95
+ return (await rl.question(message)).trim();
96
+ }
97
+ finally {
98
+ rl.close();
99
+ }
100
+ }
101
+ async function promptForReplacement(candidate) {
102
+ if (candidate.alreadyInstalled) {
103
+ const answer = await promptLine(`[skiller] '${candidate.ruleName}' is already installed upstream. Remove the local rule copy? [y/N] `);
104
+ return /^y(es)?$/i.test(answer) ? 'cleanup' : null;
105
+ }
106
+ if (candidate.matches.length === 1) {
107
+ const match = candidate.matches[0];
108
+ const answer = await promptLine(`[skiller] Replace '${candidate.ruleName}' with ${formatMatch(match)}? [y/N] `);
109
+ return /^y(es)?$/i.test(answer) ? match : null;
110
+ }
111
+ console.log(`[skiller] Multiple exact matches for '${candidate.ruleName}':`);
112
+ for (const [index, match] of candidate.matches.entries()) {
113
+ console.log(` ${index + 1}. ${formatMatch(match)}`);
114
+ }
115
+ const answer = await promptLine(`[skiller] Choose 1-${candidate.matches.length} or press Enter to skip: `);
116
+ if (answer.length === 0)
117
+ return null;
118
+ const selectedIndex = Number.parseInt(answer, 10);
119
+ if (Number.isNaN(selectedIndex) ||
120
+ selectedIndex < 1 ||
121
+ selectedIndex > candidate.matches.length) {
122
+ console.log(`[skiller] Skipping '${candidate.ruleName}' (invalid choice).`);
123
+ return null;
124
+ }
125
+ return candidate.matches[selectedIndex - 1];
126
+ }
127
+ function printRulesToSkillsPlan(plan) {
128
+ console.log(`[skiller] Scanned ${plan.scannedRules.length} local rule(s) from .agents/rules.`);
129
+ if (plan.candidates.length > 0) {
130
+ console.log('[skiller] Exact skills.sh matches:');
131
+ for (const candidate of plan.candidates) {
132
+ if (candidate.alreadyInstalled) {
133
+ console.log(`- ${candidate.ruleName}: already installed upstream; local rule can be removed`);
134
+ continue;
135
+ }
136
+ if (candidate.matches.length === 1) {
137
+ console.log(`- ${candidate.ruleName}: ${formatMatch(candidate.matches[0])}`);
138
+ continue;
139
+ }
140
+ console.log(`- ${candidate.ruleName}: ${candidate.matches.length} exact matches`);
141
+ for (const match of candidate.matches) {
142
+ console.log(` - ${formatMatch(match)}`);
143
+ }
144
+ }
145
+ }
146
+ else {
147
+ console.log('[skiller] No exact skills.sh matches found.');
148
+ }
149
+ if (plan.unmatched.length > 0) {
150
+ console.log(`[skiller] No exact match for: ${plan.unmatched.join(', ')}`);
151
+ }
152
+ if (plan.missingRequested.length > 0) {
153
+ console.log(`[skiller] Requested rules not found: ${plan.missingRequested.join(', ')}`);
154
+ }
155
+ }
47
156
  /**
48
157
  * Handler for the 'apply' command.
49
158
  */
@@ -125,10 +234,10 @@ async function initHandler(argv) {
125
234
  const isGlobal = argv['global'];
126
235
  const skillerDir = isGlobal
127
236
  ? path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'skiller')
128
- : path.join(projectRoot, '.claude');
237
+ : path.join(projectRoot, project_paths_1.CANONICAL_SKILLER_DIR);
129
238
  await fs.mkdir(skillerDir, { recursive: true });
130
- const instructionsPath = path.join(skillerDir, constants_1.DEFAULT_RULES_FILENAME); // .claude/AGENTS.md
131
- const tomlPath = path.join(skillerDir, 'skiller.toml');
239
+ const instructionsPath = path.join(skillerDir, constants_1.DEFAULT_RULES_FILENAME);
240
+ const tomlPath = path.join(skillerDir, project_paths_1.SKILLER_CONFIG_FILE);
132
241
  const exists = async (p) => {
133
242
  try {
134
243
  await fs.access(p);
@@ -146,8 +255,8 @@ async function initHandler(argv) {
146
255
  # uncomment and populate the following line. If omitted, all agents are active.
147
256
  # default_agents = ["github-copilot", "claude-code"]
148
257
 
149
- # Enable nested rule loading from nested .claude directories
150
- # When enabled, skiller will search for and process .claude directories throughout the project hierarchy
258
+ # Enable nested rule loading from nested .agents directories
259
+ # When enabled, skiller will search for and process .agents directories throughout the project hierarchy
151
260
  # nested = false
152
261
 
153
262
  # --- Agent Specific Configurations ---
@@ -195,6 +304,132 @@ async function initHandler(argv) {
195
304
  console.log(`[skiller] skiller.toml already exists, skipping`);
196
305
  }
197
306
  }
307
+ async function migrateClaudePluginsHandler(argv) {
308
+ const projectRoot = argv['project-root'];
309
+ try {
310
+ const plan = await (0, ClaudePluginMigration_1.planClaudePluginSkillsMigration)(projectRoot);
311
+ if (plan.installs.length === 0 && plan.unresolved.length === 0) {
312
+ console.log('[skiller] No Claude plugin repos found to migrate.');
313
+ return;
314
+ }
315
+ console.log('[skiller] Claude plugin migration plan:');
316
+ for (const install of plan.installs) {
317
+ console.log(`- ${install.source} <- ${install.pluginIds.join(', ')} (${install.strategy})`);
318
+ }
319
+ for (const unresolved of plan.unresolved) {
320
+ console.log(`- unresolved ${unresolved.pluginId}: ${unresolved.reason}`);
321
+ }
322
+ if (!argv.execute) {
323
+ console.log('[skiller] Run again with --execute to install the resolved repos through skills.');
324
+ return;
325
+ }
326
+ if (plan.unresolved.length > 0) {
327
+ throw new Error(`Cannot execute migration until all plugins resolve:\n${plan.unresolved.map((entry) => `- ${entry.pluginId}: ${entry.reason}`).join('\n')}`);
328
+ }
329
+ for (const install of plan.installs) {
330
+ await (0, skills_cli_1.runSkillsCli)(projectRoot, buildClaudePluginMigrationArgs(install.source));
331
+ }
332
+ console.log('[skiller] Claude plugin repo migration completed. Remove the plugin entries from .claude/settings.json, then rerun skiller apply.');
333
+ }
334
+ catch (err) {
335
+ const message = err instanceof Error ? err.message : String(err);
336
+ console.error(`${constants_1.ERROR_PREFIX} ${message}`);
337
+ process.exit(1);
338
+ }
339
+ }
340
+ async function migrateRulesToSkillsHandler(argv) {
341
+ const projectRoot = argv['project-root'];
342
+ try {
343
+ const plan = await (0, RulesToSkillsMigration_1.planRulesToSkillsMigration)(projectRoot, argv.rules);
344
+ printRulesToSkillsPlan(plan);
345
+ if (!argv.execute) {
346
+ console.log('[skiller] Run again with --execute to replace interactively, or --execute --yes to auto-replace unambiguous matches.');
347
+ return;
348
+ }
349
+ if (!argv.yes && !process.stdin.isTTY) {
350
+ throw new Error('Interactive replacement requires a TTY. Re-run with --yes to auto-replace only unambiguous matches.');
351
+ }
352
+ let replacedCount = 0;
353
+ for (const candidate of plan.candidates) {
354
+ let selection = null;
355
+ if (argv.yes) {
356
+ if (candidate.alreadyInstalled) {
357
+ selection = 'cleanup';
358
+ }
359
+ else if (candidate.matches.length === 1) {
360
+ selection = candidate.matches[0];
361
+ }
362
+ else {
363
+ console.log(`[skiller] Skipping '${candidate.ruleName}' because it has multiple exact matches.`);
364
+ continue;
365
+ }
366
+ }
367
+ else {
368
+ selection = await promptForReplacement(candidate);
369
+ }
370
+ if (!selection)
371
+ continue;
372
+ if (selection !== 'cleanup') {
373
+ await (0, skills_cli_1.runSkillsCli)(projectRoot, (0, RulesToSkillsMigration_1.buildRulesReplacementInstallArgs)(selection));
374
+ }
375
+ await (0, RulesToSkillsMigration_1.removeLocalRuleReplacementState)(projectRoot, candidate.ruleName, false);
376
+ replacedCount += 1;
377
+ console.log(`[skiller] Replaced local rule '${candidate.ruleName}'.`);
378
+ }
379
+ if (replacedCount === 0) {
380
+ console.log('[skiller] No local rules were replaced.');
381
+ return;
382
+ }
383
+ console.log('[skiller] Replacement pass completed. Run skiller apply to refresh derived agent outputs.');
384
+ }
385
+ catch (err) {
386
+ const message = err instanceof Error ? err.message : String(err);
387
+ console.error(`${constants_1.ERROR_PREFIX} ${message}`);
388
+ process.exit(1);
389
+ }
390
+ }
391
+ async function addHandler(argv) {
392
+ await executeSkillsWrapper(argv['project-root'], [
393
+ 'add',
394
+ ...(argv.args ?? []),
395
+ ]);
396
+ }
397
+ async function removeHandler(argv) {
398
+ await executeSkillsWrapper(argv['project-root'], [
399
+ 'remove',
400
+ ...(argv.args ?? []),
401
+ ]);
402
+ }
403
+ async function listHandler(argv) {
404
+ await executeSkillsWrapper(argv['project-root'], [
405
+ 'list',
406
+ ...(argv.args ?? []),
407
+ ]);
408
+ }
409
+ async function findHandler(argv) {
410
+ await executeSkillsWrapper(argv['project-root'], [
411
+ 'find',
412
+ ...(argv.args ?? []),
413
+ ]);
414
+ }
415
+ async function checkHandler(argv) {
416
+ await executeSkillsWrapper(argv['project-root'], [
417
+ 'check',
418
+ ...(argv.args ?? []),
419
+ ]);
420
+ }
421
+ async function updateHandler(argv) {
422
+ await executeSkillsWrapper(argv['project-root'], [
423
+ 'update',
424
+ ...(argv.args ?? []),
425
+ ]);
426
+ }
427
+ async function skillsHandler(argv) {
428
+ await executeSkillsWrapper(argv['project-root'], [
429
+ argv.subcommand,
430
+ ...(argv.args ?? []),
431
+ ]);
432
+ }
198
433
  /**
199
434
  * Handler for the 'revert' command.
200
435
  */
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runSkillsCli = runSkillsCli;
37
+ const child_process_1 = require("child_process");
38
+ const path = __importStar(require("path"));
39
+ const module_1 = require("module");
40
+ function resolveSkillsCliEntrypoint(projectRoot) {
41
+ const resolvers = [
42
+ (0, module_1.createRequire)(path.join(projectRoot, 'package.json')),
43
+ (0, module_1.createRequire)(path.join(__dirname, '..', '..', 'package.json')),
44
+ ];
45
+ for (const resolver of resolvers) {
46
+ try {
47
+ const skillsPackageJson = resolver.resolve('skills/package.json');
48
+ return path.join(path.dirname(skillsPackageJson), 'bin', 'cli.mjs');
49
+ }
50
+ catch {
51
+ // Try the next resolver.
52
+ }
53
+ }
54
+ throw new Error("Cannot find module 'skills/package.json' from the project root or skiller installation");
55
+ }
56
+ async function runSkillsCli(projectRoot, args) {
57
+ const cliEntrypoint = resolveSkillsCliEntrypoint(projectRoot);
58
+ await new Promise((resolve, reject) => {
59
+ const child = (0, child_process_1.spawn)(process.execPath, [cliEntrypoint, ...args], {
60
+ cwd: projectRoot,
61
+ stdio: 'inherit',
62
+ });
63
+ child.on('error', reject);
64
+ child.on('exit', (code) => {
65
+ if (code === 0) {
66
+ resolve();
67
+ return;
68
+ }
69
+ reject(new Error(`skills ${args[0] ?? ''} failed with exit code ${code}`.trim()));
70
+ });
71
+ });
72
+ }
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MAX_RECURSION_DEPTH = exports.SKILL_MD_FILENAME = exports.CLAUDE_SKILLS_PATH = exports.SKILLS_DIR = exports.DEFAULT_RULES_FILENAME = exports.ERROR_PREFIX = void 0;
3
+ exports.MAX_RECURSION_DEPTH = exports.SKILL_MD_FILENAME = exports.CLAUDE_SKILLS_PATH = exports.CANONICAL_SKILLS_PATH = exports.SKILLS_DIR = exports.DEFAULT_RULES_FILENAME = exports.ERROR_PREFIX = void 0;
4
4
  exports.actionPrefix = actionPrefix;
5
5
  exports.createSkillerError = createSkillerError;
6
6
  exports.logVerbose = logVerbose;
@@ -50,6 +50,7 @@ function logVerboseInfo(message, isVerbose, dryRun = false) {
50
50
  }
51
51
  // Skills-related constants
52
52
  exports.SKILLS_DIR = 'skills';
53
+ exports.CANONICAL_SKILLS_PATH = '.agents/skills';
53
54
  exports.CLAUDE_SKILLS_PATH = '.claude/skills';
54
55
  exports.SKILL_MD_FILENAME = 'SKILL.md';
55
56
  // Security: Maximum recursion depth to prevent DoS via deeply nested directories
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.planClaudePluginSkillsMigration = planClaudePluginSkillsMigration;
37
+ const fs = __importStar(require("fs/promises"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const project_paths_1 = require("./project-paths");
41
+ const SkillsManifest_1 = require("./SkillsManifest");
42
+ function getClaudeHomeDir() {
43
+ return process.env.HOME || os.homedir();
44
+ }
45
+ async function readJsonFile(filePath) {
46
+ try {
47
+ return JSON.parse(await fs.readFile(filePath, 'utf8'));
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ function parsePluginId(pluginId) {
54
+ const atIndex = pluginId.lastIndexOf('@');
55
+ if (atIndex <= 0 || atIndex === pluginId.length - 1)
56
+ return null;
57
+ return {
58
+ pluginName: pluginId.slice(0, atIndex),
59
+ marketplaceId: pluginId.slice(atIndex + 1),
60
+ };
61
+ }
62
+ function normalizeInstallSource(source) {
63
+ if (!source || typeof source !== 'object')
64
+ return null;
65
+ const raw = source;
66
+ if (raw.source === 'github' && typeof raw.repo === 'string') {
67
+ return raw.repo;
68
+ }
69
+ if ((raw.source === 'git' || raw.source === 'url') &&
70
+ typeof raw.url === 'string') {
71
+ return raw.url;
72
+ }
73
+ return null;
74
+ }
75
+ async function readEnabledPluginIds(projectRoot) {
76
+ const settingsPath = path.join(projectRoot, project_paths_1.LEGACY_SKILLER_DIR, 'settings.json');
77
+ const raw = await readJsonFile(settingsPath);
78
+ if (!raw || typeof raw !== 'object')
79
+ return [];
80
+ const enabledPlugins = raw.enabledPlugins;
81
+ if (!enabledPlugins || typeof enabledPlugins !== 'object')
82
+ return [];
83
+ return Object.entries(enabledPlugins)
84
+ .filter(([, enabled]) => enabled === true)
85
+ .map(([pluginId]) => pluginId)
86
+ .sort((a, b) => a.localeCompare(b));
87
+ }
88
+ async function readManifestPluginIds(projectRoot) {
89
+ const manifestPaths = [
90
+ path.join(projectRoot, project_paths_1.CANONICAL_SKILLER_DIR, SkillsManifest_1.SKILLS_MANIFEST_FILENAME),
91
+ path.join(projectRoot, project_paths_1.LEGACY_SKILLER_DIR, SkillsManifest_1.SKILLS_MANIFEST_FILENAME),
92
+ ];
93
+ const pluginIds = new Set();
94
+ for (const manifestPath of manifestPaths) {
95
+ const raw = await readJsonFile(manifestPath);
96
+ if (!raw || typeof raw !== 'object')
97
+ continue;
98
+ const targets = raw.targets;
99
+ if (!targets || typeof targets !== 'object')
100
+ continue;
101
+ for (const entries of Object.values(targets)) {
102
+ if (!Array.isArray(entries))
103
+ continue;
104
+ for (const entry of entries) {
105
+ if (!entry || typeof entry !== 'object')
106
+ continue;
107
+ const sourceType = entry.sourceType;
108
+ const pluginId = entry.pluginId;
109
+ if (sourceType === 'plugin' && typeof pluginId === 'string') {
110
+ pluginIds.add(pluginId);
111
+ }
112
+ }
113
+ }
114
+ }
115
+ return [...pluginIds].sort((a, b) => a.localeCompare(b));
116
+ }
117
+ async function readProjectMarketplaceRecords(projectRoot) {
118
+ const settingsPath = path.join(projectRoot, project_paths_1.LEGACY_SKILLER_DIR, 'settings.json');
119
+ const raw = await readJsonFile(settingsPath);
120
+ if (!raw || typeof raw !== 'object')
121
+ return {};
122
+ const marketplaces = raw.extraKnownMarketplaces;
123
+ if (!marketplaces || typeof marketplaces !== 'object')
124
+ return {};
125
+ return Object.fromEntries(Object.entries(marketplaces).filter(([, value]) => value && typeof value === 'object'));
126
+ }
127
+ async function readHomeMarketplaceRecords() {
128
+ const knownPath = path.join(getClaudeHomeDir(), '.claude', 'plugins', 'known_marketplaces.json');
129
+ const raw = await readJsonFile(knownPath);
130
+ if (!raw || typeof raw !== 'object')
131
+ return {};
132
+ return raw;
133
+ }
134
+ function mergeMarketplaceRecords(homeRecords, projectRecords) {
135
+ return {
136
+ ...homeRecords,
137
+ ...projectRecords,
138
+ };
139
+ }
140
+ async function readMarketplacePlugins(marketplaceId, marketplace) {
141
+ const candidates = [];
142
+ if (typeof marketplace?.installLocation === 'string') {
143
+ candidates.push(path.join(marketplace.installLocation, '.claude-plugin', 'marketplace.json'));
144
+ }
145
+ candidates.push(path.join(getClaudeHomeDir(), '.claude', 'plugins', 'marketplaces', marketplaceId, '.claude-plugin', 'marketplace.json'));
146
+ for (const candidate of candidates) {
147
+ const raw = await readJsonFile(candidate);
148
+ if (!raw || typeof raw !== 'object')
149
+ continue;
150
+ const plugins = raw.plugins;
151
+ if (!Array.isArray(plugins))
152
+ continue;
153
+ const next = {};
154
+ for (const plugin of plugins) {
155
+ if (!plugin || typeof plugin !== 'object')
156
+ continue;
157
+ const pluginName = plugin.name;
158
+ if (typeof pluginName !== 'string')
159
+ continue;
160
+ next[pluginName] = {
161
+ source: plugin.source,
162
+ };
163
+ }
164
+ return next;
165
+ }
166
+ return {};
167
+ }
168
+ function sortPlan(installsBySource, unresolved) {
169
+ const installs = [...installsBySource.entries()]
170
+ .map(([source, info]) => ({
171
+ source,
172
+ pluginIds: [...info.pluginIds].sort((a, b) => a.localeCompare(b)),
173
+ strategy: info.strategy,
174
+ }))
175
+ .sort((a, b) => a.source.localeCompare(b.source));
176
+ return {
177
+ installs,
178
+ unresolved: [...unresolved].sort((a, b) => a.pluginId.localeCompare(b.pluginId)),
179
+ };
180
+ }
181
+ async function planClaudePluginSkillsMigration(projectRoot) {
182
+ const pluginIds = new Set([
183
+ ...(await readEnabledPluginIds(projectRoot)),
184
+ ...(await readManifestPluginIds(projectRoot)),
185
+ ]);
186
+ const homeMarketplaces = await readHomeMarketplaceRecords();
187
+ const projectMarketplaces = await readProjectMarketplaceRecords(projectRoot);
188
+ const marketplaces = mergeMarketplaceRecords(homeMarketplaces, projectMarketplaces);
189
+ const installsBySource = new Map();
190
+ const unresolved = [];
191
+ const pluginCatalogCache = new Map();
192
+ for (const pluginId of [...pluginIds].sort((a, b) => a.localeCompare(b))) {
193
+ const parts = parsePluginId(pluginId);
194
+ if (!parts) {
195
+ unresolved.push({
196
+ pluginId,
197
+ reason: 'Plugin id is not in <plugin>@<marketplace> format',
198
+ });
199
+ continue;
200
+ }
201
+ const marketplace = marketplaces[parts.marketplaceId];
202
+ let pluginDescriptors = pluginCatalogCache.get(parts.marketplaceId);
203
+ if (!pluginDescriptors) {
204
+ pluginDescriptors = await readMarketplacePlugins(parts.marketplaceId, marketplace);
205
+ pluginCatalogCache.set(parts.marketplaceId, pluginDescriptors);
206
+ }
207
+ const pluginDescriptor = pluginDescriptors[parts.pluginName];
208
+ const pluginSource = normalizeInstallSource(pluginDescriptor?.source);
209
+ const marketplaceSource = normalizeInstallSource(marketplace?.source);
210
+ const resolvedSource = pluginSource ?? marketplaceSource;
211
+ if (!resolvedSource) {
212
+ unresolved.push({
213
+ pluginId,
214
+ reason: `No repo or URL source could be inferred for marketplace ${parts.marketplaceId}`,
215
+ });
216
+ continue;
217
+ }
218
+ const existing = installsBySource.get(resolvedSource);
219
+ if (existing) {
220
+ existing.pluginIds.add(pluginId);
221
+ continue;
222
+ }
223
+ installsBySource.set(resolvedSource, {
224
+ pluginIds: new Set([pluginId]),
225
+ strategy: pluginSource ? 'plugin-source' : 'marketplace-source',
226
+ });
227
+ }
228
+ return sortPlan(installsBySource, unresolved);
229
+ }