Skip to content

Commit 49b4489

Browse files
ADFA-6252 | Fold the format reference into templates/README.md
Two documents covered one subject and overlapped on the parts people actually need — the Pebble delimiters, the identifiers, path substitution and the gotchas were in both, which is how they drift. One file now: Part 1 is how to write a template, Part 2 is the format. Dropped on the way in, rather than carried across: - The five-gap table (G01-G05) listing what this repository had to add. All of it is implemented; it described a plan, not the system. - The record of what the Flutter conversion involved, for the same reason the migration plan went. - The dev-assets publication path, the local build-core-cgt.sh warning, and the CodeOnTheGoPlugin jar-fetching detail. That is how core.cgt reaches the app assets over SSH; addons here go to R2 and nothing about it guided a reader. - The absolute /Users/eisen paths in the sources table, replaced by repository- relative pointers to the CodeOnTheGo files that define the format. Added while merging, because the reference described dev-assets and not this repository: how scripts/build-cgt.sh builds the archive, that an unrecognised root entry is ignored (which is what makes cgt-build.properties safe), and how publishing is wired from discovery through to the catalog entry. core.cgt stays in as the larger worked example. It exercises the language chooser, user parameters and path flags, none of which the Flutter bundle shows.
1 parent 0319120 commit 49b4489

5 files changed

Lines changed: 216 additions & 459 deletions

File tree

‎CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Read the area's own README before working in it. This file holds only what is tr
2121

2222
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.
2323

24-
The format reference for `.cgt` is [`templates/cgt-templates.md`](templates/cgt-templates.md).
24+
`templates/README.md` is both halves: how to write a template, then the `.cgt` format reference. The format has no other written specification.
2525

2626
## Git workflow
2727

‎scripts/build-cgt.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
#
2121
# WHY STORED AND 1980: entries are stored (-0) and every mtime is pinned, so one commit always
2222
# produces one archive, byte for byte. Compression happens at the HTTP layer -- Cloudflare
23-
# serves the gallery. This mirrors dev-assets' core.cgt build; see templates/cgt-templates.md.
23+
# serves the gallery. This mirrors dev-assets' core.cgt build; see templates/README.md.
2424
#
2525
set -euo pipefail
2626

‎templates/Flutter-Templates/README.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ The `.cgt` is **never committed**. The publish workflow generates it. To build o
3737
unzip -l out/flutter-templates.cgt
3838
```
3939

40-
See [`../README.md`](../README.md) for authoring guidance and
41-
[`../cgt-templates.md`](../cgt-templates.md) for the format.
40+
See [`../README.md`](../README.md) for authoring guidance and the format reference.
4241

4342
## This replaces the Flutter Templates plugin
4443

‎templates/README.md‎

Lines changed: 213 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
11
# Project templates
22

33
A template adds an entry to the **New Project** screen in Code on the Go. It ships as a
4-
`.cgt` file: a ZIP that holds a project skeleton plus a small metadata file. One `.cgt` can
5-
hold several templates.
4+
`.cgt` file: a ZIP holding one or more project skeletons and their metadata. The IDE reads it
5+
directly.
66

77
A template is not a plugin. It runs no code. It has no `build.gradle.kts`, no
8-
`AndroidManifest.xml` and no Kotlin. If your idea needs to run code, write a plugin instead —
9-
see `../plugins/` and the repository `CLAUDE.md`.
8+
`AndroidManifest.xml` and no Kotlin, and it declares no permissions. If your idea needs to run
9+
code, write a plugin instead — see [`../plugins/README.md`](../plugins/README.md).
1010

11-
For the format itself, read [`cgt-templates.md`](cgt-templates.md).
11+
Part 1 below is how to write one. Part 2 is the format reference.
1212

1313
## Start from `Flutter-Templates`
1414

1515
`templates/Flutter-Templates/` is the reference. Copy it and change what is yours. It is small
1616
but complete: five templates in one bundle, an `addon.json` with the `template` block, a
17-
gallery page, both icons, and thumbnails. Every rule below is visible in it.
17+
gallery page, both icons, and thumbnails. Every rule in Part 1 is visible in it.
1818

1919
It is to a template what `plugins/Random-XKCD/` is to a plugin — the thing to copy, and the
2020
thing a reviewer compares your submission against.
2121

22+
For a larger example, `core.cgt` in the `dev-assets` repository holds the nine built-in
23+
Android templates. It exercises parts of the format the Flutter bundle does not: the language
24+
chooser, user-supplied parameters, and placeholders in directory names.
25+
2226
---
2327

24-
## Make a template
28+
# Part 1 — Make a template
2529

26-
### 1. Make the directory
30+
## 1. Make the directory
2731

2832
Name it in MixedCase with single hyphens, ASCII letters and digits only, as
2933
`docs/addon-naming-standards.md` describes. `Flutter-Templates`, not `flutter_templates`.
@@ -46,19 +50,19 @@ The first five files describe your addon to the gallery. They stay out of the `.
4650
archive holds `templates.json` and the directories it names, and nothing else — the build
4751
takes its file list from `templates.json`, so a file you do not list is never shipped.
4852

