-
Notifications
You must be signed in to change notification settings - Fork 125
Add Claude Code plugin marketplace #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
reidbaker
wants to merge
5
commits into
main
Choose a base branch
from
add-claude-plugin-marketplace
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
d3298c3
Add Claude Code plugin marketplace
reidbaker 276da0d
Trim duplicated plugin metadata from marketplace entry
reidbaker 5343191
Scope plugin copy via a meta-plugin symlink
reidbaker ebcb8bb
Add CI test verifying the plugin marketplace wiring
reidbaker 2ca1c02
Add claude plugin validate job to marketplace workflow
reidbaker File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "name": "flutter", | ||
| "owner": { | ||
| "name": "The Flutter Authors" | ||
| }, | ||
| "description": "Agent skills for Flutter app development, authored by the Flutter and Dart teams.", | ||
| "plugins": [ | ||
| { | ||
| "name": "flutter-skills", | ||
| "source": "./plugins/flutter-skills", | ||
| "description": "Task-based skills that teach coding agents happy-path Flutter workflows: widget and integration tests, widget previews, responsive layouts, declarative routing, localization, HTTP networking, JSON serialization, and layered architecture best practices.", | ||
| "category": "development" | ||
| } | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| name: marketplace | ||
| permissions: read-all | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - '.github/workflows/marketplace_workflow.yaml' | ||
| - 'tool/marketplace_check/**' | ||
| - '.claude-plugin/**' | ||
| - 'plugins/**' | ||
| push: | ||
| branches: [ main ] | ||
| paths: | ||
| - '.github/workflows/marketplace_workflow.yaml' | ||
| - 'tool/marketplace_check/**' | ||
| - '.claude-plugin/**' | ||
| - 'plugins/**' | ||
| schedule: | ||
| - cron: '0 0 * * 0' # weekly | ||
|
|
||
| defaults: | ||
| run: | ||
| working-directory: tool/marketplace_check | ||
|
|
||
| jobs: | ||
| analyze_and_test: | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [ubuntu-latest, macos-latest, windows-latest] | ||
| runs-on: ${{ matrix.os }} | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: dart-lang/setup-dart@v1 | ||
| with: | ||
| sdk: stable | ||
|
|
||
| - run: dart pub get | ||
|
|
||
| - run: dart analyze --fatal-infos | ||
|
|
||
| - run: dart test | ||
|
|
||
| formatting: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: dart-lang/setup-dart@v1 | ||
| with: | ||
| sdk: stable | ||
|
|
||
| - run: dart pub get | ||
|
|
||
| - run: dart format --output=none --set-exit-if-changed . | ||
|
|
||
| validate: | ||
| runs-on: ubuntu-latest | ||
| # Override the workflow-level working directory so the validator runs | ||
| # against the repository root, not tool/marketplace_check. | ||
| defaults: | ||
| run: | ||
| working-directory: . | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
|
|
||
| - run: npm install -g @anthropic-ai/claude-code | ||
|
|
||
| # The canonical schema validator (unknown fields, duplicate names, path | ||
| # traversal, version mismatches). Runs offline; no API key needed. | ||
| - run: claude plugin validate . | ||
|
|
||
| - run: claude plugin validate ./plugins/flutter-skills |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "name": "flutter-skills", | ||
| "description": "Task-based skills that teach coding agents happy-path Flutter workflows: widget and integration tests, widget previews, responsive layouts, declarative routing, localization, HTTP networking, JSON serialization, and layered architecture best practices.", | ||
| "author": { | ||
| "name": "The Flutter Authors" | ||
| }, | ||
| "homepage": "https://github.com/flutter/skills", | ||
| "repository": "https://github.com/flutter/skills", | ||
| "license": "BSD-3-Clause", | ||
| "keywords": ["flutter", "dart", "mobile", "testing", "ui", "skills"] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../../skills |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| include: package:lints/recommended.yaml | ||
|
|
||
| analyzer: | ||
| language: | ||
| strict-casts: true | ||
| strict-inference: true | ||
| strict-raw-types: true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| name: marketplace_check | ||
| description: >- | ||
| Tests that verify the Claude plugin marketplace wiring (marketplace.json, | ||
| the flutter-skills plugin manifest, and the skills symlink) stays correct. | ||
| publish_to: 'none' | ||
|
|
||
| environment: | ||
| sdk: ^3.10.8 | ||
|
|
||
| dev_dependencies: | ||
| lints: ^6.0.0 | ||
| path: ^1.9.1 | ||
| test: ^1.25.6 |
144 changes: 144 additions & 0 deletions
144
tool/marketplace_check/test/marketplace_plugin_test.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file | ||
| // for details. All rights reserved. Use of this source code is governed by a | ||
| // BSD-style license that can be found in the LICENSE file. | ||
|
|
||
| import 'dart:convert'; | ||
| import 'dart:io'; | ||
|
|
||
| import 'package:path/path.dart' as p; | ||
| import 'package:test/test.dart'; | ||
|
|
||
| /// Walks up from [start] until it finds the directory that contains | ||
| /// `.claude-plugin/marketplace.json`, which marks the repository root. | ||
| /// | ||
| /// This lets the test run both from the repository root and from the | ||
| /// `tool/marketplace_check` working directory used by CI. | ||
| Directory _findRepoRoot(Directory start) { | ||
| var dir = start; | ||
| while (true) { | ||
| final marker = File(p.join(dir.path, '.claude-plugin', 'marketplace.json')); | ||
| if (marker.existsSync()) { | ||
| return dir; | ||
| } | ||
| final parent = dir.parent; | ||
| if (p.equals(parent.path, dir.path)) { | ||
| throw StateError( | ||
| 'Could not find .claude-plugin/marketplace.json walking up from ' | ||
| '${start.path}', | ||
| ); | ||
| } | ||
| dir = parent; | ||
| } | ||
| } | ||
|
|
||
| Map<String, dynamic> _readJson(File file) { | ||
| expect(file.existsSync(), isTrue, reason: '${file.path} should exist'); | ||
| return jsonDecode(file.readAsStringSync()) as Map<String, dynamic>; | ||
| } | ||
|
|
||
| void main() { | ||
| final repoRoot = _findRepoRoot(Directory.current); | ||
| final marketplaceFile = File( | ||
| p.join(repoRoot.path, '.claude-plugin', 'marketplace.json'), | ||
| ); | ||
| final pluginLink = p.join( | ||
| repoRoot.path, | ||
| 'plugins', | ||
| 'flutter-skills', | ||
| 'skills', | ||
| ); | ||
|
|
||
| group('Claude plugin marketplace', () { | ||
| test('marketplace.json declares the flutter-skills plugin', () { | ||
| final marketplace = _readJson(marketplaceFile); | ||
|
|
||
| expect(marketplace['name'], 'flutter'); | ||
| expect(marketplace['owner'], isA<Map<String, dynamic>>()); | ||
|
|
||
| final plugins = marketplace['plugins'] as List<dynamic>; | ||
| expect(plugins, hasLength(1)); | ||
|
|
||
| final plugin = plugins.single as Map<String, dynamic>; | ||
| expect(plugin['name'], 'flutter-skills'); | ||
|
|
||
| // Guards against regressing to `source: "./"`, which would copy the | ||
| // entire repo (tool/, resources/, .agents/) into every user's cache. | ||
| expect(plugin['source'], './plugins/flutter-skills'); | ||
| }); | ||
|
|
||
| test('plugin source resolves to a directory with a plugin manifest', () { | ||
| final marketplace = _readJson(marketplaceFile); | ||
| final plugin = | ||
| (marketplace['plugins'] as List<dynamic>).single | ||
| as Map<String, dynamic>; | ||
| final source = plugin['source'] as String; | ||
|
|
||
| expect(source, startsWith('./')); | ||
| expect(source, isNot(contains('..'))); | ||
|
|
||
| final pluginDir = Directory(p.join(repoRoot.path, source)); | ||
| expect( | ||
| pluginDir.existsSync(), | ||
| isTrue, | ||
| reason: 'plugin source "$source" should exist', | ||
| ); | ||
|
|
||
| final manifest = _readJson( | ||
| File(p.join(pluginDir.path, '.claude-plugin', 'plugin.json')), | ||
| ); | ||
| expect(manifest['name'], plugin['name']); | ||
| }); | ||
|
|
||
| test('skills symlink resolves to the top-level skills/ directory', () { | ||
| expect( | ||
| FileSystemEntity.isLinkSync(pluginLink), | ||
| isTrue, | ||
| reason: | ||
| '"$pluginLink" must be a symlink. On Windows, ensure git checks ' | ||
| 'out symlinks (core.symlinks=true / Developer Mode).', | ||
| ); | ||
|
|
||
| final resolvedLink = Link(pluginLink).resolveSymbolicLinksSync(); | ||
| final canonicalSkills = Directory( | ||
| p.join(repoRoot.path, 'skills'), | ||
| ).resolveSymbolicLinksSync(); | ||
|
|
||
| expect( | ||
| p.equals(resolvedLink, canonicalSkills), | ||
| isTrue, | ||
| reason: | ||
| 'symlink should resolve to "$canonicalSkills", ' | ||
| 'got "$resolvedLink"', | ||
| ); | ||
| }); | ||
|
|
||
| test('every top-level skill is reachable through the plugin symlink', () { | ||
| final canonicalSkills = Directory(p.join(repoRoot.path, 'skills')); | ||
| final skillNames = canonicalSkills | ||
| .listSync() | ||
| .whereType<Directory>() | ||
| .map((entity) => p.basename(entity.path)) | ||
| .where( | ||
| (name) => File( | ||
| p.join(canonicalSkills.path, name, 'SKILL.md'), | ||
| ).existsSync(), | ||
| ) | ||
| .toList(); | ||
|
|
||
| expect( | ||
| skillNames, | ||
| isNotEmpty, | ||
| reason: 'expected at least one skill in skills/', | ||
| ); | ||
|
|
||
| for (final name in skillNames) { | ||
| final viaLink = File(p.join(pluginLink, name, 'SKILL.md')); | ||
| expect( | ||
| viaLink.existsSync(), | ||
| isTrue, | ||
| reason: 'skill "$name" should be reachable via the plugin symlink', | ||
| ); | ||
| } | ||
| }); | ||
| }); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to test before merging use
/plugin marketplace add flutter/skills@add-claude-plugin-marketplaceNo code change required.