Files

95 lines
4.5 KiB
TypeScript
Raw Permalink Normal View History

/**
* @fileoverview Static guard for the Claude Code plugin the repo publishes about itself.
*
* `.claude-plugin/marketplace.json` at the repo root makes `/plugin marketplace add
* Ark0N/Codeman` work; the one plugin it lists is `plugins/codeman/`, whose `skills/codeman`
* is a MIRROR of the real `skills/codeman/` (see `scripts/sync-plugin.mjs` for why it is a
* copy and not the source or a symlink). Pinned here:
*
* - the mirror is byte-identical to the source (edit the source, run the sync script);
* - both manifests carry package.json's version, or `plugin update` never sees a release;
* - the plugin root has NO `package.json`: a plugin root with one gets an npm install at
* install time, which for this repo meant 832 MB, 511 packages and the postinstall build
* on every installer's machine (measured 2026-09-14 with the repo root as plugin root);
* - the plugin ships exactly one component, the skill, and nothing else that would ride
* along silently (`commands/`, `agents/`, `hooks/`, `.mcp.json`, `settings.json`);
* - the skill's frontmatter names it, so the installed skill is `codeman:codeman` and not a
* versioned cache-directory name;
* - the repo root `.claude-plugin/` holds only the marketplace manifest, so the repo itself
* never reads as a plugin again.
*
* Pure filesystem reads against the real tree. Port: N/A.
*/
import { describe, it, expect } from 'vitest';
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
const PLUGIN_DIR = join(ROOT, 'plugins/codeman');
const readJson = (rel: string) => JSON.parse(readFileSync(join(ROOT, rel), 'utf8'));
function walk(dir: string): string[] {
const out: string[] = [];
for (const name of readdirSync(dir).sort()) {
const p = join(dir, name);
if (statSync(p).isDirectory()) out.push(...walk(p));
else out.push(p);
}
return out;
}
const pkg = readJson('package.json');
const plugin = readJson('plugins/codeman/.claude-plugin/plugin.json');
const marketplace = readJson('.claude-plugin/marketplace.json');
describe('Claude Code plugin (plugins/codeman)', () => {
it('mirrors skills/codeman byte for byte', () => {
const source = join(ROOT, 'skills/codeman');
const mirror = join(PLUGIN_DIR, 'skills/codeman');
const srcFiles = walk(source).map((p) => relative(source, p));
const dstFiles = walk(mirror).map((p) => relative(mirror, p));
expect(dstFiles).toEqual(srcFiles);
for (const rel of srcFiles) {
expect(readFileSync(join(mirror, rel)).equals(readFileSync(join(source, rel))), `${rel} drifted`).toBe(true);
}
});
it('plugin.json names the codeman plugin at the package version', () => {
expect(plugin.name).toBe('codeman');
expect(plugin.version).toBe(pkg.version);
expect(plugin.license).toBe('MIT');
expect(plugin.repository).toBe('https://github.com/Ark0N/Codeman');
});
it('marketplace.json lists exactly that plugin, sourced from plugins/codeman, at the same version', () => {
expect(marketplace.name).toBe('codeman');
expect(marketplace.owner?.name).toBeTruthy();
expect(marketplace.plugins).toHaveLength(1);
const [entry] = marketplace.plugins;
expect(entry.name).toBe(plugin.name);
expect(entry.source).toBe('./plugins/codeman');
expect(entry.version).toBe(pkg.version);
});
it('the plugin root carries no package.json (an npm-install trigger) and no component but the skill', () => {
expect(existsSync(join(PLUGIN_DIR, 'package.json')), 'plugins/codeman/package.json').toBe(false);
for (const rel of ['commands', 'agents', 'hooks', '.mcp.json', '.lsp.json', 'settings.json', 'monitors']) {
expect(existsSync(join(PLUGIN_DIR, rel)), `plugins/codeman/${rel} would ship with the plugin`).toBe(false);
}
for (const key of ['skills', 'commands', 'agents', 'hooks', 'mcpServers', 'lspServers']) {
expect(plugin[key], `plugin.json "${key}" override`).toBeUndefined();
}
expect(readdirSync(join(PLUGIN_DIR, 'skills'))).toEqual(['codeman']);
});
it('the skill declares its own name, so the installed skill is codeman:codeman', () => {
const skill = readFileSync(join(ROOT, 'skills/codeman/SKILL.md'), 'utf8');
expect(skill.split('---')[1] ?? '').toMatch(/^name: codeman$/m);
});
it('the repo root .claude-plugin holds only the marketplace manifest', () => {
expect(readdirSync(join(ROOT, '.claude-plugin'))).toEqual(['marketplace.json']);
});
});