From 9dd002dd254adeb8b752835ba29438f65ebecf11 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Fri, 25 Sep 2026 15:48:38 -0700 Subject: [PATCH 01/12] ADFA-6252 | Convert Flutter Templates from a plugin to a .cgt bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The templates shipped as a headless plugin only because no .cgt path existed when they were added: on activate() it built a .cgt on the device from bundled Pebble skeletons and registered it. The Extensions manager reads a .cgt directly now, so the plugin wrapper is dead weight. plugins/Flutter-Templates/ becomes templates/Flutter-Templates/ — the first addon under templates/. The five .peb skeletons move unchanged (git mv, so their history follows); the Gradle project, the manifest, the Kotlin class and the res/ tree are deleted. A GitHub Action zips the directory into flutter-templates.cgt and publishes it to R2 like every other addon. The bundle is never committed. Generalizing the tooling from plugins to addons: - discover.py gains a second rule. A directory under templates/ holding templates.json is an addon; the plugin predicate can never match one, because a template has no Gradle build. - model.py reads template.id / template.version / template.minAppVersion from addon.json where a plugin reads its manifest. addon.schema.json gains the template block. - catalog.py renames pluginId to addonId and bumps schemaVersion to 2. The field named one of four addon types; site/app.js never read it and no CodeOnTheGo code references catalog.json, so nothing observes the change today, which is what makes now the cheap moment. - Templates ship no source tarball: the .cgt is plain text throughout, so the download already is the source, and sourceUrl still names the directory. tarball.build() returns None, sourceTarball leaves the schema's required list, and site/app.js drops the Source link. - build-plugins.yml becomes build-addons.yml. It already called its subjects addons in its own comments and input description; once it also produces .cgt files the old name was simply wrong. Two bugs found while verifying rather than by review: - site/app.js read addon.sourceTarball.url unguarded, inside the loop that builds every card. One entry without the field would throw a TypeError and leave the whole gallery on "Loading…", not merely drop one link. - The dirty check in build-cgt.sh passed the addon path as a pathspec after git -C had already resolved against it, so it looked for /, matched nothing, and no build was ever reported dirty. Provenance: every published .cgt carries cgt-build.properties at its archive root, recording the commit and that commit's committer date. The gallery serves one URL per addon and overwrites it, so without this "version 1.0.0" names every build that number ever had. A root entry is inert to all three readers — ZipTemplateReader reads only templates.json and the paths it lists, ZipRecipeExecutor skips everything outside /, and CgtTemplateReader matches only */template/template.json. scripts/build-cgt.sh takes its file list from templates.json rather than excluding the four known metadata files. That fails closed (a metadata file added later is never listed, so it cannot leak into a user-facing download) and fails loudly (find exits non-zero when templates.json names a path that does not exist — an entry the IDE otherwise skips in silence). Entries are stored with mtimes pinned to 1980 and the list C-sorted, so one commit gives one archive; verified byte-identical across two builds. check-toolchain.yml gains the dev-assets lint steps (junk files, XML, strict JSON, JSON5, Pebble braces) plus a build of every bundle. It is the only workflow here that runs on a pull request, so it is the only place a broken bundle can be caught before merge. Docs: the plugin-specific guidance moves out of CLAUDE.md into plugins/README.md, CLAUDE.md gains a table pointing at each of the four areas, and the headless template-installer section is gone. docs/plugin-naming-standards.md becomes docs/addon-naming-standards.md. templates/README.md is authoring guidance for community contributors, with Flutter-Templates as the reference example, and templates/cgt-templates.md documents the format — which had no written specification; ZipTemplateConstants.kt was the only definition. Users with the old plugin installed must disable it before adding the bundle, or each template appears twice from two unlinked sources. Noted in the addon README and on the gallery page. --- .github/workflows/build-addons.yml | 129 ++++ .github/workflows/build-plugins.yml | 87 --- .github/workflows/check-toolchain.yml | 114 +++- .github/workflows/publish-addons.yml | 35 +- .gitignore | 7 + CLAUDE.md | 189 +----- README.md | 24 +- docs/addon-naming-standards.md | 107 +++ docs/plugin-naming-standards.md | 75 --- plugins/Flutter-Templates/.gitignore | 35 - plugins/Flutter-Templates/README.md | 54 -- plugins/Flutter-Templates/build.gradle.kts | 72 -- plugins/Flutter-Templates/gradle.properties | 5 - plugins/Flutter-Templates/settings.gradle.kts | 30 - .../src/main/AndroidManifest.xml | 63 -- .../ali/fluttertemplate/FlutterTemplate.kt | 137 ---- .../src/main/res/drawable/ic_plugin.xml | 10 - .../src/main/res/values-night/colors.xml | 20 - .../src/main/res/values/colors.xml | 20 - .../src/main/res/values/strings.xml | 5 - .../src/main/res/values/styles.xml | 16 - plugins/README.md | 345 +++++++++- scripts/build-cgt.sh | 139 ++++ scripts/verify-provenance.sh | 66 +- site/app.js | 16 +- site/catalog.schema.json | 11 +- templates/Flutter-Templates/.gitignore | 4 + .../FlutterBasic/README.md.peb | 0 .../FlutterBasic/analysis_options.yaml | 0 .../FlutterBasic/lib/main.dart.peb | 0 .../FlutterBasic/pubspec.yaml.peb | 0 .../FlutterBasic/template/template.json | 13 + .../FlutterBasic/template/thumb.png | Bin .../FlutterBloc/README.md.peb | 0 .../FlutterBloc/analysis_options.yaml | 0 .../lib/blocs/counter_bloc.dart.peb | 0 .../FlutterBloc/lib/main.dart.peb | 0 .../FlutterBloc/pubspec.yaml.peb | 0 .../FlutterBloc/template/template.json | 13 + .../FlutterBloc/template/thumb.png | Bin .../FlutterGetx/README.md.peb | 0 .../FlutterGetx/analysis_options.yaml | 0 .../controllers/counter_controller.dart.peb | 0 .../FlutterGetx/lib/main.dart.peb | 0 .../FlutterGetx/pubspec.yaml.peb | 0 .../FlutterGetx/template/template.json | 13 + .../FlutterGetx/template/thumb.png | Bin .../FlutterProvider/README.md.peb | 0 .../FlutterProvider/analysis_options.yaml | 0 .../FlutterProvider/lib/main.dart.peb | 0 .../lib/providers/counter_provider.dart.peb | 0 .../FlutterProvider/pubspec.yaml.peb | 0 .../FlutterProvider/template/template.json | 13 + .../FlutterProvider/template/thumb.png | Bin .../FlutterRiverpod/README.md.peb | 0 .../FlutterRiverpod/analysis_options.yaml | 0 .../FlutterRiverpod/lib/main.dart.peb | 0 .../lib/providers/counter_provider.dart.peb | 0 .../FlutterRiverpod/pubspec.yaml.peb | 0 .../FlutterRiverpod/template/template.json | 13 + .../FlutterRiverpod/template/thumb.png | Bin templates/Flutter-Templates/README.md | 68 ++ .../Flutter-Templates/addon.json | 7 +- .../Flutter-Templates/flutter-templates.html | 141 ++-- .../Flutter-Templates}/icon_day.png | Bin .../Flutter-Templates}/icon_night.png | Bin templates/Flutter-Templates/templates.json | 9 + templates/README.md | 369 +++++++++- templates/cgt-templates.md | 434 ++++++++++++ templates/flutter-cgt-migration-plan.md | 636 ++++++++++++++++++ tools/addons/src/addons/addon.schema.json | 26 + tools/addons/src/addons/catalog.py | 34 +- tools/addons/src/addons/check.py | 115 +++- tools/addons/src/addons/cli.py | 26 +- tools/addons/src/addons/discover.py | 17 + tools/addons/src/addons/model.py | 41 +- tools/addons/src/addons/publish.py | 3 +- tools/addons/src/addons/tarball.py | 15 +- tools/addons/tests/test_catalog.py | 77 ++- tools/addons/tests/test_check_templates.py | 136 ++++ tools/addons/tests/test_discover.py | 53 ++ tools/addons/tests/test_tarball.py | 19 + 82 files changed, 3177 insertions(+), 929 deletions(-) create mode 100644 .github/workflows/build-addons.yml delete mode 100644 .github/workflows/build-plugins.yml create mode 100644 docs/addon-naming-standards.md delete mode 100644 docs/plugin-naming-standards.md delete mode 100644 plugins/Flutter-Templates/.gitignore delete mode 100644 plugins/Flutter-Templates/README.md delete mode 100644 plugins/Flutter-Templates/build.gradle.kts delete mode 100644 plugins/Flutter-Templates/gradle.properties delete mode 100644 plugins/Flutter-Templates/settings.gradle.kts delete mode 100644 plugins/Flutter-Templates/src/main/AndroidManifest.xml delete mode 100644 plugins/Flutter-Templates/src/main/kotlin/com/ali/fluttertemplate/FlutterTemplate.kt delete mode 100644 plugins/Flutter-Templates/src/main/res/drawable/ic_plugin.xml delete mode 100644 plugins/Flutter-Templates/src/main/res/values-night/colors.xml delete mode 100644 plugins/Flutter-Templates/src/main/res/values/colors.xml delete mode 100644 plugins/Flutter-Templates/src/main/res/values/strings.xml delete mode 100644 plugins/Flutter-Templates/src/main/res/values/styles.xml create mode 100755 scripts/build-cgt.sh create mode 100644 templates/Flutter-Templates/.gitignore rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterBasic/README.md.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterBasic/analysis_options.yaml (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterBasic/lib/main.dart.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterBasic/pubspec.yaml.peb (100%) create mode 100644 templates/Flutter-Templates/FlutterBasic/template/template.json rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterBasic/template/thumb.png (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterBloc/README.md.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterBloc/analysis_options.yaml (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterBloc/lib/blocs/counter_bloc.dart.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterBloc/lib/main.dart.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterBloc/pubspec.yaml.peb (100%) create mode 100644 templates/Flutter-Templates/FlutterBloc/template/template.json rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterBloc/template/thumb.png (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterGetx/README.md.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterGetx/analysis_options.yaml (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterGetx/lib/controllers/counter_controller.dart.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterGetx/lib/main.dart.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterGetx/pubspec.yaml.peb (100%) create mode 100644 templates/Flutter-Templates/FlutterGetx/template/template.json rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterGetx/template/thumb.png (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterProvider/README.md.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterProvider/analysis_options.yaml (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterProvider/lib/main.dart.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterProvider/lib/providers/counter_provider.dart.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterProvider/pubspec.yaml.peb (100%) create mode 100644 templates/Flutter-Templates/FlutterProvider/template/template.json rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterProvider/template/thumb.png (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterRiverpod/README.md.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterRiverpod/analysis_options.yaml (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterRiverpod/lib/main.dart.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterRiverpod/lib/providers/counter_provider.dart.peb (100%) rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterRiverpod/pubspec.yaml.peb (100%) create mode 100644 templates/Flutter-Templates/FlutterRiverpod/template/template.json rename {plugins/Flutter-Templates/src/main/assets/templates => templates/Flutter-Templates}/FlutterRiverpod/template/thumb.png (100%) create mode 100644 templates/Flutter-Templates/README.md rename {plugins => templates}/Flutter-Templates/addon.json (53%) rename {plugins => templates}/Flutter-Templates/flutter-templates.html (64%) rename {plugins/Flutter-Templates/src/main/assets => templates/Flutter-Templates}/icon_day.png (100%) rename {plugins/Flutter-Templates/src/main/assets => templates/Flutter-Templates}/icon_night.png (100%) create mode 100644 templates/Flutter-Templates/templates.json create mode 100644 templates/cgt-templates.md create mode 100644 templates/flutter-cgt-migration-plan.md create mode 100644 tools/addons/tests/test_check_templates.py diff --git a/.github/workflows/build-addons.yml b/.github/workflows/build-addons.yml new file mode 100644 index 00000000..af2f9cf8 --- /dev/null +++ b/.github/workflows/build-addons.yml @@ -0,0 +1,129 @@ +name: Build addon artifacts + +# Builds every addon (or one) to prove the tree compiles and packages. +# It publishes nothing and uploads no artifacts: to get a downloadable URL, +# run "Publish addons" with staging: true. +# +# Two kinds of addon, two build paths (ADFA-6252). A plugin is a Gradle project +# that compiles to a .cgp; a template is a plain-text tree that zips to a .cgt +# and has no Gradle build at all. scripts/update-libs.sh only knows the first, +# so templates are built here by scripts/build-cgt.sh instead. + +on: + workflow_dispatch: + inputs: + codeonthego_ref: + description: CodeOnTheGo branch or tag to build the libs against + required: false + default: stage + addon: + description: One addon directory name, e.g. Random-XKCD (blank builds all) + required: false + default: '' + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout plugin-examples + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v3 + with: + cache-disabled: true + add-job-summary: 'never' + + - name: Build libs and plugins + env: + CODEONTHEGO_REF: ${{ github.event.inputs.codeonthego_ref }} + ONLY_ADDON: ${{ github.event.inputs.addon }} + run: | + if [ -n "$ONLY_ADDON" ]; then + ./scripts/update-libs.sh --ref "$CODEONTHEGO_REF" --plugin "$ONLY_ADDON" + else + ./scripts/update-libs.sh --ref "$CODEONTHEGO_REF" + fi + + - name: Build the template bundles + env: + ONLY_ADDON: ${{ inputs.addon }} + run: | + set -euo pipefail + # Templates are invisible to update-libs.sh: no build.gradle.kts, nothing + # to compile. Zip each one and assert its provenance record, so a broken + # bundle fails here rather than at publish time. + all="$(uv run --directory tools/addons addons --root "$GITHUB_WORKSPACE" discover --include-skipped)" + templates="$(printf '%s\n' "$all" | grep '^templates/' || true)" + if [ -n "$ONLY_ADDON" ]; then + templates="$(printf '%s\n' "$templates" | awk -v want="$ONLY_ADDON" ' + { n = $0; sub(/.*\//, "", n) + if (n == want || $0 == want || tolower(n) == tolower(want)) print }')" + fi + if [ -z "$templates" ]; then + echo "No template addon to build." + exit 0 + fi + for dir in $templates; do + echo "==> $dir" + "$GITHUB_WORKSPACE/scripts/build-cgt.sh" "$dir" "$GITHUB_WORKSPACE/dist" + "$GITHUB_WORKSPACE/scripts/verify-provenance.sh" "$dir" + done + + - name: Check every addon produced its artifact + env: + ONLY_ADDON: ${{ inputs.addon }} + run: | + set -euo pipefail + # --include-skipped, the same list scripts/update-libs.sh builds and + # resolves --plugin against. An addon merely held out of the gallery + # is still built here and must still package, and a single-addon run + # naming one has to find it in this list. Only "!" entries in + # skip.txt are absent from both. + all="$(uv run --directory tools/addons addons --root "$GITHUB_WORKSPACE" discover --include-skipped)" + # A single-addon run only builds that module, so the others have no + # artifact by design and must not be reported as missing. + if [ -n "$ONLY_ADDON" ]; then + all="$(printf '%s\n' "$all" | grep -Fx -e "$ONLY_ADDON" -e "plugins/$ONLY_ADDON" -e "templates/$ONLY_ADDON" || true)" + # Never let a no-match end the step on grep's bare exit 1: that + # reports a build that in fact succeeded as a red run, with no + # line saying why. + if [ -z "$all" ]; then + echo "ERROR: '${ONLY_ADDON}' is not a known addon, so nothing was checked." >&2 + exit 1 + fi + fi + missing=0 + for dir in $all; do + case "$dir" in + templates/*) + # build-cgt.sh names the bundle after the directory, lowercased + slug="$(basename "$dir" | tr '[:upper:]' '[:lower:]')" + if [ ! -f "dist/${slug}.cgt" ]; then + echo "ERROR: no ${slug}.cgt under dist/" + missing=$((missing + 1)) + fi + ;; + *) + if ! find "$dir/build/plugin" -name '*.cgp' ! -name '*-debug.cgp' 2>/dev/null | grep -q .; then + echo "ERROR: no release .cgp under ${dir}/build/plugin/" + missing=$((missing + 1)) + fi + ;; + esac + done + [ "$missing" -eq 0 ] || exit 1 + echo "Every discovered addon produced its artifact." diff --git a/.github/workflows/build-plugins.yml b/.github/workflows/build-plugins.yml deleted file mode 100644 index b6b3e79b..00000000 --- a/.github/workflows/build-plugins.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Build plugin artifacts - -# Builds every addon (or one) to prove the tree compiles. -# It publishes nothing and uploads no artifacts: to get a downloadable URL, -# run "Publish addons" with staging: true. - -on: - workflow_dispatch: - inputs: - codeonthego_ref: - description: CodeOnTheGo branch or tag to build the libs against - required: false - default: stage - plugin: - description: One addon directory name, e.g. Random-XKCD (blank builds all) - required: false - default: '' - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - name: Checkout plugin-examples - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '17' - - - name: Install uv - uses: astral-sh/setup-uv@v5 - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v3 - with: - cache-disabled: true - add-job-summary: 'never' - - - name: Build libs and plugins - env: - CODEONTHEGO_REF: ${{ github.event.inputs.codeonthego_ref }} - ONLY_PLUGIN: ${{ github.event.inputs.plugin }} - run: | - if [ -n "$ONLY_PLUGIN" ]; then - ./scripts/update-libs.sh --ref "$CODEONTHEGO_REF" --plugin "$ONLY_PLUGIN" - else - ./scripts/update-libs.sh --ref "$CODEONTHEGO_REF" - fi - - - name: Check every addon produced a release .cgp - env: - ONLY_PLUGIN: ${{ inputs.plugin }} - run: | - set -euo pipefail - # --include-skipped, the same list scripts/update-libs.sh builds and - # resolves --plugin against. An addon merely held out of the gallery - # is still built here and must still package, and a single-addon run - # naming one has to find it in this list. Only "!" entries in - # skip.txt are absent from both. - all="$(uv run --directory tools/addons addons --root "$GITHUB_WORKSPACE" discover --include-skipped)" - # A single-plugin run only builds that module, so the others have no - # .cgp by design and must not be reported as missing. - if [ -n "$ONLY_PLUGIN" ]; then - all="$(printf '%s\n' "$all" | grep -Fx -e "$ONLY_PLUGIN" -e "plugins/$ONLY_PLUGIN" || true)" - # Never let a no-match end the step on grep's bare exit 1: that - # reports a build that in fact succeeded as a red run, with no - # line saying why. - if [ -z "$all" ]; then - echo "ERROR: '${ONLY_PLUGIN}' is not a known addon, so nothing was checked." >&2 - exit 1 - fi - fi - missing=0 - for dir in $all; do - if ! find "$dir/build/plugin" -name '*.cgp' ! -name '*-debug.cgp' 2>/dev/null | grep -q .; then - echo "ERROR: no release .cgp under ${dir}/build/plugin/" - missing=$((missing + 1)) - fi - done - [ "$missing" -eq 0 ] || exit 1 - echo "Every discovered addon produced a release .cgp." diff --git a/.github/workflows/check-toolchain.yml b/.github/workflows/check-toolchain.yml index 9dfd001e..731cf3b9 100644 --- a/.github/workflows/check-toolchain.yml +++ b/.github/workflows/check-toolchain.yml @@ -5,12 +5,17 @@ name: Check toolchain # deliberately not restated here, so there is only one copy of the numbers. # # This is the only workflow here that runs automatically on pull requests. -# "Build plugin artifacts" and "Update libs from CodeOnTheGo" are both +# "Build addon artifacts" and "Update libs from CodeOnTheGo" are both # workflow_dispatch-only, which is why toolchain drift previously reached # main with no CI signal at all. # # It is pure text inspection — no JDK, no Gradle, no network — so it costs a # few seconds and is safe to make a required check. +# +# It also lints the template bundle sources (ADFA-6252). Those checks are +# borrowed from dev-assets' lint-templates.yml, which guards core.cgt the same +# way. They are cheap, and since this is the only workflow that runs on a pull +# request, it is the only place a broken bundle can be caught before merge. on: pull_request: @@ -53,3 +58,110 @@ jobs: - name: Check addon names and metadata run: uv run --directory tools/addons addons --root "$GITHUB_WORKSPACE" check + + # From here down: the template bundle sources. `addons check` already + # asserts the structure (templates.json parses, every path it names + # exists and carries a template.json, the addon.json block is present). + # These add the syntax checks it does not do, on the file contents. + - name: Install the template lint tools + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq libxml2-utils jq zip + pip install --quiet json5 + + - name: Reject junk files committed under templates/ + run: | + set -e + BAD=$(git ls-files templates/ | grep -E '(^|/)(\.gradle|build|local\.properties)(/|$)|\.(swp|swo|bak)$|(^|/)\.DS_Store$' || true) + if [ -n "$BAD" ]; then + echo "::error::Junk files committed under templates/:" + echo "$BAD" + exit 1 + fi + + - name: Reject a committed bundle or provenance record + run: | + set -e + # Both are generated at publish time. A committed one goes stale + # silently and would ship instead of a fresh build (ADFA-6252). + BAD=$(git ls-files templates/ | grep -E '\.cgt$|(^|/)cgt-build\.properties$' || true) + if [ -n "$BAD" ]; then + echo "::error::A .cgt or cgt-build.properties is committed; both are generated:" + echo "$BAD" + exit 1 + fi + + - name: Validate XML under templates/ + run: | + set -e + fail=0 + while IFS= read -r f; do + if ! xmllint --noout "$f" 2>/dev/null; then + echo "::error file=$f::Invalid XML" + fail=1 + fi + done < <(find templates -type f -name '*.xml') + exit $fail + + - name: Validate each templates.json (strict JSON) + run: | + set -e + fail=0 + while IFS= read -r f; do + if ! jq empty "$f" 2>/dev/null; then + echo "::error file=$f::Invalid JSON" + fail=1 + fi + done < <(find templates -maxdepth 2 -type f -name 'templates.json') + exit $fail + + - name: Validate each template.json (JSON5) + run: | + set -e + fail=0 + while IFS= read -r f; do + if ! python3 -c "import json5,sys; json5.load(open(sys.argv[1]))" "$f" >/dev/null 2>&1; then + echo "::error file=$f::Invalid JSON5" + fail=1 + fi + done < <(find templates -type f -path '*/template/template.json') + exit $fail + + - name: Pebble brace balance check + run: | + set -e + # Counts delimiters only. It cannot tell whether an identifier is + # declared, and the renderer runs with strictVariables(true), so an + # undeclared one still fails on the device at project generation. + fail=0 + while IFS= read -r f; do + opens=$(grep -o '{{' "$f" | wc -l | tr -d ' ') + closes=$(grep -o '}}' "$f" | wc -l | tr -d ' ') + if [ "$opens" != "$closes" ]; then + echo "::error file=$f::Pebble brace imbalance: $opens '{{' vs $closes '}}'" + fail=1 + fi + done < <(find templates -type f -name '*.peb') + exit $fail + + - name: Build every template bundle + run: | + set -euo pipefail + # Proves the bundle packages and that templates.json names only paths + # that exist: build-cgt.sh derives its file list from that manifest and + # fails when one is missing. + for dir in $(uv run --directory tools/addons addons --root "$GITHUB_WORKSPACE" discover --include-skipped | grep '^templates/' || true); do + echo "==> $dir" + ./scripts/build-cgt.sh "$dir" "$GITHUB_WORKSPACE/dist" + ./scripts/verify-provenance.sh "$dir" + slug="$(basename "$dir" | tr '[:upper:]' '[:lower:]')" + # The IDE looks templates.json up by that exact bare name, so a + # nested one means no template is ever found. + unzip -l "$GITHUB_WORKSPACE/dist/${slug}.cgt" | grep -qE '[[:space:]]templates\.json$' \ + || { echo "::error::${slug}.cgt has no bare templates.json entry"; exit 1; } + # Repository metadata must not reach a user-facing download. + if unzip -l "$GITHUB_WORKSPACE/dist/${slug}.cgt" | grep -qE '[[:space:]](addon\.json|.*\.html|icon_(day|night)\.png)$'; then + echo "::error::${slug}.cgt contains gallery metadata that belongs only in the repository" + exit 1 + fi + done diff --git a/.github/workflows/publish-addons.yml b/.github/workflows/publish-addons.yml index cb8a378b..67150fff 100644 --- a/.github/workflows/publish-addons.yml +++ b/.github/workflows/publish-addons.yml @@ -113,23 +113,38 @@ jobs: printf 'only<> "$GITHUB_OUTPUT" for dir in $names; do echo "==> $dir" - ( cd "$dir" - gradlew="$GITHUB_WORKSPACE/gradlew" - if [ -x ./gradlew ]; then gradlew=./gradlew; fi - if grep -q downloadAssets build.gradle.kts; then - "$gradlew" --console=plain downloadAssets - fi - "$gradlew" --console=plain assemblePlugin ) - "$GITHUB_WORKSPACE/scripts/verify-provenance.sh" "$dir" slug="$(basename "$dir" | tr '[:upper:]' '[:lower:]')" - src="$(ls "$dir"/build/plugin/*.cgp | grep -v -- '-debug\.cgp$' | head -n1)" - cp "$src" "dist/${slug}.cgp" + # Two kinds of addon, two build paths (ADFA-6252). A template has no + # Gradle build and produces a .cgt; running assemblePlugin on it would + # fail on the missing build.gradle.kts. + case "$dir" in + templates/*) + "$GITHUB_WORKSPACE/scripts/build-cgt.sh" "$dir" "$GITHUB_WORKSPACE/dist" + "$GITHUB_WORKSPACE/scripts/verify-provenance.sh" "$dir" + ;; + *) + ( cd "$dir" + gradlew="$GITHUB_WORKSPACE/gradlew" + if [ -x ./gradlew ]; then gradlew=./gradlew; fi + if grep -q downloadAssets build.gradle.kts; then + "$gradlew" --console=plain downloadAssets + fi + "$gradlew" --console=plain assemblePlugin ) + "$GITHUB_WORKSPACE/scripts/verify-provenance.sh" "$dir" + src="$(ls "$dir"/build/plugin/*.cgp | grep -v -- '-debug\.cgp$' | head -n1)" + cp "$src" "dist/${slug}.cgp" + ;; + esac done - name: Build the source tarballs env: ONLY: ${{ steps.stage.outputs.only }} run: | + # Templates are passed in with everything else and skipped inside the + # tool, which logs the skip: a template ships no source tarball because + # its .cgt is plain text and already is its source (ADFA-6252). Filtering + # here instead would put the rule in two places. # shellcheck disable=SC2086 uv run --directory tools/addons addons --root "$GITHUB_WORKSPACE" \ tarball --out "$GITHUB_WORKSPACE/dist" --only $ONLY diff --git a/.gitignore b/.gitignore index 402b3185..cd69f4cd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,13 @@ **/.DS_Store **/*.keystore **/*.cgp +# Template bundles and their provenance record are generated, never committed +# (ADFA-6252). Each template addon also ignores its own, but keep the repo-wide +# rule beside *.cgp so a stray one anywhere cannot be added by accident. +**/*.cgt +**/cgt-build.properties +# Where the publish and build workflows stage artifacts +dist/ **/release.properties **/RecentProject_database* **/agent/ diff --git a/CLAUDE.md b/CLAUDE.md index 02c74db7..d730b5e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,197 +4,58 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Repository purpose -Reference plugins for [CodeOnTheGo](https://github.com/appdevforall/CodeOnTheGo) (CoGo / CotG). Addons live under `plugins/`, with `templates/`, `snippets/`, and `code-actions/` reserved beside it. Two unmigrated addons (`cotg-ndk`, `pebble-custom-function-template-installer`) are still at the repository root; that is the only reason `addons discover` still globs `*/build.gradle.kts`. Each addon directory is an independent Gradle project that builds a `.cgp` installable via the Plugin Manager. They are held together only by the shared `libs/` jars at the repo root, which an addon references as `../../libs/`. +Reference addons for [Code on the Go](https://github.com/appdevforall/CodeOnTheGo). Addons live under four area directories — `plugins/`, `templates/`, `snippets/` and `code-actions/`. Two unmigrated addons (`cotg-ndk`, `pebble-custom-function-template-installer`) are still at the repository root; that is the only reason `addons discover` still globs `*/build.gradle.kts`. -## Common commands +**The four areas are not alike.** A plugin is a Gradle project that compiles to a `.cgp`; a template is a plain-text tree that zips to a `.cgt`. They share the gallery metadata (`addon.json`), the naming rule and the publish path, and almost nothing else. -Build one plugin: +## Which kind of addon? -```sh -cd plugins/Voice-Alerts # or any addon folder -./gradlew assemblePlugin # release .cgp -> build/plugin/.cgp -./gradlew assemblePluginDebug # debug variant -``` +Read the area's own README before working in it. This file holds only what is true for all four. -Build every plugin from scratch (after rebuilding libs): +| Area | What it is | Artifact | Guidance | +|---|---|---|---| +| `plugins/` | Code that runs inside the IDE. An Android application module | `.cgp` | [`plugins/README.md`](plugins/README.md) | +| `templates/` | A project skeleton on the New Project screen. Plain text, no build | `.cgt` | [`templates/README.md`](templates/README.md) | +| `snippets/` | Reusable editor snippets. Placeholder — nothing here yet | `.cgs` | [`snippets/README.md`](snippets/README.md) | +| `code-actions/` | Editor quick-fixes. Placeholder — nothing here yet | — | [`code-actions/README.md`](code-actions/README.md) | -```sh -./scripts/update-libs.sh # default: github.com/appdevforall/CodeOnTheGo@stage -./scripts/update-libs.sh --ref -./scripts/update-libs.sh --local ../CodeOnTheGo # use an existing checkout instead of cloning -``` +Working on a plugin means reading `plugins/README.md` first: the `libs/` coupling, the credential store, the manifest shape, build provenance and the tooltip wiring all live there, and each has a failure that a green build does not catch. -The script clones CoGo into `.cache/CodeOnTheGo/` on first run, rebuilds both jars, copies them into `libs/`, then runs `assemblePlugin` for every example. It auto-detects examples by scanning for `build.gradle.kts` files that apply `com.itsaky.androidide.plugins.build`. - -`local.properties` must contain `sdk.dir=...`. The committed `local.properties` at the repo root is harmless leftover; each plugin needs its own. +The format reference for `.cgt` is [`templates/cgt-templates.md`](templates/cgt-templates.md). ## Git workflow - **Always work on a branch.** Never commit directly to `main` — branch first (`git switch -c ...`) even for a one-line fix. Working on `main` is almost never right for this repo. - **Fetch before you diff against `main`.** Any time you compute or reason about a diff against `main` (code review, PR base, "what changed"), run `git fetch origin` first and compare against `origin/main`. A stale local `main` produces phantom findings — a `/code-review` here once flagged 3 issues that were outside the actual PR diff because local `main` was ~50 files behind `origin/main`. When a diff-against-main is requested, suggest fetching first. -## Architecture - -### `libs/` is the load-bearing piece - -`libs/` holds **five** jars. Every plugin depends on at least these two: - -- **`plugin-api.jar`** — the IDE-side API surface (`IPlugin`, `PluginContext`, `BuildStatusListener`, `IdeBuildService`, etc.). Each plugin uses it as `compileOnly` (provided by the IDE at runtime) AND as `buildscript classpath` so the Gradle plugin can resolve symbols at configuration time. -- **`gradle-plugin.jar`** — the Gradle plugin with id `com.itsaky.androidide.plugins.build`, applied by every plugin. It's the output of CoGo's `plugin-api/plugin-builder/` module (separate from CoGo's `gradle-plugin/` module, which is unrelated despite the name). It packages the compiled Android library into a `.cgp`. - -There is also **one shared Gradle wrapper at the repo root** (`gradlew` + `gradle/wrapper/`). New plugins should use it — build them with `cd plugins/ && ../../gradlew assemblePlugin` rather than bundling a per-plugin `gradlew`/`gradle/wrapper/` copy. (`Flutter-Templates` and the five `AI-*` addons follow this; most older plugins still carry their own local wrapper and can be migrated opportunistically.) - -An addon under `plugins/` references the shared jars as `../../libs/*.jar`. **Always use the repo-root `libs/` jars and the repo-root Gradle wrapper — never bundle per-plugin copies.** A plugin that ships its own `libs/plugin-api.jar` / `libs/gradle-plugin.jar` (e.g. copied from another plugin) can drift out of sync with the rest of the repo; point `build.gradle.kts` (`compileOnly`) and `settings.gradle.kts` (buildscript `classpath`) at `../../libs/*.jar` and delete any local `libs/`. The root `plugin-api.jar` already carries the full API surface (including `IdeTemplateService`/`CgtTemplateBuilder`), so newer sub-APIs do not justify a local copy. **A plugin folder is not standalone in isolation** — copy the root `libs/` along if you move one elsewhere. When CoGo's API changes, refresh via the script above or the **Update libs from CodeOnTheGo** GitHub Action (which commits the refreshed jars and cuts a release). Publishing addons is a separate workflow, **Publish addons**, which uploads to Cloudflare R2. - -### Credentials: use the host's `KeystoreSecretStore`, never your own crypto - -A plugin that stores a credential encrypts it with `com.itsaky.androidide.plugins.security.KeystoreSecretStore` -from `plugin-api.jar` (**26.36+** — set `plugin.min_ide_version` accordingly). It is `compileOnly` -like the rest of the API, so there is one implementation in the IDE's process rather than a copy -compiled into each `.cgp`. Do not re-implement AES/GCM in a plugin; three AI plugins each grew a -copy that started to diverge, which is what ADFA-5255 removed. - -Construct it with **this plugin's own alias** (`KeystoreSecretStore(ALIAS)`) as a single -top-level `val` in a `SecureApiKeyStore.kt`/`SecureTokenStore.kt` that holds nothing but the alias; -callers use that instance directly. It takes no log tag — the store logs under its own name — and -that single-argument constructor is the only one a plugin can reach: the two-argument form takes an -`internal` `SecretKeySource`, which is not on the plugin's compile classpath at all. -`ai-agent-mcp`, `ai-agent-gemini` and `ai-agent-openai` are the reference shape. Do **not** wrap -it in an object of forwarding methods — that is just a second copy of the store's contract to keep -in step. The alias must be unique per plugin (all plugins share the host's UID and Keystore, so a -shared alias lets one plugin's invalidated-key recovery delete another's secret) and must never -change across releases. - -`readAndMigrate` returns a four-way `Stored` rather than a nullable String on purpose — each state -needs different advice, and a plugin that collapses them tells a user their credential was refused -when it was never sent: - -- `Absent` — nothing was ever saved. The ordinary first run; say nothing. -- `Value` — the plaintext. Trim it at the call site if your credential format wants it; - `readAndMigrate` migrates verbatim. -- `Unreadable` — stored, but this device's Keystore can no longer open it (a restored backup, an - OEM Keystore reset). Permanent: the user has to enter it again. -- `Unavailable` — the Keystore would not answer this time. **Transient: the credential is intact, - so retry and never re-prompt.** In particular a pane that reads `Unavailable` must not dress - itself as never-configured, and nothing on that screen may write over the credential it could - not read — an empty field then means "not shown", not "removed". - -Handle all four; collapse them only where the caller genuinely has one answer for every state, and -say so in a comment. - -### Plugin shape - -A plugin is an Android *application* module (despite installing as a library) with: - -1. **`build.gradle.kts`** applies `com.android.application` and `com.itsaky.androidide.plugins.build`. It must **not** apply `org.jetbrains.kotlin.android` — AGP 9 compiles Kotlin itself and refuses that plugin ("already on the classpath with an unknown version"); the Kotlin version is pinned on the buildscript classpath in `settings.gradle.kts` instead, which is where AGP 9 takes its compiler from. Configures `pluginBuilder { pluginName = "..." }`. Uses `compileOnly(files("../../libs/plugin-api.jar"))` — never `implementation`. -2. **`settings.gradle.kts`** declares the jars it needs on the buildscript classpath plus AGP and Kotlin. -3. **`src/main/AndroidManifest.xml`** declares plugin identity as `` entries on ``: `plugin.id`, `plugin.name`, `plugin.version` (resolved from `${pluginVersion}`), `plugin.description`, `plugin.author`, `plugin.main_class`, `plugin.min_ide_version`, and optional `plugin.permissions`. Optionally `plugin.vcs_revision` / `plugin.build_timestamp` — see **Build provenance** below. -4. **Main class** implements `com.itsaky.androidide.plugins.IPlugin`. Lifecycle: `initialize(PluginContext) → activate() → deactivate() → dispose()`. Services are obtained via `context.services.get(SomeService::class.java)` (e.g. `IdeBuildService` for build hooks). Android `Context` is `context.androidContext`. - -Available permission strings (declared comma-separated in `plugin.permissions`): `filesystem.read`, `filesystem.write`, `network.access`, `system.commands`, `ide.settings`, `project.structure`. - -### Build provenance (`plugin.vcs_revision`, `cgp-build.properties`) - -Every `.cgp` records the commit it was built from, so a crash report or a support question traces back to source (ADFA-5256). The builder resolves it once per build and publishes it three ways: two `` entries, `assets/cgp-build.properties` inside the archive, and the IDE's plugin details dialog. - -Manifests opt in by referencing the placeholders — the builder never injects `` on your behalf: - -```xml - - -``` - -**This is a hard build-time coupling to `libs/gradle-plugin.jar`.** A manifest that references a placeholder the builder does not define fails the manifest merger outright (*"requires a placeholder substitution but no value ... is provided"*), and all plugins resolve the builder from the single committed jar. So a manifest may only adopt these **after** the builder change is merged in CoGo and the **Update libs from CodeOnTheGo** Action has refreshed `libs/`. Never the other way round. The same coupling hits on-device builders, whose builder jar ships in `plugin-maven-repo.zip` and refreshes only with a CoGo **app release** — a plugin referencing these cannot be built inside an older CoGo at all. The builder change (ADFA-5394) first ships in **26.37**, so every adopting plugin's source is unbuildable on device in 26.36 and earlier, with only the merger's placeholder error to go on. `plugin.min_ide_version` does not express this: it gates installing the `.cgp`, not building its source. - -Read the record out of a built artifact: +## The check every addon passes ```sh -unzip -p /build/plugin/.cgp assets/cgp-build.properties +uv run --directory tools/addons addons --root "$PWD" check ``` -`revision_source` says how the revision was found, in the order the builder tries: `explicit` (you set `pluginBuilder { pluginVcsRevision = "..." }`) → `env:` (`PLUGIN_VCS_REVISION`, `GITHUB_SHA`, `CI_COMMIT_SHA`, `GIT_COMMIT`) → `git` → `git-dir` (reads `.git` directly; this is the on-device path, since CoGo ships JGit in-process and no `git` binary) → `none`, which means `revision=unknown`. `+dirty` is appended when the plugin's **own** directory has uncommitted changes; the check is scoped to that directory so a `libs/` refresh elsewhere in the tree does not flag the build. - -`timestamp` is the committer date of that revision in UTC, not the wall clock, and it lands in the version string as well (`1.0.0-release.`), so two builds of one commit produce a `.cgp` whose every entry matches by CRC and mtime (see CoGo's ADR-0012). The archives are not byte-identical: the v2 APK Signing Block differs between runs, so compare entry CRCs rather than a file hash. That determinism holds only where the builder could reach a `git` binary; it falls back to the clock and says so with `timestamp_source=wall-clock`, and the stamp it puts in the version string then changes on every build, so the artifact is **not** reproducible even entry-by-entry. On device it is always the fallback — CoGo ships no `git` — and under `--configuration-cache` the clock reading additionally freezes into the cached configuration. - -`+dirty` has one systemic cause worth designing against: **a build-time download must land on a gitignored path.** A `downloadAssets` task that overwrites a git-tracked file (`ndk-installer` shipped a committed placeholder `ndk-cmake.tar.xz` until it was untracked) dirties the plugin directory on every build, so every artifact it ever produces records `+dirty` and no build of that plugin is traceable to a clean commit. Both download plugins now fetch onto ignored paths (`plugins/NDK-Installer/.gitignore`, `plugins/AI-Literacy-Course/.gitignore`); keep it that way when adding a new one. - -`libs_revision` records which CoGo commit produced the jars the plugin was compiled against. The builder cannot see that checkout, so each build path exports `PLUGIN_LIBS_REVISION` first: `scripts/update-libs.sh` from the CodeOnTheGo checkout it just built, and **Publish addons** from the subject of the most recent commit touching `libs/` that names one (ordinary commits touch it too, so it scans back rather than reading only the newest). Compare two artifacts' `libs_revision` by prefix, not equality: subjects written before `--short=12` carry a 9-character sha. Note that under **Update libs from CodeOnTheGo** the plugin's own `revision` is the commit *before* the `chore: update libs` commit, because plugins are built before that commit is created; `libs_revision` is what pins the pairing. - -`scripts/verify-provenance.sh` asserts the record after each `assemblePlugin`: a `.cgp` missing `assets/cgp-build.properties`, missing any required key, or disagreeing with the exported `PLUGIN_LIBS_REVISION` fails the run. `revision=unknown`, `+dirty` and `timestamp_source=wall-clock` warn instead — all three are legitimate off-CI (no `.git`, no `git` binary). All three workflows call it — **Build plugin artifacts** and **Update libs from CodeOnTheGo** through `scripts/update-libs.sh`, **Publish addons** directly, since it builds the addons itself. The last one is the one that matters most: those are the artifacts users install from the gallery. - -### In-app help wiring (tooltips + Tier 3, `DocumentationExtension`) +Run it from the repository root. `--root` must be absolute: `--directory` moves uv into `tools/addons`, so `--root .` resolves there and finds no addons. It is the same gate `check-toolchain.yml` runs on every pull request, it costs a second, and it names the exact file and value it wants. **Treat it as the authority — do not restate its rules here, or the two copies drift.** -Every plugin with UI implements `com.itsaky.androidide.plugins.extensions.DocumentationExtension`. This wiring is fixed and foundational — get **all** of it right or the tooltip renders the literal string **`n/a`** at runtime. The build stays green and the manifest looks fine, so **only device long-press testing catches a mistake** (this bit us once). All symbols are in `plugin-api.jar`. - -1. **Category is `"plugin_"` — exactly.** `getTooltipCategory()` MUST return `"plugin_"` + the full `plugin.id` (e.g. `"plugin_org.appdevforall.projecttotemplate"`). The host registers your entries under this string **and** derives the same string when resolving a lookup. Any other value — a short slug, a dotless/underscore form — silently mismatches → `n/a`. -2. **Entries.** `getTooltipEntries()` returns `PluginTooltipEntry(tag, summary, detail, buttons)`: `summary` = Tier 1 (one line shown on long-press), `detail` = Tier 2 (HTML behind "See more"). Keep the `tag` in one shared `const val` used by steps 3–4. -3. **Look tooltips up with the 3-arg overload.** Call `IdeTooltipService.showTooltip(anchorView, category, tag)` and pass `category = "plugin_"` explicitly. **Never use the 2-arg `showTooltip(view, tag)`** — it resolves under a different default category and renders `n/a` even when the entry is registered correctly. Param order is `(anchorView, category, tag)`. -4. **Attach tags to UI.** Set `tooltipTag = ` on every contributed `NavigationItem` / `TabItem` / menu item / FAB; `EditorTabItem` instead takes a literal `tooltip = "..."` string. A contributed element with no tooltip fails review clause 6.7. -5. **Tier 3 (offline page).** Override `getTier3DocsAssetPath()` to return an assets subdir name (convention: `"docs"`), ship real HTML at `src/main/assets//index.html` (white background, black text, English), and link it from an entry via `PluginTooltipButton(description, uri = "index.html", order = 0)` — leave `directPath` false (`true` targets the host's shared docs tree, not your bundle). - -Debug a mismatch against the on-device store (`adb root` first): -`sqlite3 /data/data/com.itsaky.androidide/databases/documentation.db "SELECT c.category, t.tag, substr(t.summary,1,40) FROM Tooltips t JOIN TooltipCategories c ON c.id=t.categoryId WHERE c.category LIKE 'plugin_%'"`. If the row is present but the tooltip still shows `n/a`, the bug is the **lookup** (step 1 or 3), not registration. (The unused `ide_tooltip_table` is a red herring — plugin entries live in `Tooltips` + `TooltipCategories`.) - -### Convention: AAR metadata checks are disabled - -Most plugins end with: - -```kotlin -tasks.matching { - it.name.contains("checkDebugAarMetadata") || - it.name.contains("checkReleaseAarMetadata") -}.configureEach { enabled = false } -``` +Naming is derived, not chosen twice: the directory name in MixedCase with single hyphens is the one human decision, and the slug, display name, page file name and artifact name all follow from it. See `docs/addon-naming-standards.md`. -This is intentional — the `application`-as-library packaging trips those checks. Keep it. +## Publishing -### Asset downloads (rare) +Every addon publishes to `addons.appdevforall.org` through Cloudflare R2, by the **Publish addons** workflow. The catalog, the gallery page and the download all derive from the directory name and `addon.json`. Nothing needs wiring up by hand. -Some plugins (`ndk-installer`, `ai-literacy-course`) register a `downloadAssets` task that fetches large files at build time with pinned-MD5 verification. These assets are **not committed to git** — each plugin gitignores its own download paths (`ai-literacy-course` pulls a ~110 MB course ZIP plus `pdfjs.zip`; `ndk-installer` pulls `ndk-cmake.tar.xz`). Committing one, even as a placeholder, makes every build dirty — see **Build provenance** above. `scripts/update-libs.sh` runs `downloadAssets` automatically before `assemblePlugin` when the build file references it. - -**A bare `./gradlew assemblePlugin` does NOT run `downloadAssets`.** Both download plugins now fail the asset merge outright when their archives are absent, rather than silently packaging a broken `.cgp` (a course with no PDF viewer, an NDK-less installer) — that silent packaging is the failure the guards exist to prevent, so keep one on any new download plugin. When building such a plugin by hand, run `./gradlew downloadAssets` and then `./gradlew assemblePlugin` as two separate invocations (or use the script, which does exactly that). Combining them in one invocation fails: `downloadAssets` declares an output inside `src/main/assets`, which Gradle sees as an undeclared dependency of `mergeReleaseAssets`. - -### One-time on-device install markers - -Plugins that extract bundled assets on-device once (currently `ai-literacy-course`, via `CourseInstaller`) gate the work behind a marker file named from a version constant (`INSTALL_VERSION` → `.installed-vN`). If the marker for the current version exists, extraction **and** any post-extraction generation (e.g. `CourseShell.generate()`) are skipped entirely. - -**Any change to extraction OR post-extraction generation logic must bump `INSTALL_VERSION`.** Otherwise the change compiles and packages cleanly but has zero effect on existing installs — they keep the stale extracted tree, and it looks like "my fix didn't work" (costing a device round-trip). Bumping the constant forces a clean re-extract. On a device with a prior install, confirm the marker version changed (or wipe plugin data) before concluding a fix works — see Verification below. - -### Template-installer plugins (Pebble `.cgt`) - -Some plugins are headless template installers (`flutter-template`, `pebble-custom-function-template-installer`): on `activate()` they register project templates with `IdeTemplateService` (building each `.cgt` from Pebble `.peb` skeletons under `src/main/assets/templates//`), and unregister on `deactivate()`. The templates then appear on the New Project screen beside the core ones. The source-of-truth skeletons follow the pattern in `~/src/dev-assets/templates`. - -**Pebble gotcha:** a bare `${{TAG}}` at end-of-line loses its trailing newline to Pebble's newline-trimming and merges with the next line (this silently produced invalid YAML by collapsing `name:` and `description:` into one line). Ensure a non-newline character follows `}}` — the convention is to quote the value, e.g. `name: "${{APP_NAME | lower}}"`. +Build artifacts are **never committed**. A `.cgp` is built by Gradle; a `.cgt` is zipped by the Action. ## Verification -**`./gradlew assemblePlugin` succeeding is not verification** — it only proves the plugin compiles and packages. Real verification for these plugins is device-level: push the built `.cgp` to a connected emulator/device, install through CoGo's Plugin Manager, exercise the feature end-to-end, and observe the expected behavior (UI element appears, file written, build hook fires, DB row replaced, etc.). - -If device verification isn't possible in-session, say so explicitly rather than calling the change verified. Build success is necessary but never sufficient — this applies especially to plugins that mutate IDE state (`documentation.db`, settings, filesystem, project structure). - -**Launching CoGo via adb.** Do **not** launch with `monkey` or a bare LAUNCHER intent (`adb shell monkey -p com.itsaky.androidide …`) — debug builds bundle **LeakCanary**, which registers its own launcher activity, so the intent can open LeakCanary's "Leaks" screen or a disambiguation chooser instead of the IDE. Start the explicit component: `adb shell am start -n com.itsaky.androidide/.activities.SplashActivity`. When re-verifying a plugin **icon** change under the same plugin id, note that the Plugin Manager caches icons via Glide (`cache/image_manager_disk_cache`) keyed by path without mtime invalidation — the old icon persists until that cache is cleared (`adb root`, delete the dir, restart) or you install on a clean device. +**A successful build is not verification.** It only proves the addon compiles or packages. Real verification is device-level: install on a connected emulator or device, exercise the feature end to end, and observe the expected behaviour (UI element appears, file written, build hook fires, DB row replaced, template generates a project that opens). -## Adding a new plugin +If device verification isn't possible in-session, say so explicitly rather than calling the change verified. This applies especially to addons that mutate IDE state (`documentation.db`, settings, filesystem, project structure). -1. Copy `plugins/Random-XKCD/` — it's the canonical starting template (small but complete, includes the in-IDE help HTML pattern that submissions are expected to follow). Name the new directory in MixedCase with single hyphens between words (`APK-Analyzer`), ASCII letters and digits only. Every other name, filename, and URL derives from it — see `docs/plugin-naming-standards.md`. -2. Update **every** copied file that still names the template. Two values come from the directory name: the **slug** is it lowercased (`apk-analyzer`), the **display name** is it with hyphens replaced by spaces (`APK Analyzer`). - - | File | Change | - |---|---| - | `settings.gradle.kts` | `rootProject.name` — Gradle's own name for the build. Nothing derives from it; keep it in step with the slug anyway. | - | `build.gradle.kts` | `pluginBuilder { pluginName }` → the slug; `android { namespace, applicationId }` | - | `src/main/AndroidManifest.xml` | `plugin.id`, `plugin.name` → the display name, `plugin.main_class` | - | `random-xkcd.html` | rename to `.html`; set `` to the display name exactly, and make the `<h1>` contain it | - | `addon.json` | `summary`, `description`, `tags`, `origin`, `license`, `author`. The schema checks the shape, not the words, so a copied one passes every check and puts xkcd's description on your gallery card. | - | `src/main/assets/icon_day.png`, `icon_night.png` | replace both; both must be present | - | `src/main/kotlin/...` | your implementation | - -3. Run `uv run --directory tools/addons addons --root "$PWD" check` from the repository root before pushing. `--root` must be absolute: `--directory` moves uv into `tools/addons`, so `--root .` resolves there and finds no addons. It is the same gate `check-toolchain.yml` runs on every pull request, it costs a second, and it names the exact file and value it wants. Treat it as the authority — do not restate its rules here, or the two copies drift. -4. Add a row to the README's Examples table. -5. Nothing else to wire up. Publishing is automatic: `addons discover` finds any directory whose `build.gradle.kts` applies the plugin-builder, and the name, filenames, and URLs all derive from the directory name. The `MAP` array this file used to describe was deleted in #66 and no longer exists. +**Launching Code on the Go via adb.** Do **not** launch with `monkey` or a bare LAUNCHER intent (`adb shell monkey -p com.itsaky.androidide …`) — debug builds bundle **LeakCanary**, which registers its own launcher activity, so the intent can open LeakCanary's "Leaks" screen or a disambiguation chooser instead of the IDE. Start the explicit component: `adb shell am start -n com.itsaky.androidide/.activities.SplashActivity`. When re-verifying a plugin **icon** change under the same plugin id, note that the Plugin Manager caches icons via Glide (`cache/image_manager_disk_cache`) keyed by path without mtime invalidation — the old icon persists until that cache is cleared (`adb root`, delete the dir, restart) or you install on a clean device. ## Plugin review skill -`.claude/skills/plugin-review/` contains the `cogo-plugin-review` skill — invoke via `/plugin-review` or `/cotg-plugin-review` when the user asks to review, audit, or check a CoGo plugin for submission readiness. It builds, audits security, and scores against the submission rubric. +`.claude/skills/plugin-review/` contains the `cogo-plugin-review` skill — invoke via `/plugin-review` or `/cotg-plugin-review` when the user asks to review, audit, or check a Code on the Go plugin for submission readiness. It builds, audits security, and scores against the submission rubric. **Proactively offer `/plugin-review`** (don't wait for the user to ask) after any substantive change to a plugin: importing a new plugin folder, modifying dependencies, touching the `IPlugin`/`PluginContext` API surface, adding shipped assets, or updating `libs/`. It has caught real defects (resource leaks, missing manifest entries, missing in-IDE help HTML) that aren't visible from a clean `assemblePlugin` build. + +The rubric is written for plugins. It does not apply to a template. diff --git a/README.md b/README.md index c30a4823..0a5813fe 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,19 @@ # plugin-examples -Reference plugins for [Code on the Go](https://github.com/appdevforall/CodeOnTheGo). Each folder is a fully self-contained Gradle project that builds to a `.cgp` installable plugin file. +Reference add-ons for [Code on the Go](https://github.com/appdevforall/CodeOnTheGo). + +There are four kinds, and they are built in different ways. Read the area's own README before adding one. + +| Area | What it is | Artifact | How to add one | +|---|---|---|---| +| [`plugins/`](plugins/) | Code that runs inside the IDE. A self-contained Gradle project | `.cgp` | [`plugins/README.md`](plugins/README.md) | +| [`templates/`](templates/) | A project skeleton on the New Project screen. Plain text, no build | `.cgt` | [`templates/README.md`](templates/README.md) | +| [`snippets/`](snippets/) | Reusable editor snippets. Placeholder — nothing here yet | `.cgs` | [`snippets/README.md`](snippets/README.md) | +| [`code-actions/`](code-actions/) | Editor quick-fixes. Placeholder — nothing here yet | — | [`code-actions/README.md`](code-actions/README.md) | See the official [plugin documentation](https://www.appdevforall.org/codeonthego/help/exp-plugins-top.html) for concepts, the plugin API surface, and install workflow. -## Examples +## Plugins | Plugin | Purpose | | -------------------------------------------------- | ----------------------------------------------------------------- | @@ -20,7 +29,6 @@ See the official [plugin documentation](https://www.appdevforall.org/codeonthego | [`plugins/Code-Together/`](plugins/Code-Together/) | Pair programming between two devices on the same network. | | [`plugins/AI-Core/`](plugins/AI-Core/) | The agent chat and the inference router every other AI addon needs. | | [`plugins/Favorite-Snippets/`](plugins/Favorite-Snippets/) | Saves your own code snippets and inserts them in the editor. | -| [`plugins/Flutter-Templates/`](plugins/Flutter-Templates/) | Adds five Flutter starter projects to the New Project screen. | | [`plugins/Get-AI-Models/`](plugins/Get-AI-Models/) | Downloads small language models for on-device AI addons. | | [`plugins/Icons-Repository/`](plugins/Icons-Repository/) | Adds vector icons to a project from inside the editor. | | [`plugins/Jetpack-Compose-Preview/`](plugins/Jetpack-Compose-Preview/) | Renders Compose preview functions on the device. | @@ -40,6 +48,14 @@ See the official [plugin documentation](https://www.appdevforall.org/codeonthego > `cotg-ndk` and `pebble-custom-function-template-installer` are not listed. They are still at the repository root, beside `libs/` and `tools/`, until their own work lands. > The `AI-*` addons above build from `plugins/` like every other example but are still held out of the published gallery; `tools/addons/skip.txt` states the reason for each. +## Templates + +| Template bundle | Purpose | +| --- | --- | +| [`templates/Flutter-Templates/`](templates/Flutter-Templates/) | Five Flutter starter projects, one per state-management approach. | + +A bundle holds one or more templates. It runs no code, so there is nothing to compile: the publish workflow zips the directory into a `.cgt` that Code on the Go reads directly. Start from `Flutter-Templates/`, which is the reference example. + ## Building a plugin @@ -99,7 +115,7 @@ First local run clones Code on the Go into `.cache/CodeOnTheGo/` (gitignored); s ## Adding a new plugin example -1. Copy `plugins/Random-XKCD/` to a new folder under `plugins/`. Name it in MixedCase with single hyphens between words (`My-Plugin`), ASCII letters and digits only. Every other name, filename, and URL is derived from this one — see [`docs/plugin-naming-standards.md`](docs/plugin-naming-standards.md). +1. Copy `plugins/Random-XKCD/` to a new folder under `plugins/`. Name it in MixedCase with single hyphens between words (`My-Plugin`), ASCII letters and digits only. Every other name, filename, and URL is derived from this one — see [`docs/addon-naming-standards.md`](docs/addon-naming-standards.md). 2. Update **every** copied file that still names the template. Two values come from the folder name: the **slug** is it lowercased (`my-plugin`), and the **display name** is it with hyphens replaced by spaces (`My Plugin`). diff --git a/docs/addon-naming-standards.md b/docs/addon-naming-standards.md new file mode 100644 index 00000000..eec8b427 --- /dev/null +++ b/docs/addon-naming-standards.md @@ -0,0 +1,107 @@ +The problem + +Every addon has up to six places its name or slug appears: the published name on the site, the name in the Code on the Go manager that installs it, the artifact filename, the GitHub src directory, the documentation HTML filename, and the title shown inside that documentation page. The current addons already show drift: different hyphenation, plurals, abbreviations, or leftover names from earlier drafts. This standard fixes that by making five of the six values derived automatically from one: the GitHub src directory name. + +The rules are the same for all four addon types. Only the artifact filename differs by type — a plugin compiles to a .cgp, a template zips to a .cgt (ADFA-6252). + +Rules + +GitHub src directory. This is the single source of truth, and the only one of the six values that's a human decision. Name it in MixedCase, with words separated by hyphens (e.g., APK-Analyzer, Template-Manager). Everything else below is derived mechanically from this — no judgment calls about which words are "generic" versus part of the addon's identity. + +Display name, derived from the GitHub src directory with the area prefix removed: + +Replace hyphens with spaces (APK-Analyzer → "APK Analyzer"). The display name then appears verbatim, with no variation, in exactly three places: + +The published name on the site + +The name in the Code on the Go manager that installs it — the Plugin Manager for a plugin, the Templates manager for a template + +The title/heading in the documentation HTML page + +Slug, derived from the GitHub src directory: + +Lowercase the directory name (APK-Analyzer → apk-analyzer). The slug is then reused exactly in both of these: + +the artifact filename → slug.cgp for a plugin, slug.cgt for a template + +the documentation HTML filename → slug.html + +No addon may have more than one spelling of its own slug across these two. The GitHub src directory itself keeps its own MixedCase spelling because it's the source, not a derived copy. + +Worked example: a plugin + +Display Name: APK Analyzer + +Artifact + +Value + +GitHub src directory (source of truth) + +/plugins/APK-Analyzer + +Published name on site + +APK Analyzer + +Name in the Code on the Go Plugin Manager + +APK Analyzer + +Title inside documentation HTML + +APK Analyzer + +Artifact filename + +apk-analyzer.cgp + +Documentation HTML filename + +apk-analyzer.html + +Worked example: a template + +Display Name: Flutter Templates + +Artifact + +Value + +GitHub src directory (source of truth) + +/templates/Flutter-Templates + +Published name on site + +Flutter Templates + +Name in the Code on the Go Templates manager + +Flutter Templates + +Title inside documentation HTML + +Flutter Templates + +Artifact filename + +flutter-templates.cgt + +Documentation HTML filename + +flutter-templates.html + +Note that the names of the individual templates inside a bundle are a separate matter. Those come from each template/template.json "name" field and are what a user picks on the New Project screen; they are not derived from the directory name. The directory name names the bundle. + +Naming guidelines + +Leave implementation details out of the directory name. Naming an addon after its implementation locks you in if that implementation ever changes. Put that detail in the documentation body instead. + +Drop generic type-words when naming the directory. Words that describe the artifact rather than the addon's identity ("plugin," "CGP," "CGT," "Documentation") shouldn't be part of the directory name. Keep words that are genuinely part of the name even if generic-sounding on their own. E.g., "Manager" stays in "Template-Manager," because the addon's name is genuinely "Template Manager," not just "Template." + +Prefer the spelling that most of the addon's existing artifacts already use, unless one spelling is clearly more accurate or more common going forward. + +Match number and form exactly. Decide once whether the directory name is singular or plural — that choice propagates automatically to the display name everywhere it's used, so get it right at the source. Flutter-Templates is plural because the bundle genuinely holds five of them. + +No stray path fragments. Branch names, parent folders, or build artifacts (e.g., a main/ prefix) should never leak into the GitHub src directory name or the doc path. diff --git a/docs/plugin-naming-standards.md b/docs/plugin-naming-standards.md deleted file mode 100644 index fe7cfd48..00000000 --- a/docs/plugin-naming-standards.md +++ /dev/null @@ -1,75 +0,0 @@ -The problem - -Every plugin has up to six places its name or slug appears: the published name on the site, the name in the Code On the Go plugin manager, the .cgp filename, the GitHub src directory, the documentation HTML filename, and the title shown inside that documentation page. The current plugins already show drift: different hyphenation, plurals, abbreviations, or leftover names from earlier drafts. This standard fixes that by making five of the six values derived automatically from one: the GitHub src directory name. - -Rules - -GitHub src directory. This is the single source of truth, and the only one of the six values that's a human decision. Name it in MixedCase, with words separated by hyphens (e.g., APK-Analyzer, Template-Manager). Everything else below is derived mechanically from this — no judgment calls about which words are "generic" versus part of the plugin's identity. - -Display name, derived from the GitHub src directory with the /plugins prefix removed: - -Replace hyphens with spaces (APK-Analyzer → "APK Analyzer"). The display name then appears verbatim, with no variation, in exactly three places: - -The published name on the site - -The name in the Code On the Go plugin manager - -The title/heading in the documentation HTML page - -Slug, derived from the GitHub src directory: - -Lowercase the directory name (APK-Analyzer → apk-analyzer). The slug is then reused exactly in both of these: - -the .cgp filename → slug.cgp - -the documentation HTML filename → slug.html - -No plugin may have more than one spelling of its own slug across these two. The GitHub src directory itself keeps its own MixedCase spelling because it's the source, not a derived copy. - -Worked example - -Display Name: APK Analyzer - -Artifact - -Value - -GitHub src directory (source of truth) - -/plugins/APK-Analyzer - -Published name on site - -APK Analyzer - -Name in Code on the Go plugin manager - -APK Analyzer - -Title inside documentation HTML - -APK Analyzer - -.cgp filename - -apk-analyzer.cgp - -Documentation HTML filename - -apk-analyzer.html - -Naming guidelines - -Leave implementation details out of the directory name. Naming a plugin after its implementation locks you in if that implementation ever changes. Put that detail in the documentation body instead. - -Drop generic type-words when naming the directory. Words that describe the artifact rather than the plugin's identity ("plugin," "CGP," "Documentation") shouldn't be part of the directory name. Keep words that are genuinely part of the name even if generic-sounding on their own. E.g., "Manager" stays in "Template-Manager," because the plugin's name is genuinely "Template Manager," not just "Template." - -Prefer the spelling that most of the plugin's existing artifacts already use, unless one spelling is clearly more accurate or more common going forward. - -Match number and form exactly. Decide once whether the directory name is singular or plural (e.g., Flutter-Template, not Flutter-Templates) — that choice propagates automatically to the display name everywhere it's used, so get it right at the source. - -No stray path fragments. Branch names, parent folders, or build artifacts (e.g., a main/ prefix) should never leak into the GitHub src directory name or the doc path. - - - - diff --git a/plugins/Flutter-Templates/.gitignore b/plugins/Flutter-Templates/.gitignore deleted file mode 100644 index 24227e33..00000000 --- a/plugins/Flutter-Templates/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# Gradle files -.gradle/ -build/ - -# Local configuration file (sdk path, etc) -local.properties - -# Log/OS Files -*.log -.DS_Store - -# Android Studio generated files and folders -captures/ -.externalNativeBuild/ -.cxx/ -*.aab -*.apk -output-metadata.json - -# IntelliJ -*.iml -.idea/ - -# Keystore / signing material — never commit -*.jks -*.keystore -release.properties -signing.properties -keystore.properties - -# Google Services -google-services.json - -# Android Profiling -*.hprof diff --git a/plugins/Flutter-Templates/README.md b/plugins/Flutter-Templates/README.md deleted file mode 100644 index 7ab5189d..00000000 --- a/plugins/Flutter-Templates/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Flutter Template - -A [Code on the Go](https://github.com/appdevforall/CodeOnTheGo) plugin that adds **Flutter starter -project templates** to the IDE's New Project screen, alongside the built-in core templates. - -It contributes five templates, one per state-management approach: - -| Template | State management | -|---|---| -| Flutter Basic | none (plain `setState`) | -| Flutter BLoC | `flutter_bloc` | -| Flutter Provider | `provider` | -| Flutter GetX | `get` | -| Flutter Riverpod | `flutter_riverpod` | - -Each generates a small, idiomatic Flutter project (a counter app) with `pubspec.yaml`, `lib/`, -and `analysis_options.yaml`, substituting the app name and package name you enter in the New -Project dialog. - -## How it works - -The plugin is a headless installer. On `activate()` it registers each template with the IDE via -`IdeTemplateService`, building a `.cgt` from the Pebble skeletons bundled under -`src/main/assets/templates/<Variant>/`; on `deactivate()` it unregisters them and deletes the -staged files. There is no UI — the templates simply appear in **New Project** after the plugin is -enabled. This mirrors the `pebble-custom-function-template-installer` example. - -## Building - -This plugin uses the repo-root Gradle wrapper and shared `../libs/` jars — it has no local copies. -Build it from its own directory via the parent wrapper: - -```sh -cd flutter-template -../gradlew clean assemblePlugin -``` - -The `.cgp` lands in `build/plugin/flutter-templates.cgp`. Install it from inside Code on the Go via -the Plugin Manager, then open **New Project** to see the Flutter templates. - -## Note on the Flutter SDK - -This plugin **scaffolds project files only** — it does not install the Flutter/Dart SDK. Code on -the Go does not yet ship an on-device Flutter toolchain, so building or running a generated project -requires Flutter on a separate machine (or a future on-device SDK installer). Enter an app name -that is a valid Dart package identifier (lowercase, no spaces — underscores are fine); it is -lower-cased for `pubspec.yaml`'s `name:` field. - -## Credit - -Original idea by **Raju Kumar** ([ADFA-2599](https://appdevforall.atlassian.net/browse/ADFA-2599)). - -Contributed by **RJ Ali** <rjali3232@gmail.com> via the Code on the Go community submission -process, rebuilt on the IDE's template system ([ADFA-3857](https://appdevforall.atlassian.net/browse/ADFA-3857)). diff --git a/plugins/Flutter-Templates/build.gradle.kts b/plugins/Flutter-Templates/build.gradle.kts deleted file mode 100644 index e59dd94e..00000000 --- a/plugins/Flutter-Templates/build.gradle.kts +++ /dev/null @@ -1,72 +0,0 @@ -plugins { - id("com.android.application") - id("com.itsaky.androidide.plugins.build") -} - -pluginBuilder { - pluginName = "flutter-templates" -} - -android { - namespace = "com.ali.fluttertemplate" - compileSdk = 36 - - defaultConfig { - applicationId = "com.ali.fluttertemplate" - minSdk = 21 - targetSdk = 36 - versionCode = 2 - versionName = "2.0.0" - } - - buildTypes { - release { - isMinifyEnabled = false - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - packaging { - resources { - excludes += setOf( - "META-INF/versions/9/OSGI-INF/MANIFEST.MF", - "META-INF/DEPENDENCIES", - "META-INF/LICENSE", - "META-INF/LICENSE.txt", - "META-INF/NOTICE", - "META-INF/NOTICE.txt" - ) - } - } -} - -dependencies { - compileOnly(files("../../libs/plugin-api.jar")) - - implementation("androidx.appcompat:appcompat:1.6.1") - implementation("com.google.android.material:material:1.10.0") - implementation("org.jetbrains.kotlin:kotlin-stdlib:2.3.21") -} - -tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { - compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) - freeCompilerArgs.add("-Xskip-metadata-version-check") - } -} - -tasks.wrapper { - gradleVersion = "9.6.1" - distributionType = Wrapper.DistributionType.BIN -} - -tasks.matching { - it.name.contains("checkDebugAarMetadata") || - it.name.contains("checkReleaseAarMetadata") -}.configureEach { - enabled = false -} diff --git a/plugins/Flutter-Templates/gradle.properties b/plugins/Flutter-Templates/gradle.properties deleted file mode 100644 index 21a36623..00000000 --- a/plugins/Flutter-Templates/gradle.properties +++ /dev/null @@ -1,5 +0,0 @@ -android.useAndroidX=true -android.nonTransitiveRClass=true -kotlin.code.style=official -org.gradle.jvmargs=-Xmx2560m -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -org.gradle.caching=true diff --git a/plugins/Flutter-Templates/settings.gradle.kts b/plugins/Flutter-Templates/settings.gradle.kts deleted file mode 100644 index 9ffee102..00000000 --- a/plugins/Flutter-Templates/settings.gradle.kts +++ /dev/null @@ -1,30 +0,0 @@ -pluginManagement { - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -buildscript { - repositories { - google() - mavenCentral() - } - dependencies { - classpath(files("../../libs/plugin-api.jar")) - classpath(files("../../libs/gradle-plugin.jar")) - classpath("com.android.tools.build:gradle:9.3.1") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.21") - } -} - -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } -} - -rootProject.name = "flutter-templates" diff --git a/plugins/Flutter-Templates/src/main/AndroidManifest.xml b/plugins/Flutter-Templates/src/main/AndroidManifest.xml deleted file mode 100644 index 89173f3e..00000000 --- a/plugins/Flutter-Templates/src/main/AndroidManifest.xml +++ /dev/null @@ -1,63 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android"> - - <application - android:label="@string/app_name" - android:theme="@style/PluginTheme"> - - <meta-data - android:name="plugin.id" - android:value="com.ali.fluttertemplate" /> - - <meta-data - android:name="plugin.name" - android:value="Flutter Templates" /> - - <meta-data - android:name="plugin.version" - android:value="${pluginVersion}" /> - - <meta-data - android:name="plugin.description" - android:value="Adds Flutter starter project templates (Basic, BLoC, Provider, GetX, Riverpod) to the New Project screen." /> - - <meta-data - android:name="plugin.author" - android:value="RJ Ali" /> - - <meta-data - android:name="plugin.min_ide_version" - android:value="26.22" /> - - <meta-data - android:name="plugin.max_ide_version" - android:value="26.51" /> - - <!-- - Available permissions (comma-separated): - - filesystem.read : Read files from the filesystem - - filesystem.write : Write files to the filesystem - - network.access : Access the network - - system.commands : Execute system commands - - ide.settings : Access IDE settings - - project.structure : Access project structure - --> - <meta-data - android:name="plugin.permissions" - android:value="filesystem.read,filesystem.write" /> - - <meta-data - android:name="plugin.main_class" - android:value="com.ali.fluttertemplate.FlutterTemplate" /> - - <meta-data - android:name="plugin.icon_day" - android:value="assets/icon_day.png" /> - - <meta-data - android:name="plugin.icon_night" - android:value="assets/icon_night.png" /> - - </application> - -</manifest> diff --git a/plugins/Flutter-Templates/src/main/kotlin/com/ali/fluttertemplate/FlutterTemplate.kt b/plugins/Flutter-Templates/src/main/kotlin/com/ali/fluttertemplate/FlutterTemplate.kt deleted file mode 100644 index 3566a2a9..00000000 --- a/plugins/Flutter-Templates/src/main/kotlin/com/ali/fluttertemplate/FlutterTemplate.kt +++ /dev/null @@ -1,137 +0,0 @@ -package com.ali.fluttertemplate - -import android.content.res.AssetManager -import android.util.Log -import com.itsaky.androidide.plugins.IPlugin -import com.itsaky.androidide.plugins.PluginContext -import com.itsaky.androidide.plugins.services.IdeFileService -import com.itsaky.androidide.plugins.services.IdeTemplateService -import java.io.File - -/** - * Headless installer plugin. On [activate] it registers five Flutter starter - * templates with the IDE so they appear on the New Project screen alongside the - * built-in core templates; on [deactivate] it unregisters them and deletes the - * staged `.cgt` files. - * - * Each template is authored as a Pebble skeleton bundled under - * `assets/templates/<Variant>/`. The template metadata (name, description, - * package-name option, parameters) is generated by [IdeTemplateService]'s - * builder — the bundled `template/` folder only supplies the thumbnail. - * - * This replaces the original submission's approach of writing hardcoded Dart - * files to external storage, which fails under scoped storage on modern devices. - */ -class FlutterTemplate : IPlugin { - - private lateinit var context: PluginContext - private var templateService: IdeTemplateService? = null - private var fileService: IdeFileService? = null - - /** Registered template name -> staged .cgt file, for cleanup on deactivate. */ - private val registered = mutableMapOf<String, File>() - - override fun initialize(context: PluginContext): Boolean { - return try { - this.context = context - templateService = context.services.get(IdeTemplateService::class.java) ?: return false - fileService = context.services.get(IdeFileService::class.java) ?: return false - context.logger.info("FlutterTemplate initialized successfully") - true - } catch (e: Exception) { - context.logger.error("FlutterTemplate initialization failed", e) - false - } - } - - override fun activate(): Boolean { - for (variant in VARIANTS) { - registerTemplate(variant)?.let { registered[variant.name] = it } - } - context.logger.info("FlutterTemplate: registered ${registered.size}/${VARIANTS.size} templates") - return true - } - - override fun deactivate(): Boolean { - val ts = templateService - for ((name, file) in registered) { - runCatching { - val unregistered = ts?.unregisterTemplate(name) ?: false - context.logger.info("$name unregistered: $unregistered") - if (file.exists()) { - val deleted = fileService?.delete(file) ?: false - context.logger.info("$name staged file removed: $deleted") - } - }.onFailure { context.logger.error("FlutterTemplate: cleanup failed for $name", it) } - } - registered.clear() - return true - } - - override fun dispose() { - templateService = null - fileService = null - registered.clear() - context.logger.info("FlutterTemplate: Disposing plugin") - } - - private fun registerTemplate(variant: Variant): File? { - val assetRoot = "templates/${variant.assetDir}" - return runCatching { - var builder = templateService!!.createTemplateBuilder(variant.name) - .description(variant.description) - .showPackageNameOption() - .thumbnailFromAssets("$assetRoot/template/thumb.png", context) - - for (assetFile in listBundledAssetFiles(assetRoot)) { - if (assetFile.startsWith("template/")) continue - builder = builder.addStaticFromAssets(assetFile, "$assetRoot/$assetFile", context) - } - - val cgtFile = builder.build(context.resources.getPluginDirectory()) - templateService!!.registerTemplate(cgtFile) - Log.i(TAG, "${variant.name} template registered") - cgtFile - }.onFailure { - Log.e(TAG, "Failed to register ${variant.name} template", it) - }.getOrNull() - } - - private fun listBundledAssetFiles(rootAssetPath: String): List<String> { - val results = mutableListOf<String>() - walkAssets(context.androidContext.assets, rootAssetPath, "", results) - return results - } - - private fun walkAssets(assets: AssetManager, rootPath: String, relativePath: String, out: MutableList<String>) { - val absolute = if (relativePath.isEmpty()) rootPath else "$rootPath/$relativePath" - val children = assets.list(absolute) ?: return - if (children.isEmpty()) { - out.add(relativePath) - return - } - for (child in children) { - val nextRelative = if (relativePath.isEmpty()) child else "$relativePath/$child" - walkAssets(assets, rootPath, nextRelative, out) - } - } - - private data class Variant(val name: String, val assetDir: String, val description: String) - - private companion object { - const val TAG = "FlutterTemplate" - - val VARIANTS = listOf( - Variant("Flutter Basic", "FlutterBasic", - "A minimal Flutter starter app with a counter screen"), - Variant("Flutter BLoC", "FlutterBloc", - "A Flutter app wired with the BLoC state-management pattern"), - Variant("Flutter Provider", "FlutterProvider", - "A Flutter app wired with the Provider state-management pattern"), - Variant("Flutter GetX", "FlutterGetx", - "A Flutter app wired with the GetX state-management pattern"), - Variant("Flutter Riverpod", "FlutterRiverpod", - "A Flutter app wired with the Riverpod state-management pattern"), - ) - } -} diff --git a/plugins/Flutter-Templates/src/main/res/drawable/ic_plugin.xml b/plugins/Flutter-Templates/src/main/res/drawable/ic_plugin.xml deleted file mode 100644 index 70f6741f..00000000 --- a/plugins/Flutter-Templates/src/main/res/drawable/ic_plugin.xml +++ /dev/null @@ -1,10 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorOnSurface"> - <path - android:fillColor="@android:color/white" - android:pathData="M20.5,11H19V7c0,-1.1 -0.9,-2 -2,-2h-4V3.5C13,2.12 11.88,1 10.5,1S8,2.12 8,3.5V5H4C2.9,5 2.01,5.9 2.01,7L2,17c0,1.1 0.9,2 2,2h3.5v-1.5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5V19H17c1.1,0 2,-0.9 2,-2v-4h1.5c1.38,0 2.5,-1.12 2.5,-2.5S21.88,11 20.5,11z"/> -</vector> diff --git a/plugins/Flutter-Templates/src/main/res/values-night/colors.xml b/plugins/Flutter-Templates/src/main/res/values-night/colors.xml deleted file mode 100644 index 35516a83..00000000 --- a/plugins/Flutter-Templates/src/main/res/values-night/colors.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <color name="plugin_primary">#88D6B8</color> - <color name="plugin_on_primary">#00382A</color> - <color name="plugin_primary_container">#00513D</color> - <color name="plugin_on_primary_container">#A4F2D3</color> - <color name="plugin_secondary">#B3CCBF</color> - <color name="plugin_on_secondary">#1F352C</color> - <color name="plugin_surface">#191C1A</color> - <color name="plugin_on_surface">#E1E3DF</color> - <color name="plugin_on_surface_variant">#BFC9C1</color> - <color name="plugin_outline">#89938C</color> - <color name="plugin_outline_variant">#404943</color> - <color name="status_text">#E1E3DF</color> - <color name="status_background">#2E3230</color> - <color name="status_success_text">#88D6B8</color> - <color name="status_success_background">#0D2E22</color> - <color name="status_error_text">#FFB4AB</color> - <color name="status_error_background">#690005</color> -</resources> diff --git a/plugins/Flutter-Templates/src/main/res/values/colors.xml b/plugins/Flutter-Templates/src/main/res/values/colors.xml deleted file mode 100644 index d6b2fcf1..00000000 --- a/plugins/Flutter-Templates/src/main/res/values/colors.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <color name="plugin_primary">#1A6B52</color> - <color name="plugin_on_primary">#FFFFFF</color> - <color name="plugin_primary_container">#A4F2D3</color> - <color name="plugin_on_primary_container">#002117</color> - <color name="plugin_secondary">#4C6359</color> - <color name="plugin_on_secondary">#FFFFFF</color> - <color name="plugin_surface">#FBFDF9</color> - <color name="plugin_on_surface">#191C1A</color> - <color name="plugin_on_surface_variant">#404943</color> - <color name="plugin_outline">#707973</color> - <color name="plugin_outline_variant">#BFC9C1</color> - <color name="status_text">#191C1A</color> - <color name="status_background">#E1E7E2</color> - <color name="status_success_text">#1A6B52</color> - <color name="status_success_background">#D4F5E6</color> - <color name="status_error_text">#BA1A1A</color> - <color name="status_error_background">#FFDAD6</color> -</resources> diff --git a/plugins/Flutter-Templates/src/main/res/values/strings.xml b/plugins/Flutter-Templates/src/main/res/values/strings.xml deleted file mode 100644 index 27bb8b8a..00000000 --- a/plugins/Flutter-Templates/src/main/res/values/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <string name="app_name">Flutter Template</string> - <string name="plugin_description">Installs Flutter starter project templates</string> -</resources> diff --git a/plugins/Flutter-Templates/src/main/res/values/styles.xml b/plugins/Flutter-Templates/src/main/res/values/styles.xml deleted file mode 100644 index f38efd34..00000000 --- a/plugins/Flutter-Templates/src/main/res/values/styles.xml +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <style name="PluginTheme" parent="Theme.Material3.DayNight.NoActionBar"> - <item name="colorPrimary">@color/plugin_primary</item> - <item name="colorOnPrimary">@color/plugin_on_primary</item> - <item name="colorPrimaryContainer">@color/plugin_primary_container</item> - <item name="colorOnPrimaryContainer">@color/plugin_on_primary_container</item> - <item name="colorSecondary">@color/plugin_secondary</item> - <item name="colorOnSecondary">@color/plugin_on_secondary</item> - <item name="colorSurface">@color/plugin_surface</item> - <item name="colorOnSurface">@color/plugin_on_surface</item> - <item name="colorOnSurfaceVariant">@color/plugin_on_surface_variant</item> - <item name="colorOutline">@color/plugin_outline</item> - <item name="colorOutlineVariant">@color/plugin_outline_variant</item> - </style> -</resources> diff --git a/plugins/README.md b/plugins/README.md index 3b94f915..7d848b85 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -1 +1,344 @@ -Placeholder +# Plugins + +A plugin runs code inside Code on the Go. It is an Android *application* module that packages +into a `.cgp` and installs through the Plugin Manager. + +This file holds everything specific to building one. Repository-wide rules — the branch +policy, the addon check, verification on a device — are in the root `CLAUDE.md`. + +If you want a project template, a snippet or a code action, you are in the wrong directory. +See the table in `CLAUDE.md`. + +--- + +## Common commands + +Build one plugin: + +```sh +cd plugins/Voice-Alerts # or any addon folder +../../gradlew assemblePlugin # release .cgp -> build/plugin/<pluginName>.cgp +../../gradlew assemblePluginDebug # debug variant +``` + +Build every plugin from scratch (after rebuilding libs): + +```sh +./scripts/update-libs.sh # default: github.com/appdevforall/CodeOnTheGo@stage +./scripts/update-libs.sh --ref <branch-or-tag> +./scripts/update-libs.sh --local ../CodeOnTheGo # use an existing checkout instead of cloning +``` + +The script clones CoGo into `.cache/CodeOnTheGo/` on first run, rebuilds both jars, copies +them into `libs/`, then runs `assemblePlugin` for every example. It auto-detects examples by +scanning for `build.gradle.kts` files that apply `com.itsaky.androidide.plugins.build`. + +`local.properties` must contain `sdk.dir=...`. The committed `local.properties` at the repo +root is harmless leftover; each plugin needs its own. + +--- + +## `libs/` is the load-bearing piece + +`libs/` holds **five** jars. Every plugin depends on at least these two: + +- **`plugin-api.jar`** — the IDE-side API surface (`IPlugin`, `PluginContext`, + `BuildStatusListener`, `IdeBuildService`, etc.). Each plugin uses it as `compileOnly` + (provided by the IDE at runtime) AND as `buildscript classpath` so the Gradle plugin can + resolve symbols at configuration time. +- **`gradle-plugin.jar`** — the Gradle plugin with id `com.itsaky.androidide.plugins.build`, + applied by every plugin. It's the output of CoGo's `plugin-api/plugin-builder/` module + (separate from CoGo's `gradle-plugin/` module, which is unrelated despite the name). It + packages the compiled Android library into a `.cgp`. + +There is also **one shared Gradle wrapper at the repo root** (`gradlew` + `gradle/wrapper/`). +New plugins should use it — build them with `cd plugins/<Addon> && ../../gradlew +assemblePlugin` rather than bundling a per-plugin `gradlew`/`gradle/wrapper/` copy. (The five +`AI-*` addons follow this; most older plugins still carry their own local wrapper and can be +migrated opportunistically.) + +An addon under `plugins/` references the shared jars as `../../libs/*.jar`. **Always use the +repo-root `libs/` jars and the repo-root Gradle wrapper — never bundle per-plugin copies.** A +plugin that ships its own `libs/plugin-api.jar` / `libs/gradle-plugin.jar` (e.g. copied from +another plugin) can drift out of sync with the rest of the repo; point `build.gradle.kts` +(`compileOnly`) and `settings.gradle.kts` (buildscript `classpath`) at `../../libs/*.jar` and +delete any local `libs/`. The root `plugin-api.jar` already carries the full API surface +(including `IdeTemplateService`/`CgtTemplateBuilder`), so newer sub-APIs do not justify a +local copy. **A plugin folder is not standalone in isolation** — copy the root `libs/` along +if you move one elsewhere. When CoGo's API changes, refresh via the script above or the +**Update libs from CodeOnTheGo** GitHub Action (which commits the refreshed jars and cuts a +release). Publishing addons is a separate workflow, **Publish addons**, which uploads to +Cloudflare R2. + +--- + +## Credentials: use the host's `KeystoreSecretStore`, never your own crypto + +A plugin that stores a credential encrypts it with +`com.itsaky.androidide.plugins.security.KeystoreSecretStore` from `plugin-api.jar` +(**26.36+** — set `plugin.min_ide_version` accordingly). It is `compileOnly` like the rest of +the API, so there is one implementation in the IDE's process rather than a copy compiled into +each `.cgp`. Do not re-implement AES/GCM in a plugin; three AI plugins each grew a copy that +started to diverge, which is what ADFA-5255 removed. + +Construct it with **this plugin's own alias** (`KeystoreSecretStore(ALIAS)`) as a single +top-level `val` in a `SecureApiKeyStore.kt`/`SecureTokenStore.kt` that holds nothing but the +alias; callers use that instance directly. It takes no log tag — the store logs under its own +name — and that single-argument constructor is the only one a plugin can reach: the +two-argument form takes an `internal` `SecretKeySource`, which is not on the plugin's compile +classpath at all. `ai-agent-mcp`, `ai-agent-gemini` and `ai-agent-openai` are the reference +shape. Do **not** wrap it in an object of forwarding methods — that is just a second copy of +the store's contract to keep in step. The alias must be unique per plugin (all plugins share +the host's UID and Keystore, so a shared alias lets one plugin's invalidated-key recovery +delete another's secret) and must never change across releases. + +`readAndMigrate` returns a four-way `Stored` rather than a nullable String on purpose — each +state needs different advice, and a plugin that collapses them tells a user their credential +was refused when it was never sent: + +- `Absent` — nothing was ever saved. The ordinary first run; say nothing. +- `Value` — the plaintext. Trim it at the call site if your credential format wants it; + `readAndMigrate` migrates verbatim. +- `Unreadable` — stored, but this device's Keystore can no longer open it (a restored backup, + an OEM Keystore reset). Permanent: the user has to enter it again. +- `Unavailable` — the Keystore would not answer this time. **Transient: the credential is + intact, so retry and never re-prompt.** In particular a pane that reads `Unavailable` must + not dress itself as never-configured, and nothing on that screen may write over the + credential it could not read — an empty field then means "not shown", not "removed". + +Handle all four; collapse them only where the caller genuinely has one answer for every state, +and say so in a comment. + +--- + +## Plugin shape + +A plugin is an Android *application* module (despite installing as a library) with: + +1. **`build.gradle.kts`** applies `com.android.application` and + `com.itsaky.androidide.plugins.build`. It must **not** apply + `org.jetbrains.kotlin.android` — AGP 9 compiles Kotlin itself and refuses that plugin + ("already on the classpath with an unknown version"); the Kotlin version is pinned on the + buildscript classpath in `settings.gradle.kts` instead, which is where AGP 9 takes its + compiler from. Configures `pluginBuilder { pluginName = "..." }`. Uses + `compileOnly(files("../../libs/plugin-api.jar"))` — never `implementation`. +2. **`settings.gradle.kts`** declares the jars it needs on the buildscript classpath plus AGP + and Kotlin. +3. **`src/main/AndroidManifest.xml`** declares plugin identity as `<meta-data>` entries on + `<application>`: `plugin.id`, `plugin.name`, `plugin.version` (resolved from + `${pluginVersion}`), `plugin.description`, `plugin.author`, `plugin.main_class`, + `plugin.min_ide_version`, and optional `plugin.permissions`. Optionally + `plugin.vcs_revision` / `plugin.build_timestamp` — see **Build provenance** below. +4. **Main class** implements `com.itsaky.androidide.plugins.IPlugin`. Lifecycle: + `initialize(PluginContext) → activate() → deactivate() → dispose()`. Services are obtained + via `context.services.get(SomeService::class.java)` (e.g. `IdeBuildService` for build + hooks). Android `Context` is `context.androidContext`. + +Available permission strings (declared comma-separated in `plugin.permissions`): +`filesystem.read`, `filesystem.write`, `network.access`, `system.commands`, `ide.settings`, +`project.structure`. + +--- + +## Build provenance (`plugin.vcs_revision`, `cgp-build.properties`) + +Every `.cgp` records the commit it was built from, so a crash report or a support question +traces back to source (ADFA-5256). The builder resolves it once per build and publishes it +three ways: two `<meta-data>` entries, `assets/cgp-build.properties` inside the archive, and +the IDE's plugin details dialog. + +Manifests opt in by referencing the placeholders — the builder never injects `<meta-data>` on +your behalf: + +```xml +<meta-data android:name="plugin.vcs_revision" android:value="${pluginVcsRevision}" /> +<meta-data android:name="plugin.build_timestamp" android:value="${pluginBuildTimestamp}" /> +``` + +**This is a hard build-time coupling to `libs/gradle-plugin.jar`.** A manifest that references +a placeholder the builder does not define fails the manifest merger outright (*"requires a +placeholder substitution but no value ... is provided"*), and all plugins resolve the builder +from the single committed jar. So a manifest may only adopt these **after** the builder change +is merged in CoGo and the **Update libs from CodeOnTheGo** Action has refreshed `libs/`. Never +the other way round. The same coupling hits on-device builders, whose builder jar ships in +`plugin-maven-repo.zip` and refreshes only with a CoGo **app release** — a plugin referencing +these cannot be built inside an older CoGo at all. The builder change (ADFA-5394) first ships +in **26.37**, so every adopting plugin's source is unbuildable on device in 26.36 and earlier, +with only the merger's placeholder error to go on. `plugin.min_ide_version` does not express +this: it gates installing the `.cgp`, not building its source. + +Read the record out of a built artifact: + +```sh +unzip -p <plugin>/build/plugin/<name>.cgp assets/cgp-build.properties +``` + +`revision_source` says how the revision was found, in the order the builder tries: `explicit` +(you set `pluginBuilder { pluginVcsRevision = "..." }`) → `env:<VAR>` +(`PLUGIN_VCS_REVISION`, `GITHUB_SHA`, `CI_COMMIT_SHA`, `GIT_COMMIT`) → `git` → `git-dir` +(reads `.git` directly; this is the on-device path, since CoGo ships JGit in-process and no +`git` binary) → `none`, which means `revision=unknown`. `+dirty` is appended when the plugin's +**own** directory has uncommitted changes; the check is scoped to that directory so a `libs/` +refresh elsewhere in the tree does not flag the build. + +`timestamp` is the committer date of that revision in UTC, not the wall clock, and it lands in +the version string as well (`1.0.0-release.<timestamp>`), so two builds of one commit produce +a `.cgp` whose every entry matches by CRC and mtime (see CoGo's ADR-0012). The archives are +not byte-identical: the v2 APK Signing Block differs between runs, so compare entry CRCs +rather than a file hash. That determinism holds only where the builder could reach a `git` +binary; it falls back to the clock and says so with `timestamp_source=wall-clock`, and the +stamp it puts in the version string then changes on every build, so the artifact is **not** +reproducible even entry-by-entry. On device it is always the fallback — CoGo ships no `git` — +and under `--configuration-cache` the clock reading additionally freezes into the cached +configuration. + +`+dirty` has one systemic cause worth designing against: **a build-time download must land on +a gitignored path.** A `downloadAssets` task that overwrites a git-tracked file +(`ndk-installer` shipped a committed placeholder `ndk-cmake.tar.xz` until it was untracked) +dirties the plugin directory on every build, so every artifact it ever produces records +`+dirty` and no build of that plugin is traceable to a clean commit. Both download plugins now +fetch onto ignored paths (`plugins/NDK-Installer/.gitignore`, +`plugins/AI-Literacy-Course/.gitignore`); keep it that way when adding a new one. + +`libs_revision` records which CoGo commit produced the jars the plugin was compiled against. +The builder cannot see that checkout, so each build path exports `PLUGIN_LIBS_REVISION` first: +`scripts/update-libs.sh` from the CodeOnTheGo checkout it just built, and **Publish addons** +from the subject of the most recent commit touching `libs/` that names one (ordinary commits +touch it too, so it scans back rather than reading only the newest). Compare two artifacts' +`libs_revision` by prefix, not equality: subjects written before `--short=12` carry a +9-character sha. Note that under **Update libs from CodeOnTheGo** the plugin's own `revision` +is the commit *before* the `chore: update libs` commit, because plugins are built before that +commit is created; `libs_revision` is what pins the pairing. + +`scripts/verify-provenance.sh` asserts the record after each `assemblePlugin`: a `.cgp` +missing `assets/cgp-build.properties`, missing any required key, or disagreeing with the +exported `PLUGIN_LIBS_REVISION` fails the run. `revision=unknown`, `+dirty` and +`timestamp_source=wall-clock` warn instead — all three are legitimate off-CI (no `.git`, no +`git` binary). All three workflows call it — **Build addon artifacts** and **Update libs from +CodeOnTheGo** through `scripts/update-libs.sh`, **Publish addons** directly, since it builds +the addons itself. The last one is the one that matters most: those are the artifacts users +install from the gallery. + +--- + +## In-app help wiring (tooltips + Tier 3, `DocumentationExtension`) + +Every plugin with UI implements +`com.itsaky.androidide.plugins.extensions.DocumentationExtension`. This wiring is fixed and +foundational — get **all** of it right or the tooltip renders the literal string **`n/a`** at +runtime. The build stays green and the manifest looks fine, so **only device long-press +testing catches a mistake** (this bit us once). All symbols are in `plugin-api.jar`. + +1. **Category is `"plugin_<pluginId>"` — exactly.** `getTooltipCategory()` MUST return + `"plugin_"` + the full `plugin.id` (e.g. `"plugin_org.appdevforall.projecttotemplate"`). + The host registers your entries under this string **and** derives the same string when + resolving a lookup. Any other value — a short slug, a dotless/underscore form — silently + mismatches → `n/a`. +2. **Entries.** `getTooltipEntries()` returns `PluginTooltipEntry(tag, summary, detail, + buttons)`: `summary` = Tier 1 (one line shown on long-press), `detail` = Tier 2 (HTML + behind "See more"). Keep the `tag` in one shared `const val` used by steps 3–4. +3. **Look tooltips up with the 3-arg overload.** Call + `IdeTooltipService.showTooltip(anchorView, category, tag)` and pass + `category = "plugin_<pluginId>"` explicitly. **Never use the 2-arg `showTooltip(view, + tag)`** — it resolves under a different default category and renders `n/a` even when the + entry is registered correctly. Param order is `(anchorView, category, tag)`. +4. **Attach tags to UI.** Set `tooltipTag = <that same tag>` on every contributed + `NavigationItem` / `TabItem` / menu item / FAB; `EditorTabItem` instead takes a literal + `tooltip = "..."` string. A contributed element with no tooltip fails review clause 6.7. +5. **Tier 3 (offline page).** Override `getTier3DocsAssetPath()` to return an assets subdir + name (convention: `"docs"`), ship real HTML at `src/main/assets/<dir>/index.html` (white + background, black text, English), and link it from an entry via + `PluginTooltipButton(description, uri = "index.html", order = 0)` — leave `directPath` + false (`true` targets the host's shared docs tree, not your bundle). + +Debug a mismatch against the on-device store (`adb root` first): +`sqlite3 /data/data/com.itsaky.androidide/databases/documentation.db "SELECT c.category, +t.tag, substr(t.summary,1,40) FROM Tooltips t JOIN TooltipCategories c ON c.id=t.categoryId +WHERE c.category LIKE 'plugin_%'"`. If the row is present but the tooltip still shows `n/a`, +the bug is the **lookup** (step 1 or 3), not registration. (The unused `ide_tooltip_table` is +a red herring — plugin entries live in `Tooltips` + `TooltipCategories`.) + +--- + +## Convention: AAR metadata checks are disabled + +Most plugins end with: + +```kotlin +tasks.matching { + it.name.contains("checkDebugAarMetadata") || + it.name.contains("checkReleaseAarMetadata") +}.configureEach { enabled = false } +``` + +This is intentional — the `application`-as-library packaging trips those checks. Keep it. + +--- + +## Asset downloads (rare) + +Some plugins (`ndk-installer`, `ai-literacy-course`) register a `downloadAssets` task that +fetches large files at build time with pinned-MD5 verification. These assets are **not +committed to git** — each plugin gitignores its own download paths (`ai-literacy-course` pulls +a ~110 MB course ZIP plus `pdfjs.zip`; `ndk-installer` pulls `ndk-cmake.tar.xz`). Committing +one, even as a placeholder, makes every build dirty — see **Build provenance** above. +`scripts/update-libs.sh` runs `downloadAssets` automatically before `assemblePlugin` when the +build file references it. + +**A bare `./gradlew assemblePlugin` does NOT run `downloadAssets`.** Both download plugins now +fail the asset merge outright when their archives are absent, rather than silently packaging a +broken `.cgp` (a course with no PDF viewer, an NDK-less installer) — that silent packaging is +the failure the guards exist to prevent, so keep one on any new download plugin. When building +such a plugin by hand, run `./gradlew downloadAssets` and then `./gradlew assemblePlugin` as +two separate invocations (or use the script, which does exactly that). Combining them in one +invocation fails: `downloadAssets` declares an output inside `src/main/assets`, which Gradle +sees as an undeclared dependency of `mergeReleaseAssets`. + +--- + +## One-time on-device install markers + +Plugins that extract bundled assets on-device once (currently `ai-literacy-course`, via +`CourseInstaller`) gate the work behind a marker file named from a version constant +(`INSTALL_VERSION` → `.installed-vN`). If the marker for the current version exists, +extraction **and** any post-extraction generation (e.g. `CourseShell.generate()`) are skipped +entirely. + +**Any change to extraction OR post-extraction generation logic must bump `INSTALL_VERSION`.** +Otherwise the change compiles and packages cleanly but has zero effect on existing installs — +they keep the stale extracted tree, and it looks like "my fix didn't work" (costing a device +round-trip). Bumping the constant forces a clean re-extract. On a device with a prior install, +confirm the marker version changed (or wipe plugin data) before concluding a fix works. + +--- + +## Adding a new plugin + +1. Copy `plugins/Random-XKCD/` — it's the canonical starting template (small but complete, + includes the in-IDE help HTML pattern that submissions are expected to follow). Name the + new directory in MixedCase with single hyphens between words (`APK-Analyzer`), ASCII + letters and digits only. Every other name, filename, and URL derives from it — see + `docs/addon-naming-standards.md`. +2. Update **every** copied file that still names the template. Two values come from the + directory name: the **slug** is it lowercased (`apk-analyzer`), the **display name** is it + with hyphens replaced by spaces (`APK Analyzer`). + + | File | Change | + |---|---| + | `settings.gradle.kts` | `rootProject.name` — Gradle's own name for the build. Nothing derives from it; keep it in step with the slug anyway. | + | `build.gradle.kts` | `pluginBuilder { pluginName }` → the slug; `android { namespace, applicationId }` | + | `src/main/AndroidManifest.xml` | `plugin.id`, `plugin.name` → the display name, `plugin.main_class` | + | `random-xkcd.html` | rename to `<slug>.html`; set `<title>` to the display name exactly, and make the `<h1>` contain it | + | `addon.json` | `summary`, `description`, `tags`, `origin`, `license`, `author`. The schema checks the shape, not the words, so a copied one passes every check and puts xkcd's description on your gallery card. | + | `src/main/assets/icon_day.png`, `icon_night.png` | replace both; both must be present | + | `src/main/kotlin/...` | your implementation | + +3. Run `uv run --directory tools/addons addons --root "$PWD" check` from the repository root + before pushing. `--root` must be absolute: `--directory` moves uv into `tools/addons`, so + `--root .` resolves there and finds no addons. It is the same gate `check-toolchain.yml` + runs on every pull request, it costs a second, and it names the exact file and value it + wants. Treat it as the authority — do not restate its rules here, or the two copies drift. +4. Add a row to the root `README.md` Examples table. +5. Nothing else to wire up. Publishing is automatic: `addons discover` finds any directory + whose `build.gradle.kts` applies the plugin-builder, and the name, filenames, and URLs all + derive from the directory name. diff --git a/scripts/build-cgt.sh b/scripts/build-cgt.sh new file mode 100755 index 00000000..61025886 --- /dev/null +++ b/scripts/build-cgt.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# +# build-cgt.sh (ADFA-6252) +# +# Zips a template addon directory into a .cgt bundle. +# +# Usage: +# ./scripts/build-cgt.sh <addon-dir> <output-dir> +# +# Output: +# <output-dir>/<slug>.cgt where <slug> is the addon directory name, lowercased +# +# WHY AN ALLOW LIST: the addon directory holds gallery metadata (addon.json, the HTML page, +# the icons, README.md) beside the template tree, and none of that belongs in a user-facing +# download. An exclude list would fail open -- a metadata file added later would ship until +# somebody remembered to extend the list. templates.json already names every directory that +# belongs in the archive, so the file list is derived from it. A file that is not listed can +# never be packaged, and a path in templates.json that does not exist on disk fails the build +# instead of being skipped in silence by the IDE. +# +# WHY STORED AND 1980: entries are stored (-0) and every mtime is pinned, so one commit always +# produces one archive, byte for byte. Compression happens at the HTTP layer -- Cloudflare +# serves the gallery. This mirrors dev-assets' core.cgt build; see templates/cgt-templates.md. +# +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 <addon-dir> <output-dir>" >&2 + exit 1 +fi + +ADDON_DIR="$1" +OUTPUT_DIR="$2" + +for tool in jq zip; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "Error: $tool is required to build a .cgt." >&2 + exit 1 + fi +done + +if [[ ! -d "$ADDON_DIR" ]]; then + echo "Error: '$ADDON_DIR' is not a directory." >&2 + exit 1 +fi + +if [[ ! -f "$ADDON_DIR/templates.json" ]]; then + echo "Error: '$ADDON_DIR/templates.json' is missing, so this is not a template addon." >&2 + exit 1 +fi + +ADDON_NAME="$(basename "$(cd "$ADDON_DIR" && pwd)")" +SLUG="$(printf '%s' "$ADDON_NAME" | tr '[:upper:]' '[:lower:]')" + +mkdir -p "$OUTPUT_DIR" +OUT="$(cd "$OUTPUT_DIR" && pwd)" +OUT_CGT="$OUT/$SLUG.cgt" +rm -f "$OUT_CGT" + +# The provenance record. Written into the addon directory because it belongs at the archive +# root, and gitignored there. The timestamp is the committer date of the revision, never the +# wall clock: a clock reading would change on every build and end the determinism above. +write_provenance() { + local dir="$1" revision revision_source timestamp timestamp_source addon_id version + + revision="" + revision_source="none" + for var in PLUGIN_VCS_REVISION GITHUB_SHA CI_COMMIT_SHA GIT_COMMIT; do + if [[ -n "${!var:-}" ]]; then + revision="${!var}" + revision_source="env:$var" + break + fi + done + if [[ -z "$revision" ]] && command -v git >/dev/null 2>&1 \ + && git -C "$dir" rev-parse --git-dir >/dev/null 2>&1; then + revision="$(git -C "$dir" rev-parse HEAD)" + revision_source="git" + fi + revision="${revision:0:12}" + [[ -n "$revision" ]] || revision="unknown" + + # Scoped to this addon's own directory, so a change elsewhere in the tree -- a libs/ + # refresh, another addon -- does not mark this artifact dirty. + # The pathspec is "." and not "$dir": with -C the pathspec already resolves against that + # directory, so passing the path again looked for <dir>/<dir>, matched nothing, and no + # build was ever reported dirty. + if command -v git >/dev/null 2>&1 && git -C "$dir" rev-parse --git-dir >/dev/null 2>&1; then + if [[ -n "$(git -C "$dir" status --porcelain -- . 2>/dev/null)" ]]; then + revision="${revision}+dirty" + fi + fi + + timestamp="" + timestamp_source="wall-clock" + if [[ "$revision_source" != "none" ]] && command -v git >/dev/null 2>&1 \ + && git -C "$dir" rev-parse --git-dir >/dev/null 2>&1; then + timestamp="$(git -C "$dir" show -s --format=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ HEAD 2>/dev/null || true)" + [[ -n "$timestamp" ]] && timestamp_source="git" + fi + if [[ -z "$timestamp" ]]; then + timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + fi + + addon_id="$(jq -r '.template.id // ""' "$dir/addon.json" 2>/dev/null || true)" + version="$(jq -r '.template.version // ""' "$dir/addon.json" 2>/dev/null || true)" + + cat > "$dir/cgt-build.properties" <<EOF +revision=$revision +revision_source=$revision_source +timestamp=$timestamp +timestamp_source=$timestamp_source +addon_id=$addon_id +version=$version +EOF +} + +write_provenance "$ADDON_DIR" + +export TZ=UTC +( + cd "$ADDON_DIR" + + # Fails loudly when templates.json names a path that is not on disk: find exits non-zero + # and set -e ends the build. The IDE would skip such an entry without an error. + mapfile -t paths < <(jq -r '.templates[].path' templates.json) + if [[ "${#paths[@]}" -eq 0 ]]; then + echo "Error: templates.json lists no templates." >&2 + exit 1 + fi + + find cgt-build.properties templates.json "${paths[@]}" -type f \ + -exec touch -t 198001010000 {} + + find cgt-build.properties templates.json "${paths[@]}" -type f \ + | LC_ALL=C sort \ + | zip -0 -D -X -q "$OUT_CGT" -@ +) + +echo "Wrote $OUT_CGT ($(wc -c < "$OUT_CGT" | tr -d ' ') bytes)" diff --git a/scripts/verify-provenance.sh b/scripts/verify-provenance.sh index 22cf555d..61deae23 100755 --- a/scripts/verify-provenance.sh +++ b/scripts/verify-provenance.sh @@ -26,6 +26,64 @@ fi REQUIRED_PROVENANCE_KEYS=(name version variant revision revision_source timestamp timestamp_source) +# A template addon ships a .cgt, not a .cgp (ADFA-6252). Its record sits at the archive +# root rather than under assets/ (a .cgt has no assets/ directory), and the key set differs: +# there is no Android build `variant`, and no `libs_revision` because a template compiles +# against nothing. Everything else -- the required-key sweep, and the unknown/+dirty/wall-clock +# warnings -- means the same for either artifact. +REQUIRED_CGT_PROVENANCE_KEYS=(revision revision_source timestamp timestamp_source addon_id version) +CGT_PROVENANCE_ENTRY="cgt-build.properties" + +verify_cgt_provenance() { + local addon="$1" cgt props key value revision revision_source timestamp_source + + cgt="$REPO_ROOT/$addon/build/plugin/$(basename "$addon" | tr '[:upper:]' '[:lower:]').cgt" + if [ ! -f "$cgt" ]; then + # The publish workflow builds into dist/, not into the addon directory, so accept + # either. Naming both paths matters: "no .cgt" otherwise reads as a build failure + # when it is only a different output directory. + cgt="$REPO_ROOT/dist/$(basename "$addon" | tr '[:upper:]' '[:lower:]').cgt" + fi + if [ ! -f "$cgt" ]; then + echo "Error: $addon produced no .cgt under build/plugin/ or dist/." >&2 + return 1 + fi + + if ! props="$(unzip -p "$cgt" "$CGT_PROVENANCE_ENTRY" 2>/dev/null)" || [ -z "$props" ]; then + echo "Error: $(basename "$cgt") does not contain $CGT_PROVENANCE_ENTRY." >&2 + echo " scripts/build-cgt.sh either no longer writes the provenance record or no" >&2 + echo " longer includes it in the archive." >&2 + return 1 + fi + + for key in "${REQUIRED_CGT_PROVENANCE_KEYS[@]}"; do + value="$(printf '%s\n' "$props" | sed -n "s/^${key}=//p" | head -n1)" + if [ -z "$value" ]; then + echo "Error: $(basename "$cgt") provenance record has no '$key' value." >&2 + printf '%s\n' "$props" | sed 's/^/ /' >&2 + return 1 + fi + done + + revision="$(printf '%s\n' "$props" | sed -n 's/^revision=//p' | head -n1)" + revision_source="$(printf '%s\n' "$props" | sed -n 's/^revision_source=//p' | head -n1)" + timestamp_source="$(printf '%s\n' "$props" | sed -n 's/^timestamp_source=//p' | head -n1)" + + if [ "$revision" = "unknown" ] || [ "$revision_source" = "none" ]; then + echo "Warning: $addon recorded revision=unknown — the .cgt cannot be traced to a commit." >&2 + fi + case "$revision" in + *+dirty) + echo "Warning: $addon was built from a dirty $addon/ directory, recorded as '$revision'." >&2 + ;; + esac + if [ "$timestamp_source" = "wall-clock" ]; then + echo "Warning: $addon stamped a wall-clock timestamp — this .cgt is not reproducible." >&2 + fi + + echo " provenance: revision=$revision ($revision_source) timestamp_source=$timestamp_source" +} + verify_provenance() { local plugin="$1" local cgp props key value revision revision_source timestamp_source recorded_libs @@ -102,5 +160,11 @@ if [ "$#" -eq 0 ]; then fi for plugin in "$@"; do - verify_provenance "$plugin" + # templates.json is the same marker discover.py keys on, so the two agree on + # what a template is without either restating the rule. + if [ -f "$REPO_ROOT/$plugin/templates.json" ]; then + verify_cgt_provenance "$plugin" + else + verify_provenance "$plugin" + fi done diff --git a/site/app.js b/site/app.js index 2e4eae01..8dc456c0 100644 --- a/site/app.js +++ b/site/app.js @@ -131,11 +131,19 @@ function render() { node.querySelector(".icon-dark").srcset = safeUrl(addon.iconDarkUrl); node.querySelector('[data-slot="download"]').href = safeUrl(addon.download.url); node.querySelector('[data-slot="page"]').href = safeUrl(addon.pageUrl); - // the tarball is the source deliverable (R39); sourceUrl stays in the - // catalog as provenance for consumers that want the repository + // For a plugin the tarball is the source deliverable (R39); sourceUrl stays + // in the catalog as provenance for consumers that want the repository. + // A template has no tarball: its .cgt is plain text, so the download is + // already the source (ADFA-6252, schemaVersion 2 made the field optional). + // Reading it unguarded threw a TypeError here, and because this runs inside + // the card loop one such entry stopped the whole gallery rendering. const src = node.querySelector('[data-slot="source"]'); - src.href = safeUrl(addon.sourceTarball.url); - src.title = `Source tarball, ${size(addon.sourceTarball.size)}`; + if (addon.sourceTarball) { + src.href = safeUrl(addon.sourceTarball.url); + src.title = `Source tarball, ${size(addon.sourceTarball.size)}`; + } else { + src.remove(); + } cards.append(node); } renderActive(); diff --git a/site/catalog.schema.json b/site/catalog.schema.json index bddb7e30..1b776ba2 100644 --- a/site/catalog.schema.json +++ b/site/catalog.schema.json @@ -10,7 +10,7 @@ ], "properties": { "schemaVersion": { - "const": 1 + "const": 2 }, "generated": { "type": "string" @@ -29,7 +29,7 @@ "required": [ "type", "slug", - "pluginId", + "addonId", "name", "version", "summary", @@ -43,8 +43,7 @@ "iconDarkUrl", "pageUrl", "sourceUrl", - "download", - "sourceTarball" + "download" ], "properties": { "type": { @@ -59,7 +58,7 @@ "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, - "pluginId": { + "addonId": { "type": "string", "minLength": 1 }, @@ -155,5 +154,5 @@ } } }, - "$comment": "Additive by design: section 10.5 permits new fields within a major version and requires consumers to ignore unknown ones, so this schema must not reject them. Field removal or retyping needs a new major." + "$comment": "Additive by design: section 10.5 permits new fields within a major version and requires consumers to ignore unknown ones, so this schema must not reject them. Field removal or retyping needs a new major. Version 2 (ADFA-6252) made two such changes: pluginId became addonId, because the catalog carries four addon types and the name fitted only one; and sourceTarball left the required list, because a template ships none - its .cgt is plain text throughout, so the download already is the source, and sourceUrl still names the directory. A consumer reading a v2 catalog must treat sourceTarball as optional." } diff --git a/templates/Flutter-Templates/.gitignore b/templates/Flutter-Templates/.gitignore new file mode 100644 index 00000000..73042af8 --- /dev/null +++ b/templates/Flutter-Templates/.gitignore @@ -0,0 +1,4 @@ +# The bundle and its provenance record are generated by the publish workflow. +# Neither is ever committed — see templates/README.md, "Build provenance". +*.cgt +cgt-build.properties diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/README.md.peb b/templates/Flutter-Templates/FlutterBasic/README.md.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/README.md.peb rename to templates/Flutter-Templates/FlutterBasic/README.md.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/analysis_options.yaml b/templates/Flutter-Templates/FlutterBasic/analysis_options.yaml similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/analysis_options.yaml rename to templates/Flutter-Templates/FlutterBasic/analysis_options.yaml diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/lib/main.dart.peb b/templates/Flutter-Templates/FlutterBasic/lib/main.dart.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/lib/main.dart.peb rename to templates/Flutter-Templates/FlutterBasic/lib/main.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/pubspec.yaml.peb b/templates/Flutter-Templates/FlutterBasic/pubspec.yaml.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/pubspec.yaml.peb rename to templates/Flutter-Templates/FlutterBasic/pubspec.yaml.peb diff --git a/templates/Flutter-Templates/FlutterBasic/template/template.json b/templates/Flutter-Templates/FlutterBasic/template/template.json new file mode 100644 index 00000000..1adbd897 --- /dev/null +++ b/templates/Flutter-Templates/FlutterBasic/template/template.json @@ -0,0 +1,13 @@ +{ + "name": "Flutter Basic", + "description": "A minimal Flutter starter app with a counter screen", + "version": "1.0.0", + "tooltipTag": "template.flutter.basic", + "parameters": { + "required": { + "appName": { "identifier": "APP_NAME" }, + "packageName": { "identifier": "PACKAGE_NAME" }, + "saveLocation": { "identifier": "SAVE_LOCATION" } + } + } +} diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/template/thumb.png b/templates/Flutter-Templates/FlutterBasic/template/thumb.png similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/template/thumb.png rename to templates/Flutter-Templates/FlutterBasic/template/thumb.png diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/README.md.peb b/templates/Flutter-Templates/FlutterBloc/README.md.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/README.md.peb rename to templates/Flutter-Templates/FlutterBloc/README.md.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/analysis_options.yaml b/templates/Flutter-Templates/FlutterBloc/analysis_options.yaml similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/analysis_options.yaml rename to templates/Flutter-Templates/FlutterBloc/analysis_options.yaml diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/lib/blocs/counter_bloc.dart.peb b/templates/Flutter-Templates/FlutterBloc/lib/blocs/counter_bloc.dart.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/lib/blocs/counter_bloc.dart.peb rename to templates/Flutter-Templates/FlutterBloc/lib/blocs/counter_bloc.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/lib/main.dart.peb b/templates/Flutter-Templates/FlutterBloc/lib/main.dart.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/lib/main.dart.peb rename to templates/Flutter-Templates/FlutterBloc/lib/main.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/pubspec.yaml.peb b/templates/Flutter-Templates/FlutterBloc/pubspec.yaml.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/pubspec.yaml.peb rename to templates/Flutter-Templates/FlutterBloc/pubspec.yaml.peb diff --git a/templates/Flutter-Templates/FlutterBloc/template/template.json b/templates/Flutter-Templates/FlutterBloc/template/template.json new file mode 100644 index 00000000..fee7ae7b --- /dev/null +++ b/templates/Flutter-Templates/FlutterBloc/template/template.json @@ -0,0 +1,13 @@ +{ + "name": "Flutter BLoC", + "description": "A Flutter app wired with the BLoC state-management pattern", + "version": "1.0.0", + "tooltipTag": "template.flutter.bloc", + "parameters": { + "required": { + "appName": { "identifier": "APP_NAME" }, + "packageName": { "identifier": "PACKAGE_NAME" }, + "saveLocation": { "identifier": "SAVE_LOCATION" } + } + } +} diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/template/thumb.png b/templates/Flutter-Templates/FlutterBloc/template/thumb.png similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/template/thumb.png rename to templates/Flutter-Templates/FlutterBloc/template/thumb.png diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/README.md.peb b/templates/Flutter-Templates/FlutterGetx/README.md.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/README.md.peb rename to templates/Flutter-Templates/FlutterGetx/README.md.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/analysis_options.yaml b/templates/Flutter-Templates/FlutterGetx/analysis_options.yaml similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/analysis_options.yaml rename to templates/Flutter-Templates/FlutterGetx/analysis_options.yaml diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/lib/controllers/counter_controller.dart.peb b/templates/Flutter-Templates/FlutterGetx/lib/controllers/counter_controller.dart.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/lib/controllers/counter_controller.dart.peb rename to templates/Flutter-Templates/FlutterGetx/lib/controllers/counter_controller.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/lib/main.dart.peb b/templates/Flutter-Templates/FlutterGetx/lib/main.dart.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/lib/main.dart.peb rename to templates/Flutter-Templates/FlutterGetx/lib/main.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/pubspec.yaml.peb b/templates/Flutter-Templates/FlutterGetx/pubspec.yaml.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/pubspec.yaml.peb rename to templates/Flutter-Templates/FlutterGetx/pubspec.yaml.peb diff --git a/templates/Flutter-Templates/FlutterGetx/template/template.json b/templates/Flutter-Templates/FlutterGetx/template/template.json new file mode 100644 index 00000000..9ba5cd38 --- /dev/null +++ b/templates/Flutter-Templates/FlutterGetx/template/template.json @@ -0,0 +1,13 @@ +{ + "name": "Flutter GetX", + "description": "A Flutter app wired with the GetX state-management pattern", + "version": "1.0.0", + "tooltipTag": "template.flutter.getx", + "parameters": { + "required": { + "appName": { "identifier": "APP_NAME" }, + "packageName": { "identifier": "PACKAGE_NAME" }, + "saveLocation": { "identifier": "SAVE_LOCATION" } + } + } +} diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/template/thumb.png b/templates/Flutter-Templates/FlutterGetx/template/thumb.png similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/template/thumb.png rename to templates/Flutter-Templates/FlutterGetx/template/thumb.png diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/README.md.peb b/templates/Flutter-Templates/FlutterProvider/README.md.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/README.md.peb rename to templates/Flutter-Templates/FlutterProvider/README.md.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/analysis_options.yaml b/templates/Flutter-Templates/FlutterProvider/analysis_options.yaml similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/analysis_options.yaml rename to templates/Flutter-Templates/FlutterProvider/analysis_options.yaml diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/lib/main.dart.peb b/templates/Flutter-Templates/FlutterProvider/lib/main.dart.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/lib/main.dart.peb rename to templates/Flutter-Templates/FlutterProvider/lib/main.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/lib/providers/counter_provider.dart.peb b/templates/Flutter-Templates/FlutterProvider/lib/providers/counter_provider.dart.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/lib/providers/counter_provider.dart.peb rename to templates/Flutter-Templates/FlutterProvider/lib/providers/counter_provider.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/pubspec.yaml.peb b/templates/Flutter-Templates/FlutterProvider/pubspec.yaml.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/pubspec.yaml.peb rename to templates/Flutter-Templates/FlutterProvider/pubspec.yaml.peb diff --git a/templates/Flutter-Templates/FlutterProvider/template/template.json b/templates/Flutter-Templates/FlutterProvider/template/template.json new file mode 100644 index 00000000..686bcfbc --- /dev/null +++ b/templates/Flutter-Templates/FlutterProvider/template/template.json @@ -0,0 +1,13 @@ +{ + "name": "Flutter Provider", + "description": "A Flutter app wired with the Provider state-management pattern", + "version": "1.0.0", + "tooltipTag": "template.flutter.provider", + "parameters": { + "required": { + "appName": { "identifier": "APP_NAME" }, + "packageName": { "identifier": "PACKAGE_NAME" }, + "saveLocation": { "identifier": "SAVE_LOCATION" } + } + } +} diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/template/thumb.png b/templates/Flutter-Templates/FlutterProvider/template/thumb.png similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/template/thumb.png rename to templates/Flutter-Templates/FlutterProvider/template/thumb.png diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/README.md.peb b/templates/Flutter-Templates/FlutterRiverpod/README.md.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/README.md.peb rename to templates/Flutter-Templates/FlutterRiverpod/README.md.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/analysis_options.yaml b/templates/Flutter-Templates/FlutterRiverpod/analysis_options.yaml similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/analysis_options.yaml rename to templates/Flutter-Templates/FlutterRiverpod/analysis_options.yaml diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/lib/main.dart.peb b/templates/Flutter-Templates/FlutterRiverpod/lib/main.dart.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/lib/main.dart.peb rename to templates/Flutter-Templates/FlutterRiverpod/lib/main.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/lib/providers/counter_provider.dart.peb b/templates/Flutter-Templates/FlutterRiverpod/lib/providers/counter_provider.dart.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/lib/providers/counter_provider.dart.peb rename to templates/Flutter-Templates/FlutterRiverpod/lib/providers/counter_provider.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/pubspec.yaml.peb b/templates/Flutter-Templates/FlutterRiverpod/pubspec.yaml.peb similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/pubspec.yaml.peb rename to templates/Flutter-Templates/FlutterRiverpod/pubspec.yaml.peb diff --git a/templates/Flutter-Templates/FlutterRiverpod/template/template.json b/templates/Flutter-Templates/FlutterRiverpod/template/template.json new file mode 100644 index 00000000..5e94edfa --- /dev/null +++ b/templates/Flutter-Templates/FlutterRiverpod/template/template.json @@ -0,0 +1,13 @@ +{ + "name": "Flutter Riverpod", + "description": "A Flutter app wired with the Riverpod state-management pattern", + "version": "1.0.0", + "tooltipTag": "template.flutter.riverpod", + "parameters": { + "required": { + "appName": { "identifier": "APP_NAME" }, + "packageName": { "identifier": "PACKAGE_NAME" }, + "saveLocation": { "identifier": "SAVE_LOCATION" } + } + } +} diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/template/thumb.png b/templates/Flutter-Templates/FlutterRiverpod/template/thumb.png similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/template/thumb.png rename to templates/Flutter-Templates/FlutterRiverpod/template/thumb.png diff --git a/templates/Flutter-Templates/README.md b/templates/Flutter-Templates/README.md new file mode 100644 index 00000000..1b25d11d --- /dev/null +++ b/templates/Flutter-Templates/README.md @@ -0,0 +1,68 @@ +# Flutter Templates + +A [Code on the Go](https://github.com/appdevforall/CodeOnTheGo) template bundle that adds +**Flutter starter projects** to the New Project screen, alongside the built-in core templates. + +It contributes five templates, one per state-management approach: + +| Template | Directory | State management | +|---|---|---| +| Flutter Basic | `FlutterBasic/` | none (plain `setState`) | +| Flutter BLoC | `FlutterBloc/` | `flutter_bloc` | +| Flutter Provider | `FlutterProvider/` | `provider` | +| Flutter GetX | `FlutterGetx/` | `get` | +| Flutter Riverpod | `FlutterRiverpod/` | `flutter_riverpod` | + +Each generates a small, idiomatic Flutter project (a counter app) with `pubspec.yaml`, `lib/` +and `analysis_options.yaml`, substituting the app name and package name entered in the New +Project dialog. + +## How it works + +There is no code. The bundle is a `.cgt` file: a zip of the five directories plus +`templates.json`. Code on the Go reads it directly, so the templates appear in **New Project** +once the file is in the Templates manager or the Downloads folder. Nothing to enable, no +permissions. + +Files ending in `.peb` are rendered by Pebble at project-creation time and lose the suffix; +everything else is copied byte for byte. These templates use three expressions only: +`${{APP_NAME}}`, `${{APP_NAME | lower}}` and `${{PACKAGE_NAME}}`. + +## Building + +The `.cgt` is **never committed**. The publish workflow generates it. To build one locally: + +```sh +./scripts/build-cgt.sh templates/Flutter-Templates out +unzip -l out/flutter-templates.cgt +``` + +See [`../README.md`](../README.md) for authoring guidance and +[`../cgt-templates.md`](../cgt-templates.md) for the format. + +## This replaces the Flutter Templates plugin + +Until ADFA-6252 these templates shipped as a plugin that built a `.cgt` on the device at +`activate()` and registered it. The plugin is gone; the skeletons are unchanged. + +If a device still has the old plugin installed, **disable or uninstall it before adding this +bundle**. Both register the same five templates from unlinked sources, so each would appear +twice. Disabling the plugin calls its `deactivate()`, which removes its copies. + +## Note on the Flutter SDK + +This bundle **scaffolds project files only** — it does not install the Flutter or Dart SDK. +Code on the Go does not yet ship an on-device Flutter toolchain, so building or running a +generated project needs Flutter on a separate machine. Enter an app name that is a valid Dart +package identifier (lowercase, no spaces — underscores are fine); it is lower-cased for the +`name:` field in `pubspec.yaml`. + +## Credit + +Original idea by **Raju Kumar** +([ADFA-2599](https://appdevforall.atlassian.net/browse/ADFA-2599)). + +Contributed by **RJ Ali** <rjali3232@gmail.com> via the Code on the Go community +submission process, rebuilt on the IDE's template system +([ADFA-3857](https://appdevforall.atlassian.net/browse/ADFA-3857)), and converted to a +template bundle in [ADFA-6252](https://appdevforall.atlassian.net/browse/ADFA-6252). diff --git a/plugins/Flutter-Templates/addon.json b/templates/Flutter-Templates/addon.json similarity index 53% rename from plugins/Flutter-Templates/addon.json rename to templates/Flutter-Templates/addon.json index 593387c9..adb9f35c 100644 --- a/plugins/Flutter-Templates/addon.json +++ b/templates/Flutter-Templates/addon.json @@ -1,6 +1,6 @@ { "summary": "Adds five Flutter starter projects to the New Project screen.", - "description": "Installs Flutter project templates so you can start a Flutter app from the New Project screen instead of building the structure by hand. Contributed to Code on the Go by RJ Ali, from an original idea by Raju Kumar.", + "description": "Installs Flutter project templates so you can start a Flutter app from the New Project screen instead of building the structure by hand. Five starters, one for each state-management approach: plain setState, BLoC, Provider, GetX and Riverpod. Contributed to Code on the Go by RJ Ali, from an original idea by Raju Kumar.", "tags": [ "flutter", "templates", @@ -11,5 +11,10 @@ "author": { "name": "RJ Ali", "url": "" + }, + "template": { + "id": "org.appdevforall.fluttertemplates", + "version": "1.0.0", + "minAppVersion": "26.38" } } diff --git a/plugins/Flutter-Templates/flutter-templates.html b/templates/Flutter-Templates/flutter-templates.html similarity index 64% rename from plugins/Flutter-Templates/flutter-templates.html rename to templates/Flutter-Templates/flutter-templates.html index 8a0e6b1b..2ad447c3 100644 --- a/plugins/Flutter-Templates/flutter-templates.html +++ b/templates/Flutter-Templates/flutter-templates.html @@ -87,7 +87,7 @@ <header> <h1>Flutter Templates</h1> <p class="subtitle">Adds Flutter starter project templates to Code on the Go's New Project screen</p> - <div class="meta">Version 2.0.0 · Author: RJ Ali · Package: <code>com.ali.fluttertemplate</code></div> + <div class="meta">Version 1.0.0 · Author: RJ Ali · Template bundle: <code>org.appdevforall.fluttertemplates</code></div> </header> <nav> @@ -106,21 +106,21 @@ <h2>Contents</h2> <section id="overview"> <h2>1. Executive Overview</h2> <p> - Flutter Template is a Code on the Go plugin that adds ready-to-use <strong>Flutter starter - project templates</strong> to the IDE's New Project screen, sitting alongside the built-in - core (Android) templates. Instead of starting from an empty folder, a developer picks a - Flutter template, enters an app name and package name, and the IDE generates a complete, - idiomatic Flutter project skeleton. + Flutter Templates is a <strong>template bundle</strong> for Code on the Go. It adds + ready-to-use <strong>Flutter starter projects</strong> to the New Project screen, sitting + alongside the built-in core (Android) templates. Instead of starting from an empty folder, + a developer picks a Flutter template, enters an app name and package name, and the IDE + generates a complete, idiomatic Flutter project skeleton. </p> <p> - The plugin contributes five templates — one for a plain Flutter app and four wired to + The bundle contributes five templates — one for a plain Flutter app and four wired to popular state-management libraries — so a project starts with the chosen architecture already in place. </p> <p> - It is a <strong>headless installer</strong>: it has no screens of its own. When enabled, it - registers its templates with the IDE through the public <code>plugin-api</code> template - service; when disabled, it removes them cleanly. + It runs no code. A <code>.cgt</code> bundle is a zip archive of project skeletons and their + metadata, which the IDE reads directly. There is nothing to enable, no permissions to + grant, and no process to load. </p> </section> @@ -167,62 +167,59 @@ <h3>Parameter substitution</h3> <section id="architecture"> <h2>3. Technical Architecture</h2> - <h3>Template system integration</h3> + <h3>What is in the archive</h3> <p> - Code on the Go discovers project templates by scanning a templates directory for <code>.cgt</code> - archives. A plugin adds its own by calling <code>IdeTemplateService.registerTemplate()</code>, - which places the archive where the IDE will find it and reloads the template list — so the - plugin's templates appear next to the core ones with no host changes. + A <code>.cgt</code> is a zip archive. Its root holds <code>templates.json</code>, which lists + the templates, and one directory per template. Each of those carries + <code>template/template.json</code> for its name, description and parameters, and + <code>template/thumb.png</code> for the picture on the New Project screen. </p> + <div class="diagram"> + flutter-templates.cgt + | + +-- templates.json lists the five templates + +-- cgt-build.properties the commit this bundle was built from + | + +-- FlutterBasic/ + | +-- template/template.json + | +-- template/thumb.png + | +-- pubspec.yaml.peb + | +-- analysis_options.yaml + | +-- lib/main.dart.peb + | + +-- FlutterBloc/ ... and three more</div> <h3>Authoring with Pebble</h3> <p> - Each template is authored as a project skeleton bundled in the plugin's assets. Files ending in - <code>.peb</code> are processed by the Pebble template engine at project-creation time - (<code>${{APP_NAME}}</code>, <code>${{PACKAGE_NAME}}</code> and friends are substituted); other - files are copied verbatim. This is the same authoring format the core templates use. Dart's own - <code>$variable</code> string interpolation passes through untouched because it does not match - Pebble's <code>${{ }}</code> delimiter. + Files ending in <code>.peb</code> are processed by the Pebble template engine at + project-creation time (<code>${{APP_NAME}}</code>, <code>${{PACKAGE_NAME}}</code> and friends + are substituted) and the suffix is dropped; other files are copied verbatim. This is the same + authoring format the core Android templates use. Dart's own <code>$variable</code> string + interpolation passes through untouched, because it does not match Pebble's + <code>${{ }}</code> delimiter. </p> - <h3>Lifecycle</h3> - <div class="diagram"> - initialize(context) - | acquire IdeTemplateService + IdeFileService - v - activate() - | for each of the 5 variants: - | createTemplateBuilder(name) - | .description(...).showPackageNameOption() - | .thumbnailFromAssets(...) - | .addStaticFromAssets(each bundled file) - | build() -> .cgt -> registerTemplate(.cgt) - v - (templates now visible on the New Project screen) - | - v - deactivate() - | unregisterTemplate(name) + delete staged .cgt for each - v - dispose() -> release service references</div> + <h3>How the IDE finds it</h3> + <p> + Code on the Go discovers project templates by scanning its templates directory and the + Downloads folder for <code>.cgt</code> archives. Putting the file in either place is the whole + installation. Nothing registers it, because there is no code to run. + </p> - <h3>Extension interfaces & services</h3> - <table> - <thead> - <tr><th>Type</th><th>Role</th></tr> - </thead> - <tbody> - <tr><td><code>IPlugin</code></td><td>Lifecycle: <code>initialize</code> / <code>activate</code> / <code>deactivate</code> / <code>dispose</code>.</td></tr> - <tr><td><code>IdeTemplateService</code></td><td>Build and register/unregister the <code>.cgt</code> templates.</td></tr> - <tr><td><code>IdeFileService</code></td><td>Delete staged template files on deactivation.</td></tr> - </tbody> - </table> + <h3>Provenance</h3> + <p> + Every published bundle carries <code>cgt-build.properties</code> at its archive root, + recording the commit it was built from and the date of that commit. Read it with + <code>unzip -p flutter-templates.cgt cgt-build.properties</code>. It exists so a bug report + can be traced to the exact source that produced the file, which matters because the download + URL stays the same across releases. + </p> <h3>Permissions</h3> <p> - The plugin requests only <code>filesystem.read</code> and <code>filesystem.write</code> — - what the template service needs to stage and register archives. No network, no native code, no - system commands. + None. A template bundle declares no permissions, because it runs no code. No network, no + native code, no system commands, no filesystem access of its own — the IDE writes the + generated project, exactly as it does for a built-in template. </p> </section> @@ -231,13 +228,19 @@ <h2>4. Usage</h2> <h3>Install</h3> <ol> - <li>Build the plugin: <code>cd plugins/Flutter-Templates && ./gradlew clean assemblePlugin</code> - (produces <code>build/plugin/flutter-templates.cgp</code>).</li> - <li>In Code on the Go, open <em>Preferences → Plugin Manager → +</em> and select the - <code>flutter-templates.cgp</code> file.</li> - <li>Enable the plugin. Its templates register automatically.</li> + <li>Download <code>flutter-templates.cgt</code> from the add-on gallery.</li> + <li>Open the <em>Templates manager</em> in Code on the Go. It scans the Downloads folder, so + the bundle appears with its five templates listed.</li> + <li>That is all. There is nothing to enable.</li> </ol> + <div class="callout"> + <strong>Replacing the old plugin.</strong> Earlier releases shipped these templates as a + plugin. If you still have <em>Flutter Templates</em> installed in the Plugin Manager, disable + or uninstall it before adding this bundle. Otherwise each template appears twice, once from + each source. Disabling the plugin removes its copies. + </div> + <h3>Create a project</h3> <ol> <li>Open <strong>New Project</strong>.</li> @@ -263,17 +266,19 @@ <h2>5. Key Benefits</h2> start from working, idiomatic boilerplate.</li> <li><strong>Parameterized.</strong> App name and package name flow into the generated files, including a Dart-valid <code>pubspec.yaml</code> name.</li> - <li><strong>Clean and reversible.</strong> A headless installer that registers on enable and - fully unregisters on disable — no leftover state.</li> - <li><strong>Plugin-API only.</strong> Built entirely on the stable <code>plugin-api</code> - contract, using the same Pebble template format as the built-in templates.</li> + <li><strong>No code, no permissions.</strong> A data bundle the IDE reads. Nothing loads into + the editor's process, so there is nothing to grant and nothing to go wrong at runtime.</li> + <li><strong>Plain text throughout.</strong> Every template file is readable and reviewable. + The download is the source.</li> + <li><strong>Traceable.</strong> Each published bundle records the commit it was built from, so + a report about one download points at one commit.</li> </ul> </section> <section id="limitations"> <h2>6. Limitations & Scope</h2> <ul> - <li>The plugin <strong>scaffolds project files only</strong>; it does not install the + <li>The bundle <strong>scaffolds project files only</strong>; it does not install the Flutter/Dart SDK or build/run Flutter projects on-device.</li> <li>Code on the Go's editor does not currently provide Dart syntax highlighting, completion, or analysis — these templates create the files, but rich Dart editing is outside the IDE's @@ -290,11 +295,15 @@ <h2>7. Credits</h2> <li><strong>Author / implementation:</strong> RJ Ali <rjali3232@gmail.com>, contributed via the Code on the Go community submission process and rebuilt on the IDE's template system (<a href="https://appdevforall.atlassian.net/browse/ADFA-3857">ADFA-3857</a>).</li> + <li><strong>Converted to a template bundle:</strong> + <a href="https://appdevforall.atlassian.net/browse/ADFA-6252">ADFA-6252</a>. The template + skeletons are RJ Ali's work, unchanged; only the packaging moved from a plugin to a + <code>.cgt</code>.</li> </ul> </section> <footer> - Flutter Templates Documentation · Version 2.0.0 · com.ali.fluttertemplate + Flutter Templates Documentation · Version 1.0.0 · org.appdevforall.fluttertemplates </footer> </body> diff --git a/plugins/Flutter-Templates/src/main/assets/icon_day.png b/templates/Flutter-Templates/icon_day.png similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/icon_day.png rename to templates/Flutter-Templates/icon_day.png diff --git a/plugins/Flutter-Templates/src/main/assets/icon_night.png b/templates/Flutter-Templates/icon_night.png similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/icon_night.png rename to templates/Flutter-Templates/icon_night.png diff --git a/templates/Flutter-Templates/templates.json b/templates/Flutter-Templates/templates.json new file mode 100644 index 00000000..6b8b3fdc --- /dev/null +++ b/templates/Flutter-Templates/templates.json @@ -0,0 +1,9 @@ +{ + "templates": [ + { "path": "FlutterBasic" }, + { "path": "FlutterBloc" }, + { "path": "FlutterProvider" }, + { "path": "FlutterGetx" }, + { "path": "FlutterRiverpod" } + ] +} diff --git a/templates/README.md b/templates/README.md index 3b94f915..8f2d24b6 100644 --- a/templates/README.md +++ b/templates/README.md @@ -1 +1,368 @@ -Placeholder +# Project templates + +A template adds an entry to the **New Project** screen in Code on the Go. It ships as a +`.cgt` file: a ZIP that holds a project skeleton plus a small metadata file. One `.cgt` can +hold several templates. + +A template is not a plugin. It runs no code. It has no `build.gradle.kts`, no +`AndroidManifest.xml` and no Kotlin. If your idea needs to run code, write a plugin instead — +see `../plugins/` and the repository `CLAUDE.md`. + +For the format itself, read [`cgt-templates.md`](cgt-templates.md). + +## Start from `Flutter-Templates` + +`templates/Flutter-Templates/` is the reference. Copy it and change what is yours. It is small +but complete: five templates in one bundle, an `addon.json` with the `template` block, a +gallery page, both icons, and thumbnails. Every rule below is visible in it. + +It is to a template what `plugins/Random-XKCD/` is to a plugin — the thing to copy, and the +thing a reviewer compares your submission against. + +--- + +## Make a template + +### 1. Make the directory + +Name it in MixedCase with single hyphens, ASCII letters and digits only, as +`docs/addon-naming-standards.md` describes. `Flutter-Templates`, not `flutter_templates`. + +``` +templates/My-Templates/ +├── addon.json <- gallery metadata +├── my-templates.html <- gallery page +├── icon_day.png <- gallery icon +├── icon_night.png +├── README.md +├── templates.json <- from here down, this is the archive +└── MyFirstTemplate/ + ├── template/template.json + ├── template/thumb.png + └── ... your project skeleton ... +``` + +The first five files describe your addon to the gallery. They stay out of the `.cgt`. The +archive holds `templates.json` and the directories it names, and nothing else — the build +takes its file list from `templates.json`, so a file you do not list is never shipped. + +### 2. Put your project skeleton under `<TemplateName>/` + +Start from a project that already builds. Copy it in whole, then replace the parts that +change per project with placeholders. + +### 3. Mark the files that need substitution + +Add `.peb` to any file that contains a placeholder. Code on the Go renders it and removes the +suffix. Every other file is copied byte for byte, so binary files are safe. + +`app/build.gradle.kts` becomes `app/build.gradle.kts.peb`. + +### 4. Write the placeholders + +The delimiters carry a `$` prefix. This is not stock Pebble syntax. + +| Purpose | Syntax | +|---|---| +| Print a value | `${{APP_NAME}}` | +| Run a statement | `${% if LANGUAGE == 'kotlin' %} ... ${% endif %}` | +| Comment | `${# not rendered #}` | + +Filters work: + +``` +name: "${{APP_NAME | lower}}" +${{PACKAGE_NAME | replace({"." : "_"})}} +``` + +Values you can use: + +| Identifier | Value | +|---|---| +| `APP_NAME` | What the user typed | +| `PACKAGE_NAME` | The package, for example `com.example.myapp` | +| `CLASS_NAME` | The app name with every non-alphanumeric character removed | +| `MIN_SDK`, `COMPILE_SDK`, `TARGET_SDK` | SDK levels | +| `AGP_VERSION`, `KOTLIN_VERSION`, `GRADLE_VERSION` | Tool versions the IDE supplies | +| `JAVA_SOURCE_COMPAT`, `JAVA_TARGET_COMPAT`, `JAVA_TARGET` | Java compatibility | +| `LANGUAGE` | `kotlin` or `java`, only if you offer the choice | + +You can add your own — see *Ask the user for more* below. + +### 5. Use placeholders in paths too + +A directory or a file name is substituted as well. + +``` +app/src/main/java/PACKAGE_NAME/MainActivity.kt.peb +``` + +With the package `com.example.myapp` this becomes +`app/src/main/java/com/example/myapp/MainActivity.kt`. `CLASS_NAME` works the same way. + +### 6. Write `<TemplateName>/template/template.json` + +```json +{ + "name": "My First Template", + "description": "One line the user reads under the name", + "version": "1.0.0", + "tooltipTag": "template.my.first", + "parameters": { + "required": { + "appName": { "identifier": "APP_NAME" }, + "packageName": { "identifier": "PACKAGE_NAME" }, + "saveLocation": { "identifier": "SAVE_LOCATION" } + }, + "optional": { + "language": { "identifier": "LANGUAGE" }, + "minsdk": { "identifier": "MIN_SDK" } + } + }, + "system": { + "agpVersion": { "identifier": "AGP_VERSION" }, + "kotlinVersion": { "identifier": "KOTLIN_VERSION" }, + "gradleVersion": { "identifier": "GRADLE_VERSION" }, + "compileSdk": { "identifier": "COMPILE_SDK" }, + "targetSdk": { "identifier": "TARGET_SDK" }, + "javaSourceCompat": { "identifier": "JAVA_SOURCE_COMPAT" }, + "javaTargetCompat": { "identifier": "JAVA_TARGET_COMPAT" }, + "javaTarget": { "identifier": "JAVA_TARGET" } + } +} +``` + +**Declare only what you use.** Presence controls the wizard: + +- Leave out `optional.language` and the language chooser hides. Do this when your template + offers one language only. +- Leave out `optional.minsdk` and the minimum SDK field hides. +- Leave out the whole `system` block when no `.peb` file reads a tool version. + +Add `template/thumb.png` for the image on the New Project screen. It is optional, but an +entry without one looks unfinished. + +### 7. Write `templates.json` + +List each directory, in the order you want them shown. This file is also the build's allow +list: a directory you leave out is not packaged. + +```json +{ + "templates": [ + { "path": "MyFirstTemplate" }, + { "path": "MySecondTemplate" } + ] +} +``` + +Add `"experimental": true` to an entry to hide it unless the user turns experiments on. + +### 8. Write `addon.json` + +This is what the gallery shows. + +```json +{ + "summary": "One line, 120 characters at most.", + "description": "A full paragraph. This is the gallery card.", + "tags": ["flutter", "starter"], + "origin": "community", + "license": "AGPL-3.0-or-later", + "author": { "name": "Your Name", "url": "" }, + "template": { + "id": "com.example.mytemplates", + "version": "1.0.0", + "minAppVersion": "26.36" + } +} +``` + +`template.id` must be unique and must never change across releases. `template.version` is a +dotted number. `minAppVersion` is the oldest Code on the Go release your bundle works on, +written as `YY.ww`. + +Your gallery card shows **Download** and **Details**. It has no **Source** link, because a +template ships no source tarball — the `.cgt` is plain text throughout, so the download +already is the source. The card's Details page links back to your directory in this +repository. + +### 9. Add the page and the icons + +`my-templates.html` is your gallery page. Copy the shape from +`../plugins/Random-XKCD/random-xkcd.html`. The rules the checker enforces: + +- The file name is the slug: the directory name in lower case. +- `<title>` is the display name exactly: the directory name with hyphens replaced by spaces. +- The `<h1>` contains that name. +- No `<script>` and no inline event handler. These pages are served from the site origin. +- Write **Code on the Go** in full. Not CoGo, not CotG, not CodeOnTheGo. + +`icon_day.png` and `icon_night.png` are both required. + +### 10. Check it + +```sh +uv run --directory tools/addons addons --root "$PWD" check +``` + +Run it from the repository root. `--root` must be absolute. This is the same gate that runs +on every pull request. Treat its output as the authority. + +### 11. Build it and look inside + +```sh +./scripts/build-cgt.sh templates/My-Templates out +unzip -l out/my-templates.cgt +``` + +This is the same script the publish workflow runs, so a local build and a published one agree. +It takes the file list from `templates.json`, pins every mtime, and writes a stored, +sorted archive. + +Confirm `templates.json` appears bare at the root, with no directory in front of it. The IDE +looks it up by that exact name and finds nothing otherwise. Confirm too that `addon.json`, +your HTML page and your icons are **not** in the listing. + +If `find` reports a missing path, `templates.json` names a directory that does not exist. +Fix the spelling. The IDE would skip it in silence. + +The published `.cgt` also carries `cgt-build.properties` at the root, which the Action writes. +It records the commit the bundle was built from. Your local build does not produce it. + +**Never commit the `.cgt`.** The Action builds it on publication. Add `*.cgt` to your +`.gitignore`. + +### 12. Test it on a device + +Copy the `.cgt` to the device Downloads folder, open the Templates manager, then open New +Project and generate a project from every template you shipped. + +A passing check and a clean zip prove nothing about whether the template works. The renderer +runs with strict variables: a placeholder that no parameter declares fails **at project +generation**, on the device, and nowhere earlier. + +--- + +## Ask the user for more + +`parameters.user` adds fields to the New Project dialog. + +```json +"user": { + "text": [ + { "label": "Author", "identifier": "AUTHOR_NAME", "default": "" } + ], + "checkbox": [ + { "label": "Include sample code", "identifier": "WITH_SAMPLE", "default": true } + ] +} +``` + +A text field gives you `${{AUTHOR_NAME}}` in any `.peb` file. + +A checkbox gives you `${% if WITH_SAMPLE %}`, and it also works **in a path**. A directory +named after a checkbox identifier disappears from the path when the box is checked, and the +whole entry is dropped when it is not: + +``` +src/main/kotlin/PACKAGE_NAME/WITH_SAMPLE/demo/Sample.kt.peb +``` + +Checked, with the package `com.example`, this becomes +`src/main/kotlin/com/example/demo/Sample.kt`. Clear, and it is not written at all. + +## Offer Java and Kotlin + +Ship both files side by side, and declare `optional.language`: + +``` +MainActivity.kt.peb +MainActivity.java.peb +``` + +The IDE keeps the one that matches the choice and drops the other. This works **only** when +`parameters.optional.language` is declared. Without it both files are written. + +--- + +## Things that go wrong + +| Symptom | Cause | +|---|---| +| The template never appears | `templates.json` is not bare at the archive root, or the `path` does not match the directory name | +| One template is missing, with no error | Its `template/template.json` is absent. The reader skips it in silence | +| Generation fails on the device | A placeholder that no parameter declares. Strict variables is on | +| Two lines merged into one, and broken YAML | A bare `${{TAG}}` ended a line. Pebble trims the newline. Put a character after `}}` — quote the value | +| A `template/` directory appears in the generated project | It cannot. If you see one, it is not at `<TemplateName>/template/` | +| `${pluginVersion}` was not substituted | It is a Gradle placeholder with one brace, not Pebble. Leave it alone | + +Metadata parsing is lenient in two different ways, which is a trap. `templates.json` is +**strict JSON**. Each `template/template.json` is **JSON5**, so unquoted keys are accepted +there and refused in the other file. Quote every key and both stay valid. + +--- + +## Build provenance + +Every published `.cgt` records the commit it was built from, in `cgt-build.properties` at the +archive root. You do not write this file. The publish workflow generates it, adds it to the +archive, and it is gitignored. + +```sh +unzip -p flutter-templates.cgt cgt-build.properties +``` + +```properties +revision=a1b2c3d4e5f6 +revision_source=env:GITHUB_SHA +timestamp=2026-09-25T14:02:11Z +timestamp_source=git +addon_id=org.appdevforall.fluttertemplates +version=1.0.0 +``` + +### Why it exists + +The gallery serves one URL per addon and overwrites it on each publish. Without this record, +"version 1.0.0" names every build that version number ever had, and a bug report cannot be +tied to the source that produced it. With it, any published bundle traces back to one commit. + +`scripts/verify-provenance.sh` asserts the record after each build. A `.cgt` with no +`cgt-build.properties`, or missing a key, fails the run. + +### What it asks of you + +Two things, and both are about keeping the build reproducible. One commit must always produce +one archive, byte for byte. + +| Rule | Why | +|---|---| +| Never commit a `.cgt` | The Action builds it. A committed one goes stale and nobody notices | +| Keep everything in the bundle plain text | A binary that changes on every build breaks determinism. The images are the deliberate exception | + +Two values in the record are warnings rather than failures, and you will see both locally: + +- `revision=unknown` — no commit was found. Normal outside CI. +- `timestamp_source=wall-clock` — no `git` was available, so the clock was used. A bundle + built this way is **not** reproducible, because the stamp changes on every build. + +A third, `+dirty` appended to the revision, means your directory had uncommitted changes when +the archive was built. On a published artifact that is a real problem: it says the bundle does +not match any commit. Commit your work before the publish workflow runs. + +--- + +## Rules for the archive + +The build pins every mtime to 1980 and sorts the entry list, so one commit always gives one +archive. Keep it that way. + +Never commit these under your template directory. The checker refuses them: + +``` +.gradle/ build/ local.properties *.swp *.swo *.bak .DS_Store +``` + +Keep template content plain text. Images are the exception: `thumb.png` and the two gallery +icons are PNG. diff --git a/templates/cgt-templates.md b/templates/cgt-templates.md new file mode 100644 index 00000000..6087e389 --- /dev/null +++ b/templates/cgt-templates.md @@ -0,0 +1,434 @@ +# How `.cgt` template bundles are made and managed + +Reference for ADFA-6252. It records how `core.cgt` works today, so a new bundle in this +repository can follow the same model. + +Sources examined: + +| Source | Path | +|---|---| +| Template source tree | `/Users/eisen/src/dev-assets/templates/` | +| Local builder | `/Users/eisen/src/dev-assets/build-core-cgt.sh` | +| Validation workflow | `/Users/eisen/src/dev-assets/.github/workflows/lint-templates.yml` | +| Publication workflow | `/Users/eisen/src/dev-assets/.github/workflows/deploy.yml` | +| Programmatic builder | `com.itsaky.androidide.plugins.templates.CgtTemplateBuilder` in `libs/plugin-api.jar` | +| Format definition | `CodeOnTheGo/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateConstants.kt` | +| Project generator | `.../impl/zip/ZipTemplateReader.kt` and `.../impl/zip/ZipRecipeExecutor.kt` | +| Manager UI reader | `CodeOnTheGo/app/src/main/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReader.kt` | + +The format has no written specification. `ZipTemplateConstants.kt` is the only definition. + +--- + +## 1. What a `.cgt` file is + +A `.cgt` file is a ZIP archive. It has no custom container and no header. It holds one or +more project templates. Code on the Go reads it and shows each template on the New Project +screen. + +Two things produce a `.cgt`: + +| Producer | Used by | Method | +|---|---|---| +| A ZIP of a source tree | `core.cgt` in `dev-assets` | `zip -0` over `templates/` | +| `CgtTemplateBuilder` | Template-installer plugins, such as `plugins/Flutter-Templates` | Java `ZipOutputStream` at run time on the device | + +Both write the same internal layout. This is why ADFA-6252 is possible: the Flutter plugin +already builds a `.cgt` on the device, so the same files can ship as a `.cgt` directly. + +--- + +## 2. Bundle layout + +The archive root holds a manifest and one directory per template. + +``` +templates.json <- manifest, lists the templates +extensions.jar <- optional, custom Pebble extensions +<TemplateName>/template/template.json <- metadata for this template +<TemplateName>/template/thumb.png <- thumbnail for the New Project screen +<TemplateName>/<any path>.peb <- a file that Pebble renders +<TemplateName>/<any path> <- a file that is copied without change +``` + +The names are fixed. `ZipTemplateConstants.kt` declares them: + +```kotlin +const val ARCHIVE_JSON = "templates.json" +const val META_FOLDER = "template" +const val META_JSON = "template.json" +const val META_THUMBNAIL = "thumb.png" +const val META_EXTENSION_JAR = "extensions.jar" +const val TEMPLATE_EXTENSION = ".peb" +``` + +`core.cgt` holds nine templates: `NoActivity`, `EmptyActivity`, `BasicActivity`, +`NavigationDrawer`, `BottomNavActivity`, `NoAndroidX`, `TabbedActivity`, `ComposeActivity`, +`CodeOnTheGoPlugin`. It has 378 entries, all stored, all dated 1980-01-01, and no directory +entries. + +`extensions.jar` is optional. It supplies custom Pebble `Extension` classes, which the IDE +dexes into `dex_opt/`. `core.cgt` does not use it. + +`CgtTemplateBuilder` writes the same names. + +### A bundle holds one or more templates + +Two readers exist. They find the templates in different ways, so a bundle must satisfy both. + +| Reader | Purpose | Method | +|---|---|---| +| `ZipTemplateReader` | Builds the New Project list | Reads `templates.json` by that exact bare name at the archive root, then reads `<path>/template/template.json` for each entry | +| `CgtTemplateReader` | Shows the Templates manager list | Scans for any entry that ends in `/template/template.json`, and ignores `templates.json` | + +A missing `template.json` makes `ZipTemplateReader` skip that entry without an error. + +--- + +## 3. `templates.json` + +The root manifest. It is strict JSON. It lists the directory of each template. + +```json +{ + "templates": [ + { "path": "NoActivity" }, + { "path": "EmptyActivity" } + ] +} +``` + +An entry also accepts `"experimental": true`. The IDE then hides that template unless +`FeatureFlags.isExperimentsEnabled` is set. `core.cgt` does not use it. + +The manifest holds no version and no schema version. Nothing checks that each `path` exists +on disk. + +--- + +## 4. `template/template.json` + +One file for each template. It is **JSON5**, not strict JSON. Unquoted keys are permitted, +and `core.cgt` uses them: `{identifier: "APP_NAME"}`. + +| Field | Purpose | +|---|---| +| `name` | The name on the New Project screen | +| `description` | One line under the name | +| `version` | The template version, such as `"0.1"` | +| `tooltipTag` | The in-app help tag, such as `"template.empty.activity"` | +| `defaultAppName` | Optional. Pre-fills the app name field | +| `parameters.required` | `appName`, `packageName`, `saveLocation` | +| `parameters.optional` | `language`, `minsdk` — omit one to hide that field | +| `parameters.user.text` | Extra text fields: `label`, `identifier`, `default` | +| `parameters.user.checkbox` | Extra checkboxes: `label`, `identifier`, `default` | +| `system` | Values the IDE supplies: `agpVersion`, `kotlinVersion`, `gradleVersion`, `compileSdk`, `targetSdk`, `javaSourceCompat`, `javaTargetCompat`, `javaTarget` | + +Each entry maps a name to an `identifier`. The identifier is the token that the Pebble files +use. + +Presence controls the wizard. Declare `parameters.optional.language` and the language picker +shows; omit it and the picker hides **and** the `.java`/`.kt` filter stops running. +`ComposeActivity` omits it, and it ships no `.java.peb` files. + +`CodeOnTheGoPlugin/template/template.json` is the only one that uses `parameters.user`. It +adds a text field `PLUGIN_AUTHOR` and a checkbox `PLUGIN_SAMPLE`. A checkbox identifier is +also a path flag — see section 5. + +--- + +## 5. Pebble files + +A file with the `.peb` suffix is rendered. The IDE removes the suffix and writes the result +into the new project. Every other file is copied byte for byte. + +### Delimiters + +Code on the Go changes the Pebble delimiters. Every one carries a `$` prefix: + +| Purpose | Open | Close | +|---|---|---| +| Print | `${{` | `}}` | +| Execute | `${%` | `%}` | +| Comment | `${#` | `#}` | + +```kotlin +android { + namespace = "${{PACKAGE_NAME}}" + compileSdk = ${{COMPILE_SDK}} +} +${% if LANGUAGE == 'kotlin' %} +kotlin("android") version "${{KOTLIN_VERSION}}" +${% endif %} +``` + +Filters work: `${{CLASS_NAME | lower}}` and `${{PACKAGE_NAME | replace({"." : "_"})}}` both +appear in `core.cgt`. + +The engine runs with `strictVariables(true)`. An identifier that no parameter declares fails +the render. + +### Identifiers + +`core.cgt` uses these: `APP_NAME`, `PACKAGE_NAME`, `CLASS_NAME`, `MIN_SDK`, `COMPILE_SDK`, +`TARGET_SDK`, `AGP_VERSION`, `KOTLIN_VERSION`, `GRADLE_VERSION`, `JAVA_SOURCE_COMPAT`, +`JAVA_TARGET_COMPAT`, `JAVA_TARGET`, `PLUGIN_AUTHOR`. + +Three more are injected at render time, and no `template.json` declares them: `CLASS_NAME`, +`COGO_CGT_PATH` and `COGO_CGT_DIRECTORY`. + +### Path transformation + +`ZipRecipeExecutor.renderProject` transforms each entry path in this order: + +1. Entries outside `<TemplateName>/` are ignored, and `<TemplateName>/template/` is dropped. + The metadata never reaches the new project. +2. If `parameters.optional.language` is declared and the user picked a language, the other + language is filtered out. A Kotlin project skips every `*.java` and `*.java.peb`; a Java + project skips every `*.kt` and `*.kt.peb`. This is why each template ships both. +3. A path segment that matches a **checkbox identifier** acts as a flag. The segment is + removed when the box is checked, and the whole entry is skipped when it is not. +4. The literal text `PACKAGE_NAME` in the path becomes the package as directories + (`com.foo.bar` becomes `com/foo/bar`). The literal text `CLASS_NAME` becomes the class + name. +5. The `.peb` suffix is removed from the output name. +6. The output path is canonicalized. An entry that escapes the project root is refused. + +`CodeOnTheGoPlugin` shows steps 3 and 4 together: + +``` +CodeOnTheGoPlugin/src/main/kotlin/PACKAGE_NAME/PLUGIN_SAMPLE/fragments/CLASS_NAMEFragment.kt.peb +``` + +With "Include Sample Code" checked, and the package `com.foo`, this becomes +`src/main/kotlin/com/foo/fragments/MyPluginFragment.kt`. With the box clear, it disappears. + +### Three gotchas + +1. A bare `${{TAG}}` at the end of a line loses its newline. Pebble trims newlines, so the + line merges with the next one. This produced invalid YAML once, because `name:` and + `description:` collapsed into one line. Put a non-newline character after `}}`. The + convention is to quote the value: `name: "${{APP_NAME | lower}}"`. +2. The lint step counts `{{` against `}}`. It does not render the template. A balanced but + wrong expression passes, and `strictVariables(true)` then fails on the device. +3. `CodeOnTheGoPlugin/src/main/AndroidManifest.xml.peb` holds a literal `${pluginVersion}`. + That is a **Gradle** manifest placeholder, not Pebble. It has one brace. Do not change it + to `${{...}}`. + +--- + +## 6. How the archive is built + +### In CI + +`lint-templates.yml` and `deploy.yml` hold the same inline steps. There is no shared script. + +```bash +export TZ=UTC +( cd ./templates \ + && find . -type f -exec touch -t 198001010000 {} + \ + && find . -type f | LC_ALL=C sort | zip -0 -D -X -q "$OUT/core.cgt" -@ ) +brotli -q 11 -f -o "$OUT/core.cgt.br" "$OUT/core.cgt" +``` + +| Flag | Effect | +|---|---| +| `-0` | Store every entry. No compression inside the ZIP | +| `-D` | Write no directory entries | +| `-X` | Strip uid, gid and extended attributes | +| `LC_ALL=C sort` | Fix the entry order | +| `touch -t 198001010000` | Fix every mtime to 1980-01-01 | + +The result is deterministic. The same source gives the same MD5. + +Compression happens one level up. `core.cgt.br` is Brotli quality 11 over the stored ZIP. +This is the same debug/release split that the other assets use: the plain archive is the +debug form, the Brotli archive is the release form. + +### Locally + +`build-core-cgt.sh <templates-dir> <output-dir>` writes both files. It checks that +`templates.json` is present and that the two plugin jars are present. + +**Warning.** The local script uses Python `zipfile` and **writes directory entries**. CI uses +`zip -0 -D -X` and writes none. The two archives are therefore not byte-identical. The local +script shows that a change builds; it does not reproduce the CI MD5. + +### On the device + +`CgtTemplateBuilder` writes the same layout through `ZipOutputStream`. A plugin calls +`IdeTemplateService.createTemplateBuilder(name)`, adds files, then calls `build(dir)` and +`registerTemplate(file)`. + +--- + +## 7. Validation + +`lint-templates.yml` runs on every pull request that touches `templates/**`. + +| Step | Check | +|---|---| +| Junk files | Rejects `.gradle/`, `build/`, `local.properties`, `*.swp`, `*.swo`, `*.bak`, `.DS_Store` | +| XML | `xmllint --noout` on every `*.xml` | +| Root manifest | `jq empty templates/templates.json` — strict JSON | +| Template metadata | Python `json5.load` on every `*/template/template.json` | +| Pebble braces | Counts `{{` against `}}` in every `*.peb` | +| Plugin jars | `sha256sum -c checksums.txt` on the fetched jars | +| Build | Rebuilds `core.cgt` and `core.cgt.br`, prints the sizes and MD5s | + +There is **no JSON Schema** anywhere in `dev-assets`. Syntax is checked; structure is not. +Nothing confirms that a `path` in `templates.json` exists, that `template/template.json` and +`template/thumb.png` are present, or that the required fields are filled. + +There is **no render test**. No template is ever rendered in CI. + +--- + +## 8. Versioning and publication + +### Versioning + +`core.cgt` has no version. There is no tag, no version file, and no version field in +`templates.json`. The name is always `core.cgt`, and each deploy overwrites it. Only the +individual templates carry a `version` field, and nothing aggregates them. + +Clients detect a change through the MD5 sidecar. + +### Publication + +This section describes **dev-assets only**. `core.cgt` ships with the app assets, so it takes +the app asset pipeline. It does not set a precedent for this repository: everything here +publishes to Cloudflare R2, and a template bundle will do the same. Read this section for how +the archive is built, not for where it goes. + +`deploy.yml` runs on manual dispatch only. It publishes to the GreenGeeks web host over SSH. + +| Artifact | Path on the site | +|---|---| +| `core.cgt` + `core.cgt.md5` | `https://appdevforall.org/dev-assets/debug/` | +| `core.cgt.br` + `core.cgt.br.md5` | `https://appdevforall.org/dev-assets/release/` | + +The step packs the whole asset set as `asset<yymmdd>.zip`, copies it with `scp`, compares the +remote MD5, backs up the live tree, removes the live tree, then unpacks the new set. It is a +full-set replace: a file that is absent from the zip disappears from the site. + +Integrity is MD5 only. Nothing signs `core.cgt`. + +### How a bundle reaches the device + +`core.cgt` ships with the app assets. Any other `.cgt` arrives as a file. + +| Path | Mechanism | +|---|---| +| A plugin registers one | `IdeTemplateService.registerTemplate(file)` copies it into `Environment.TEMPLATES_DIR` under a prefixed name | +| A user installs one | The Templates manager scans `Environment.TEMPLATES_DIR` and the Downloads folder | + +The second path is the one ADFA-6252 needs. A `.cgt` downloaded from the gallery needs no +plugin, no manifest and no Kotlin. + +--- + +## 9. The `CodeOnTheGoPlugin` template needs two jars + +`templates/CodeOnTheGoPlugin/libs/{plugin-api,gradle-plugin}.jar` are **not committed**. They +are gitignored. Both the lint workflow and the deploy workflow fetch them from the +`plugin-api-latest` GitHub Release on `appdevforall/CodeOnTheGo`, then verify them against +`checksums.txt`. + +They are gitignored but they **do ship**. Both jars are entries in the built `core.cgt`, +because the build runs after the fetch. A template that needs a binary can therefore hold +one, as long as the build puts it in place first. + +To fetch them by hand: + +```bash +BASE=https://github.com/appdevforall/CodeOnTheGo/releases/download/plugin-api-latest +LIBS=templates/CodeOnTheGoPlugin/libs +mkdir -p "$LIBS" +curl -fL "$BASE/plugin-api.jar" -o "$LIBS/plugin-api.jar" +curl -fL "$BASE/gradle-plugin.jar" -o "$LIBS/gradle-plugin.jar" +``` + +The `README.md` of `dev-assets` mentions `scripts/update-plugin-libs.sh`. That script does not +exist. A later section of the same file says so. The line is stale. + +--- + +## 10. To add a template to `core.cgt` + +1. Make `templates/<NewTemplate>/` with the project skeleton. +2. Add `template/template.json` and `template/thumb.png`. +3. Give the `.peb` suffix to every file that needs substitution. +4. Add `{ "path": "<NewTemplate>" }` to `templates/templates.json`. +5. Open a pull request. `lint-templates.yml` runs. +6. Dispatch `deploy.yml` after the merge. + +--- + +## 11. What this repository must add for ADFA-6252 + +A template bundle under `templates/` publishes to `addons.appdevforall.org` through +Cloudflare R2, the same as every plugin. The tooling is written for plugins only, so five +places need a template branch. **A template is not a plugin**: it builds no APK, it has no +Gradle build, and it has no `AndroidManifest.xml`. + +| ID | Gap | Resolution | +|---|---|---| +| G01 | Discovery | `discover.find_addons` requires a `build.gradle.kts` with the plugin-builder predicate. Template discovery is a separate rule: a directory under `templates/` that holds `templates.json`. | +| G02 | Packaging | `publish-addons.yml` runs `assemblePlugin` and copies a `.cgp`. A template is zipped into a `.cgt` instead. The step branches on the addon kind. | +| G03 | Artifact suffix | `catalog.entry()` hardcodes `dl/<slug>.cgp`, and `publish.py` maps a content type for `.cgp` and `.gz` only. Both need `.cgt`. | +| G04 | Metadata source | `plugin.id` has no meaning here. A template declares `template.id`, `template.version` and `template.minAppVersion` in `addon.json`, which `model.py` reads in place of the manifest. The catalog field becomes `addonId`, at `schemaVersion` 2. | +| G05 | Provenance | `scripts/verify-provenance.sh` asserts `assets/cgp-build.properties` inside a `.cgp`. A `.cgt` carries `cgt-build.properties` at its root instead, and the script gains that branch. | + +Already in place: + +| ID | Item | +|---|---| +| P01 | `templates` is one of the four areas in `discover.AREAS`. | +| P02 | `catalog.TYPES` maps `templates` to the type `template`. | +| P03 | `site/catalog.schema.json` accepts `"template"` in the `type` enum. | + +The `.cgt` is **never committed**. A GitHub Action generates it at publication. The plan is in +[`flutter-cgt-migration-plan.md`](flutter-cgt-migration-plan.md); community guidance is in +[`README.md`](README.md). + +--- + +## 12. What the Flutter conversion involves + +`plugins/Flutter-Templates` already holds five Pebble skeletons under +`src/main/assets/templates/<Template>/`, and each one already uses the `${{TAG}}` syntax that +`core.cgt` uses. The conversion is mostly a move. + +The five templates use only three expressions: `${{APP_NAME}}`, `${{APP_NAME | lower}}` and +`${{PACKAGE_NAME}}`. They declare no language option, they hold no `PACKAGE_NAME` or +`CLASS_NAME` directory, and they use no execute block. None of the harder rules in section 5 +applies to them. + +| Present today | Needed in the bundle | +|---|---| +| `src/main/assets/templates/FlutterBasic/**` | `FlutterBasic/**` at the bundle root | +| `template/thumb.png` for each template | Unchanged | +| The name and description in the `VARIANTS` list in `FlutterTemplate.kt` | `FlutterBasic/template/template.json` | +| `showPackageNameOption()` in the Kotlin code | `parameters.required.packageName` | +| The five entries in `VARIANTS` | Five entries in `templates.json` | +| `AndroidManifest.xml`, `build.gradle.kts`, `FlutterTemplate.kt`, the icons | Removed. A `.cgt` needs none of them | +| `addon.json` | Kept, and extended to carry the values that the manifest used to hold | + +Settled. The detail is in [`flutter-cgt-migration-plan.md`](flutter-cgt-migration-plan.md). + +| Question | Answer | +|---|---| +| One bundle or five? | One, `flutter-templates.cgt`, holding five template directories | +| Where do the version, id and minimum come from? | `addon.json`, in a `template` object. The id is `org.appdevforall.fluttertemplates`, the minimum is `26.38` | +| Is a Brotli form published? | No. Cloudflare compresses on the fly | +| Do the `.peb` files keep the suffix? | Yes | +| Does this repository lint `templates/`? | Yes, in `check-toolchain.yml`, using the checks borrowed from `dev-assets` | + +### One behaviour change to expect + +Today the plugin builds each `.cgt` on the device and calls `registerTemplate`. The templates +appear when the user enables the plugin. After the conversion, the user downloads a `.cgt` +and the Templates manager picks it up. The New Project screen is the same, but the install +step moves from the Plugin Manager to the Templates manager. Verify this on a device before +the work is called complete. diff --git a/templates/flutter-cgt-migration-plan.md b/templates/flutter-cgt-migration-plan.md new file mode 100644 index 00000000..53a1a0b1 --- /dev/null +++ b/templates/flutter-cgt-migration-plan.md @@ -0,0 +1,636 @@ +# Plan: move Flutter Templates from a plugin to a `.cgt` bundle + +ADFA-6252. Read `cgt-templates.md` first for the format. + +## Goal + +`plugins/Flutter-Templates/` becomes `templates/Flutter-Templates/`. It stops being an +Android application module. It becomes a source tree that a GitHub Action zips into one +`flutter-templates.cgt` holding five template directories, and publishes to +`addons.appdevforall.org` through Cloudflare R2. + +## Rules this plan follows + +| Rule | Effect | +|---|---| +| The `.cgt` is never committed | The Action generates it. `.gitignore` blocks it | +| Template content is plain text | Dart, YAML, Gradle and JSON only. No archives, no jars | +| Images stay as PNG | Seven files: two gallery icons and five thumbnails. These are packaging, not template content | +| A template is not a plugin | No `build.gradle.kts`, no `AndroidManifest.xml`, no Kotlin, no `.cgp` | +| Publication goes to R2 | The same path every other addon in this repository uses | + +--- + +## 1. Target layout + +The template directories sit at the addon root, the same shape `dev-assets/templates/` uses. + +``` +templates/Flutter-Templates/ +├── addon.json <- gallery metadata + template id, version, minAppVersion +├── flutter-templates.html <- gallery page, name fixed by the slug +├── icon_day.png <- gallery icon +├── icon_night.png +├── README.md +├── templates.json <- archive root manifest +├── FlutterBasic/ +│ ├── template/template.json +│ ├── template/thumb.png +│ ├── analysis_options.yaml +│ ├── pubspec.yaml.peb +│ ├── README.md.peb +│ └── lib/main.dart.peb +├── FlutterBloc/ +├── FlutterGetx/ +├── FlutterProvider/ +└── FlutterRiverpod/ +``` + +`addon.json`, the HTML page, the two icons and `README.md` are repository metadata. They must +not reach the archive. + +### Select the files with an allow list, not an exclude list + +An `-x` exclude list is the obvious way to keep those four out, and it is the wrong one. It +fails open: a metadata file added next year ships inside the `.cgt` until somebody remembers +to extend the list, and nothing reports it. + +`templates.json` already names every directory that belongs in the archive. Build the file +list from it: + +```bash +paths="$(jq -r '.templates[].path' templates.json)" +find templates.json $paths -type f | LC_ALL=C sort +``` + +This fails closed. A new metadata file is never listed, so it can never ship. It also fails +**loudly**: `find` exits non-zero when a path in `templates.json` does not exist on disk, +which catches a typo that the IDE would otherwise swallow in silence. + +### Files to delete + +`plugins/Flutter-Templates/` goes away whole. The `.peb` skeletons, the thumbnails and the +icons move to the new location first; everything else is discarded: +`build.gradle.kts`, `settings.gradle.kts`, `gradle.properties`, +`src/main/AndroidManifest.xml`, `src/main/kotlin/com/ali/fluttertemplate/FlutterTemplate.kt`, +and all of `src/main/res/`. None has a meaning in a `.cgt`. + +Do this as a `git mv` of the files that survive, so the history of each `.peb` file follows +it. See D07 in section 9 for what the deletion means for a user who installed the plugin. + +### Files to move without change + +Every `.peb` file and `analysis_options.yaml` under +`src/main/assets/templates/<Template>/`. They already use `${{APP_NAME}}`, +`${{APP_NAME | lower}}` and `${{PACKAGE_NAME}}`, which is exactly what the format wants. +Each `template/thumb.png` moves as it is. + +--- + +## 2. New files to write + +### `templates.json` + +```json +{ + "templates": [ + { "path": "FlutterBasic" }, + { "path": "FlutterBloc" }, + { "path": "FlutterProvider" }, + { "path": "FlutterGetx" }, + { "path": "FlutterRiverpod" } + ] +} +``` + +### `<Template>/template/template.json` + +One for each template. The name and the description come from the `VARIANTS` list in +`FlutterTemplate.kt`, so nothing a user reads changes. + +```json +{ + "name": "Flutter Basic", + "description": "A minimal Flutter starter app with a counter screen", + "version": "1.0.0", + "tooltipTag": "template.flutter.basic", + "parameters": { + "required": { + "appName": { "identifier": "APP_NAME" }, + "packageName": { "identifier": "PACKAGE_NAME" }, + "saveLocation": { "identifier": "SAVE_LOCATION" } + } + } +} +``` + +There is no `parameters.optional`. Flutter offers no Java or Kotlin choice, so the language +picker must stay hidden, and Flutter needs no `minSdk`. There is no `system` block, because +no `.peb` file reads an AGP, Gradle or SDK value. + +The old plugin called `showPackageNameOption()`. `parameters.required.packageName` replaces +it. + +### `addon.json` + +The gallery metadata stays. A `template` object carries what the `AndroidManifest.xml` used +to hold. + +```json +{ + "summary": "Five Flutter starter projects for the New Project screen.", + "description": "...", + "tags": ["flutter", "templates", "dart"], + "origin": "community", + "license": "AGPL-3.0-or-later", + "author": { "name": "RJ Ali", "url": "" }, + "template": { + "id": "org.appdevforall.fluttertemplates", + "version": "1.0.0", + "minAppVersion": "26.38" + } +} +``` + +`template.id` replaces `plugin.id`. `template.version` replaces `plugin.version`. +`minAppVersion` replaces `plugin.min_ide_version`. + +The id is `org.appdevforall.fluttertemplates`, not the old `com.ali.fluttertemplate`. The +bundle is maintained here, so it takes the maintainer's namespace. Credit to the original +contributor stays where a user reads it: `addon.json` `author`, the gallery description and +the addon README. Set the id once and never change it. + +`minAppVersion` is **26.38**. + +### `flutter-templates.html` + +`check.py` requires a page named `<slug>.html`, with `<title>` equal to the display name, an +`<h1>` that contains it, no `<script>`, no inline event handler, and the product name written +in full. Copy the shape from `plugins/Random-XKCD/random-xkcd.html`. + +### `.gitignore` + +``` +*.cgt +``` + +--- + +## 3. What to borrow from dev-assets + +Borrow the commands, not the files. Neither workflow in dev-assets calls a script; both hold +the steps inline. + +| Borrow | From | Why | +|---|---|---| +| The zip command | `lint-templates.yml` and `deploy.yml` | It is the build that produced the shipped `core.cgt` | +| The lint checks | `lint-templates.yml` | Junk files, XML, JSON, JSON5, Pebble braces | + +The zip command, with the file list narrowed to what `templates.json` declares and the +provenance record added: + +```bash +export TZ=UTC +( cd templates/Flutter-Templates \ + && paths="$(jq -r '.templates[].path' templates.json)" \ + && write-provenance cgt-build.properties \ + && find cgt-build.properties templates.json $paths -type f \ + -exec touch -t 198001010000 {} + \ + && find cgt-build.properties templates.json $paths -type f \ + | LC_ALL=C sort \ + | zip -0 -D -X -q "$OUT/flutter-templates.cgt" -@ ) +``` + +`-0 -D -X` and the fixed mtime are unchanged from dev-assets. They are what makes one commit +give one archive. + +### Do not borrow `build-core-cgt.sh` + +It writes directory entries. The CI command does not. The two archives differ, and the +shipped `core.cgt` matches CI. Borrowing the script would make a local build disagree with +the Action for no gain. + +### Do not produce a `.br` file + +`core.cgt.br` exists because the app fetches assets from a plain web host that does no +negotiation. The gallery sits behind Cloudflare, which compresses on the fly. One `.cgt` is +enough. + +--- + +## 4. Changes to `tools/addons` + +Five small changes. Each matches a gap in `cgt-templates.md`. + +### G01 — discovery + +`discover.find_addons` requires `build.gradle.kts` with the plugin-builder predicate. A +template has no Gradle build. + +Add a second rule: a directory under `templates/` that holds `templates.json` is a +template addon. Return the two kinds together, and let callers ask which kind a path is. A +helper such as `is_template(path)` keeps the branch in one place. + +### G02 — packaging + +`publish-addons.yml` runs `assemblePlugin` and copies `build/plugin/*.cgp`. A template takes +the zip command above instead. The step branches on the addon kind and produces +`dist/<slug>.cgt`. + +### G03 — the artifact suffix + +Three edits: + +| File | Edit | +|---|---| +| `publish.py` | Add `".cgt": "application/octet-stream"` to `CONTENT_TYPES`, and `.cgt` to `ATTACHMENTS` | +| `catalog.py` | `download` becomes `dl/<slug>.cgt` for a template | +| `cli.py` | The publish object list uses `dl/<slug>.cgt` for a template | + +### G04 — metadata + +`model.py` reads `plugin.id`, `plugin.version` and `plugin.min_ide_version` from +`src/main/AndroidManifest.xml`. A template has no manifest, so these read from `addon.json` +instead: + +| Accessor | Plugin source | Template source | +|---|---|---| +| `plugin_id` | manifest `plugin.id` | `addon.json` `template.id` | +| `version` | manifest, then `build.gradle.kts` | `addon.json` `template.version` | +| `min_app_version` | manifest `plugin.min_ide_version` | `addon.json` `template.minAppVersion` | + +`addon.schema.json` sets `additionalProperties: false`, so it must gain the `template` object, +required for a template and absent for a plugin. + +Icons move to the addon root for a template, because `src/main/assets/` is an Android path +with no meaning here. `check.py` and `cli.py` both need the branch. + +### The catalog renames `pluginId` to `addonId` + +`pluginId` is wrong for three of the four addon types. The catalog carries plugins, templates, +snippets and code-actions, so the field becomes `addonId` and `schemaVersion` goes from 1 +to 2. + +`catalog.schema.json` says field removal needs a new major, so the bump is required rather +than optional. The cost is low and this is the moment to pay it: `site/app.js` never reads +the field, and no code in Code on the Go references `catalog.json` at all, so no consumer +observes the change today. Renaming later, once a gallery client exists, costs far more. + +| File | Edit | +|---|---| +| `site/catalog.schema.json` | `pluginId` becomes `addonId`, in `properties` and in `required`. `schemaVersion` const becomes 2 | +| `catalog.py` | The entry key becomes `addonId`. `document["schemaVersion"]` becomes 2 | +| `tests/test_catalog.py` | Update the expected key and version | + +### `check.py` + +A template must skip the plugin checks: `pluginName` in `build.gradle.kts`, `plugin.name`, +`plugin.id` and `plugin.min_ide_version`. It gains its own: + +- `templates.json` exists and is strict JSON. +- Every `path` in it names a directory that exists. +- Every one of those holds `template/template.json`, and that file parses as JSON5. +- `icon_day.png` and `icon_night.png` exist at the addon root. +- `<slug>.html` passes the checks that already exist. + +The directory name, slug and page rules stay as they are. They are not plugin-specific. + +### `tarball.py` — templates ship no source tarball + +**Decision: `tarball.build()` returns early for a template. No archive is produced, and the +gallery card omits the Source link.** + +A plugin tarball exists so somebody can unpack the folder and build it. Its archive root is +the project root, because Code on the Go reads a folder as a plugin project only when +`build.gradle.kts` and `libs/plugin-api.jar` sit in it. That is why `tarball.py` copies the +shared jars in, copies the Gradle wrapper in, rewrites `../libs/` references, and writes a +`BUILDING.md` that says `./gradlew assemblePlugin`. + +None of it applies. A template has no project to open and no compiler to run. + +Nothing is lost by omitting it. **The `.cgt` is the source.** Every file in it is plain text +by rule, so the published download already is what a second tarball would carry. `sourceUrl` +also stays in the catalog entry, pointing at the directory in this repository, so the tree +remains one click away. The AGPL obligation is met by the artifact itself. + +`build()` therefore returns before `jars_for()`, which would otherwise raise +`"<name>: it references no shared jar"` on its first line. + +#### Five places assume a tarball exists + +Skipping the build is the small part. These are the rest: + +| File | Change | +|---|---| +| `catalog.py` | `build()` raises when `<slug>-src.tar.gz` is missing from `dist`. Require it for plugins only. `entry()` omits the `sourceTarball` key for a template | +| `site/catalog.schema.json` | `sourceTarball` moves out of `required`. This is a removal, and `schemaVersion` is already going to 2 for `addonId`, so it costs nothing extra | +| `cli.py` | The publish object list drops `src/<slug>-src.tar.gz` for a template | +| `publish-addons.yml` | The "Build the source tarballs" step passes only the plugin directories | +| `site/app.js` | Guard the Source link — see below | + +#### `site/app.js` would crash, not degrade + +Line 137 reads the field with no guard: + +```js +const src = node.querySelector('[data-slot="source"]'); +src.href = safeUrl(addon.sourceTarball.url); +src.title = `Source tarball, ${size(addon.sourceTarball.size)}`; +``` + +On an entry with no `sourceTarball` this throws a `TypeError`. The call sits inside the loop +that builds every card, so the exception leaves the gallery stuck on "Loading…" with a partial +list — **one template would break the page for every addon**, not just its own card. + +Hide the anchor instead: + +```js +const src = node.querySelector('[data-slot="source"]'); +if (addon.sourceTarball) { + src.href = safeUrl(addon.sourceTarball.url); + src.title = `Source tarball, ${size(addon.sourceTarball.size)}`; +} else { + src.remove(); +} +``` + +`site/index.html` needs no change. The anchor stays in the card template and is removed per +card. + +--- + +## 5. Workflow changes + +### Rename `build-plugins.yml` to `build-addons.yml` + +The rename is overdue, and this change forces it. The file already calls its subjects addons +in its own words — the header comment says "Builds every addon (or one)", the input is +described as "One addon directory name", and the last step prints "Every discovered addon +produced a release `.cgp`." Only four things still say plugin: the file name, the `name:` +field, the `plugin` input key and the `ONLY_PLUGIN` environment variable. Once the workflow +also produces `.cgt` files, the name is simply wrong. + +It matches the `pluginId` to `addonId` decision in section 4, and the umbrella term the +repository already uses everywhere else: `tools/addons`, `addon.json`, `addons discover`, +`publish-addons.yml`, `addons.appdevforall.org`. + +| Rename step | Note | +|---|---| +| `build-plugins.yml` → `build-addons.yml` | The `name:` field becomes `Build addon artifacts` | +| The `plugin` input → `addon` | With `ONLY_PLUGIN` becoming `ONLY_ADDON` | +| `check-toolchain.yml` line 8 | Its comment names "Build plugin artifacts". Update the text | + +The rename is safe. The workflow is `workflow_dispatch` only, so it cannot be a required pull +request check and no branch protection rule depends on the name. + +Leave the three references under `docs/superpowers/` alone. Those are the plan and the specs +for the addon-distribution work as it was carried out. They are a record, not live +documentation, and rewriting history to match a later rename makes them harder to trust. + +### The changes themselves + +| Workflow | Change | +|---|---| +| `check-toolchain.yml` | `addons check` now covers templates. Add the dev-assets lint steps: junk files, `xmllint`, `jq`, JSON5, Pebble brace balance. This is the **only** workflow here that runs on a pull request, so it is the only place a broken bundle can be caught before merge | +| `publish-addons.yml` | Branch the build step. A plugin runs Gradle; a template runs `scripts/build-cgt.sh` | +| `build-addons.yml` | Same branch. It delegates to `scripts/update-libs.sh`, which rebuilds the two jars and runs `assemblePlugin` for each addon — a template has neither, so it must be skipped there and built by the script instead | + +### Put the zip in one script, not in each workflow + +Section 3 says to borrow the commands rather than the files. That advice holds for +`build-core-cgt.sh`, and it stops there. `dev-assets` inlines the same steps in two workflows +and keeps a third copy in a script, and the script has already drifted: it writes directory +entries and the workflows do not, so it cannot reproduce what CI ships. + +Two workflows and one local instruction need this build. Write `scripts/build-cgt.sh` once +and call it from all three. One copy cannot drift from itself. + +--- + +## 6. Documentation to update + +Three documents describe a repository that holds plugins only. Each states something that +becomes false. + +### `docs/plugin-naming-standards.md` + +It derives six values from one directory name, and one of the six is "the `.cgp` filename". +A template produces a `.cgt`. The rest of the standard needs no change at all — the +MixedCase rule, the slug, the display name, the HTML file name and the page title all apply +unchanged, which is why the file needs an edit rather than a sibling. + +| Edit | Detail | +|---|---| +| The artifact row | "the `.cgp` filename" becomes the addon file name: `<slug>.cgp` for a plugin, `<slug>.cgt` for a template | +| The wording | "Every plugin has up to six places" becomes "Every addon". The same for the body text | +| The manager reference | "the name in the Code on the Go plugin manager" is the Templates manager for a template | + +Renaming the file to `addon-naming-standards.md` is consistent with `addonId` and +`build-addons.yml`. It is referenced from `CLAUDE.md` and from `templates/README.md`, so a +rename means updating both. + +### Root `README.md` + +The Examples table is one flat list of `plugins/…` rows. `Flutter-Templates` has a row there +today and must move. + +Split the table by area, with a heading for each, so the four areas in `discover.AREAS` are +visible to a reader who is deciding what to build. A template contributor currently has no +sign that templates are even possible. + +### `CLAUDE.md` + +Four claims stop being true for every addon: + +| Section | Problem | +|---|---| +| "Plugin shape" | States an addon is an Android application module with a manifest and a main class. True for a plugin only | +| "Adding a new plugin" | Step 1 says to copy `plugins/Random-XKCD/`. Wrong starting point for a template | +| "`libs/` is the load-bearing piece" | A template depends on neither jar | +| "Build provenance" | Describes `cgp-build.properties` as the record. A template has `cgt-build.properties` | + +The cheapest honest fix is a short **Templates** section that states what a template is, what +it does not have, and points at `templates/README.md` for the detail — plus the word "plugin" +narrowed to "addon" where a claim covers both. Do not restate the template format in +`CLAUDE.md`. Two copies of a format specification drift, and `cgt-templates.md` is the one +that gets read. + +--- + +## 7. Order of work + +1. Move the directory and delete the plugin scaffolding. +2. Write `templates.json`, the five `template.json` files, `addon.json`, the HTML page and + the `.gitignore`. +3. Change `tools/addons`: discovery, model, check, catalog, publish, cli and **tarball**. Add + tests beside the ones that exist. +4. Write `scripts/build-cgt.sh`, and branch `scripts/verify-provenance.sh`. +5. Change the three workflows, and rename `build-plugins.yml`. +6. Update `docs/plugin-naming-standards.md`, the root `README.md` and `CLAUDE.md`. The root + README is where the `Flutter-Templates` row moves out of the plugins table. +7. Build the `.cgt` locally with `scripts/build-cgt.sh`. Confirm with `unzip -l` that + `templates.json` is bare at the root, that `cgt-build.properties` is beside it, and that + the archive holds no `addon.json`, no HTML page, no icon, no `.gradle` and no `build/`. +8. Verify on a device. + +--- + +## 8. Verification + +A green build proves nothing here. The bundle must be installed and used. + +1. Copy `flutter-templates.cgt` to the device Downloads folder. +2. Open the Templates manager. Confirm five entries appear, with the right names, + descriptions and thumbnails. +3. Open New Project. Confirm the five Flutter templates appear beside the core ones. +4. Generate one project from each. Confirm the app name reaches `pubspec.yaml` in lower case + and `main.dart` unchanged, and that the package name reaches `README.md`. +5. Confirm no `template/` directory reaches the generated project. + +The engine runs with `strictVariables(true)`. An identifier that `template.json` does not +declare fails at generation, not at build. Only step 4 catches it. + +--- + +## 9. Decisions + +Nothing is open. Every question this plan raised has an answer. + +| ID | Decision | +|---|---| +| D01 | Images stay PNG. "Plain text" governs template content, not packaging | +| D02 | The template directories sit at the addon root. The build's file list comes from `templates.json` | +| D03 | The catalog field is `addonId`, at `schemaVersion` 2 | +| D04 | Provenance is `cgt-build.properties` at the archive root | +| D05 | `minAppVersion` is `26.38` | +| D06 | `template.id` is `org.appdevforall.fluttertemplates` | +| D07 | The old plugin is deleted in this pull request | +| D08 | Templates ship no source tarball, and the gallery card omits the Source link | + +### D07 — what deleting the plugin means for existing users + +`plugins/Flutter-Templates/` is removed whole, in the same pull request. + +A user who installed the old plugin keeps five registered templates on their device. The +plugin copied a built `.cgt` into `Environment.TEMPLATES_DIR` under a prefixed name on +`activate()`, and removes it on `deactivate()`. Deleting the directory here removes the addon +from the gallery; it does nothing to a device. + +So a user who installs the new bundle **and** still has the old plugin enabled sees each of +the five templates twice, from two independent sources. The bundle cannot detect or clean up +the plugin's copies — nothing links them. + +The remedy is the release note, not code: say that the Flutter Templates plugin is replaced +by the Flutter Templates bundle, and that the plugin should be disabled or uninstalled +first. Disabling it calls `deactivate()`, which unregisters its templates and deletes its +staged files, so the duplicates disappear by the plugin's own existing path. + +--- + +## 10. Provenance, and what it would mean for a `.cgt` + +### What provenance is + +Every `.cgp` this repository publishes records the commit it was built from. A user reports a +fault. A crash report arrives. Provenance answers one question: **which source produced the +exact file that user installed?** + +Without it, the answer is a guess. The gallery serves one URL per addon and overwrites it on +each publish, so "version 1.0.0" can name many different builds. + +### How it works for a plugin + +The Gradle plugin-builder resolves the revision once per build and writes it three ways: two +`<meta-data>` entries in the manifest, `assets/cgp-build.properties` inside the archive, and +the plugin details dialog in the IDE. + +```sh +unzip -p <plugin>/build/plugin/<name>.cgp assets/cgp-build.properties +``` + +| Key | Meaning | +|---|---| +| `revision` | The commit. `unknown` when none was found | +| `revision_source` | How it was found: `explicit`, `env:<VAR>`, `git`, `git-dir`, or `none` | +| `timestamp` | The committer date in UTC, not the clock | +| `timestamp_source` | `wall-clock` when it fell back to the clock | +| `libs_revision` | Which Code on the Go commit produced the jars it compiled against | + +`+dirty` is appended to the revision when the addon's own directory has uncommitted changes. +`scripts/verify-provenance.sh` asserts the record after each build. A missing file or a +missing key fails the run. + +### Why a `.cgt` has none of this + +All of it is Gradle work, done by `libs/gradle-plugin.jar`. A template has no Gradle build, +so nothing writes the record, and `verify-provenance.sh` would fail on an archive that has no +`assets/cgp-build.properties`. + +The gap is real, not cosmetic. A Flutter template bundle is generated on every publish. Two +publishes a month apart produce two different files at the same URL, with nothing inside +either one to tell them apart. + +### Decided: write `cgt-build.properties` at the archive root + +The build step generates the file, includes it in the archive, and gitignores it. Read it +back the same way as a plugin: + +```sh +unzip -p flutter-templates.cgt cgt-build.properties +``` + +```properties +revision=a1b2c3d4e5f6 +revision_source=env:GITHUB_SHA +timestamp=2026-09-25T14:02:11Z +timestamp_source=git +addon_id=org.appdevforall.fluttertemplates +version=1.0.0 +``` + +The key names match `cgp-build.properties` where they mean the same thing, so one reader +serves both. `libs_revision` is absent on purpose: a template compiles against nothing. + +A root entry is inert to all three readers. `ZipTemplateReader` reads only `templates.json` +and the paths it lists. `ZipRecipeExecutor` skips every entry outside `<TemplateName>/`. +`CgtTemplateReader` matches only names ending in `/template/template.json`. I read all three +to confirm this. + +### Two rules the build step must keep + +1. **Write the committer date, not the clock.** The archive is deterministic today: fixed + 1980 mtimes and a sorted entry list mean one commit gives one archive. A wall-clock + timestamp would end that, and every rebuild of one commit would differ. +2. **Pin the mtime of the generated file too.** It joins the sorted file list like any other + entry, so it takes the same `touch -t 198001010000`. + +### `verify-provenance.sh` + +Extend it rather than exempting templates. Three things in it are plugin-shaped. + +**The required keys.** Today: + +```bash +REQUIRED_PROVENANCE_KEYS=(name version variant revision revision_source timestamp timestamp_source) +``` + +`variant` here is the **Android build variant**, debug or release. A `.cgt` has no such +thing, and neither `name` nor `version` is written the same way. The `.cgt` list is +`revision revision_source timestamp timestamp_source addon_id version`. + +This is also why the plan says "template" and never "variant" for the five Flutter +directories. The word is already taken in this exact file, for a different thing. + +**Where it looks.** It globs `build/plugin/*.cgp`, rejects a run that finds none, and rejects +a run that finds more than one — the second guard exists because a `pluginName` rename leaves +a stale artifact behind. Neither applies. A template's archive is written once, to the path +`scripts/build-cgt.sh` was given. + +**What it reads.** `unzip -p "$cgp" assets/cgp-build.properties` becomes +`unzip -p "$cgt" cgt-build.properties`. The record is at the archive root, not under +`assets/`, because a `.cgt` has no `assets/` directory. + +Keep unchanged: the warnings for `revision=unknown`, `+dirty` and +`timestamp_source=wall-clock`. All three are legitimate off CI, and they mean the same thing +for either artifact. Drop only the `libs_revision` comparison — a template compiles against +nothing, so there is no pairing to record. diff --git a/tools/addons/src/addons/addon.schema.json b/tools/addons/src/addons/addon.schema.json index d0ebefc9..0fe940c0 100644 --- a/tools/addons/src/addons/addon.schema.json +++ b/tools/addons/src/addons/addon.schema.json @@ -57,6 +57,32 @@ "type": "string" } } + }, + "template": { + "$comment": "Template addons only (ADFA-6252). A template has no AndroidManifest.xml, so the three values a plugin keeps as <meta-data> live here. check.py requires this object for an addon under templates/ and model.py reads it in place of the manifest. A plugin must not carry it.", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "version", + "minAppVersion" + ], + "properties": { + "id": { + "$comment": "Unique, and never changed across releases.", + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)*$" + }, + "minAppVersion": { + "$comment": "A YY.ww Code on the Go release, e.g. 26.38.", + "type": "string", + "pattern": "^[0-9]{2}\\.[0-9]{2}$" + } + } } } } diff --git a/tools/addons/src/addons/catalog.py b/tools/addons/src/addons/catalog.py index 881d95b1..5bae2056 100644 --- a/tools/addons/src/addons/catalog.py +++ b/tools/addons/src/addons/catalog.py @@ -34,7 +34,12 @@ def _file(path: Path, url: str) -> dict: return {"url": url, "sha256": hashlib.sha256(data).hexdigest(), "size": len(data)} -def entry(root: Path, addon: Path, cgp: Path, archive: Path, +def artifact_suffix(addon: Path) -> str: + """What this addon downloads as: a plugin compiles, a template zips.""" + return "cgt" if discover.is_template(addon) else "cgp" + + +def entry(root: Path, addon: Path, cgp: Path, archive: Path | None, base: str = BASE) -> dict: directory = addon.name slug = model.slug(directory) @@ -43,10 +48,10 @@ def entry(root: Path, addon: Path, cgp: Path, archive: Path, if not VERSION.match(version): raise RuntimeError(f"{directory}: the version '{version}' is not a number") relative = addon.relative_to(root).as_posix() - return { + document = { "type": TYPES.get(addon.parent.name, "plugin"), "slug": slug, - "pluginId": model.plugin_id(addon), + "addonId": model.addon_id(addon), "name": model.display_name(directory), "version": version, "summary": meta["summary"], @@ -60,9 +65,16 @@ def entry(root: Path, addon: Path, cgp: Path, archive: Path, "iconDarkUrl": f"{base}/p/{slug}-night.png", "pageUrl": f"{base}/p/{slug}.html", "sourceUrl": f"{REPO}/tree/{REF}/{relative}", - "download": _file(cgp, f"{base}/dl/{slug}.cgp"), - "sourceTarball": _file(archive, f"{base}/src/{slug}-src.tar.gz"), + "download": _file(cgp, f"{base}/dl/{slug}.{artifact_suffix(addon)}"), } + # A template ships no source tarball: the .cgt is plain text throughout, so + # the download already is the source, and sourceUrl above points at the + # directory (ADFA-6252). sourceTarball is therefore optional from + # schemaVersion 2 on, and consumers must tolerate its absence. + if archive is not None: + document["sourceTarball"] = _file( + archive, f"{base}/src/{slug}-src.tar.gz") + return document def build(root: Path, dist: Path, base: str = BASE, @@ -70,15 +82,19 @@ def build(root: Path, dist: Path, base: str = BASE, entries = [] for addon in discover.find_addons(root, only): slug = model.slug(addon.name) - cgp = dist / f"{slug}.cgp" - archive = dist / f"{slug}-src.tar.gz" - for f in (cgp, archive): + cgp = dist / f"{slug}.{artifact_suffix(addon)}" + required = [cgp] + archive = None + if not discover.is_template(addon): + archive = dist / f"{slug}-src.tar.gz" + required.append(archive) + for f in required: if not f.exists(): raise RuntimeError(f"{addon.name}: {f.name} is missing from {dist}") entries.append(entry(root, addon, cgp, archive, base.rstrip('/'))) document = { - "schemaVersion": 1, + "schemaVersion": 2, "generated": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "addons": entries, } diff --git a/tools/addons/src/addons/check.py b/tools/addons/src/addons/check.py index 21be8c1b..18b3f0bd 100644 --- a/tools/addons/src/addons/check.py +++ b/tools/addons/src/addons/check.py @@ -17,6 +17,71 @@ re.S | re.I) +def check_template(path: Path, directory: str) -> list[str]: + """The structural rules a .cgt bundle source must satisfy (ADFA-6252). + + These are the failures the IDE handles in silence. A path in templates.json + that does not exist, or a template directory with no template.json, is + skipped at load with no error shown to the user, so the template simply + never appears. Catch both here. + """ + problems = [] + index = path / "templates.json" + try: + data = json.loads(index.read_text()) + except json.JSONDecodeError as error: + return [f"{directory}: templates.json is not valid JSON: {error}"] + + listed = data.get("templates") + if not isinstance(listed, list) or not listed: + return [f"{directory}: templates.json lists no templates"] + + for item in listed: + name = item.get("path") if isinstance(item, dict) else None + if not name: + problems.append(f"{directory}: a templates.json entry has no 'path'") + continue + # keep the archive flat and inside itself: these become zip entry names + if "/" in name or name.startswith("."): + problems.append( + f"{directory}: the template path '{name}' must be a plain " + f"directory name") + continue + folder = path / name + if not folder.is_dir(): + problems.append( + f"{directory}: templates.json names '{name}', which is not a " + f"directory") + continue + meta = folder / "template" / "template.json" + if not meta.exists(): + problems.append(f"{directory}: {name}/template/template.json is missing") + continue + # JSON5, not JSON: core.cgt uses unquoted keys and the IDE parses it + # leniently, so only report what no lenient parser could read. + text = meta.read_text() + for key in ("name", "description"): + if f'"{key}"' not in text and f"{key}:" not in text: + problems.append( + f"{directory}: {name}/template/template.json declares no {key}") + + # Every directory that is present but unlisted is dead weight in the + # repository and never reaches the archive, so say so. + listed_names = {item.get("path") for item in listed if isinstance(item, dict)} + for child in sorted(path.iterdir()): + if (child.is_dir() and not child.name.startswith(".") + and child.name not in listed_names): + problems.append( + f"{directory}: the directory '{child.name}' is not listed in " + f"templates.json, so it is not packaged") + + if not model.template_block(path): + problems.append( + f"{directory}: addon.json declares no 'template' object with id, " + f"version and minAppVersion") + return problems + + def check_names(root: Path) -> list[str]: problems = [] # read once: the same contract governs every addon in the run @@ -38,30 +103,39 @@ def check_names(root: Path) -> list[str]: f"catalog schema refuses ({slug_shape.pattern}). Use ASCII " f"letters and digits, separated by single hyphens") - build = (path / "build.gradle.kts").read_text() - found = re.search(r'pluginName\s*=\s*"([^"]*)"', build) - if found is None: - problems.append(f"{directory}: build.gradle.kts declares no pluginName") - elif found.group(1) != addon_slug: - problems.append( - f"{directory}: pluginName is '{found.group(1)}', expected '{addon_slug}'") + template = discover.is_template(path) - plugin_name = model.manifest_value(path, "plugin.name") - if not plugin_name: - problems.append(f"{directory}: the manifest declares no plugin.name") - elif plugin_name != name: - problems.append( - f"{directory}: plugin.name is '{plugin_name}', expected '{name}'") + if template: + problems += check_template(path, directory) + else: + build = (path / "build.gradle.kts").read_text() + found = re.search(r'pluginName\s*=\s*"([^"]*)"', build) + if found is None: + problems.append(f"{directory}: build.gradle.kts declares no pluginName") + elif found.group(1) != addon_slug: + problems.append( + f"{directory}: pluginName is '{found.group(1)}', expected '{addon_slug}'") + + plugin_name = model.manifest_value(path, "plugin.name") + if not plugin_name: + problems.append(f"{directory}: the manifest declares no plugin.name") + elif plugin_name != name: + problems.append( + f"{directory}: plugin.name is '{plugin_name}', expected '{name}'") # the app keys every install on this; an empty one orphans them all - if not model.plugin_id(path): - problems.append(f"{directory}: the manifest declares no plugin.id") + if not model.addon_id(path): + problems.append( + f"{directory}: no addon id — a template declares template.id in " + f"addon.json, a plugin declares plugin.id in its manifest") - declared = model.manifest_value(path, "plugin.min_ide_version") + declared = (model.template_block(path).get("minAppVersion", "") if template + else model.manifest_value(path, "plugin.min_ide_version")) + field = "template.minAppVersion" if template else "plugin.min_ide_version" if (declared and declared != model.LEGACY_MIN_VERSION and not model.RELEASE_VERSION.match(declared)): problems.append( - f"{directory}: plugin.min_ide_version is '{declared}', " + f"{directory}: {field} is '{declared}', " f"expected a YY.ww release like 26.29") if not model.VERSION_SHAPE.match(model.version(path)): @@ -69,9 +143,12 @@ def check_names(root: Path) -> list[str]: f"{directory}: the version '{model.version(path)}' is not a " f"dotted number") + # A template has no Android module, so its icons sit at the addon root. + icon_dir = path if template else path / "src" / "main" / "assets" + shown = "" if template else "src/main/assets/" for icon in ("icon_day.png", "icon_night.png"): - if not (path / "src" / "main" / "assets" / icon).exists(): - problems.append(f"{directory}: src/main/assets/{icon} is missing") + if not (icon_dir / icon).exists(): + problems.append(f"{directory}: {shown}{icon} is missing") page = path / f"{addon_slug}.html" if not page.exists(): diff --git a/tools/addons/src/addons/cli.py b/tools/addons/src/addons/cli.py index fc53bdf5..2a0bad02 100644 --- a/tools/addons/src/addons/cli.py +++ b/tools/addons/src/addons/cli.py @@ -59,7 +59,13 @@ def main(argv: list[str] | None = None) -> int: for addon in discover.find_addons(args.root, args.only): meta = model.metadata(addon) archive = tarball.build(args.root, addon, args.out, meta) - print(f"built {archive.name}") + # None means the addon ships no source tarball; a template's .cgt + # is its own source. Say so rather than printing nothing, so a + # workflow log shows the addon was considered. + if archive is None: + print(f"skipped {addon.name}: no source tarball for a template") + else: + print(f"built {archive.name}") return 0 if args.command == "publish": @@ -100,15 +106,21 @@ def with_hashed_assets(text: str) -> str: model.display_name(addon.name), template) page_file = dist / f"{slug}.page.html" page_file.write_text(with_hashed_assets(wrapped)) + # A template keeps its icons at the addon root: src/main/assets is + # an Android path and it has no Android module (ADFA-6252). + icons = (addon if discover.is_template(addon) + else addon / "src" / "main" / "assets") + suffix = catalog.artifact_suffix(addon) objects += [ (f"{prefix}p/{slug}.html", page_file), - (f"{prefix}p/{slug}.png", - addon / "src" / "main" / "assets" / "icon_day.png"), - (f"{prefix}p/{slug}-night.png", - addon / "src" / "main" / "assets" / "icon_night.png"), - (f"{prefix}dl/{slug}.cgp", dist / f"{slug}.cgp"), - (f"{prefix}src/{slug}-src.tar.gz", dist / f"{slug}-src.tar.gz"), + (f"{prefix}p/{slug}.png", icons / "icon_day.png"), + (f"{prefix}p/{slug}-night.png", icons / "icon_night.png"), + (f"{prefix}dl/{slug}.{suffix}", dist / f"{slug}.{suffix}"), ] + # No source tarball for a template; see tarball.build(). + if not discover.is_template(addon): + objects.append((f"{prefix}src/{slug}-src.tar.gz", + dist / f"{slug}-src.tar.gz")) objects.append((f"{prefix}v1/catalog.schema.json", site / "catalog.schema.json")) publish.publish(publish.client_from_env(), publish.bucket_from_env(), diff --git a/tools/addons/src/addons/discover.py b/tools/addons/src/addons/discover.py index 562f6d7b..e5728496 100644 --- a/tools/addons/src/addons/discover.py +++ b/tools/addons/src/addons/discover.py @@ -31,6 +31,22 @@ def read_skip(root: Path, only_never_build: bool = False) -> set[str]: return names +def is_template(addon: Path) -> bool: + """A template addon is a directory under templates/ holding templates.json. + + Deliberately not the plugin rule. A template has no Gradle build, nothing + to compile and no .cgp — it zips to a .cgt (ADFA-6252). Keying on + templates.json rather than on the area alone means templates/ can also hold + documentation without every markdown file becoming an addon. + """ + return addon.parent.name == "templates" and (addon / "templates.json").is_file() + + +def find_templates(root: Path, skip: set[str]) -> list[Path]: + return [f.parent for f in root.glob("templates/*/templates.json") + if f.parent.name not in skip] + + def find_addons(root: Path, only: list[str] | None = None, include_skipped: bool = False) -> list[Path]: skip = (read_skip(root, only_never_build=True) if include_skipped @@ -45,6 +61,7 @@ def find_addons(root: Path, only: list[str] | None = None, for f in root.glob(pattern): if PREDICATE in f.read_text(errors="ignore") and f.parent.name not in skip: found.append(f.parent) + found.extend(find_templates(root, skip)) found = sorted(found, key=lambda p: p.name) if only is None: return found diff --git a/tools/addons/src/addons/model.py b/tools/addons/src/addons/model.py index 27423874..6b7a6a28 100644 --- a/tools/addons/src/addons/model.py +++ b/tools/addons/src/addons/model.py @@ -48,10 +48,38 @@ def manifest_value(addon: Path, key: str) -> str: return "" -def plugin_id(addon: Path) -> str: +def template_block(addon: Path) -> dict: + """The `template` object in a template addon's addon.json. + + A template has no AndroidManifest.xml, so the three values a plugin keeps + there live here instead (ADFA-6252). Returns {} for a plugin, so each + accessor below can ask without knowing the addon kind. + """ + from addons import discover + if not discover.is_template(addon): + return {} + try: + return metadata(addon).get("template") or {} + except RuntimeError: + return {} # addon.json missing or invalid; check.py reports it + + +def addon_id(addon: Path) -> str: + """The id the catalog publishes as `addonId`. + + `plugin.id` has no meaning for a template, which declares `template.id`. + """ + block = template_block(addon) + if block: + return block.get("id", "") return manifest_value(addon, "plugin.id") +# The catalog field was renamed to addonId at schemaVersion 2; this alias keeps +# the older call sites working while they are updated. +plugin_id = addon_id + + def version(addon: Path) -> str: """The version a user sees for this addon. @@ -59,7 +87,12 @@ def version(addon: Path) -> str: wins. Otherwise the builder replaces ${pluginVersion} with versionName plus a build-timestamp suffix; the catalog reports the semantic part, since the suffix changes on every build and carries no meaning. + + A template declares it in addon.json: there is no manifest and no build. """ + block = template_block(addon) + if block: + return block.get("version", DEFAULT_VERSION) declared = manifest_value(addon, "plugin.version") if declared and not declared.startswith("${"): return declared @@ -82,7 +115,13 @@ def min_app_version(addon: Path) -> str: The app's release version is YY.ww. Thirteen addons still carry the legacy "1.0.0" placeholder, which states no real minimum, so it is reported as no minimum rather than invented. + + A template declares it as template.minAppVersion in addon.json. """ + block = template_block(addon) + if block: + declared = block.get("minAppVersion", "") + return "" if declared == LEGACY_MIN_VERSION else declared declared = manifest_value(addon, "plugin.min_ide_version") if not declared or declared == LEGACY_MIN_VERSION: return "" # the legacy placeholder states no real minimum diff --git a/tools/addons/src/addons/publish.py b/tools/addons/src/addons/publish.py index 521c8c1e..9a194d4c 100644 --- a/tools/addons/src/addons/publish.py +++ b/tools/addons/src/addons/publish.py @@ -15,9 +15,10 @@ ".png": "image/png", ".svg": "image/svg+xml", ".cgp": "application/octet-stream", + ".cgt": "application/octet-stream", ".gz": "application/gzip", } -ATTACHMENTS = {".cgp", ".gz"} +ATTACHMENTS = {".cgp", ".cgt", ".gz"} def hashed_name(path: Path) -> str: diff --git a/tools/addons/src/addons/tarball.py b/tools/addons/src/addons/tarball.py index ae984f92..013f7602 100644 --- a/tools/addons/src/addons/tarball.py +++ b/tools/addons/src/addons/tarball.py @@ -213,7 +213,20 @@ def verify(top: Path, inside: str, jars: list[str]) -> None: def build(root: Path, addon: Path, out: Path, - meta: dict | None = None) -> Path: + meta: dict | None = None) -> Path | None: + """Stage and archive an addon's source, or None when it has none to ship. + + A template returns None (ADFA-6252). Everything below exists so somebody can + unpack the archive and build it: the shared jars are copied in, the Gradle + wrapper is copied in, ../libs/ references are flattened, and BUILDING.md + says to run assemblePlugin. A template has no project to open and no + compiler to run, and its .cgt is plain text throughout, so the published + download already is its source. The catalog omits sourceTarball for it and + the gallery card drops the Source link. + """ + from addons import discover + if discover.is_template(addon): + return None jars = jars_for(addon) if not jars: raise RuntimeError(f"{addon.name}: it references no shared jar") diff --git a/tools/addons/tests/test_catalog.py b/tools/addons/tests/test_catalog.py index 9879e79d..7f54711c 100644 --- a/tools/addons/tests/test_catalog.py +++ b/tools/addons/tests/test_catalog.py @@ -45,16 +45,87 @@ def make(tmp_path: Path, build_extra: str = "") -> Path: def test_builds_a_valid_entry(tmp_path): dist = make(tmp_path) result = catalog.build(tmp_path, dist) - assert result["schemaVersion"] == 1 + assert result["schemaVersion"] == 2 entry = result["addons"][0] assert entry["type"] == "plugin" assert entry["slug"] == "keystore-generator" assert entry["name"] == "Keystore Generator" - assert entry["pluginId"] == "com.appdevforall.keygen.plugin" + assert entry["addonId"] == "com.appdevforall.keygen.plugin" assert entry["version"] == "1.0.0" - assert entry["download"]["url"].startswith("https://") + assert entry["download"]["url"].endswith("/dl/keystore-generator.cgp") assert entry["download"]["size"] == 3 assert len(entry["download"]["sha256"]) == 64 + # a plugin still ships one; only a template omits it + assert entry["sourceTarball"]["url"].endswith("/src/keystore-generator-src.tar.gz") + + +TEMPLATE_METADATA = METADATA | { + "template": { + "id": "org.appdevforall.fluttertemplates", + "version": "1.0.0", + "minAppVersion": "26.38", + }, +} + + +def make_template(tmp_path: Path, with_tarball: bool = False) -> Path: + """A template addon: no Gradle build, no manifest, metadata in addon.json.""" + addon = tmp_path / "templates" / "Flutter-Templates" + (addon / "FlutterBasic" / "template").mkdir(parents=True) + (addon / "templates.json").write_text( + json.dumps({"templates": [{"path": "FlutterBasic"}]})) + (addon / "FlutterBasic" / "template" / "template.json").write_text( + json.dumps({"name": "Flutter Basic", "description": "A starter"})) + (addon / "addon.json").write_text(json.dumps(TEMPLATE_METADATA)) + dist = tmp_path / "dist" + dist.mkdir(exist_ok=True) + (dist / "flutter-templates.cgt").write_bytes(b"cgt") + if with_tarball: + (dist / "flutter-templates-src.tar.gz").write_bytes(b"tar") + site = tmp_path / "site" + site.mkdir(exist_ok=True) + (site / "catalog.schema.json").write_text( + Path(__file__).parents[3].joinpath("site/catalog.schema.json").read_text()) + return dist + + +def test_a_template_entry_downloads_a_cgt(tmp_path): + dist = make_template(tmp_path) + entry = catalog.build(tmp_path, dist)["addons"][0] + assert entry["type"] == "template" + assert entry["slug"] == "flutter-templates" + assert entry["name"] == "Flutter Templates" + # from addon.json's template block, not from a manifest it does not have + assert entry["addonId"] == "org.appdevforall.fluttertemplates" + assert entry["version"] == "1.0.0" + assert entry["minAppVersion"] == "26.38" + assert entry["download"]["url"].endswith("/dl/flutter-templates.cgt") + + +def test_a_template_entry_omits_the_source_tarball(tmp_path): + """The .cgt is plain text, so it is its own source (ADFA-6252). + + The schema must also accept the omission, which catalog.build() validates, + so this covers site/catalog.schema.json as much as catalog.py. + """ + dist = make_template(tmp_path) + entry = catalog.build(tmp_path, dist)["addons"][0] + assert "sourceTarball" not in entry + assert entry["sourceUrl"].endswith("/templates/Flutter-Templates") + + +def test_a_template_does_not_need_a_tarball_in_dist(tmp_path): + """Requiring one would fail the publish for every template.""" + dist = make_template(tmp_path) + assert not (dist / "flutter-templates-src.tar.gz").exists() + catalog.build(tmp_path, dist) # must not raise + + +def test_a_missing_cgt_still_stops_the_build(tmp_path): + dist = make_template(tmp_path) + (dist / "flutter-templates.cgt").unlink() + with pytest.raises(RuntimeError, match="flutter-templates.cgt"): + catalog.build(tmp_path, dist) def test_no_field_is_null(tmp_path): diff --git a/tools/addons/tests/test_check_templates.py b/tools/addons/tests/test_check_templates.py new file mode 100644 index 00000000..efbde228 --- /dev/null +++ b/tools/addons/tests/test_check_templates.py @@ -0,0 +1,136 @@ +"""check.py rules for template addons (ADFA-6252). + +Most of these cover failures the IDE handles in silence: a path in +templates.json that names nothing, or a template directory with no +template.json, is skipped at load with no error shown, so the template simply +never appears on the New Project screen. The check is the only thing that says +so before a release. +""" +import json +from pathlib import Path + +from addons import check + +CATALOG_SCHEMA = Path(__file__).parents[3] / "site" / "catalog.schema.json" + +BLOCK = {"id": "org.appdevforall.fluttertemplates", + "version": "1.0.0", "minAppVersion": "26.38"} + +BASE_META = {"summary": "s", "description": "d", "tags": ["flutter"], + "origin": "community", "license": "AGPL-3.0-or-later", + "author": {"name": "RJ Ali", "url": ""}} + +PAGE = ("<html><title>Flutter Templates" + "

Flutter Templates

") + + +def make_template(root: Path, index=None, block=BLOCK, + with_meta: bool = True) -> Path: + site = root / "site" + site.mkdir(exist_ok=True) + (site / "catalog.schema.json").write_text(CATALOG_SCHEMA.read_text()) + + d = root / "templates" / "Flutter-Templates" + (d / "FlutterBasic" / "template").mkdir(parents=True) + for icon in ("icon_day.png", "icon_night.png"): + (d / icon).write_bytes(b"png") + + if index is None: + index = {"templates": [{"path": "FlutterBasic"}]} + (d / "templates.json").write_text( + index if isinstance(index, str) else json.dumps(index)) + + if with_meta: + (d / "FlutterBasic" / "template" / "template.json").write_text( + json.dumps({"name": "Flutter Basic", "description": "A starter"})) + + meta = dict(BASE_META) + if block is not None: + meta["template"] = block + (d / "addon.json").write_text(json.dumps(meta)) + (d / "flutter-templates.html").write_text(PAGE) + return d + + +def test_a_compliant_template_passes(tmp_path): + make_template(tmp_path) + assert check.check_names(tmp_path) == [] + + +def test_a_template_needs_no_gradle_build_or_manifest(tmp_path): + """The plugin checks must not run against a template.""" + d = make_template(tmp_path) + assert not (d / "build.gradle.kts").exists() + assert not (d / "src").exists() + assert check.check_names(tmp_path) == [] + + +def test_template_icons_live_at_the_addon_root(tmp_path): + d = make_template(tmp_path) + (d / "icon_day.png").unlink() + problems = check.check_names(tmp_path) + assert any("icon_day.png is missing" in p for p in problems) + # and the message must not name the Android path a template does not have + assert not any("src/main/assets" in p for p in problems) + + +def test_a_missing_template_directory_fails(tmp_path): + make_template(tmp_path, index={"templates": [{"path": "NoSuchThing"}]}) + problems = check.check_names(tmp_path) + assert any("NoSuchThing" in p and "not a directory" in p for p in problems) + + +def test_a_template_without_template_json_fails(tmp_path): + make_template(tmp_path, with_meta=False) + problems = check.check_names(tmp_path) + assert any("FlutterBasic/template/template.json is missing" in p + for p in problems) + + +def test_an_unlisted_template_directory_fails(tmp_path): + """A directory nobody listed is never packaged, so say so.""" + d = make_template(tmp_path) + (d / "FlutterBloc" / "template").mkdir(parents=True) + problems = check.check_names(tmp_path) + assert any("FlutterBloc" in p and "not listed" in p for p in problems) + + +def test_a_template_path_with_a_slash_fails(tmp_path): + make_template(tmp_path, index={"templates": [{"path": "a/b"}]}) + problems = check.check_names(tmp_path) + assert any("plain directory name" in p for p in problems) + + +def test_invalid_templates_json_fails(tmp_path): + make_template(tmp_path, index="{ not json") + problems = check.check_names(tmp_path) + assert any("templates.json is not valid JSON" in p for p in problems) + + +def test_an_empty_templates_json_fails(tmp_path): + make_template(tmp_path, index={"templates": []}) + problems = check.check_names(tmp_path) + assert any("lists no templates" in p for p in problems) + + +def test_a_template_without_the_addon_json_block_fails(tmp_path): + make_template(tmp_path, block=None) + problems = check.check_names(tmp_path) + assert any("no 'template' object" in p for p in problems) + assert any("no addon id" in p for p in problems) + + +def test_a_bad_min_app_version_fails(tmp_path): + bad = dict(BLOCK) + bad["minAppVersion"] = "1.2.3" + make_template(tmp_path, block=bad) + problems = check.check_names(tmp_path) + assert any("template.minAppVersion" in p for p in problems) + + +def test_the_addon_json_schema_rejects_a_bad_template_block(tmp_path): + bad = dict(BLOCK) + del bad["minAppVersion"] + make_template(tmp_path, block=bad) + problems = check.check_metadata(tmp_path) + assert any("addon.json is invalid" in p for p in problems) diff --git a/tools/addons/tests/test_discover.py b/tools/addons/tests/test_discover.py index b2dc9819..2f0767d8 100644 --- a/tools/addons/tests/test_discover.py +++ b/tools/addons/tests/test_discover.py @@ -90,3 +90,56 @@ def test_a_bang_prefix_means_never_build(tmp_path): # compile coverage picks up the held-back one but never the broken one assert [p.name for p in discover.find_addons(tmp_path, include_skipped=True)] \ == ["Shipping", "held-back"] + + +def make_template(root: Path, path: str, with_index: bool = True) -> None: + d = root / path + d.mkdir(parents=True) + if with_index: + (d / "templates.json").write_text('{"templates": [{"path": "One"}]}') + + +def test_finds_a_template_with_no_gradle_build(tmp_path): + """A template is discovered by templates.json, not build.gradle.kts. + + This is the whole point of the second rule (ADFA-6252): a template has no + Gradle project, so the plugin predicate can never match it. + """ + make_template(tmp_path, "templates/Flutter-Templates") + found = discover.find_addons(tmp_path) + assert [p.name for p in found] == ["Flutter-Templates"] + assert discover.is_template(found[0]) + + +def test_a_templates_dir_without_the_index_is_not_an_addon(tmp_path): + """templates/ also holds documentation; only a bundle source is an addon.""" + make_template(tmp_path, "templates/Notes", with_index=False) + assert discover.find_addons(tmp_path) == [] + + +def test_templates_and_plugins_are_listed_together(tmp_path): + make_addon(tmp_path, "plugins/Random-XKCD") + make_template(tmp_path, "templates/Flutter-Templates") + found = discover.find_addons(tmp_path) + assert [p.name for p in found] == ["Flutter-Templates", "Random-XKCD"] + assert [discover.is_template(p) for p in found] == [True, False] + + +def test_a_plugin_is_not_a_template(tmp_path): + make_addon(tmp_path, "plugins/Random-XKCD") + assert not discover.is_template(tmp_path / "plugins" / "Random-XKCD") + + +def test_skip_txt_holds_back_a_template_too(tmp_path): + make_template(tmp_path, "templates/Flutter-Templates") + (tmp_path / "tools" / "addons").mkdir(parents=True) + (tmp_path / "tools" / "addons" / "skip.txt").write_text( + "Flutter-Templates not ready\n") + assert discover.find_addons(tmp_path) == [] + + +def test_only_resolves_a_template_by_path_or_name(tmp_path): + make_template(tmp_path, "templates/Flutter-Templates") + by_name = discover.find_addons(tmp_path, ["Flutter-Templates"]) + by_path = discover.find_addons(tmp_path, ["templates/Flutter-Templates"]) + assert by_name == by_path diff --git a/tools/addons/tests/test_tarball.py b/tools/addons/tests/test_tarball.py index 222280d3..6b10e851 100644 --- a/tools/addons/tests/test_tarball.py +++ b/tools/addons/tests/test_tarball.py @@ -226,3 +226,22 @@ def test_no_builder_identity_leaks_into_the_archive(tmp_path): with tarfile.open(build(tmp_path, addon, META)) as tar: for m in tar.getmembers(): assert m.uname == "" and m.gname == "" and m.uid == 0 and m.mtime == 0 + + +def test_a_template_ships_no_tarball(tmp_path): + """build() returns None instead of raising (ADFA-6252). + + Without the early exit, jars_for() raises "references no shared jar" on the + first line, because a template references none — so this guards the failure + that would block every template publish. + """ + subprocess.run(["git", "init", "-q", str(tmp_path)], check=True) + addon = tmp_path / "templates" / "Flutter-Templates" + (addon / "FlutterBasic" / "template").mkdir(parents=True) + (addon / "templates.json").write_text('{"templates": [{"path": "FlutterBasic"}]}') + (addon / "FlutterBasic" / "pubspec.yaml.peb").write_text("name: x\n") + out = tmp_path / "out" + out.mkdir() + + assert tarball.build(tmp_path, addon, out) is None + assert list(out.iterdir()) == [] From e2555efcf690a4944978cde5877003f3c61891d7 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Fri, 25 Sep 2026 16:00:12 -0700 Subject: [PATCH 02/12] ADFA-6252 | Fix invalid pubspec.yaml from every Flutter template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device verification caught this; no build or lint step could. Generating a project produced: name: bloccounterdemodescription: Flutter BLoC project created by ... which a YAML parser rejects outright with "mapping values are not allowed here", so `flutter pub get` could never have run on a generated project. The cause is the newline-trimming gotcha already documented in templates/cgt-templates.md: a bare ${{TAG}} at end of line loses its trailing newline to Pebble, so the line merges with the next. All five pubspec.yaml.peb opened with name: ${{APP_NAME | lower}} description: ... and lost the break between them. The documented convention is to put a non-newline character after }}, so the value is now quoted — which is ordinary YAML anyway: name: "${{APP_NAME | lower}}" The same trimming hit `# ${{APP_NAME}}` at the top of each README.md.peb, eating the blank line between the heading and the first paragraph. Markdown still renders it, so this one is cosmetic; a compensating blank line restores the intended output. This predates the conversion — the plugin shipped the same skeletons and produced the same broken YAML. It survived because the only check anyone ran was that the plugin compiled, and the brace-balance lint counts delimiters without rendering anything. Also updated the description and README line that both generated projects carry: they credited "the Flutter Template plugin", which no longer exists. Verified on emulator-5554: all five templates appear on New Project with their thumbnails, the wizard shows exactly the three declared parameters (no language picker, no minSdk), a generated project has the .peb suffixes stripped and no template/ directory, and the resulting pubspec.yaml parses with all eight expected keys. --- templates/Flutter-Templates/FlutterBasic/README.md.peb | 3 ++- templates/Flutter-Templates/FlutterBasic/pubspec.yaml.peb | 4 ++-- templates/Flutter-Templates/FlutterBloc/README.md.peb | 3 ++- templates/Flutter-Templates/FlutterBloc/pubspec.yaml.peb | 4 ++-- templates/Flutter-Templates/FlutterGetx/README.md.peb | 3 ++- templates/Flutter-Templates/FlutterGetx/pubspec.yaml.peb | 4 ++-- templates/Flutter-Templates/FlutterProvider/README.md.peb | 3 ++- templates/Flutter-Templates/FlutterProvider/pubspec.yaml.peb | 4 ++-- templates/Flutter-Templates/FlutterRiverpod/README.md.peb | 3 ++- templates/Flutter-Templates/FlutterRiverpod/pubspec.yaml.peb | 4 ++-- 10 files changed, 20 insertions(+), 15 deletions(-) diff --git a/templates/Flutter-Templates/FlutterBasic/README.md.peb b/templates/Flutter-Templates/FlutterBasic/README.md.peb index faacc132..53463d67 100644 --- a/templates/Flutter-Templates/FlutterBasic/README.md.peb +++ b/templates/Flutter-Templates/FlutterBasic/README.md.peb @@ -1,6 +1,7 @@ # ${{APP_NAME}} -Generated by the Flutter Template plugin for Code on the Go. + +Generated from a Flutter Templates starter for Code on the Go. - Package: `${{PACKAGE_NAME}}` - Template: Basic diff --git a/templates/Flutter-Templates/FlutterBasic/pubspec.yaml.peb b/templates/Flutter-Templates/FlutterBasic/pubspec.yaml.peb index 034a4b63..b8a4be1d 100644 --- a/templates/Flutter-Templates/FlutterBasic/pubspec.yaml.peb +++ b/templates/Flutter-Templates/FlutterBasic/pubspec.yaml.peb @@ -1,5 +1,5 @@ -name: ${{APP_NAME | lower}} -description: Flutter project created by the Flutter Template plugin +name: "${{APP_NAME | lower}}" +description: Flutter project created from a Flutter Templates starter for Code on the Go publish_to: 'none' version: 1.0.0+1 diff --git a/templates/Flutter-Templates/FlutterBloc/README.md.peb b/templates/Flutter-Templates/FlutterBloc/README.md.peb index dd2e5ce3..cbbeb4fa 100644 --- a/templates/Flutter-Templates/FlutterBloc/README.md.peb +++ b/templates/Flutter-Templates/FlutterBloc/README.md.peb @@ -1,6 +1,7 @@ # ${{APP_NAME}} -Generated by the Flutter Template plugin for Code on the Go. + +Generated from a Flutter Templates starter for Code on the Go. - Package: `${{PACKAGE_NAME}}` - Template: BLoC diff --git a/templates/Flutter-Templates/FlutterBloc/pubspec.yaml.peb b/templates/Flutter-Templates/FlutterBloc/pubspec.yaml.peb index 8e915cfc..7ed0d473 100644 --- a/templates/Flutter-Templates/FlutterBloc/pubspec.yaml.peb +++ b/templates/Flutter-Templates/FlutterBloc/pubspec.yaml.peb @@ -1,5 +1,5 @@ -name: ${{APP_NAME | lower}} -description: Flutter BLoC project created by the Flutter Template plugin +name: "${{APP_NAME | lower}}" +description: Flutter BLoC project created from a Flutter Templates starter for Code on the Go publish_to: 'none' version: 1.0.0+1 diff --git a/templates/Flutter-Templates/FlutterGetx/README.md.peb b/templates/Flutter-Templates/FlutterGetx/README.md.peb index 8f6ae015..3943c7b6 100644 --- a/templates/Flutter-Templates/FlutterGetx/README.md.peb +++ b/templates/Flutter-Templates/FlutterGetx/README.md.peb @@ -1,6 +1,7 @@ # ${{APP_NAME}} -Generated by the Flutter Template plugin for Code on the Go. + +Generated from a Flutter Templates starter for Code on the Go. - Package: `${{PACKAGE_NAME}}` - Template: GetX diff --git a/templates/Flutter-Templates/FlutterGetx/pubspec.yaml.peb b/templates/Flutter-Templates/FlutterGetx/pubspec.yaml.peb index 05578007..7836d320 100644 --- a/templates/Flutter-Templates/FlutterGetx/pubspec.yaml.peb +++ b/templates/Flutter-Templates/FlutterGetx/pubspec.yaml.peb @@ -1,5 +1,5 @@ -name: ${{APP_NAME | lower}} -description: Flutter GetX project created by the Flutter Template plugin +name: "${{APP_NAME | lower}}" +description: Flutter GetX project created from a Flutter Templates starter for Code on the Go publish_to: 'none' version: 1.0.0+1 diff --git a/templates/Flutter-Templates/FlutterProvider/README.md.peb b/templates/Flutter-Templates/FlutterProvider/README.md.peb index e6d0e236..45951e85 100644 --- a/templates/Flutter-Templates/FlutterProvider/README.md.peb +++ b/templates/Flutter-Templates/FlutterProvider/README.md.peb @@ -1,6 +1,7 @@ # ${{APP_NAME}} -Generated by the Flutter Template plugin for Code on the Go. + +Generated from a Flutter Templates starter for Code on the Go. - Package: `${{PACKAGE_NAME}}` - Template: Provider diff --git a/templates/Flutter-Templates/FlutterProvider/pubspec.yaml.peb b/templates/Flutter-Templates/FlutterProvider/pubspec.yaml.peb index a18cf920..79acb3c7 100644 --- a/templates/Flutter-Templates/FlutterProvider/pubspec.yaml.peb +++ b/templates/Flutter-Templates/FlutterProvider/pubspec.yaml.peb @@ -1,5 +1,5 @@ -name: ${{APP_NAME | lower}} -description: Flutter Provider project created by the Flutter Template plugin +name: "${{APP_NAME | lower}}" +description: Flutter Provider project created from a Flutter Templates starter for Code on the Go publish_to: 'none' version: 1.0.0+1 diff --git a/templates/Flutter-Templates/FlutterRiverpod/README.md.peb b/templates/Flutter-Templates/FlutterRiverpod/README.md.peb index 54f3b1be..31254576 100644 --- a/templates/Flutter-Templates/FlutterRiverpod/README.md.peb +++ b/templates/Flutter-Templates/FlutterRiverpod/README.md.peb @@ -1,6 +1,7 @@ # ${{APP_NAME}} -Generated by the Flutter Template plugin for Code on the Go. + +Generated from a Flutter Templates starter for Code on the Go. - Package: `${{PACKAGE_NAME}}` - Template: Riverpod diff --git a/templates/Flutter-Templates/FlutterRiverpod/pubspec.yaml.peb b/templates/Flutter-Templates/FlutterRiverpod/pubspec.yaml.peb index b958ea1a..dfb51aa8 100644 --- a/templates/Flutter-Templates/FlutterRiverpod/pubspec.yaml.peb +++ b/templates/Flutter-Templates/FlutterRiverpod/pubspec.yaml.peb @@ -1,5 +1,5 @@ -name: ${{APP_NAME | lower}} -description: Flutter Riverpod project created by the Flutter Template plugin +name: "${{APP_NAME | lower}}" +description: Flutter Riverpod project created from a Flutter Templates starter for Code on the Go publish_to: 'none' version: 1.0.0+1 From 03191209ce0039e69477308c293825a41d016f5c Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Fri, 25 Sep 2026 16:17:55 -0700 Subject: [PATCH 03/12] ADFA-6252 | Drop the migration plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was scaffolding for this change and has no reader once the work has landed. The two documents that outlive it stay: cgt-templates.md for the format, README.md for writing a template. cgt-templates.md linked to it twice. Both links are replaced rather than deleted — the decision table they introduced is the part worth keeping, so it now stands on its own under the ticket number. --- templates/cgt-templates.md | 7 +- templates/flutter-cgt-migration-plan.md | 636 ------------------------ 2 files changed, 3 insertions(+), 640 deletions(-) delete mode 100644 templates/flutter-cgt-migration-plan.md diff --git a/templates/cgt-templates.md b/templates/cgt-templates.md index 6087e389..3e865324 100644 --- a/templates/cgt-templates.md +++ b/templates/cgt-templates.md @@ -388,9 +388,8 @@ Already in place: | P02 | `catalog.TYPES` maps `templates` to the type `template`. | | P03 | `site/catalog.schema.json` accepts `"template"` in the `type` enum. | -The `.cgt` is **never committed**. A GitHub Action generates it at publication. The plan is in -[`flutter-cgt-migration-plan.md`](flutter-cgt-migration-plan.md); community guidance is in -[`README.md`](README.md). +The `.cgt` is **never committed**. A GitHub Action generates it at publication. Community +guidance for writing a template is in [`README.md`](README.md). --- @@ -415,7 +414,7 @@ applies to them. | `AndroidManifest.xml`, `build.gradle.kts`, `FlutterTemplate.kt`, the icons | Removed. A `.cgt` needs none of them | | `addon.json` | Kept, and extended to carry the values that the manifest used to hold | -Settled. The detail is in [`flutter-cgt-migration-plan.md`](flutter-cgt-migration-plan.md). +How it was settled (ADFA-6252): | Question | Answer | |---|---| diff --git a/templates/flutter-cgt-migration-plan.md b/templates/flutter-cgt-migration-plan.md deleted file mode 100644 index 53a1a0b1..00000000 --- a/templates/flutter-cgt-migration-plan.md +++ /dev/null @@ -1,636 +0,0 @@ -# Plan: move Flutter Templates from a plugin to a `.cgt` bundle - -ADFA-6252. Read `cgt-templates.md` first for the format. - -## Goal - -`plugins/Flutter-Templates/` becomes `templates/Flutter-Templates/`. It stops being an -Android application module. It becomes a source tree that a GitHub Action zips into one -`flutter-templates.cgt` holding five template directories, and publishes to -`addons.appdevforall.org` through Cloudflare R2. - -## Rules this plan follows - -| Rule | Effect | -|---|---| -| The `.cgt` is never committed | The Action generates it. `.gitignore` blocks it | -| Template content is plain text | Dart, YAML, Gradle and JSON only. No archives, no jars | -| Images stay as PNG | Seven files: two gallery icons and five thumbnails. These are packaging, not template content | -| A template is not a plugin | No `build.gradle.kts`, no `AndroidManifest.xml`, no Kotlin, no `.cgp` | -| Publication goes to R2 | The same path every other addon in this repository uses | - ---- - -## 1. Target layout - -The template directories sit at the addon root, the same shape `dev-assets/templates/` uses. - -``` -templates/Flutter-Templates/ -├── addon.json <- gallery metadata + template id, version, minAppVersion -├── flutter-templates.html <- gallery page, name fixed by the slug -├── icon_day.png <- gallery icon -├── icon_night.png -├── README.md -├── templates.json <- archive root manifest -├── FlutterBasic/ -│ ├── template/template.json -│ ├── template/thumb.png -│ ├── analysis_options.yaml -│ ├── pubspec.yaml.peb -│ ├── README.md.peb -│ └── lib/main.dart.peb -├── FlutterBloc/ -├── FlutterGetx/ -├── FlutterProvider/ -└── FlutterRiverpod/ -``` - -`addon.json`, the HTML page, the two icons and `README.md` are repository metadata. They must -not reach the archive. - -### Select the files with an allow list, not an exclude list - -An `-x` exclude list is the obvious way to keep those four out, and it is the wrong one. It -fails open: a metadata file added next year ships inside the `.cgt` until somebody remembers -to extend the list, and nothing reports it. - -`templates.json` already names every directory that belongs in the archive. Build the file -list from it: - -```bash -paths="$(jq -r '.templates[].path' templates.json)" -find templates.json $paths -type f | LC_ALL=C sort -``` - -This fails closed. A new metadata file is never listed, so it can never ship. It also fails -**loudly**: `find` exits non-zero when a path in `templates.json` does not exist on disk, -which catches a typo that the IDE would otherwise swallow in silence. - -### Files to delete - -`plugins/Flutter-Templates/` goes away whole. The `.peb` skeletons, the thumbnails and the -icons move to the new location first; everything else is discarded: -`build.gradle.kts`, `settings.gradle.kts`, `gradle.properties`, -`src/main/AndroidManifest.xml`, `src/main/kotlin/com/ali/fluttertemplate/FlutterTemplate.kt`, -and all of `src/main/res/`. None has a meaning in a `.cgt`. - -Do this as a `git mv` of the files that survive, so the history of each `.peb` file follows -it. See D07 in section 9 for what the deletion means for a user who installed the plugin. - -### Files to move without change - -Every `.peb` file and `analysis_options.yaml` under -`src/main/assets/templates/