diff --git a/.github/workflows/build-addons.yml b/.github/workflows/build-addons.yml new file mode 100644 index 00000000..eb43a12d --- /dev/null +++ b/.github/workflows/build-addons.yml @@ -0,0 +1,59 @@ +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 scripts (ADFA-6252). A plugin is a Gradle project +# that compiles to a .cgp against the CodeOnTheGo jars; a template is a plain-text +# tree that zips to a .cgt and needs neither. Each script builds only its own kind +# and ignores a name of the other, so both steps get the same input. + +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: Refresh libs and build plugins + env: + CODEONTHEGO_REF: ${{ inputs.codeonthego_ref }} + ONLY_ADDON: ${{ inputs.addon }} + run: ./scripts/build-plugins.sh --ref "$CODEONTHEGO_REF" ${ONLY_ADDON:+"$ONLY_ADDON"} + + - name: Build templates + env: + ONLY_ADDON: ${{ inputs.addon }} + run: ./scripts/build-templates.sh ${ONLY_ADDON:+"$ONLY_ADDON"} 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..1469ad08 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,109 @@ 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. + ./scripts/build-templates.sh + for cgt in dist/*.cgt; do + [ -e "$cgt" ] || continue + slug="$(basename "$cgt" .cgt)" + # The IDE looks templates.json up by that exact bare name, so a + # nested one means no template is ever found. + unzip -l "$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 "$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 1149da4f..184fd6c0 100644 --- a/.github/workflows/publish-addons.yml +++ b/.github/workflows/publish-addons.yml @@ -86,42 +86,32 @@ jobs: PLUGIN_LIBS_REVISION: ${{ steps.libs.outputs.revision }} run: | set -euo pipefail - mkdir -p dist - all="$(uv run --directory tools/addons addons --root "$GITHUB_WORKSPACE" discover)" + # Without a name, discover lists every addon not held back by skip.txt. + # With one, it resolves the directory name or the slug, in any case, and + # fails on an unknown one, so a hostile value never reaches the shell as + # a path. if [ "$ADDON" = "all" ]; then - names="$all" + names="$(uv run --directory tools/addons addons --root "$GITHUB_WORKSPACE" discover)" else - # resolve through discover so an unknown or hostile value cannot - # reach the shell as a path - # match the directory name or the lowercase slug the rest of the - # system uses (dl/.cgp, the catalog's slug field) - names="$(printf '%s\n' "$all" | awk -v want="$ADDON" ' - { n = $0; sub(/.*\//, "", n); l = tolower(n) - if (n == want || $0 == want || l == tolower(want)) print }')" - if [ -z "$names" ]; then - echo "Unknown addon: $ADDON" >&2; exit 1 - fi + names="$(uv run --directory tools/addons addons --root "$GITHUB_WORKSPACE" discover "$ADDON")" fi 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" - done + # Two kinds of addon, two build scripts (ADFA-6252). Each builds only its + # own kind from the list and ignores the rest. No --ref: the previous + # step already put the plugin-api-latest jars in libs/. + # shellcheck disable=SC2086 + ./scripts/build-plugins.sh --out dist $names + # shellcheck disable=SC2086 + ./scripts/build-templates.sh $names - 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/.github/workflows/update-libs.yml b/.github/workflows/update-libs.yml index 226b7163..f86a9eaf 100644 --- a/.github/workflows/update-libs.yml +++ b/.github/workflows/update-libs.yml @@ -71,8 +71,10 @@ jobs: RUN_NUMBER: ${{ github.run_number }} run: echo "name=build-$(date -u +%Y-%m-%d)-${RUN_NUMBER}" >> "$GITHUB_OUTPUT" - - name: Run update-libs script - run: ./scripts/update-libs.sh + # Refreshes libs/ from CodeOnTheGo@stage, then proves every plugin still + # compiles against the new jars. Templates use no jars, so none are built. + - name: Refresh libs and build plugins + run: ./scripts/build-plugins.sh --ref stage - name: Commit updated jars id: commit 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 7e6ff580..3ed0ca66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,197 +4,69 @@ 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 -``` - -Build every plugin from scratch (after rebuilding libs): +Read the area's own README before working in it. This file holds only what is true for all four. -```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 -``` +| 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) | -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`. +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. -`local.properties` must contain `sdk.dir=...`. The committed `local.properties` at the repo root is harmless leftover; each plugin needs its own. +`templates/README.md` is both halves: how to write a template, then the `.cgt` format reference. The format has no other written specification. ## 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. It does **not** build against the committed `libs/`: it overwrites both jars with the assets of CoGo's [`plugin-api-latest`](https://github.com/appdevforall/CodeOnTheGo/releases/tag/plugin-api-latest) release (cut by hand with CoGo's **Release plugin-api** workflow, from CoGo `main`) after checking them against its `checksums.txt`. So a plugin that adopts a new API publishes once that API is in a `plugin-api-latest` release, whether or not `libs/` has been refreshed. PR CI and local builds still use the committed `libs/`, which **Update libs** fills from CoGo `stage`, so an API that is on `stage` but not yet released passes PR CI and fails **Publish addons**. - -### 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. +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.** -`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 target commit of the `plugin-api-latest` release it downloaded. Compare two artifacts' `libs_revision` by prefix, not equality: `.cgp`s published before that change took theirs from a commit subject, and some of those 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. +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`. -`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. +## Publishing -### In-app help wiring (tooltips + Tier 3, `DocumentationExtension`) +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. -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 } -``` +Build artifacts are **never committed**. -This is intentional — the `application`-as-library packaging trips those checks. Keep it. +## Building -### Asset downloads (rare) +Each kind has its own script. Each script builds only its own kind and ignores a name of the other kind, so both take the same arguments. An unknown name fails. -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. +| Kind | Script | What it does | +|---|---|---| +| Plugin | `scripts/build-plugins.sh [--ref R \| --local PATH] [--out DIR] [NAME...]` | `assemblePlugin` per plugin, to `.cgp`. With `--ref`/`--local` it first calls `scripts/update-libs.sh` to refresh `libs/` from Code on the Go; without either it uses the committed `libs/` | +| Template | `scripts/build-templates.sh [NAME...]` | `scripts/build-cgt.sh` per template, to `dist/.cgt`. Needs no Code on the Go jars and no Gradle | -**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}}"`. +`update-libs.sh` is plugin-only and builds nothing by itself. Both scripts take their lists from `addons discover --kind plugin|template`, which is the single definition of each kind and matches a NAME by directory name or slug, in any case. Every workflow (**Build addon artifacts**, **Publish addons**, **Update libs from CodeOnTheGo**, **Check toolchain**) calls these scripts, so a local build and a CI build run the same code. ## 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.). +**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). -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). +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). -**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. - -## 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/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..ed15a720 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-Starter-Kit/`](templates/Flutter-Starter-Kit/) | 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-Starter-Kit/`, which is the reference example. + ## Building a plugin @@ -95,11 +111,14 @@ Go to [Actions → **Update libs from CodeOnTheGo**](../../actions/workflows/upd ./scripts/update-libs.sh --local ../CodeOnTheGo # use an existing local checkout instead of cloning ``` +`update-libs.sh` only refreshes `libs/`. To refresh and then build every plugin against the new +jars, run `./scripts/build-plugins.sh` with the same `--ref` or `--local` argument. + First local run clones Code on the Go into `.cache/CodeOnTheGo/` (gitignored); subsequent runs `git pull` in place. Review the diff in `libs/` and commit if you're happy with it. ## 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..6ae538ad --- /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 Extensions Manager — on its Plugins tab for a plugin, its Templates tab 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 Extensions Manager, Plugins tab + +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 Starter Kit + +Artifact + +Value + +GitHub src directory (source of truth) + +/templates/Flutter-Starter-Kit + +Published name on site + +Flutter Starter Kit + +Name in the Extensions Manager, Templates tab + +Flutter Starter Kit + +Title inside documentation HTML + +Flutter Starter Kit + +Artifact filename + +flutter-starter-kit.cgt + +Documentation HTML filename + +flutter-starter-kit.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," "template," "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-Starter-Kit is singular even though it holds five templates: the addon is one kit, and its name says so. + +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/AI-Literacy-Course/README.md b/plugins/AI-Literacy-Course/README.md index cab533c1..6c90ba21 100644 --- a/plugins/AI-Literacy-Course/README.md +++ b/plugins/AI-Literacy-Course/README.md @@ -41,7 +41,7 @@ the plugin's own data dir, and everything renders offline. ../../gradlew assemblePlugin # -> build/plugin/ai-literacy-course.cgp (~119 MB) ``` -`scripts/update-libs.sh` runs `downloadAssets` automatically before +`scripts/build-plugins.sh` runs `downloadAssets` automatically before `assemblePlugin`. Sideload the `.cgp` via **Preferences → Plugin Manager → +**. ## Source layout diff --git a/plugins/AI-Literacy-Course/build.gradle.kts b/plugins/AI-Literacy-Course/build.gradle.kts index d2be4af6..ac9a22f5 100644 --- a/plugins/AI-Literacy-Course/build.gradle.kts +++ b/plugins/AI-Literacy-Course/build.gradle.kts @@ -251,7 +251,7 @@ val downloadPdfjs by tasks.registering { } } -// Aggregator: scripts/update-libs.sh runs `downloadAssets` before assemblePlugin. +// Aggregator: scripts/build-plugins.sh runs `downloadAssets` before assemblePlugin. val downloadAssets by tasks.registering { dependsOn(downloadCourse, downloadPdfjs) } 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..2eb07e5c 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -1 +1,353 @@ -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, or the ones you name: + +```sh +./scripts/build-plugins.sh # every plugin, against the committed libs/ +./scripts/build-plugins.sh Voice-Alerts # only these (directory name or slug) +./scripts/build-plugins.sh --ref <branch-or-tag> # refresh libs/ from CodeOnTheGo first +./scripts/build-plugins.sh --local ../CodeOnTheGo # refresh libs/ from an existing checkout +``` + +`build-plugins.sh` lists plugins with `addons discover --kind plugin`, which finds every +`build.gradle.kts` that applies `com.itsaky.androidide.plugins.build`. With `--ref` or +`--local` it first calls `scripts/update-libs.sh`, which clones CoGo into `.cache/CodeOnTheGo/` +on first run, rebuilds both jars and copies them into `libs/`. `update-libs.sh` builds no +addon by itself. Templates have their own script, `scripts/build-templates.sh`, and never need +the jars. + +`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. It does **not** build against the committed `libs/`: it overwrites both jars +with the assets of CoGo's +[`plugin-api-latest`](https://github.com/appdevforall/CodeOnTheGo/releases/tag/plugin-api-latest) +release (cut by hand with CoGo's **Release plugin-api** workflow, from CoGo `main`) after +checking them against its `checksums.txt`. So a plugin that adopts a new API publishes once +that API is in a `plugin-api-latest` release, whether or not `libs/` has been refreshed. PR CI +and local builds still use the committed `libs/`, which **Update libs** fills from CoGo +`stage`, so an API that is on `stage` but not yet released passes PR CI and fails **Publish +addons**. + +--- + +## 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/build-plugins.sh` from the CodeOnTheGo checkout `update-libs.sh` just built, and **Publish addons** +from the target commit of the `plugin-api-latest` release it downloaded. Compare two +artifacts' `libs_revision` by prefix, not equality: `.cgp`s published before that change took +theirs from a commit subject, and some of those 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 through `scripts/build-plugins.sh`. 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/build-plugins.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..84e6777c --- /dev/null +++ b/scripts/build-cgt.sh @@ -0,0 +1,202 @@ +#!/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 path it names that is not on +# disk fails the build, rather than being skipped in silence by the IDE. +# +# WHY TRACKED FILES: within those directories the list comes from git, not from find. find +# sweeps up whatever is sitting there -- a .DS_Store, a .dart_tool/ and pubspec.lock after +# someone ran a generated project -- and ships it to users. check-toolchain.yml only rejects +# junk that was *committed*, so nothing else catches it. Selecting by commit is also what makes +# the determinism below worth anything. tarball.py selects the same way, for the same reason. +# +# WHY A STAGING DIRECTORY: pinning mtimes is part of the reproducibility contract, but doing it +# in place would rewrite timestamps across the caller's working tree. Everything is copied into +# a temp directory first, so a build has no side effects on the checkout. +# +# 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/README.md. +# +# Bash 3.2 compatible on purpose: macOS ships it, and a community contributor building a +# bundle locally is the main audience. No mapfile, no associative arrays. +# +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" + +STAGE="$(mktemp -d "${TMPDIR:-/tmp}/build-cgt.XXXXXX")" +trap 'rm -rf "$STAGE"' EXIT + +# --------------------------------------------------------------------------- +# The provenance record +# --------------------------------------------------------------------------- +# Written into the staging directory, so it lands at the archive root without ever +# touching the addon directory. The timestamp is the committer date of the recorded +# revision, never the wall clock: a clock reading changes on every build and would +# end the determinism above. +write_provenance() { + local dir="$1" out="$2" revision revision_source timestamp timestamp_source + local addon_id version var value + + revision="" + revision_source="none" + for var in PLUGIN_VCS_REVISION GITHUB_SHA CI_COMMIT_SHA GIT_COMMIT; do + eval "value=\${$var:-}" + if [[ -n "$value" ]]; then + revision="$value" + 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 + + # Resolve the date before truncating, so the sha stays usable as a git argument. + timestamp="" + timestamp_source="wall-clock" + if [[ -n "$revision" ]] && command -v git >/dev/null 2>&1 \ + && git -C "$dir" rev-parse --git-dir >/dev/null 2>&1; then + # The date of the *recorded* revision, not of HEAD: on a pull request GitHub + # checks out a merge commit, so GITHUB_SHA and HEAD are different commits and + # the record would pair one with the other's date. + # + # TZ=UTC is set on this command rather than inherited. --date=format-local + # renders in the caller's zone while the format string hard-codes a Z, so + # without it a developer in PDT records a time seven hours off and labels it + # UTC, and two machines in different zones disagree about one commit. + timestamp="$(TZ=UTC git -C "$dir" show -s --format=%cd \ + --date=format-local:%Y-%m-%dT%H:%M:%SZ "$revision" 2>/dev/null || true)" + [[ -n "$timestamp" ]] && timestamp_source="git" + fi + if [[ -z "$timestamp" ]]; then + timestamp="$(TZ=UTC date -u +%Y-%m-%dT%H:%M:%SZ)" + 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 it already resolves against that directory. + 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 + + 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 > "$out" <<EOF +revision=$revision +revision_source=$revision_source +timestamp=$timestamp +timestamp_source=$timestamp_source +addon_id=$addon_id +version=$version +EOF +} + +# --------------------------------------------------------------------------- +# Select the files +# --------------------------------------------------------------------------- +cd "$ADDON_DIR" + +paths=() +while IFS= read -r line; do + [[ -n "$line" ]] && paths+=("$line") +done < <(jq -r '.templates[].path' templates.json) + +if [[ "${#paths[@]}" -eq 0 ]]; then + echo "Error: templates.json lists no templates." >&2 + exit 1 +fi + +for p in "${paths[@]}"; do + if [[ ! -d "$p" ]]; then + echo "Error: templates.json names '$p', which is not a directory." >&2 + exit 1 + fi +done + +LIST="$STAGE/.filelist" +if git rev-parse --git-dir >/dev/null 2>&1; then + git ls-files -z -- templates.json "${paths[@]}" > "$LIST" +else + # No checkout to ask. Provenance already records revision=unknown here. + find templates.json "${paths[@]}" -type f -print0 > "$LIST" +fi + +if [[ ! -s "$LIST" ]]; then + echo "Error: no files found under the paths templates.json lists." >&2 + exit 1 +fi + +# --------------------------------------------------------------------------- +# Stage, pin, zip +# --------------------------------------------------------------------------- +TREE="$STAGE/tree" +mkdir -p "$TREE" +# -0 throughout, so a path with a space or a newline survives the round trip. +# The destination travels in the environment: with `sh -c '...' _` the file names +# arrive as "$@", so there is no room for it in the argument list. +TREE="$TREE" xargs -0 -n 50 sh -c 'for f; do + mkdir -p "$TREE/$(dirname "$f")" + cp "$f" "$TREE/$f" +done' _ < "$LIST" + +write_provenance "$PWD" "$TREE/cgt-build.properties" + +cd "$TREE" +find . -type f -exec touch -t 198001010000 {} + +find . -type f | sed 's#^\./##' | 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/build-plugins.sh b/scripts/build-plugins.sh new file mode 100755 index 00000000..d0b294b2 --- /dev/null +++ b/scripts/build-plugins.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# +# Builds plugin addons into .cgp files. Templates are built by +# scripts/build-templates.sh; each script builds only its own kind (ADFA-6252). +# +# Usage: +# ./scripts/build-plugins.sh # every plugin, against the committed libs/ +# ./scripts/build-plugins.sh Random-XKCD # only these (directory name or path, any case) +# ./scripts/build-plugins.sh --ref stage # refresh libs/ from CodeOnTheGo@stage first +# ./scripts/build-plugins.sh --local ../CodeOnTheGo # refresh libs/ from a local checkout first +# ./scripts/build-plugins.sh --out dist # also copy each release .cgp to dist/<slug>.cgp +# +# --ref and --local go to scripts/update-libs.sh. Without either, the jars already +# in libs/ are used and libs_revision comes from PLUGIN_LIBS_REVISION if the caller +# exported it (Publish addons does). +# +# A name that is a template is ignored, so one name can go to both build scripts. +# An unknown name fails. With no name, every plugin is built, including ones +# skip.txt holds out of the gallery: a libs refresh must prove they still compile. +# +# Bash 3.2 compatible on purpose: macOS ships it. No mapfile. +# +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +LIBS_ARGS=() +OUT="" +NAMES=() + +while [ $# -gt 0 ]; do + case "$1" in + --ref|--local) + [ -n "${2:-}" ] || { echo "Error: $1 requires an argument." >&2; exit 1; } + LIBS_ARGS+=("$1" "$2") + shift 2 + ;; + --out) + [ -n "${2:-}" ] || { echo "Error: --out requires a directory." >&2; exit 1; } + OUT="$2" + shift 2 + ;; + -h|--help) + sed -n '2,21p' "$0" + exit 0 + ;; + -*) + echo "Error: unknown argument: $1" >&2 + exit 1 + ;; + *) + NAMES+=("$1") + shift + ;; + esac +done + +# Resolve before refreshing libs/, so a run that names only templates does not +# clone and build CodeOnTheGo for nothing. ${NAMES[@]+...} because bash 3.2 +# treats an empty array as unset under set -u. +list="$(uv run --directory "$REPO_ROOT/tools/addons" addons --root "$REPO_ROOT" \ + discover --include-skipped --kind plugin ${NAMES[@]+"${NAMES[@]}"})" +if [ -z "$list" ]; then + if [ "${#NAMES[@]}" -gt 0 ]; then + echo "No plugin among: ${NAMES[*]}. Nothing to build." + exit 0 + fi + echo "Error: 'addons discover' found no plugins. Check that uv works and that tools/addons/skip.txt does not exclude everything." >&2 + exit 1 +fi + +if [ "${#LIBS_ARGS[@]}" -gt 0 ]; then + "$REPO_ROOT/scripts/update-libs.sh" "${LIBS_ARGS[@]}" + if [ -s "$REPO_ROOT/.cache/libs-revision" ]; then + PLUGIN_LIBS_REVISION="$(cat "$REPO_ROOT/.cache/libs-revision")" + export PLUGIN_LIBS_REVISION + else + unset PLUGIN_LIBS_REVISION + fi +fi + +[ -z "$OUT" ] || mkdir -p "$OUT" +for plugin in $list; do + echo "" + echo "→ $plugin" + ( + cd "$REPO_ROOT/$plugin" + # Newer plugins drop their per-plugin wrapper and use the repo-root + # gradlew; fall back to it when no local gradlew exists. + gradlew="./gradlew" + [ -x "$gradlew" ] || gradlew="$REPO_ROOT/gradlew" + # Two invocations, not one: downloadAssets declares an output inside + # src/main/assets, which Gradle rejects as an undeclared dependency of + # mergeReleaseAssets when both run together. + if grep -q 'downloadAssets' build.gradle.kts; then + "$gradlew" --console=plain downloadAssets + fi + "$gradlew" --console=plain assemblePlugin + ) + "$REPO_ROOT/scripts/verify-provenance.sh" "$plugin" + if [ -n "$OUT" ]; then + src="" + for f in "$REPO_ROOT/$plugin"/build/plugin/*.cgp; do + case "$f" in *-debug.cgp) ;; *) [ -f "$f" ] && src="$f" && break ;; esac + done + [ -n "$src" ] || { echo "Error: no release .cgp under $plugin/build/plugin/" >&2; exit 1; } + slug="$(basename "$plugin" | tr '[:upper:]' '[:lower:]')" + cp "$src" "$OUT/$slug.cgp" + fi +done +echo "" +echo "All plugins built successfully." diff --git a/scripts/build-templates.sh b/scripts/build-templates.sh new file mode 100755 index 00000000..c6685f60 --- /dev/null +++ b/scripts/build-templates.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# +# Builds template addons into .cgt bundles. Plugins are built by +# scripts/build-plugins.sh; each script builds only its own kind (ADFA-6252). +# A template needs no CodeOnTheGo jars and no Gradle. +# +# Usage: +# ./scripts/build-templates.sh # every template, into dist/<slug>.cgt +# ./scripts/build-templates.sh Flutter-Starter-Kit # only these (directory name or path, any case) +# +# A name that is a plugin is ignored, so one name can go to both build scripts. +# An unknown name fails. With no name, every template is built, including ones +# skip.txt holds out of the gallery. +# +# Bash 3.2 compatible on purpose: macOS ships it. +# +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +# Fixed: verify-provenance.sh looks for the bundle in dist/. +OUT="$REPO_ROOT/dist" +NAMES=() + +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + sed -n '2,15p' "$0" + exit 0 + ;; + -*) + echo "Error: unknown argument: $1" >&2 + exit 1 + ;; + *) + NAMES+=("$1") + shift + ;; + esac +done + +list="$(uv run --directory "$REPO_ROOT/tools/addons" addons --root "$REPO_ROOT" \ + discover --include-skipped --kind template ${NAMES[@]+"${NAMES[@]}"})" +if [ -z "$list" ]; then + echo "No template to build." + exit 0 +fi + +for dir in $list; do + echo "" + echo "→ $dir" + "$REPO_ROOT/scripts/build-cgt.sh" "$REPO_ROOT/$dir" "$OUT" + "$REPO_ROOT/scripts/verify-provenance.sh" "$dir" +done +echo "" +echo "All templates built successfully." diff --git a/scripts/check-toolchain.sh b/scripts/check-toolchain.sh index 9c9c2ec6..22a6bfcb 100755 --- a/scripts/check-toolchain.sh +++ b/scripts/check-toolchain.sh @@ -28,7 +28,7 @@ # # SCOPE: this walks *every* build.gradle.kts, deliberately broader than the # "top-level dir applying com.itsaky.androidide.plugins.build" definition of a -# plugin used by .githooks/pre-push (plugin_dirs()), scripts/update-libs.sh and +# plugin used by .githooks/pre-push (plugin_dirs()), scripts/build-plugins.sh and # CLAUDE.md. Subprojects such as plugins/AI-Agent-Local/llama-api and # plugins/AI-Agent-Local/llama-impl compile into a plugin and must agree on the # toolchain, but are invisible to that definition -- which is exactly how diff --git a/scripts/update-libs.sh b/scripts/update-libs.sh index aedb5411..f6e67e3c 100755 --- a/scripts/update-libs.sh +++ b/scripts/update-libs.sh @@ -1,19 +1,19 @@ #!/usr/bin/env bash # # Rebuilds plugin-api.jar and gradle-plugin.jar from the CodeOnTheGo repo -# and refreshes this repo's libs/ folder. +# and refreshes this repo's libs/ folder. It builds no addon: to refresh the +# jars and then build plugins against them, run scripts/build-plugins.sh with +# --ref or --local, which calls this script first. Templates never need it. # # Usage: # ./scripts/update-libs.sh # clone/pull github.com/appdevforall/CodeOnTheGo into .cache/, build from stage # ./scripts/update-libs.sh --ref main # build from a different branch or tag # ./scripts/update-libs.sh --local ../CodeOnTheGo # use an existing local checkout instead of cloning -# ./scripts/update-libs.sh --plugin random-xkcd # refresh libs, then build only this one plugin # set -euo pipefail REPO_URL="https://github.com/appdevforall/CodeOnTheGo.git" DEFAULT_REF="stage" -PLUGIN_BUILDER_ID="com.itsaky.androidide.plugins.build" REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" LIBS_DIR="$REPO_ROOT/libs" @@ -21,7 +21,6 @@ CACHE_DIR="$REPO_ROOT/.cache/CodeOnTheGo" LOCAL_PATH="" REF="$DEFAULT_REF" -ONLY_PLUGIN="" while [ $# -gt 0 ]; do case "$1" in @@ -41,14 +40,6 @@ while [ $# -gt 0 ]; do fi shift 2 ;; - --plugin) - ONLY_PLUGIN="${2:-}" - if [ -z "$ONLY_PLUGIN" ]; then - echo "Error: --plugin requires a plugin name argument." >&2 - exit 1 - fi - shift 2 - ;; -h|--help) sed -n '2,11p' "$0" exit 0 @@ -136,69 +127,15 @@ printf " %-20s %s\n" "gradle-plugin.jar" "$(du -h "$LIBS_DIR/gradle-plugin.jar" # The plugin builder records this in each .cgp's assets/cgp-build.properties as # libs_revision. It cannot resolve the value itself -- the CodeOnTheGo checkout is # outside the plugin build -- so without this a released plugin's own revision does -# not identify the plugin-api/gradle-plugin jars it was compiled against. Left unset -# when the sha is unknown, so the field is omitted rather than recorded as a guess. -# Re-resolved at 12 characters rather than reusing $CODEONTHEGO_SHA, which is abbreviated -# to git's default length: the builder records `revision` at 12, and two differently -# shaped shas in one properties file are needlessly hard to compare at a glance. +# not identify the plugin-api/gradle-plugin jars it was compiled against. +# scripts/build-plugins.sh reads this file and exports PLUGIN_LIBS_REVISION; a file +# rather than an export, because an export cannot reach the parent process. Not +# written when the sha is unknown, so the field is omitted rather than recorded as +# a guess. Resolved at 12 characters, the length the builder records `revision` at. +LIBS_REVISION_FILE="$REPO_ROOT/.cache/libs-revision" +rm -f "$LIBS_REVISION_FILE" LIBS_REVISION="$(git -C "$CODEONTHEGO_PATH" rev-parse --short=12 HEAD 2>/dev/null || echo unknown)" if [ "$LIBS_REVISION" != "unknown" ]; then - export PLUGIN_LIBS_REVISION="$LIBS_REVISION" -fi - -# One discovery rule for the whole repository. The tool applies the skip -# list in tools/addons/skip.txt. Do not use mapfile here: macOS ships -# bash 3.2, which does not have it. -PLUGINS=() -while IFS= read -r line; do - PLUGINS+=("$line") -# --include-skipped on purpose: a libs refresh must prove every module still -# compiles, including ones held out of the gallery. main built these too, and -# losing that check would let a jar change break them silently. -done < <(uv run --directory "$REPO_ROOT/tools/addons" addons --root "$REPO_ROOT" \ - discover --include-skipped) - -if [ "${#PLUGINS[@]}" -eq 0 ]; then - echo "Error: no addons discovered. 'addons discover' returned nothing -- check that uv works and that tools/addons/skip.txt is not excluding everything." >&2 - exit 1 + mkdir -p "$(dirname "$LIBS_REVISION_FILE")" + echo "$LIBS_REVISION" > "$LIBS_REVISION_FILE" fi - -if [ -n "$ONLY_PLUGIN" ]; then - # keep the resolved repo-relative path: the build loop cds into it, and - # the bare name the caller gave us is no longer a directory - match="" - for p in "${PLUGINS[@]}"; do - if [ "$p" = "$ONLY_PLUGIN" ] || [ "${p##*/}" = "$ONLY_PLUGIN" ]; then - match="$p" - break - fi - done - if [ -z "$match" ]; then - echo "Error: requested plugin '$ONLY_PLUGIN' is not a buildable example plugin." >&2 - echo "Available plugins: ${PLUGINS[*]}" >&2 - exit 1 - fi - PLUGINS=("$match") -fi - -echo "" -echo "Discovered example plugins: ${PLUGINS[*]}" -echo "Building all example plugins against the refreshed libs..." -for plugin in "${PLUGINS[@]}"; do - echo "" - echo "→ $plugin" - ( - cd "$REPO_ROOT/$plugin" - # Newer plugins (e.g. flutter-template) drop their per-plugin wrapper and - # use the repo-root gradlew; fall back to it when no local gradlew exists. - gradlew="./gradlew" - [ -x "$gradlew" ] || gradlew="$REPO_ROOT/gradlew" - if grep -q 'downloadAssets' build.gradle.kts; then - "$gradlew" --console=plain downloadAssets - fi - "$gradlew" --console=plain assemblePlugin - ) - "$REPO_ROOT/scripts/verify-provenance.sh" "$plugin" -done -echo "" -echo "All plugins built successfully." \ No newline at end of file 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..a31690ed 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(); @@ -175,7 +183,10 @@ document.getElementById("q").value = state.q; document.getElementById("type").value = state.type; document.getElementById("origin").value = state.origin; -fetch("v1/catalog.json") +// v2, not v1: this page ships with each publish, so it is never a stale consumer, +// and only v2 carries template addons. v1 keeps being published for the app +// (ADFA-6252, design section 10.2). +fetch("v2/catalog.json") .then((response) => { if (!response.ok) throw new Error(response.status); return response.json(); diff --git a/site/catalog.v2.schema.json b/site/catalog.v2.schema.json new file mode 100644 index 00000000..23e00817 --- /dev/null +++ b/site/catalog.v2.schema.json @@ -0,0 +1,158 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://addons.appdevforall.org/v2/catalog.schema.json", + "type": "object", + "additionalProperties": true, + "required": [ + "schemaVersion", + "generated", + "addons" + ], + "properties": { + "schemaVersion": { + "const": 2 + }, + "generated": { + "type": "string" + }, + "addons": { + "type": "array", + "items": { + "$ref": "#/$defs/addon" + } + } + }, + "$defs": { + "addon": { + "type": "object", + "additionalProperties": true, + "required": [ + "type", + "slug", + "addonId", + "name", + "version", + "summary", + "description", + "origin", + "license", + "tags", + "author", + "minAppVersion", + "iconUrl", + "iconDarkUrl", + "pageUrl", + "sourceUrl", + "download" + ], + "properties": { + "type": { + "enum": [ + "plugin", + "template", + "snippet", + "code-action" + ] + }, + "slug": { + "type": "string", + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" + }, + "addonId": { + "type": "string", + "minLength": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)*$" + }, + "summary": { + "type": "string", + "maxLength": 120 + }, + "description": { + "type": "string" + }, + "origin": { + "enum": [ + "appdevforall", + "community" + ] + }, + "license": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "author": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "url" + ], + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "minAppVersion": { + "type": "string" + }, + "iconUrl": { + "type": "string" + }, + "pageUrl": { + "type": "string" + }, + "sourceUrl": { + "type": "string" + }, + "download": { + "$ref": "#/$defs/file" + }, + "sourceTarball": { + "$ref": "#/$defs/file" + }, + "iconDarkUrl": { + "type": "string" + } + } + }, + "file": { + "type": "object", + "additionalProperties": false, + "required": [ + "url", + "sha256", + "size" + ], + "properties": { + "url": { + "type": "string" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "size": { + "type": "integer", + "minimum": 0 + } + } + } + }, + "$comment": "Version 2 (ADFA-6252) makes two changes section 10.5 forbids within a major version, which is why it is a new major published alongside v1 rather than over it. pluginId became addonId: the catalog carries four addon types and the name fitted one. sourceTarball left the required list: a template ships none, because its .cgt is plain text throughout and the download already is the source, while sourceUrl still names the directory. A v2 consumer must treat sourceTarball as optional. Everything else is as v1, and v1 keeps being published unchanged for fielded app builds that cannot be updated." +} diff --git a/templates/Flutter-Starter-Kit/.gitignore b/templates/Flutter-Starter-Kit/.gitignore new file mode 100644 index 00000000..73042af8 --- /dev/null +++ b/templates/Flutter-Starter-Kit/.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-Starter-Kit/FlutterBasic/README.md.peb similarity index 87% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/README.md.peb rename to templates/Flutter-Starter-Kit/FlutterBasic/README.md.peb index faacc132..045b1e0b 100644 --- a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/README.md.peb +++ b/templates/Flutter-Starter-Kit/FlutterBasic/README.md.peb @@ -1,6 +1,7 @@ # ${{APP_NAME}} -Generated by the Flutter Template plugin for Code on the Go. + +Generated from the Flutter Starter Kit for Code on the Go. - Package: `${{PACKAGE_NAME}}` - Template: Basic diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/analysis_options.yaml b/templates/Flutter-Starter-Kit/FlutterBasic/analysis_options.yaml similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/analysis_options.yaml rename to templates/Flutter-Starter-Kit/FlutterBasic/analysis_options.yaml diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/lib/main.dart.peb b/templates/Flutter-Starter-Kit/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-Starter-Kit/FlutterBasic/lib/main.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/pubspec.yaml.peb b/templates/Flutter-Starter-Kit/FlutterBasic/pubspec.yaml.peb similarity index 63% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/pubspec.yaml.peb rename to templates/Flutter-Starter-Kit/FlutterBasic/pubspec.yaml.peb index 034a4b63..28509c9a 100644 --- a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/pubspec.yaml.peb +++ b/templates/Flutter-Starter-Kit/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 | replace({" ": "_", "-": "_"})}}" +description: Flutter project created from the Flutter Starter Kit for Code on the Go publish_to: 'none' version: 1.0.0+1 diff --git a/templates/Flutter-Starter-Kit/FlutterBasic/template/template.json b/templates/Flutter-Starter-Kit/FlutterBasic/template/template.json new file mode 100644 index 00000000..1adbd897 --- /dev/null +++ b/templates/Flutter-Starter-Kit/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-Starter-Kit/FlutterBasic/template/thumb.png similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBasic/template/thumb.png rename to templates/Flutter-Starter-Kit/FlutterBasic/template/thumb.png diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/README.md.peb b/templates/Flutter-Starter-Kit/FlutterBloc/README.md.peb similarity index 88% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/README.md.peb rename to templates/Flutter-Starter-Kit/FlutterBloc/README.md.peb index dd2e5ce3..193780ef 100644 --- a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/README.md.peb +++ b/templates/Flutter-Starter-Kit/FlutterBloc/README.md.peb @@ -1,6 +1,7 @@ # ${{APP_NAME}} -Generated by the Flutter Template plugin for Code on the Go. + +Generated from the Flutter Starter Kit for Code on the Go. - Package: `${{PACKAGE_NAME}}` - Template: BLoC diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/analysis_options.yaml b/templates/Flutter-Starter-Kit/FlutterBloc/analysis_options.yaml similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/analysis_options.yaml rename to templates/Flutter-Starter-Kit/FlutterBloc/analysis_options.yaml diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/lib/blocs/counter_bloc.dart.peb b/templates/Flutter-Starter-Kit/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-Starter-Kit/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-Starter-Kit/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-Starter-Kit/FlutterBloc/lib/main.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/pubspec.yaml.peb b/templates/Flutter-Starter-Kit/FlutterBloc/pubspec.yaml.peb similarity index 65% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/pubspec.yaml.peb rename to templates/Flutter-Starter-Kit/FlutterBloc/pubspec.yaml.peb index 8e915cfc..383ba396 100644 --- a/plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/pubspec.yaml.peb +++ b/templates/Flutter-Starter-Kit/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 | replace({" ": "_", "-": "_"})}}" +description: Flutter BLoC project created from the Flutter Starter Kit for Code on the Go publish_to: 'none' version: 1.0.0+1 diff --git a/templates/Flutter-Starter-Kit/FlutterBloc/template/template.json b/templates/Flutter-Starter-Kit/FlutterBloc/template/template.json new file mode 100644 index 00000000..fee7ae7b --- /dev/null +++ b/templates/Flutter-Starter-Kit/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-Starter-Kit/FlutterBloc/template/thumb.png similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterBloc/template/thumb.png rename to templates/Flutter-Starter-Kit/FlutterBloc/template/thumb.png diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/README.md.peb b/templates/Flutter-Starter-Kit/FlutterGetx/README.md.peb similarity index 88% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/README.md.peb rename to templates/Flutter-Starter-Kit/FlutterGetx/README.md.peb index 8f6ae015..0c8d03d5 100644 --- a/plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/README.md.peb +++ b/templates/Flutter-Starter-Kit/FlutterGetx/README.md.peb @@ -1,6 +1,7 @@ # ${{APP_NAME}} -Generated by the Flutter Template plugin for Code on the Go. + +Generated from the Flutter Starter Kit for Code on the Go. - Package: `${{PACKAGE_NAME}}` - Template: GetX diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/analysis_options.yaml b/templates/Flutter-Starter-Kit/FlutterGetx/analysis_options.yaml similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/analysis_options.yaml rename to templates/Flutter-Starter-Kit/FlutterGetx/analysis_options.yaml diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/lib/controllers/counter_controller.dart.peb b/templates/Flutter-Starter-Kit/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-Starter-Kit/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-Starter-Kit/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-Starter-Kit/FlutterGetx/lib/main.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/pubspec.yaml.peb b/templates/Flutter-Starter-Kit/FlutterGetx/pubspec.yaml.peb similarity index 61% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/pubspec.yaml.peb rename to templates/Flutter-Starter-Kit/FlutterGetx/pubspec.yaml.peb index 05578007..0ea328d2 100644 --- a/plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/pubspec.yaml.peb +++ b/templates/Flutter-Starter-Kit/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 | replace({" ": "_", "-": "_"})}}" +description: Flutter GetX project created from the Flutter Starter Kit for Code on the Go publish_to: 'none' version: 1.0.0+1 diff --git a/templates/Flutter-Starter-Kit/FlutterGetx/template/template.json b/templates/Flutter-Starter-Kit/FlutterGetx/template/template.json new file mode 100644 index 00000000..9ba5cd38 --- /dev/null +++ b/templates/Flutter-Starter-Kit/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-Starter-Kit/FlutterGetx/template/thumb.png similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterGetx/template/thumb.png rename to templates/Flutter-Starter-Kit/FlutterGetx/template/thumb.png diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/README.md.peb b/templates/Flutter-Starter-Kit/FlutterProvider/README.md.peb similarity index 88% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/README.md.peb rename to templates/Flutter-Starter-Kit/FlutterProvider/README.md.peb index e6d0e236..6605555e 100644 --- a/plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/README.md.peb +++ b/templates/Flutter-Starter-Kit/FlutterProvider/README.md.peb @@ -1,6 +1,7 @@ # ${{APP_NAME}} -Generated by the Flutter Template plugin for Code on the Go. + +Generated from the Flutter Starter Kit for Code on the Go. - Package: `${{PACKAGE_NAME}}` - Template: Provider diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/analysis_options.yaml b/templates/Flutter-Starter-Kit/FlutterProvider/analysis_options.yaml similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/analysis_options.yaml rename to templates/Flutter-Starter-Kit/FlutterProvider/analysis_options.yaml diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/lib/main.dart.peb b/templates/Flutter-Starter-Kit/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-Starter-Kit/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-Starter-Kit/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-Starter-Kit/FlutterProvider/lib/providers/counter_provider.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/pubspec.yaml.peb b/templates/Flutter-Starter-Kit/FlutterProvider/pubspec.yaml.peb similarity index 61% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/pubspec.yaml.peb rename to templates/Flutter-Starter-Kit/FlutterProvider/pubspec.yaml.peb index a18cf920..2eeaf83e 100644 --- a/plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/pubspec.yaml.peb +++ b/templates/Flutter-Starter-Kit/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 | replace({" ": "_", "-": "_"})}}" +description: Flutter Provider project created from the Flutter Starter Kit for Code on the Go publish_to: 'none' version: 1.0.0+1 diff --git a/templates/Flutter-Starter-Kit/FlutterProvider/template/template.json b/templates/Flutter-Starter-Kit/FlutterProvider/template/template.json new file mode 100644 index 00000000..686bcfbc --- /dev/null +++ b/templates/Flutter-Starter-Kit/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-Starter-Kit/FlutterProvider/template/thumb.png similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterProvider/template/thumb.png rename to templates/Flutter-Starter-Kit/FlutterProvider/template/thumb.png diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/README.md.peb b/templates/Flutter-Starter-Kit/FlutterRiverpod/README.md.peb similarity index 88% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/README.md.peb rename to templates/Flutter-Starter-Kit/FlutterRiverpod/README.md.peb index 54f3b1be..aba24319 100644 --- a/plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/README.md.peb +++ b/templates/Flutter-Starter-Kit/FlutterRiverpod/README.md.peb @@ -1,6 +1,7 @@ # ${{APP_NAME}} -Generated by the Flutter Template plugin for Code on the Go. + +Generated from the Flutter Starter Kit for Code on the Go. - Package: `${{PACKAGE_NAME}}` - Template: Riverpod diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/analysis_options.yaml b/templates/Flutter-Starter-Kit/FlutterRiverpod/analysis_options.yaml similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/analysis_options.yaml rename to templates/Flutter-Starter-Kit/FlutterRiverpod/analysis_options.yaml diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/lib/main.dart.peb b/templates/Flutter-Starter-Kit/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-Starter-Kit/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-Starter-Kit/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-Starter-Kit/FlutterRiverpod/lib/providers/counter_provider.dart.peb diff --git a/plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/pubspec.yaml.peb b/templates/Flutter-Starter-Kit/FlutterRiverpod/pubspec.yaml.peb similarity index 64% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/pubspec.yaml.peb rename to templates/Flutter-Starter-Kit/FlutterRiverpod/pubspec.yaml.peb index b958ea1a..f08ffef4 100644 --- a/plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/pubspec.yaml.peb +++ b/templates/Flutter-Starter-Kit/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 | replace({" ": "_", "-": "_"})}}" +description: Flutter Riverpod project created from the Flutter Starter Kit for Code on the Go publish_to: 'none' version: 1.0.0+1 diff --git a/templates/Flutter-Starter-Kit/FlutterRiverpod/template/template.json b/templates/Flutter-Starter-Kit/FlutterRiverpod/template/template.json new file mode 100644 index 00000000..5e94edfa --- /dev/null +++ b/templates/Flutter-Starter-Kit/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-Starter-Kit/FlutterRiverpod/template/thumb.png similarity index 100% rename from plugins/Flutter-Templates/src/main/assets/templates/FlutterRiverpod/template/thumb.png rename to templates/Flutter-Starter-Kit/FlutterRiverpod/template/thumb.png diff --git a/templates/Flutter-Starter-Kit/README.md b/templates/Flutter-Starter-Kit/README.md new file mode 100644 index 00000000..3378a739 --- /dev/null +++ b/templates/Flutter-Starter-Kit/README.md @@ -0,0 +1,67 @@ +# Flutter Starter Kit + +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`, which Code on the Go reads directly. Install it under **Preferences → +Extensions Manager → Templates** with the **+** button, and the five templates appear in +**New Project**. 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-templates.sh Flutter-Starter-Kit +unzip -l dist/flutter-starter-kit.cgt +``` + +See [`../README.md`](../README.md) for authoring guidance and the format reference. + +## 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-Starter-Kit/addon.json similarity index 53% rename from plugins/Flutter-Templates/addon.json rename to templates/Flutter-Starter-Kit/addon.json index 593387c9..65f750d6 100644 --- a/plugins/Flutter-Templates/addon.json +++ b/templates/Flutter-Starter-Kit/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.flutterstarterkit", + "version": "1.0.0", + "minAppVersion": "26.36" } } diff --git a/plugins/Flutter-Templates/flutter-templates.html b/templates/Flutter-Starter-Kit/flutter-starter-kit.html similarity index 62% rename from plugins/Flutter-Templates/flutter-templates.html rename to templates/Flutter-Starter-Kit/flutter-starter-kit.html index 8a0e6b1b..55550a21 100644 --- a/plugins/Flutter-Templates/flutter-templates.html +++ b/templates/Flutter-Starter-Kit/flutter-starter-kit.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> -<title>Flutter Templates +Flutter Starter Kit