Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/update-libs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
["ai-literacy-course"]="ai-literacy-course.cgp"
["flutter-template"]="flutter-template.cgp"
["get-ai-models"]="get-ai-models.cgp"
["project-to-template"]="project-to-template.cgp"
)
for module in "${!MAP[@]}"; do
src=$(ls "${module}/build/plugin/"*.cgp 2>/dev/null | head -n1)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ See the official [plugin documentation](https://www.appdevforall.org/codeonthego
| [`speech-to-text-plugin/`](speech-to-text-plugin/) | Voice-to-code: converts speech to code with AI generation. |
| [`vector-search-plugin/`](vector-search-plugin/) | Semantic code search using embeddings and vector similarity. |
| [`get-ai-models/`](get-ai-models/) | Bottom-drawer catalog of curated, fully-open GGUF model files; downloads one to `/sdcard/Download` and verifies its SHA-256. |
| [`project-to-template/`](project-to-template/) | Converts the open Android project into a Code On The Go (.cgt) template and installs it into the IDE's New Project picker. |

## Building a plugin

Expand Down
15 changes: 15 additions & 0 deletions project-to-template/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
*.iml
.gradle/
/local.properties
.idea/
.androidide/
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
release.properties
*.jks
*.keystore
*.p12
104 changes: 104 additions & 0 deletions project-to-template/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Project to Template

A [Code On The Go](https://github.com/appdevforall/CodeOnTheGo) plugin (`.cgp`)
that converts the Android project currently open in the IDE into a Code On The
Go (`.cgt`) template bundle — driven entirely from a tab inside the IDE, with an
option to install the result straight into the New Project template picker. The
original project directory is never modified: the plugin copies it into a new
bundle directory next to it and templatizes the copy.

This started as a `templatize_project.py` desktop script, was ported to a
standalone Kotlin/Compose Android app, and is now a Code On The Go plugin so it
runs inside the IDE with no separate app to install.

## UI

The plugin adds a **Project to Template** item to the IDE's sidebar (under
"tools"), which opens a tab with:

- **Project** — read-only, always the project currently open in the IDE
(via `IdeProjectService.getCurrentProject()`). If no project is open,
conversion is disabled until one is.
- **Template name** — written into the generated `template.json`'s `name`
field, and used as the template subdirectory / `templates.json` `path` entry.
- **Dry run** — preview the substitutions against a disposable copy without
writing or deleting anything.
- **Skip cleanup** — skip `build/` and keystore removal.

**Convert to Template** stays disabled until a project is open and a template
name is entered. Tapping it runs the pipeline on a background thread and streams
a live log (`[OK]` / `[SKIP]` / `[REMOVED]` / `[REVIEW]`), followed by a summary
with the output paths. On a real (non-dry-run) conversion, an **Install
Template** button then appears below the log — so you can review the log first —
and tapping it registers the `.cgt` directly with the IDE via
`IdeTemplateService`, so it shows up immediately in the New Project template
picker.

## What the conversion does

For each run, it:

1. Creates a new output directory (`<project-dir>-cgt`, next to the project).
2. Copies the project into a subdirectory named after the template name,
skipping `.git`, `.gradle`, `.cg`, `.idea`, `.claude`, `.androidide`, and
`release.properties`.
3. Templatizes the copy: replaces concrete values (Gradle/AGP/Kotlin versions,
package name, app name, SDK levels, Java compatibility levels) with Pebble
tokens (`${{ TOKEN }}`), saving each modified file with a `.peb` suffix;
removes `build/` directories; deletes keystore files (`*.jks`, `*.keystore`,
`*.p12`); and flags files that may contain machine-specific or personal
information (`local.properties`, `google-services.json`, `key.properties`,
`GoogleService-Info.plist`) for manual review.
4. Writes a `templates.json` at the top of the output directory.
5. Adds a `template/` directory containing `template.json` (the parameter/
metadata schema) and a placeholder `thumb.png` (replace with a real
thumbnail before shipping).
6. Zips the output directory into a `<template-name>.cgt` file next to it.
7. If the user opts in, registers that `.cgt` with the IDE via
`IdeTemplateService.registerTemplate(...)`.

Both Kotlin DSL (`build.gradle.kts`) and Groovy DSL (`build.gradle`) projects
are supported. Assumes the app module is named `app` (falls back to the first
module).

## Pebble whitespace quirk

A line/segment that ends with a Pebble token must be followed by at least one
character of whitespace, or the parser eats the next character (often a newline,
quote, semicolon, or dot). The conversion inserts a sacrificial space after
every inserted token — after the closing quote when the token is immediately
followed by one, so the quote itself isn't eaten.

## Project layout

- `Templatizer.kt` — the substitution pipeline, pure `java.io.File` logic with
no dependency on the plugin API (easy to reason about and test in isolation),
plus the `.cgt` bundle writer/zipper.
- `ProjectToTemplatePlugin.kt` — the `IPlugin` entry point: registers the
sidebar item and the main editor tab, and provides in-IDE tooltip help.
- `fragments/ProjectToTemplateFragment.kt` — the tab's UI: the input form,
background execution, live log, and the install-template flow via
`IdeTemplateService`.
- `src/main/assets/docs/index.html` — the offline Tier 3 help page.

This plugin uses the shared repo-root `../libs/plugin-api.jar` +
`../libs/gradle-plugin.jar` and the repo-root `../gradlew` — it does not bundle
its own copies. See the repo `CLAUDE.md` for the monorepo conventions.

## Building

From this directory, using the repo-root Gradle wrapper:

```
../gradlew assemblePlugin # release .cgp -> build/plugin/project-to-template.cgp
../gradlew assemblePluginDebug # debug .cgp -> build/plugin/project-to-template-debug.cgp
```

Install the resulting `.cgp` through Code On The Go's Plugin Manager. A
`local.properties` with `sdk.dir=<Android SDK path>` is required to build.

## Next steps

After running a conversion, inspect the generated `.peb` files in the output
bundle to confirm the substitutions and spacing look right, and replace the
placeholder `thumb.png` before distributing the `.cgt` file.
82 changes: 82 additions & 0 deletions project-to-template/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.itsaky.androidide.plugins.build")
}

