From d3298c364ef75a96f0be20c94747a850f0f57d0a Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Mon, 25 May 2026 11:19:03 -0500 Subject: [PATCH 1/5] Add Claude Code plugin marketplace Make the repo installable as a Claude Code plugin in addition to the existing `npx skills` flow: - .claude-plugin/marketplace.json: marketplace `flutter` listing one plugin, `flutter-skills`, sourced from the repo root. - .claude-plugin/plugin.json: manifest for the `flutter-skills` plugin. No version field, so the commit SHA drives updates. - README.md: document `/plugin marketplace add flutter/skills` and `/plugin install flutter-skills@flutter`. --- .claude-plugin/marketplace.json | 22 ++++++++++++++++++++++ .claude-plugin/plugin.json | 11 +++++++++++ README.md | 10 ++++++++++ 3 files changed, 43 insertions(+) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..3ad4d52 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,22 @@ +{ + "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": "./", + "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"], + "category": "development" + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..14c0fe3 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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"] +} diff --git a/README.md b/README.md index 15a104d..7807f7f 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,16 @@ folder that most agents use. npx skills add flutter/skills --skill '*' --agent universal ``` +### Install with Claude Code + +If you use [Claude Code](https://www.claude.com/product/claude-code), install the +skills as a plugin from the marketplace bundled in this repo: + +```bash +/plugin marketplace add flutter/skills +/plugin install flutter-skills@flutter +``` + ## Updating Skills To update, run the following command: From 276da0de8e4c5bc41845a55f956e22652bb232bd Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Mon, 25 May 2026 11:47:55 -0500 Subject: [PATCH 2/5] Trim duplicated plugin metadata from marketplace entry Keep the marketplace plugin entry to what the catalog needs (name, source, description, category) and let plugin.json be the single source of truth for author, homepage, repository, license, and keywords. --- .claude-plugin/marketplace.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3ad4d52..33ea98d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,13 +9,6 @@ "name": "flutter-skills", "source": "./", "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"], "category": "development" } ] From 53431910b43eb42e4762bb630ef93efdc3478cdf Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Mon, 25 May 2026 12:00:19 -0500 Subject: [PATCH 3/5] Scope plugin copy via a meta-plugin symlink Move the plugin into plugins/flutter-skills/ and point the marketplace source there. A skills symlink to ../../skills lets Claude Code dereference it on install and copy only the skills, instead of copying the entire repo (tool/, resources/, .agents/) as source: "./" did. The top-level skills/ directory is unchanged, so the `npx skills` flow is unaffected. --- .claude-plugin/marketplace.json | 2 +- .../flutter-skills/.claude-plugin}/plugin.json | 0 plugins/flutter-skills/skills | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) rename {.claude-plugin => plugins/flutter-skills/.claude-plugin}/plugin.json (100%) create mode 120000 plugins/flutter-skills/skills diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 33ea98d..2ad983f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,7 +7,7 @@ "plugins": [ { "name": "flutter-skills", - "source": "./", + "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" } diff --git a/.claude-plugin/plugin.json b/plugins/flutter-skills/.claude-plugin/plugin.json similarity index 100% rename from .claude-plugin/plugin.json rename to plugins/flutter-skills/.claude-plugin/plugin.json diff --git a/plugins/flutter-skills/skills b/plugins/flutter-skills/skills new file mode 120000 index 0000000..5dcab58 --- /dev/null +++ b/plugins/flutter-skills/skills @@ -0,0 +1 @@ +../../skills \ No newline at end of file From ebcb8bb4fb0c232f604102eaab8af2e4e6dabe72 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Mon, 25 May 2026 12:18:34 -0500 Subject: [PATCH 4/5] Add CI test verifying the plugin marketplace wiring New standalone tool/marketplace_check package plus a marketplace workflow (ubuntu/macOS/windows matrix). The test asserts: - marketplace.json declares the flutter-skills plugin with source ./plugins/flutter-skills (guards against regressing to "./", which would copy the whole repo into users' caches), - the source resolves to a dir containing plugin.json, - the skills symlink resolves to the top-level skills/ directory, - every top-level skill is reachable through that symlink. The windows matrix leg also exercises whether the symlink survives checkout on Windows. --- .github/workflows/marketplace_workflow.yaml | 54 +++++++ tool/marketplace_check/analysis_options.yaml | 7 + tool/marketplace_check/pubspec.yaml | 13 ++ .../test/marketplace_plugin_test.dart | 144 ++++++++++++++++++ 4 files changed, 218 insertions(+) create mode 100644 .github/workflows/marketplace_workflow.yaml create mode 100644 tool/marketplace_check/analysis_options.yaml create mode 100644 tool/marketplace_check/pubspec.yaml create mode 100644 tool/marketplace_check/test/marketplace_plugin_test.dart diff --git a/.github/workflows/marketplace_workflow.yaml b/.github/workflows/marketplace_workflow.yaml new file mode 100644 index 0000000..b4e33dc --- /dev/null +++ b/.github/workflows/marketplace_workflow.yaml @@ -0,0 +1,54 @@ +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 . diff --git a/tool/marketplace_check/analysis_options.yaml b/tool/marketplace_check/analysis_options.yaml new file mode 100644 index 0000000..d04adaf --- /dev/null +++ b/tool/marketplace_check/analysis_options.yaml @@ -0,0 +1,7 @@ +include: package:lints/recommended.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true diff --git a/tool/marketplace_check/pubspec.yaml b/tool/marketplace_check/pubspec.yaml new file mode 100644 index 0000000..edd4932 --- /dev/null +++ b/tool/marketplace_check/pubspec.yaml @@ -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 diff --git a/tool/marketplace_check/test/marketplace_plugin_test.dart b/tool/marketplace_check/test/marketplace_plugin_test.dart new file mode 100644 index 0000000..2cc1301 --- /dev/null +++ b/tool/marketplace_check/test/marketplace_plugin_test.dart @@ -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 _readJson(File file) { + expect(file.existsSync(), isTrue, reason: '${file.path} should exist'); + return jsonDecode(file.readAsStringSync()) as Map; +} + +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>()); + + final plugins = marketplace['plugins'] as List; + expect(plugins, hasLength(1)); + + final plugin = plugins.single as Map; + 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).single + as Map; + 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() + .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', + ); + } + }); + }); +} From 2ca1c0200e9cf4d6df50461fe9ece96b07826812 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Mon, 25 May 2026 12:31:08 -0500 Subject: [PATCH 5/5] Add claude plugin validate job to marketplace workflow Run the canonical Claude Code validator (schema, duplicate names, path traversal, version mismatches) in CI on top of the Dart structural test. It runs offline, so no API key is needed; installed via npm on an ubuntu-only job. Verified headless (fresh HOME, no auth) exits 0. --- .github/workflows/marketplace_workflow.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/marketplace_workflow.yaml b/.github/workflows/marketplace_workflow.yaml index b4e33dc..0b10de2 100644 --- a/.github/workflows/marketplace_workflow.yaml +++ b/.github/workflows/marketplace_workflow.yaml @@ -52,3 +52,24 @@ jobs: - 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