49-
### 2. Put your project skeleton under `<TemplateName>/`
53+
## 2. Put your project skeleton under `<TemplateName>/`
5054

5155
Start from a project that already builds. Copy it in whole, then replace the parts that
5256
change per project with placeholders.
5357

54-
### 3. Mark the files that need substitution
58+
## 3. Mark the files that need substitution
5559

5660
Add `.peb` to any file that contains a placeholder. Code on the Go renders it and removes the
5761
suffix. Every other file is copied byte for byte, so binary files are safe.
5862

5963
`app/build.gradle.kts` becomes `app/build.gradle.kts.peb`.
6064

61-
### 4. Write the placeholders
65+
## 4. Write the placeholders
6266

6367
The delimiters carry a `$` prefix. This is not stock Pebble syntax.
6468

@@ -89,7 +93,7 @@ Values you can use:
8993

9094
You can add your own — see *Ask the user for more* below.
9195

92-
### 5. Use placeholders in paths too
96+
## 5. Use placeholders in paths too
9397

9498
A directory or a file name is substituted as well.
9599

@@ -100,7 +104,7 @@ app/src/main/java/PACKAGE_NAME/MainActivity.kt.peb
100104
With the package `com.example.myapp` this becomes
101105
`app/src/main/java/com/example/myapp/MainActivity.kt`. `CLASS_NAME` works the same way.
102106

103-
### 6. Write `<TemplateName>/template/template.json`
107+
## 6. Write `<TemplateName>/template/template.json`
104108

105109
```json
106110
{
@@ -142,7 +146,9 @@ With the package `com.example.myapp` this becomes
142146
Add `template/thumb.png` for the image on the New Project screen. It is optional, but an
143147
entry without one looks unfinished.
144148

145-
### 7. Write `templates.json`
149+
The full field list is in Part 2.
150+
151+
## 7. Write `templates.json`
146152

147153
List each directory, in the order you want them shown. This file is also the build's allow
148154
list: a directory you leave out is not packaged.
@@ -158,7 +164,7 @@ list: a directory you leave out is not packaged.
158164

159165
Add `"experimental": true` to an entry to hide it unless the user turns experiments on.
160166

161-
### 8. Write `addon.json`
167+
## 8. Write `addon.json`
162168

163169
This is what the gallery shows.
164170

@@ -173,7 +179,7 @@ This is what the gallery shows.
173179
"template": {
174180
"id": "com.example.mytemplates",
175181
"version": "1.0.0",
176-
"minAppVersion": "26.36"
182+
"minAppVersion": "26.38"
177183
}
178184
}
179185
```
@@ -187,7 +193,7 @@ template ships no source tarball — the `.cgt` is plain text throughout, so the
187193
already is the source. The card's Details page links back to your directory in this
188194
repository.
189195

