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
16 changes: 16 additions & 0 deletions packages/agents/src/commands/__tests__/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ describe(installCommand, () => {
expect(manifest.harnesses.claude?.entries.length).toBeGreaterThan(0);
});

it('skips a support directory that renders to zero installable entries', async () => {
const claudeHome = await setupClaudeHome();
// A support directory whose only content is a dotfile renders to zero entries (the renderer skips dotfiles),
// so install must skip it rather than create anything.
const emptySupportSrc = path.join(contentDir, 'skills', 'empty-support');
await mkdir(emptySupportSrc, { recursive: true });
await writeFile(path.join(emptySupportSrc, '.DS_Store'), '');

await expect(installCommand(makeOptions({ harness: 'claude' }), tempDir, contentDir)).resolves.toBeUndefined();

expect(existsSync(path.join(claudeHome, 'skills', 'empty-support'))).toBe(false);
const manifest = await readManifest(getManifestPath(tempDir));
const entries = manifest.harnesses.claude?.entries ?? [];
expect(entries.some((entry) => entry.relativePath === 'skills/empty-support')).toBe(false);
});

it('writes nothing in dry-run mode', async () => {
const claudeHome = await setupClaudeHome();

Expand Down
17 changes: 14 additions & 3 deletions packages/agents/src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,9 @@ async function installSkills(
contentDir,
toolMapping,
);
entries.push(result);
if (result !== undefined) {
entries.push(result);
}
}

// Install harness-specific skills from _harnesses/{harnessId}/
Expand Down Expand Up @@ -244,7 +246,9 @@ async function installSkills(
toolMapping,
'(harness-specific)',
);
entries.push(result);
if (result !== undefined) {
entries.push(result);
}
}

return entries;
Expand Down Expand Up @@ -286,7 +290,7 @@ async function installSkillEntry(
contentDir: string,
toolMapping: ReadonlyMap<string, string>,
label = '',
): Promise<ManifestEntry> {
): Promise<ManifestEntry | undefined> {
// Eagerly render the skill's final body before the dry-run gate, so missing include targets, cycles, out-of-tree
// references, and unmapped tool placeholders surface even when no files are written. Directory entries render the
// whole tree (include expansion, tool-name + link/template rewriting) into the exact bytes the write phase emits;
Expand All @@ -307,6 +311,13 @@ async function installSkillEntry(
expandedFileContent = rewriteToolNames(expanded, toolMapping, relativeFromContent(contentDir, srcPath));
}

// A support directory holding only dotfiles or `_partials/` renders to zero entries — nothing to install. Skip it
// entirely: no destination, no markers, no manifest entry. The orphan-prune pass clears any previously installed copy.
if (renderedDir !== undefined && renderedDir.length === 0) {
console.info(` [skip] ${relativePath}${label ? ` ${label}` : ''} (no installable entries)`);
return undefined;
}

if (options.dryRun) {
console.info(` [copy] ${relativePath}${label ? ` ${label}` : ''}`);
return { relativePath, contentHash: 'dry-run', linked: false };
Expand Down
Loading