diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5e8d5b0..74c39f3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -20,7 +20,6 @@ { "name": "ctx", "source": "./plugins/ctx", - "version": "0.1.0", "description": "Context hygiene for AI-assisted codebases. Finds stale, contradictory, and drifting AI context files (CLAUDE.md, AGENTS.md, Serena memories, Cursor rules, and 10+ more tool ecosystems).", "keywords": [ "context", @@ -39,7 +38,6 @@ { "name": "strip-ansi", "source": "./plugins/strip-ansi", - "version": "0.1.0", "description": "Prevents ANSI escape sequences from polluting agent context, commits, PRs, and file edits. Injects NO_COLOR into Bash tool calls and sanitizes any residual escapes from output.", "keywords": [ "ansi", @@ -54,7 +52,6 @@ { "name": "codeweaver", "source": "./plugins/codeweaver", - "version": "0.1.0", "description": "Exquisite Context for AI Agents — Semantic code search MCP server with hybrid search, AST-level understanding, and intelligent chunking for 166+ languages.", "keywords": [ "mcp", diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index b5e8cfd..5ae9a3f 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 1 diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index d300267..94dcce5 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -26,7 +26,7 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a7bca5..843f612 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,14 +13,15 @@ jobs: release: runs-on: ubuntu-latest strategy: + max-parallel: 1 matrix: plugin: [ctx, strip-ansi, codeweaver] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 24 @@ -32,16 +33,11 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npx semantic-release - - name: Sync version to marketplace.json and regenerate - run: | - PKG_VERSION=$(jq -r '.version' plugins/${{ matrix.plugin }}/package.json) - jq --arg plugin "${{ matrix.plugin }}" --arg v "$PKG_VERSION" \ - '(.plugins[] | select(.name == $plugin) | .version) = $v' \ - .claude-plugin/marketplace.json > tmp.json && mv tmp.json .claude-plugin/marketplace.json - node scripts/generate.mjs + - name: Regenerate plugin.json from bumped package.json + run: node scripts/generate.mjs - name: Commit version sync uses: stefanzweifel/git-auto-commit-action@v7 with: - commit_message: "chore(${{ matrix.plugin }}): sync version to marketplace.json and regenerate files" - file_pattern: ".claude-plugin/marketplace.json plugins/${{ matrix.plugin }}/.claude-plugin/plugin.json plugins/${{ matrix.plugin }}/package.json" + commit_message: "chore(${{ matrix.plugin }}): sync plugin.json version [skip ci]" + file_pattern: "plugins/${{ matrix.plugin }}/.claude-plugin/plugin.json" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 9392c5a..4501d88 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -24,11 +24,8 @@ jobs: with: node-version: 24 - - name: Regenerate and verify no drift - run: node scripts/generate.mjs - - - name: Fail if any generated file was modified - run: git diff --exit-code + - name: Check generated files for drift + run: node scripts/generate.mjs --check commitlint: runs-on: ubuntu-latest diff --git a/CLAUDE.md b/CLAUDE.md index 8834979..dfccad2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,19 +20,15 @@ All plugin logic is **markdown prompts** interpreted by Claude Code at runtime. ### Version sync -Each plugin's version lives in two files that must match: -- `plugins//package.json` — semantic-release writes this on merge to main -- `plugins//.claude-plugin/plugin.json` — Claude Code reads this at install time - -The release workflow auto-syncs `plugin.json` after semantic-release bumps `package.json`. The validation script (`scripts/validate-marketplace.sh`) catches mismatches. +`package.json` is the sole source of truth for each plugin's version — semantic-release bumps it on merge to main. The generate script reads the version from `package.json` and propagates it to `plugin.json`. The release workflow runs generate after each release to keep them in sync. ### Key files -- `.claude-plugin/marketplace.json` — Discovery manifest listing all plugins with source paths +- `.claude-plugin/marketplace.json` — Discovery manifest listing all plugins with metadata (description, keywords, etc. — but NOT version) - `plugins//.claude-plugin/plugin.json` — Per-plugin manifest (name, version, description, mcpServers, userConfig) -- `plugins//package.json` — npm package with `semantic-release-monorepo` config +- `plugins//package.json` — npm package; version source of truth, release config generated centrally - `.commitlintrc.json` — Enforces conventional commits; `scope-enum` must list all plugin names -- `.github/workflows/release.yml` — Per-plugin semantic-release via matrix; `matrix.plugin` must list all plugin names +- `.github/workflows/release.yml` — Per-plugin semantic-release via matrix (max-parallel: 1); `matrix.plugin` must list all plugin names - `.github/workflows/validate.yml` — PR gate: runs marketplace validation + commitlint ### Plugin component types @@ -87,11 +83,9 @@ The Stop event re-fires if the hook's output is treated as new model feedback. L ## Adding a new plugin -Four files must be updated in sync: -1. Create `plugins//.claude-plugin/plugin.json` and `plugins//package.json` with matching versions -2. Add entry to `.claude-plugin/marketplace.json` `plugins` array -3. Add plugin name to `scope-enum` in `.commitlintrc.json` -4. Add plugin name to `matrix.plugin` in `.github/workflows/release.yml` +1. Add the plugin entry to `.claude-plugin/marketplace.json` `plugins` array (name, source, description, keywords, etc.) +2. Run `npm run generate` — this creates all derived files (`plugin.json`, `package.json`, `.commitlintrc.json`, `release.yml`) +3. Optionally run `npm run generate -- --new ` to also scaffold `commands/` directory and `README.md` ## Commit convention diff --git a/package-lock.json b/package-lock.json index 0c643e8..0f9e1f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^12.0.6", + "@semantic-release/npm": "^13.1.1", "@semantic-release/release-notes-generator": "^14.1.0", "semantic-release": "^25.0.3", "semantic-release-monorepo": "^8.0.2" @@ -31,6 +32,7 @@ "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^12.0.6", + "@semantic-release/npm": "^13.1.1", "@semantic-release/release-notes-generator": "^14.1.0", "semantic-release": "^25.0.3", "semantic-release-monorepo": "^8.0.2" @@ -7633,15 +7635,18 @@ }, "plugins/codeweaver": { "name": "@knitli/codeweaver", - "version": "0.1.0" + "version": "1.0.1", + "license": "MIT OR Apache-2.0" }, "plugins/ctx": { "name": "@knitli/ctx", - "version": "0.1.0" + "version": "1.2.0", + "license": "MIT" }, "plugins/strip-ansi": { "name": "@knitli/strip-ansi", - "version": "0.1.0" + "version": "1.0.0", + "license": "MIT" } } } diff --git a/package.json b/package.json index 6b4ac15..fc84be2 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^12.0.6", + "@semantic-release/npm": "^13.1.1", "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/release-notes-generator": "^14.1.0", "@commitlint/cli": "^20.5.0", @@ -26,6 +27,7 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^12.0.6", + "@semantic-release/npm": "^13.1.1", "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/release-notes-generator": "^14.1.0", "@commitlint/cli": "^20.5.0", diff --git a/plugins/codeweaver/.claude-plugin/plugin.json b/plugins/codeweaver/.claude-plugin/plugin.json index deb4c63..d9ee7ee 100644 --- a/plugins/codeweaver/.claude-plugin/plugin.json +++ b/plugins/codeweaver/.claude-plugin/plugin.json @@ -9,7 +9,7 @@ } }, "name": "codeweaver", - "version": "0.1.0", + "version": "1.0.1", "description": "Exquisite Context for AI Agents — Semantic code search MCP server with hybrid search, AST-level understanding, and intelligent chunking for 166+ languages.", "author": { "name": "Knitli Inc.", diff --git a/plugins/codeweaver/package.json b/plugins/codeweaver/package.json index f4b04b3..b1659f7 100644 --- a/plugins/codeweaver/package.json +++ b/plugins/codeweaver/package.json @@ -1,6 +1,6 @@ { "name": "@knitli/codeweaver", - "version": "0.1.0", + "version": "1.0.1", "private": true, "description": "Exquisite Context for AI Agents — Semantic code search MCP server with hybrid search, AST-level understanding, and intelligent chunking for 166+ languages.", "author": { @@ -33,6 +33,12 @@ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", + [ + "@semantic-release/npm", + { + "npmPublish": false + } + ], "@semantic-release/git", "@semantic-release/github" ] diff --git a/plugins/ctx/.claude-plugin/plugin.json b/plugins/ctx/.claude-plugin/plugin.json index ef731ef..f7348d2 100644 --- a/plugins/ctx/.claude-plugin/plugin.json +++ b/plugins/ctx/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ctx", - "version": "0.1.0", + "version": "1.2.0", "description": "Context hygiene for AI-assisted codebases. Finds stale, contradictory, and drifting AI context files (CLAUDE.md, AGENTS.md, Serena memories, Cursor rules, and 10+ more tool ecosystems).", "author": { "name": "Knitli Inc.", diff --git a/plugins/ctx/package.json b/plugins/ctx/package.json index 461fcee..a8dd1a4 100644 --- a/plugins/ctx/package.json +++ b/plugins/ctx/package.json @@ -1,6 +1,6 @@ { "name": "@knitli/ctx", - "version": "0.1.0", + "version": "1.2.0", "private": true, "description": "Context hygiene for AI-assisted codebases. Finds stale, contradictory, and drifting AI context files (CLAUDE.md, AGENTS.md, Serena memories, Cursor rules, and 10+ more tool ecosystems).", "author": { @@ -29,6 +29,12 @@ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", + [ + "@semantic-release/npm", + { + "npmPublish": false + } + ], "@semantic-release/git", "@semantic-release/github" ] diff --git a/plugins/strip-ansi/.claude-plugin/plugin.json b/plugins/strip-ansi/.claude-plugin/plugin.json index c1edbc0..4a92146 100644 --- a/plugins/strip-ansi/.claude-plugin/plugin.json +++ b/plugins/strip-ansi/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "strip-ansi", - "version": "0.1.0", + "version": "1.0.0", "description": "Prevents ANSI escape sequences from polluting agent context, commits, PRs, and file edits. Injects NO_COLOR into Bash tool calls and sanitizes any residual escapes from output.", "author": { "name": "Knitli Inc.", diff --git a/plugins/strip-ansi/package.json b/plugins/strip-ansi/package.json index 35802b9..8f57087 100644 --- a/plugins/strip-ansi/package.json +++ b/plugins/strip-ansi/package.json @@ -1,6 +1,6 @@ { "name": "@knitli/strip-ansi", - "version": "0.1.0", + "version": "1.0.0", "private": true, "description": "Prevents ANSI escape sequences from polluting agent context, commits, PRs, and file edits. Injects NO_COLOR into Bash tool calls and sanitizes any residual escapes from output.", "author": { @@ -26,6 +26,12 @@ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", + [ + "@semantic-release/npm", + { + "npmPublish": false + } + ], "@semantic-release/git", "@semantic-release/github" ] diff --git a/scripts/generate.mjs b/scripts/generate.mjs index b4106ea..d73f41e 100644 --- a/scripts/generate.mjs +++ b/scripts/generate.mjs @@ -114,11 +114,34 @@ const updatedReleaseYml = releaseYml.replace( writeOrCheck(releaseYmlPath, updatedReleaseYml); // --------------------------------------------------------------------------- -// 3. plugins//.claude-plugin/plugin.json +// Shared release config — generated for every plugin's package.json. +// Includes @semantic-release/npm (npmPublish: false) so that semantic-release +// actually bumps the version in package.json on each release. +// --------------------------------------------------------------------------- + +const RELEASE_CONFIG = { + extends: 'semantic-release-monorepo', + plugins: [ + '@semantic-release/commit-analyzer', + '@semantic-release/release-notes-generator', + '@semantic-release/changelog', + ['@semantic-release/npm', { npmPublish: false }], + '@semantic-release/git', + '@semantic-release/github', + ], +}; + +// --------------------------------------------------------------------------- +// 3. Per-plugin: .claude-plugin/plugin.json + package.json +// +// Version source of truth: package.json (managed by semantic-release). +// The generate script reads the version from the existing package.json +// and propagates it to plugin.json. New plugins default to "0.0.0". // --------------------------------------------------------------------------- for (const plugin of plugins) { const pluginDir = join(ROOT, plugin.source.replace(/^\.\//, '')); + const pkgJsonPath = join(pluginDir, 'package.json'); const dotClaudePluginDir = join(pluginDir, '.claude-plugin'); const pluginJsonPath = join(dotClaudePluginDir, 'plugin.json'); @@ -126,13 +149,18 @@ for (const plugin of plugins) { mkdirSync(dotClaudePluginDir, { recursive: true }); } + // Read existing package.json — its version is the source of truth. + const existingPkg = existsSync(pkgJsonPath) ? readJSON(pkgJsonPath) : {}; + const version = existingPkg.version ?? '0.0.0'; + + // --- plugin.json --- const extensions = pluginManifestExtensions[plugin.name] ?? {}; // Spread extensions first so canonical fields always win if there's a conflict. const pluginJson = { ...extensions, name: plugin.name, - version: plugin.version, + version, description: plugin.description, author: shared.author, homepage: plugin.homepage ?? shared.homepage, @@ -143,26 +171,11 @@ for (const plugin of plugins) { }; writeOrCheck(pluginJsonPath, JSON.stringify(pluginJson, null, 2) + '\n'); -} - -// --------------------------------------------------------------------------- -// 4. plugins//package.json -// --------------------------------------------------------------------------- - -for (const plugin of plugins) { - const pluginDir = join(ROOT, plugin.source.replace(/^\.\//, '')); - const pkgJsonPath = join(pluginDir, 'package.json'); - - if (!CHECK_MODE) { - mkdirSync(pluginDir, { recursive: true }); - } - - // Preserve the release config block from the existing file if present. - const existing = existsSync(pkgJsonPath) ? readJSON(pkgJsonPath) : {}; + // --- package.json --- const pkgJson = { - name: existing.name ?? `@${shared.npmScope}/${plugin.name}`, - version: plugin.version, + name: existingPkg.name ?? `@${shared.npmScope}/${plugin.name}`, + version, private: shared.private ?? true, description: plugin.description, author: shared.author, @@ -173,14 +186,14 @@ for (const plugin of plugins) { type: 'git', url: shared.repository, }, - ...(existing.release !== undefined && { release: existing.release }), + release: RELEASE_CONFIG, }; writeOrCheck(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n'); } // --------------------------------------------------------------------------- -// 5. --new scaffolding +// 4. --new scaffolding // --------------------------------------------------------------------------- if (NEW_PLUGIN) { diff --git a/scripts/validate-marketplace.sh b/scripts/validate-marketplace.sh index d7a1a2e..d2ab63e 100755 --- a/scripts/validate-marketplace.sh +++ b/scripts/validate-marketplace.sh @@ -51,16 +51,6 @@ for i in $(seq 0 $((PLUGIN_COUNT - 1))); do continue fi - # Version sync: package.json version == plugin.json version - if [ -f "$PLUGIN_DIR/package.json" ]; then - PKG_VERSION=$(jq -r '.version' "$PLUGIN_DIR/package.json") - PLUGIN_VERSION=$(jq -r '.version' "$PLUGIN_DIR/.claude-plugin/plugin.json") - if [ "$PKG_VERSION" != "$PLUGIN_VERSION" ]; then - echo " FAIL: version mismatch — package.json=$PKG_VERSION, plugin.json=$PLUGIN_VERSION" - ERRORS=$((ERRORS + 1)) - fi - fi - echo " OK" done