Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/agents/src/commands/__tests__/install-includes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,44 @@ describe('install with include directives', () => {
expect(existsSync(path.join(claudeHome, 'skills', 'demo-skill', 'modules', '_partials'))).toBe(false);
});

it('does not install the top-level shared skills/_partials/ directory', async () => {
const contentDir = path.join(tempDir, 'fake-content');
// A `_partials` key under `skills` synthesizes the shared `content/skills/_partials/` directory as a sibling of
// real skills, covering the top-level enumeration case. The per-skill walk exclusions only match `_partials` as a
// child mid-walk, so a separate top-level exclusion is required when it is the walk root.
await buildContentTree(contentDir, {
skills: {
_partials: {
'plan-template.md': 'shared partial body\n',
},
},
});

const claudeHome = await setupClaudeHome();
await installCommand(makeOptions(), tempDir, contentDir);

expect(existsSync(path.join(claudeHome, 'skills', '_partials'))).toBe(false);
expect(existsSync(path.join(claudeHome, 'skills', '_partials', 'plan-template.md'))).toBe(false);
});

it('still installs other reserved top-level directories that skills reference at runtime (e.g. _data)', async () => {
// Guards the `_partials` exclusion from being over-generalized to an `_`-prefix skip: `_data/` is referenced by
// skills at runtime via absolute paths, so it must keep installing.
const contentDir = path.join(tempDir, 'fake-content');
await buildContentTree(contentDir, {
skills: {
_data: {
'design-priorities.md': 'shared data body\n',
},
},
});

const claudeHome = await setupClaudeHome();
await installCommand(makeOptions(), tempDir, contentDir);

expect(existsSync(path.join(claudeHome, 'skills', '_data', 'design-priorities.md'))).toBe(true);
});

it('rewrites markdown links inside expanded skill content (expand-then-rewrite ordering)', async () => {
const contentDir = path.join(tempDir, 'fake-content');
await buildContentTree(contentDir, {
Expand Down
5 changes: 4 additions & 1 deletion packages/agents/src/commands/__tests__/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ describe(installCommand, () => {
// Check that skills were installed — count should match shared + platform-specific
const contentDir = resolveContentDir();
const allSkillEntries = await readdir(path.join(contentDir, 'skills'));
const sharedSkillCount = allSkillEntries.filter((e) => e !== '_platforms' && !e.startsWith('.')).length;
// Mirror the install enumeration's skip set: `_platforms` and `_partials` are excluded, dotfiles too.
const sharedSkillCount = allSkillEntries.filter(
(e) => e !== '_platforms' && e !== '_partials' && !e.startsWith('.'),
).length;
const platformSkillsDir = path.join(contentDir, 'skills', '_platforms', 'claude');
const platformSkillEntries = existsSync(platformSkillsDir) ? await readdir(platformSkillsDir) : [];
const platformSkillCount = platformSkillEntries.filter((e) => !e.startsWith('.')).length;
Expand Down
9 changes: 7 additions & 2 deletions packages/agents/src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,14 @@ async function installSkills(
const dirEntries = await readdir(skillsSrcDir);
const entries: Array<ManifestEntry> = [];

// Install shared skills and support directories (skip _platforms and dotfiles)
// Install shared skills and support directories. Skip `_platforms` (installed by the platform-specific pass below)
// and `_partials` (an install-time include target whose content is inlined into including skills, never installed as
// a standalone directory), plus dotfiles. This mirrors the per-skill `_partials` walk exclusion, which only matches
// `_partials` as a child and so never fires when it is the enumeration root. Other reserved directories such as
// `_data/` install normally — skills reference them at runtime by absolute path, so the skip is by name, not by the
// leading-underscore convention.
for (const entry of dirEntries) {
if (entry === '_platforms' || entry.startsWith('.')) {
if (entry === '_platforms' || entry === '_partials' || entry.startsWith('.')) {
continue;
}
const result = await installSkillEntry(
Expand Down
Loading