Skip to content

Commit 886f71a

Browse files
committed
fix
1 parent e293246 commit 886f71a

4 files changed

Lines changed: 322 additions & 14 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "skiller",
3-
"version": "0.7.21",
3+
"version": "0.7.22",
44
"description": "Skiller — apply the same rules to all coding agents",
55
"main": "dist/lib.js",
66
"publishConfig": {

src/core/ClaudePluginSync.ts

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ interface PluginResolvedSource {
4848
version?: string;
4949
}
5050

51+
interface PluginPackageManifestEntry {
52+
name?: string;
53+
source?: string;
54+
}
55+
5156
interface LegacyMarkerFile {
5257
pluginId: string;
5358
pluginVersion?: string;
@@ -191,6 +196,74 @@ async function resolvePluginMarketplaceRoot(
191196
return null;
192197
}
193198

199+
async function hasPluginContent(root: string): Promise<boolean> {
200+
const candidates = [
201+
path.join(root, 'skills'),
202+
path.join(root, 'commands'),
203+
path.join(root, 'agents'),
204+
];
205+
206+
for (const candidate of candidates) {
207+
if (await dirExists(candidate)) return true;
208+
}
209+
210+
return false;
211+
}
212+
213+
async function readPluginPackageManifestEntries(
214+
installPath: string,
215+
): Promise<PluginPackageManifestEntry[]> {
216+
const packageJsonPath = path.join(installPath, 'package.json');
217+
218+
try {
219+
const raw = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) as {
220+
plugins?: unknown;
221+
};
222+
if (!Array.isArray(raw.plugins)) return [];
223+
224+
return raw.plugins.filter(
225+
(entry): entry is PluginPackageManifestEntry =>
226+
Boolean(entry) && typeof entry === 'object',
227+
);
228+
} catch {
229+
return [];
230+
}
231+
}
232+
233+
async function resolveInstalledPluginSourceRoot(
234+
pluginId: string,
235+
installPath: string,
236+
): Promise<string | null> {
237+
const manifestEntries = await readPluginPackageManifestEntries(installPath);
238+
const parsedPluginId = parsePluginId(pluginId);
239+
const matchingEntry =
240+
manifestEntries.find(
241+
(entry) => entry.name === parsedPluginId?.pluginName,
242+
) ?? (manifestEntries.length === 1 ? manifestEntries[0] : null);
243+
244+
const rawSource =
245+
typeof matchingEntry?.source === 'string' &&
246+
matchingEntry.source.trim() !== ''
247+
? matchingEntry.source
248+
: '.';
249+
250+
const sourceRoot = path.resolve(installPath, rawSource);
251+
const normalizedInstallPath = path.resolve(installPath);
252+
const isWithinInstallPath =
253+
sourceRoot === normalizedInstallPath ||
254+
sourceRoot.startsWith(normalizedInstallPath + path.sep);
255+
256+
if (isWithinInstallPath && (await hasPluginContent(sourceRoot))) {
257+
return sourceRoot;
258+
}
259+
260+
if (await hasPluginContent(installPath)) {
261+
return installPath;
262+
}
263+
264+
return null;
265+
}
266+
194267
export function resolvePluginInstall(
195268
pluginId: string,
196269
projectRoot: string,
@@ -772,13 +845,27 @@ export async function syncClaudePluginsToSkillsDirs(
772845
const unresolvedEnabled = new Set<string>();
773846

774847
for (const pluginId of enabledPlugins) {
775-
const pluginRoot = await resolvePluginMarketplaceRoot(pluginId, claudeDir);
848+
const resolved = index
849+
? resolvePluginInstall(pluginId, projectRoot, index)
850+
: null;
851+
const marketplaceRoot = await resolvePluginMarketplaceRoot(
852+
pluginId,
853+
claudeDir,
854+
);
855+
const pluginRoot =
856+
(marketplaceRoot && (await hasPluginContent(marketplaceRoot))
857+
? marketplaceRoot
858+
: null) ??
859+
(resolved
860+
? await resolveInstalledPluginSourceRoot(pluginId, resolved.installPath)
861+
: null);
862+
776863
if (!pluginRoot) {
777864
unresolvedEnabled.add(pluginId);
778865
const hasIndexEntry = Boolean(index?.plugins?.[pluginId]?.length);
779866
if (hasIndexEntry) {
780867
logVerboseInfo(
781-
`[plugins] Enabled plugin has no marketplace content, skipping: ${pluginId}`,
868+
`[plugins] Enabled plugin has no syncable content, skipping: ${pluginId}`,
782869
verbose,
783870
dryRun,
784871
);
@@ -788,10 +875,6 @@ export async function syncClaudePluginsToSkillsDirs(
788875
continue;
789876
}
790877

791-
const resolved = index
792-
? resolvePluginInstall(pluginId, projectRoot, index)
793-
: null;
794-
795878
resolvedSources.push({
796879
pluginId,
797880
pluginRoot,

tests/integration/claude-plugins-to-skills.integration.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,127 @@ Find docs and summarize.
175175
fs.access(path.join(expectedAgentDir, 'SKILL.md')),
176176
).resolves.toBeUndefined();
177177
});
178+
179+
it('syncs plugin skills from the installed plugin root when marketplace discovery has no syncable content', async () => {
180+
const { projectRoot } = testProject;
181+
182+
const pluginId = 'planning-with-files@testmarket';
183+
const emptyMarketplacePluginPath = path.join(
184+
tmpHome,
185+
'.claude',
186+
'plugins',
187+
'marketplaces',
188+
'testmarket',
189+
'plugins',
190+
'planning-with-files',
191+
);
192+
const pluginInstallPath = path.join(
193+
tmpHome,
194+
'.claude',
195+
'plugins',
196+
'cache',
197+
'testmarket',
198+
'planning-with-files',
199+
'2.21.0',
200+
);
201+
202+
await fs.mkdir(emptyMarketplacePluginPath, { recursive: true });
203+
const pluginSkillDir = path.join(pluginInstallPath, 'skills', 'plan');
204+
await fs.mkdir(pluginSkillDir, { recursive: true });
205+
await fs.writeFile(
206+
path.join(pluginInstallPath, 'package.json'),
207+
JSON.stringify(
208+
{
209+
name: 'planning-with-files',
210+
owner: {
211+
name: 'Ahmad Othman Ammar Adi',
212+
url: 'https://github.com/OthmanAdi',
213+
},
214+
plugins: [
215+
{
216+
name: 'planning-with-files',
217+
source: './',
218+
description:
219+
'Manus-style persistent markdown files for planning, progress tracking, and knowledge storage. Now with hooks integration.',
220+
version: '2.21.0',
221+
},
222+
],
223+
},
224+
null,
225+
2,
226+
),
227+
);
228+
await fs.writeFile(
229+
path.join(pluginSkillDir, 'SKILL.md'),
230+
`---
231+
name: plan
232+
description: Persistent planning
233+
---
234+
235+
Write plans to files.
236+
`,
237+
);
238+
239+
const indexPath = path.join(
240+
tmpHome,
241+
'.claude',
242+
'plugins',
243+
'installed_plugins.json',
244+
);
245+
await fs.mkdir(path.dirname(indexPath), { recursive: true });
246+
await fs.writeFile(
247+
indexPath,
248+
JSON.stringify(
249+
{
250+
version: 2,
251+
plugins: {
252+
[pluginId]: [
253+
{
254+
scope: 'project',
255+
projectPath: projectRoot,
256+
installPath: pluginInstallPath,
257+
version: '2.21.0',
258+
installedAt: '2026-03-01T00:00:00.000Z',
259+
lastUpdated: '2026-03-02T00:00:00.000Z',
260+
},
261+
],
262+
},
263+
},
264+
null,
265+
2,
266+
),
267+
);
268+
269+
await fs.writeFile(
270+
path.join(projectRoot, '.claude', 'settings.json'),
271+
JSON.stringify(
272+
{
273+
enabledPlugins: {
274+
[pluginId]: true,
275+
},
276+
},
277+
null,
278+
2,
279+
),
280+
);
281+
282+
await applyAllAgentConfigs(
283+
projectRoot,
284+
['codex'],
285+
undefined,
286+
false,
287+
undefined,
288+
false,
289+
false,
290+
false,
291+
true,
292+
false,
293+
false,
294+
true,
295+
);
296+
297+
await expect(
298+
fs.access(path.join(projectRoot, '.codex', 'skills', 'plan', 'SKILL.md')),
299+
).resolves.toBeUndefined();
300+
});
178301
});

0 commit comments

Comments
 (0)