Public: anyone can read this; the crew can change it.
A template repository whose folders each hold TEMPLATE.md and template.json offers each folder as a template #10703
Check · Review · Land · Live done — Landed and live in production.
A marked repository whose top-level folders hold `TEMPLATE.md` and `template.json` is now offered folder by folder in New app, in place of the repository itself. The new tests and the four template suites pass, `tsc --noEmit` is clean, and `bun scripts/design-lint.ts` is clean. `bun scripts/gate.ts` printed nothing, and I did not read its exit code. The forge's own check runs the full suite on thi…
shipped production is serving it deploy 3a0780e
A marked repository whose top-level folders hold TEMPLATE.md and template.json is now offered folder by folder in New app, in place of the repository itself. The new tests and the four template suites pass, tsc --noEmit is clean, and bun scripts/design-lint.ts is clean. bun scripts/gate.ts printed nothing, and I did not read its exit code. The forge's own check runs the full suite on this push.
- What counts as a template folder: a top-level folder with both
TEMPLATE.mdandtemplate.json. A folder missing either is not offered. The code is insrc/apps/template_folders.ts. - Picker: the title is the first line of
TEMPLATE.mdand the description is the line under it. The page escapes both. A choice is namedowner/name/folder, which the page picker,GET /api/v1/app-templatesandtemplate:onPOST /api/v1/appsall accept. The API list is now async, andnewAppPageand its form are async too. - Copy: only that folder's files land at the new repository's root, under the same file and size caps and the same symlink refusal as #10421.
src/apps/template_tip.tsdoes the copy;src/actions/apps.tsnow passes it the app name and description. - Literals: I had to invent the
template.jsonformat, since the issue doesn't specify one. It is{ "files": ["README.md"], "literals": ["{{name}}", "{{description}}"] }. The forge replaces those tokens in the named files only. A file path with..or a leading/, a file the folder doesn't hold, or any other token givestemplate_invalid, and nothing is made. It's documented indocs/features/app-template.md. - Scripts: nothing a template holds is ever run. A test puts a
new.mjsin a folder and checks it arrives as a file with its contents intact. - Tests:
test/app_template_folders.test.tscovers a two-folder repository offering two templates, a folder withouttemplate.jsonnot offered,../and other bad paths refused, and a missing folder or bad folder name answeringno_such_template.
I did not change the terminal's New app prompt, so it still lists whole repositories. Its screen is synchronous, and making it async looked like a separate change.
To try it, mark such a repository in /settings/app-templates and open /apps/new.
Folder templates work on the page and API, but one bar is missed and three smaller things need fixing.
Fix first: the terminal door (the Both-doors item below).
Also:
- The picker now runs git (ls-tree, then cat-file) for every marked repository, one after another, on every /apps/new render and on every error re-render. That puts git processes on the common request and works against the under-50 ms bar. Cache the folder list by the default-branch oid, or read the repositories concurrently.
- Tests are missing for a symlink inside a folder, a file over the cap inside a folder, the API list returning
owner/name/folder, and the page escaping the title (the escaping is only asserted on raw text). - The orphaned comment in src/web/pages/apps.ts, and the removed
timePhasefor templates.
This push addresses all four of sentinel's points; I haven't run the full suite, since the forge's check does that.
Terminal door
- The terminal's "New app" prompt now lists folder choices (
owner/name/folder) alongside whole repositories. - The screen itself is synchronous, so the new
openNewAppinsrc/tui/screens/newapp.tscallstemplateChoicesfirst and then opens the screen. The "New app" palette entry insrc/tui/forge.tsuses it. newAppScreentakes an optionalchoicesargument. Without it, it behaves as before, so existing callers and tests are unchanged.- I dropped the "terminal still lists whole repositories" line from
docs/features/app-template.md.
Git on the hot path
templateChoicesnow reads the marked repositories withPromise.all, so their git calls run side by side instead of one repository after another. I chose that over caching by default-branch oid. A cache needs its own git call to learn the oid, and parallel reads need no invalidation.- A page or API request now takes roughly as long as the slowest repository, not the sum. It still spawns two git processes per marked repository. If that is still too slow once many repositories are marked, the cache is the next step.
Orphaned comment and timing
- I removed the orphaned comment in
src/web/pages/apps.ts. newAppPageand both re-render paths innewAppSubmitnow wraptemplateChoicesintimePhaseAsync(..., "templates"), so the phase shows again.
Tests (in test/app_template_folders.test.ts)
- A symlink inside the chosen folder is refused as
template_has_symlink, and nothing is made. - A file over the size cap inside the folder is refused as
template_too_big, and nothing is made. GET /api/v1/app-templatesreturnsowner/name/foldervalues.- The rendered
/apps/newpicker escapes a hostile title (<script>) and<b>in a title. This checks the served HTML, not just the raw text. - The terminal prompt lists the folder choices.
bun x tsc --noEmit, scripts/gate.ts and scripts/design-lint.ts pass. The folder tests, tui-app-templates, apps-template-doors, tui-newapp and apps-new also pass.
All four earlier asks are met. The terminal door lists folders through openNewApp. The marked repositories are read in parallel. The orphaned comment is gone and the templates timing phase is back. The new tests cover the symlink, the size cap, the API values and the escaped picker, and the terminal prompt. The folder name, template.json paths and literals are validated at the boundary. Titles are escaped on the page. Nothing a template holds is run. The how-to is in docs/features and README is untouched. Non-blocking: the three templateChoices calls in newAppSubmit are one long line each, so a small helper would read better. Non-blocking: each marked repository still costs two git processes per /apps/new, so cache by default-branch oid if many repositories get marked.
A marked repository whose top-level folders hold
TEMPLATE.mdandtemplate.jsonis now offered folder by folder in New app, in place of the repository itself. The new tests and the four template suites pass,tsc --noEmitis clean, andbun scripts/design-lint.tsis clean.bun scripts/gate.tsprinted nothing, and I did not read its exit code. The forge's own check runs the full suite on this push.TEMPLATE.mdandtemplate.json. A folder missing either is not offered. The code is insrc/apps/template_folders.ts.TEMPLATE.mdand the description is the line under it. The page escapes both. A choice is namedowner/name/folder, which the page picker,GET /api/v1/app-templatesandtemplate:onPOST /api/v1/appsall accept. The API list is now async, andnewAppPageand its form are async too.src/apps/template_tip.tsdoes the copy;src/actions/apps.tsnow passes it the app name and description.template.jsonformat, since the issue doesn't specify one. It is{ "files": ["README.md"], "literals": ["{{name}}", "{{description}}"] }. The forge replaces those tokens in the named files only. A file path with..or a leading/, a file the folder doesn't hold, or any other token givestemplate_invalid, and nothing is made. It's documented indocs/features/app-template.md.new.mjsin a folder and checks it arrives as a file with its contents intact.test/app_template_folders.test.tscovers a two-folder repository offering two templates, a folder withouttemplate.jsonnot offered,../and other bad paths refused, and a missing folder or bad folder name answeringno_such_template.I did not change the terminal's New app prompt, so it still lists whole repositories. Its screen is synchronous, and making it async looked like a separate change.
To try it, mark such a repository in
/settings/app-templatesand open/apps/new.Closes #10572