190-
### 9. Add the page and the icons
196+
## 9. Add the page and the icons
191197

192198
`my-templates.html` is your gallery page. Copy the shape from
193199
`../plugins/Random-XKCD/random-xkcd.html`. The rules the checker enforces:
@@ -200,7 +206,7 @@ repository.
200206

201207
`icon_day.png` and `icon_night.png` are both required.
202208

203-
### 10. Check it
209+
## 10. Check it
204210

205211
```sh
206212
uv run --directory tools/addons addons --root "$PWD" check
@@ -209,16 +215,16 @@ uv run --directory tools/addons addons --root "$PWD" check
209215
Run it from the repository root. `--root` must be absolute. This is the same gate that runs
210216
on every pull request. Treat its output as the authority.
211217

212-
### 11. Build it and look inside
218+
## 11. Build it and look inside
213219

214220
```sh
215221
./scripts/build-cgt.sh templates/My-Templates out
216222
unzip -l out/my-templates.cgt
217223
```
218224

219225
This is the same script the publish workflow runs, so a local build and a published one agree.
220-
It takes the file list from `templates.json`, pins every mtime, and writes a stored,
221-
sorted archive.
226+
It takes the file list from `templates.json`, pins every mtime, and writes a stored, sorted
227+
archive.
222228

223229
Confirm `templates.json` appears bare at the root, with no directory in front of it. The IDE
224230
looks it up by that exact name and finds nothing otherwise. Confirm too that `addon.json`,
@@ -233,7 +239,7 @@ It records the commit the bundle was built from. Your local build does not produ
233239
**Never commit the `.cgt`.** The Action builds it on publication. Add `*.cgt` to your
234240
`.gitignore`.
235241

236-
### 12. Test it on a device
242+
## 12. Test it on a device
237243

238244
Copy the `.cgt` to the device Downloads folder, open the Templates manager, then open New
239245
Project and generate a project from every template you shipped.
@@ -366,3 +372,188 @@ Never commit these under your template directory. The checker refuses them:
366372

367373
Keep template content plain text. Images are the exception: `thumb.png` and the two gallery
368374
icons are PNG.
375+
376+
---
377+
378+
# Part 2 — The `.cgt` format
379+
380+
The format has no written specification. This part records what the code does.
381+
382+
| Component | Where |
383+
|---|---|
384+
| Format definition | `CodeOnTheGo/templates-impl/.../impl/zip/ZipTemplateConstants.kt` |
385+
| Project generator | `.../impl/zip/ZipTemplateReader.kt` and `.../impl/zip/ZipRecipeExecutor.kt` |
386+
| Templates manager reader | `CodeOnTheGo/app/.../templates/manager/parsing/CgtTemplateReader.kt` |
387+
| Programmatic builder | `CgtTemplateBuilder` in `libs/plugin-api.jar` |
388+
389+
## Bundle layout
390+
391+
A `.cgt` is a plain ZIP. No custom container, no header.
392+
393+
```
394+
templates.json <- manifest, lists the templates
395+
cgt-build.properties <- provenance, written by the publish workflow
396+
extensions.jar <- optional, custom Pebble extensions
397+
<TemplateName>/template/template.json <- metadata for this template
398+
<TemplateName>/template/thumb.png <- thumbnail for the New Project screen
399+
<TemplateName>/<any path>.peb <- a file that Pebble renders
400+
<TemplateName>/<any path> <- a file that is copied without change
401+
```
402+
403+
The names are fixed. `ZipTemplateConstants.kt` declares them:
404+
405+
```kotlin
406+
const val ARCHIVE_JSON = "templates.json"
407+
const val META_FOLDER = "template"
408+
const val META_JSON = "template.json"
409+
const val META_THUMBNAIL = "thumb.png"
410+
const val META_EXTENSION_JAR = "extensions.jar"
411+
const val TEMPLATE_EXTENSION = ".peb"
412+
```
413+
414+
`extensions.jar` is optional and rarely used. It supplies custom Pebble `Extension` classes,
415+
which the IDE dexes into `dex_opt/`. Neither `core.cgt` nor `flutter-templates.cgt` uses it.
416+
417+
An unrecognised entry at the archive root is ignored, which is why
418+
`cgt-build.properties` can sit there safely.
419+
420+
### Two readers, both of which must be satisfied
421+
422+
| Reader | Purpose | Method |
423+
|---|---|---|
424+
| `ZipTemplateReader` | Builds the New Project list | Reads `templates.json` by that exact bare name at the archive root, then reads `<path>/template/template.json` for each entry |
425+
| `CgtTemplateReader` | Shows the Templates manager list | Scans for any entry ending in `/template/template.json`, and ignores `templates.json` |
426+
427+
A missing `template.json` makes `ZipTemplateReader` skip that entry with no error.
428+
429+
## `templates.json`
430+
431+
The root manifest, in **strict** JSON. It names the directory of each template.
432+
433+
```json
434+
{
435+
"templates": [
436+
{ "path": "FlutterBasic" },
437+
{ "path": "FlutterBloc" }
438+
]
439+
}
440+
```
441+
442+
An entry also accepts `"experimental": true`. The IDE then hides that template unless
443+
`FeatureFlags.isExperimentsEnabled` is set.
444+
445+
The manifest carries no version and no schema version. The IDE checks nothing about it; the
446+
repository's `addons check` does, which is why a typo fails in CI rather than silently on a
447+
device.
448+
449+
## `template/template.json`
450+
451+
One file per template, in **JSON5** — unquoted keys are accepted, and `core.cgt` uses them
452+
(`{identifier: "APP_NAME"}`). Quote them anyway.
453+
454+
| Field | Purpose |
455+
|---|---|
456+
| `name` | The name on the New Project screen |
457+
| `description` | One line under the name |
458+
| `version` | The template version, such as `"1.0.0"` |
459+
| `tooltipTag` | The in-app help tag, such as `"template.flutter.basic"` |
460+
| `defaultAppName` | Optional. Pre-fills the app name field |
461+
| `parameters.required` | `appName`, `packageName`, `saveLocation` |
462+
| `parameters.optional` | `language`, `minsdk` — omit one to hide that field |
463+
| `parameters.user.text` | Extra text fields: `label`, `identifier`, `default` |
464+
| `parameters.user.checkbox` | Extra checkboxes: `label`, `identifier`, `default` |
465+
| `system` | Values the IDE supplies: `agpVersion`, `kotlinVersion`, `gradleVersion`, `compileSdk`, `targetSdk`, `javaSourceCompat`, `javaTargetCompat`, `javaTarget` |
466+
467+
Each entry maps a name to an `identifier`, and the identifier is the token the `.peb` files
468+
use.
469+
470+
## Rendering
471+
472+
The engine runs with `strictVariables(true)`. An identifier no parameter declares fails the
473+
render — on the device, at project generation, and nowhere earlier.
474+
475+
Three identifiers are injected at render time and no `template.json` declares them:
476+
`CLASS_NAME`, `COGO_CGT_PATH` and `COGO_CGT_DIRECTORY`.
477+
478+
### Path transformation
479+
480+
`ZipRecipeExecutor.renderProject` transforms each entry path in this order:
481+
482+
1. Entries outside `<TemplateName>/` are ignored, and `<TemplateName>/template/` is dropped.
483+
The metadata never reaches the new project.
484+
2. If `parameters.optional.language` is declared and the user picked a language, the other
485+
language is filtered out. A Kotlin project skips every `*.java` and `*.java.peb`; a Java
486+
project skips every `*.kt` and `*.kt.peb`. This is why a template that offers the choice
487+
ships both.
488+
3. A path segment matching a **checkbox identifier** acts as a flag. The segment is removed
489+
when the box is checked, and the whole entry is skipped when it is not.
490+
4. The literal text `PACKAGE_NAME` in the path becomes the package as directories
491+
(`com.foo.bar` → `com/foo/bar`). The literal `CLASS_NAME` becomes the class name.
492+
5. The `.peb` suffix is removed from the output name.
493+
6. The output path is canonicalized. An entry that escapes the project root is refused.
494+
495+
`core.cgt` shows steps 3 and 4 together:
496+
497+
```
498+
CodeOnTheGoPlugin/src/main/kotlin/PACKAGE_NAME/PLUGIN_SAMPLE/fragments/CLASS_NAMEFragment.kt.peb
499+
```
500+
501+
With "Include Sample Code" checked and the package `com.foo`, this becomes
502+
`src/main/kotlin/com/foo/fragments/MyPluginFragment.kt`. With the box clear, it disappears.
503+
504+
## How the archive is built
505+
506+
`scripts/build-cgt.sh` derives its file list from `templates.json`, writes the provenance
507+
record, then:
508+
509+
```bash
510+
export TZ=UTC
511+
find <files> -exec touch -t 198001010000 {} +
512+
find <files> | LC_ALL=C sort | zip -0 -D -X -q <slug>.cgt -@
513+
```
514+
515+
| Flag | Effect |
516+
|---|---|
517+
| `-0` | Store every entry. No compression inside the ZIP |
518+
| `-D` | Write no directory entries |
519+
| `-X` | Strip uid, gid and extended attributes |
520+
| `LC_ALL=C sort` | Fix the entry order |
521+
| `touch -t 198001010000` | Fix every mtime to 1980-01-01 |
522+
523+
One commit therefore produces one archive, byte for byte. There is no compression step:
524+
Cloudflare compresses on the fly for the gallery.
525+
526+
`core.cgt` is built by the same incantation in the `dev-assets` repository, which is where
527+
the shape came from. It additionally produces a Brotli copy, because the app fetches it from
528+
a plain web host that does no content negotiation.
529+
530+
A third producer exists: `CgtTemplateBuilder` in `plugin-api.jar` writes the same layout at
531+
run time through `ZipOutputStream`. It is how a plugin can register a template of its own.
532+
A bundle published to the gallery does not use it.
533+
534+
## How a bundle reaches the device
535+
536+
| Path | Mechanism |
537+
|---|---|
538+
| Shipped with the app | `core.cgt` only, as an app asset |
539+
| A user installs one | The Templates manager scans `Environment.TEMPLATES_DIR` and the Downloads folder |
540+
| A plugin registers one | `IdeTemplateService.registerTemplate(file)` copies it into `TEMPLATES_DIR` under a prefixed name |
541+
542+
The second is the path a gallery download takes. It needs no plugin, no manifest and no
543+
Kotlin.
544+
545+
## How publishing is wired
546+
547+
A directory under `templates/` holding `templates.json` is discovered as an addon — the rule
548+
is deliberately separate from the plugin one, which requires a `build.gradle.kts` applying the
549+
plugin-builder. From there:
550+
551+
| Stage | What happens |
552+
|---|---|
553+
| `addons check` | Structure, naming, metadata. Runs on every pull request |
554+
| `check-toolchain.yml` | Also lints the bundle sources and builds every `.cgt` |
555+
| `publish-addons.yml` | Zips the bundle and uploads `dl/<slug>.cgt` to Cloudflare R2 |
556+
| `catalog.json` | Gains an entry with `type: "template"` and no `sourceTarball` |
557+
558+
The metadata a plugin keeps in its `AndroidManifest.xml` lives in `addon.json` under
559+
`template` instead: `id`, `version` and `minAppVersion`.

0 commit comments

Comments
 (0)