pluginBuilder {
pluginName = "project-to-template"
}

android {
namespace = "org.appdevforall.projecttotemplate"
compileSdk = 34
Comment thread
hal-eisen-adfa marked this conversation as resolved.

defaultConfig {
applicationId = "org.appdevforall.projecttotemplate"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0.0"
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}

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"
)
}
}

testOptions {
unitTests.isReturnDefaultValues = true
}
}

kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}

dependencies {
// The plugin-api jar is the canonical contract for plugins. Available
// at compile time; the IDE provides it at runtime.
compileOnly(files("../libs/plugin-api.jar"))

implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.fragment:fragment-ktx:1.8.8")
implementation("androidx.core:core-ktx:1.13.1")
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.3.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")

testImplementation("junit:junit:4.13.2")
}

// Disable AAR metadata checks that fail under the plugin-builder pipeline.
tasks.matching {
it.name.contains("checkDebugAarMetadata") ||
it.name.contains("checkReleaseAarMetadata")
}.configureEach {
enabled = false
}
5 changes: 5 additions & 0 deletions project-to-template/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
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
13 changes: 13 additions & 0 deletions project-to-template/icon-src/icon_day.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions project-to-template/icon-src/icon_night.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions project-to-template/icon-src/make-icon.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Resvg } from '@resvg/resvg-js';
import { writeFileSync } from 'node:fs';

// "Project to Template": a solid source (project) card, an arrow, and a dashed
// template card. Two separate cards, left-to-right transformation.
function svg({ tile, card, line, outline, arrow }) {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192" width="192" height="192">
<rect x="8" y="8" width="176" height="176" rx="42" fill="${tile}"/>
<!-- project (source) card: solid, left -->
<rect x="22" y="56" width="52" height="80" rx="12" fill="${card}"/>
<rect x="34" y="80" width="28" height="8" rx="4" fill="${line}"/>
<rect x="34" y="98" width="28" height="8" rx="4" fill="${line}"/>
<rect x="34" y="116" width="18" height="8" rx="4" fill="${line}"/>
<!-- arrow: project -> template -->
<path d="M78,89 L98,89 L98,79 L114,96 L98,113 L98,103 L78,103 Z" fill="${arrow}"/>
<!-- template (generated) card: dashed outline, right -->
<rect x="118" y="56" width="52" height="80" rx="12" fill="none" stroke="${outline}"
stroke-width="6" stroke-linecap="round" stroke-dasharray="15 12"/>
</svg>`;
}

const day = svg({ tile: '#E7ECFB', card: '#3E5488', line: '#E7ECFB', outline: '#7A8EC4', arrow: '#3E5488' });
const night = svg({ tile: '#1E2740', card: '#B1C5FF', line: '#1E2740', outline: '#6E82BC', arrow: '#B1C5FF' });

for (const [name, s] of [['icon_day', day], ['icon_night', night]]) {
const png = new Resvg(s, { fitTo: { mode: 'width', value: 192 } }).render().asPng();
writeFileSync(new URL(`./${name}.png`, import.meta.url), png);
writeFileSync(new URL(`./${name}.svg`, import.meta.url), s);
console.log('wrote', name);
}
1 change: 1 addition & 0 deletions project-to-template/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Add project specific ProGuard rules here.
Loading