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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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.md and template.json. A folder missing either is not offered. The code is in src/apps/template_folders.ts.
  • Picker: the title is the first line of TEMPLATE.md and the description is the line under it. The page escapes both. A choice is named owner/name/folder, which the page picker, GET /api/v1/app-templates and template: on POST /api/v1/apps all accept. The API list is now async, and newAppPage and 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.ts does the copy; src/actions/apps.ts now passes it the app name and description.
  • Literals: I had to invent the template.json format, 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 gives template_invalid, and nothing is made. It's documented in docs/features/app-template.md.
  • Scripts: nothing a template holds is ever run. A test puts a new.mjs in a folder and checks it arrives as a file with its contents intact.
  • Tests: test/app_template_folders.test.ts covers a two-folder repository offering two templates, a folder without template.json not offered, ../ and other bad paths refused, and a missing folder or bad folder name answering no_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.

Closes #10572

commented

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.md and template.json. A folder missing either is not offered. The code is in src/apps/template_folders.ts.
  • Picker: the title is the first line of TEMPLATE.md and the description is the line under it. The page escapes both. A choice is named owner/name/folder, which the page picker, GET /api/v1/app-templates and template: on POST /api/v1/apps all accept. The API list is now async, and newAppPage and 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.ts does the copy; src/actions/apps.ts now passes it the app name and description.
  • Literals: I had to invent the template.json format, 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 gives template_invalid, and nothing is made. It's documented in docs/features/app-template.md.
  • Scripts: nothing a template holds is ever run. A test puts a new.mjs in a folder and checks it arrives as a file with its contents intact.
  • Tests: test/app_template_folders.test.ts covers a two-folder repository offering two templates, a folder without template.json not offered, ../ and other bad paths refused, and a missing folder or bad folder name answering no_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.

Changes requested — before the latest push

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:

  1. 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.
  2. 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).
  3. The orphaned comment in src/web/pages/apps.ts, and the removed timePhase for templates.
commented

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 openNewApp in src/tui/screens/newapp.ts calls templateChoices first and then opens the screen. The "New app" palette entry in src/tui/forge.ts uses it.
  • newAppScreen takes an optional choices argument. 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

  • templateChoices now reads the marked repositories with Promise.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.
  • newAppPage and both re-render paths in newAppSubmit now wrap templateChoices in timePhaseAsync(..., "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-templates returns owner/name/folder values.
  • The rendered /apps/new picker 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.

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs