Skip to content

Commit 6ccabf3

Browse files
committed
fix(skills): flatten skill directories to avoid nesting limitations
Each skill now gets a completely flat directory name (e.g., aipm-marketplace-plugin-skill) with no nesting, addressing Claude Code's limitation with nested directories (issue #10238). Also fixed fallback for meta-plugins where skills are defined in plugin.json rather than marketplace manifest. Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent d7796cc commit 6ccabf3

File tree

11 files changed

+519
-771
lines changed

11 files changed

+519
-771
lines changed

bun.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"ci:all": "bun run format:check && bun run typecheck && bun run test",
2222
"test": "bun test",
2323
"test:coverage": "bun test --coverage",
24-
"typecheck": "tsc --noEmit",
24+
"typecheck": "tsgo --noEmit",
2525
"dev": "bun --watch src/cli.ts",
2626
"format": "prettier --write .",
2727
"format:check": "prettier --check .",
@@ -33,6 +33,7 @@
3333
"@straw-hat/prettier-config": "3.1.5",
3434
"@types/bun": "1.2.2",
3535
"@types/lodash.merge": "4.6.9",
36+
"@typescript/native-preview": "7.0.0-dev.20251014.1",
3637
"prettier": "3.6.2"
3738
},
3839
"peerDependencies": {

src/commands/plugin-install.ts

Lines changed: 92 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import merge from 'lodash.merge';
22
import { join } from 'node:path';
33
import { z } from 'zod';
4-
import { getConfigPath, getNotInitializedMessage, loadPluginsConfig } from '../config/loader';
5-
import { DIR_CURSOR, FILE_AIPM_CONFIG, FILE_AIPM_CONFIG_LOCAL } from '../constants';
4+
import { loadClaudeCodeMarketplaces, loadPluginsConfig } from '../config/loader';
5+
import { DIR_CURSOR } from '../constants';
66
import { getErrorMessage } from '../errors';
77
import { loadTargetConfig, saveConfig } from '../helpers/aipm-config';
8+
import { isClaudeCodeInstalled } from '../helpers/claude-code-config';
89
import { fileExists } from '../helpers/fs';
910
import { resolveMarketplacePath } from '../helpers/git';
1011
import { defaultIO } from '../helpers/io';
@@ -15,9 +16,7 @@ import { formatSyncResult, syncMetaPluginToCursor, syncPluginToCursor } from '..
1516
const PluginInstallOptionsSchema = z.object({
1617
pluginId: z.string().min(1),
1718
cwd: z.string().optional(),
18-
local: z.boolean().optional(),
1919
dryRun: z.boolean().optional(),
20-
force: z.boolean().optional(),
2120
});
2221

2322
export async function pluginInstall(options: unknown): Promise<void> {
@@ -26,54 +25,92 @@ export async function pluginInstall(options: unknown): Promise<void> {
2625
const cwd = cmd.cwd || process.cwd();
2726

2827
try {
29-
const { config, sources } = await loadPluginsConfig(cwd);
28+
const { pluginName, marketplaceName } = parsePluginId(cmd.pluginId);
3029

31-
if (!sources.project && !sources.local) {
32-
const error = new Error(getNotInitializedMessage());
33-
defaultIO.logError(error.message);
34-
throw error;
30+
// Load AIPM config if available (optional for zero-config Claude Code mode)
31+
let config: any = {};
32+
let aipmInitialized = false;
33+
try {
34+
const loaded = await loadPluginsConfig(cwd);
35+
config = loaded.config;
36+
aipmInitialized = true;
37+
} catch {
38+
// AIPM not initialized - this is OK for Claude Code zero-config mode
39+
config = { marketplaces: {}, plugins: {} };
3540
}
36-
const configName = cmd.local ? getConfigPath(FILE_AIPM_CONFIG_LOCAL) : getConfigPath(FILE_AIPM_CONFIG);
3741

38-
const { pluginName, marketplaceName } = parsePluginId(cmd.pluginId);
42+
const aipmMarketplaces: Record<string, any> = config.marketplaces || {};
3943

40-
const marketplace = config.marketplaces[marketplaceName];
44+
// Check if plugin is already installed
45+
if (aipmInitialized && config.plugins[cmd.pluginId]) {
46+
defaultIO.logInfo(`Plugin '${cmd.pluginId}' is already installed`);
47+
return;
48+
}
49+
50+
const claudeMarketplaces = await loadClaudeCodeMarketplaces();
51+
const allMarketplaces = merge({}, claudeMarketplaces, aipmMarketplaces);
52+
const marketplace = allMarketplaces[marketplaceName];
4153

4254
if (!marketplace) {
43-
const error = new Error(`Marketplace '${marketplaceName}' not found. Add it first with 'marketplace add'.`);
55+
const availableMarketplaces = Object.keys(allMarketplaces);
56+
57+
let errorMessage: string;
58+
if (!availableMarketplaces.includes(marketplaceName) && marketplaceName.startsWith('claude/')) {
59+
const claudeCodeInstalled = await isClaudeCodeInstalled();
60+
if (!claudeCodeInstalled) {
61+
errorMessage = `Claude Code is not installed. Install Claude Code to use Claude Code marketplaces like '${marketplaceName}'.`;
62+
} else {
63+
errorMessage =
64+
`Marketplace '${marketplaceName}' not found in Claude Code. ` +
65+
`Available marketplaces: ${availableMarketplaces.join(', ')}`;
66+
}
67+
} else if (availableMarketplaces.length === 0) {
68+
errorMessage = `Marketplace '${marketplaceName}' not found. No marketplaces available.`;
69+
} else {
70+
errorMessage =
71+
`Marketplace '${marketplaceName}' not found. ` +
72+
`Available marketplaces: ${availableMarketplaces.join(', ')}`;
73+
}
74+
75+
const error = new Error(errorMessage);
4476
defaultIO.logError(error.message);
4577
throw error;
4678
}
4779

48-
const marketplacePath = await resolveMarketplacePath(marketplaceName, marketplace, cwd, {
49-
dryRun: cmd.dryRun,
50-
});
80+
let marketplacePath: string | null = null;
81+
82+
// For git/url sources, resolve the path (clone/download if needed)
83+
if (marketplace.source && marketplace.source !== 'directory') {
84+
marketplacePath = await resolveMarketplacePath(marketplaceName, marketplace, cwd, {
85+
dryRun: cmd.dryRun,
86+
});
87+
} else {
88+
// For directory sources, use the path directly
89+
marketplacePath = marketplace.path;
90+
}
5191

5292
if (!marketplacePath) {
5393
const error = new Error(`Marketplace '${marketplaceName}' has no path/url configured`);
5494
defaultIO.logError(error.message);
5595
throw error;
5696
}
5797

58-
// In dry-run mode, skip validation for git/url marketplaces that haven't been cached yet.
59-
// Directory marketplaces can still be validated since they exist locally.
60-
const isDirectoryMarketplace = marketplace.source === 'directory';
61-
const shouldSkipValidation = cmd.dryRun && !isDirectoryMarketplace;
62-
6398
let manifest: Awaited<ReturnType<typeof loadMarketplaceManifest>> = null;
6499
let pluginPath: string | null = null;
65100

66-
if (!shouldSkipValidation) {
101+
// Always validate for directory sources; skip expensive operations for git/url in dry-run
102+
const isDirectorySource = !marketplace.source || marketplace.source === 'directory';
103+
const shouldValidate = isDirectorySource || !cmd.dryRun;
104+
105+
if (shouldValidate) {
67106
manifest = await loadMarketplaceManifest(marketplacePath, getMarketplaceType(marketplaceName));
68107

69108
// Resolve plugin path: try manifest first, then search recursively if needed
70109
pluginPath = await resolvePluginPath(marketplacePath, pluginName, manifest);
71110

72111
if (!(await fileExists(pluginPath))) {
73112
const error = new Error(
74-
`Plugin '${pluginName}' not found in marketplace '${marketplaceName}'. ` +
75-
`Checked path: ${pluginPath}. ` +
76-
`If the plugin is in a nested directory, use the full path shown in search results.`,
113+
`Plugin '${pluginName}' not found in marketplace '${marketplaceName}'. ` + `Checked path: ${pluginPath}.`,
77114
);
78115
defaultIO.logError(error.message);
79116
throw error;
@@ -90,44 +127,51 @@ export async function pluginInstall(options: unknown): Promise<void> {
90127
}
91128
}
92129

93-
const isAlreadyEnabled = config.plugins[cmd.pluginId]?.enabled;
94-
95-
if (isAlreadyEnabled && !cmd.force) {
96-
defaultIO.logInfo(`Plugin '${cmd.pluginId}' is already installed`);
97-
return;
98-
}
99-
100130
if (cmd.dryRun) {
101-
defaultIO.logInfo(`[DRY RUN] Would enable plugin '${cmd.pluginId}' in ${configName}`);
102-
defaultIO.logInfo(`[DRY RUN] Would sync ${cmd.pluginId} to .cursor/`);
131+
defaultIO.logInfo(`[DRY RUN] Would sync ${cmd.pluginId} to .cursor/skills/`);
103132
return;
104133
}
105134

106-
const targetConfig = await loadTargetConfig(cwd, cmd.local);
107-
const updatedConfig = merge({}, targetConfig, {
108-
plugins: { [cmd.pluginId]: { enabled: true } },
109-
});
110-
111-
await saveConfig(cwd, updatedConfig, cmd.local);
112-
defaultIO.logSuccess(`Enabled plugin '${cmd.pluginId}' in ${configName}`);
113-
114-
// pluginPath is guaranteed to be set here since we skip validation only in dry-run mode,
115-
// and dry-run mode returns early above
135+
// For git/url sources in dry-run, we skip validation so pluginPath won't be set
136+
// In this case, we can't perform the sync, so we already returned above
116137
if (!pluginPath) {
117138
throw new Error(`Plugin path not resolved for '${pluginName}'`);
118139
}
119140

120141
const cursorDir = join(cwd, DIR_CURSOR);
121142
const isMetaPluginCheck = isMetaPlugin(marketplacePath, pluginPath, manifest);
122143

123-
const syncResult =
124-
isMetaPluginCheck && manifest
125-
? await syncMetaPluginToCursor(marketplacePath, manifest, pluginName, marketplaceName, cursorDir)
126-
: await syncPluginToCursor(pluginPath, marketplaceName, pluginName, cursorDir);
144+
// Check if it's a meta-plugin with skills defined in the manifest
145+
let syncResult;
146+
if (isMetaPluginCheck && manifest) {
147+
const pluginEntry = manifest.plugins.find((p) => p.name === pluginName);
148+
if (pluginEntry && pluginEntry.skills && pluginEntry.skills.length > 0) {
149+
// True meta-plugin with skills in manifest
150+
syncResult = await syncMetaPluginToCursor(marketplacePath, manifest, pluginName, marketplaceName, cursorDir);
151+
} else {
152+
// Meta-plugin path but skills are in plugin.json (not in marketplace manifest)
153+
// Fall back to regular plugin sync
154+
syncResult = await syncPluginToCursor(pluginPath, marketplaceName, pluginName, cursorDir);
155+
}
156+
} else {
157+
syncResult = await syncPluginToCursor(pluginPath, marketplaceName, pluginName, cursorDir);
158+
}
159+
160+
// Save plugin to AIPM config if initialized (enables sync and uninstall)
161+
if (aipmInitialized) {
162+
const targetConfig = await loadTargetConfig(cwd, false);
163+
const updatedConfig = merge({}, targetConfig, {
164+
plugins: { [cmd.pluginId]: { enabled: true } },
165+
});
166+
await saveConfig(cwd, updatedConfig, false);
167+
}
127168

128169
const summary = formatSyncResult(syncResult);
129170

130171
defaultIO.logSuccess(`Installed ${cmd.pluginId}`);
172+
if (aipmInitialized) {
173+
defaultIO.logSuccess(`Added plugin '${cmd.pluginId}' to .aipm/config.json`);
174+
}
131175
console.log(`\n✨ Plugin '${cmd.pluginId}' installed successfully! (${summary})\n`);
132176
} catch (error: unknown) {
133177
const message = getErrorMessage(error);

0 commit comments

Comments
 (0)