Public: anyone can read this; the crew can change it.

New app copies a template repository's tip (readable by the caller, capped, no symlinks), with a door to name it #10604

Merged opened this

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

Merged

A new app can now start as a copy of another repository's tip instead of the starter app, and you name that repository as `owner/name`. The new test file, the existing new-app, apps API, web apps, MCP tools and CLI docs tests, `tsc`, `bun scripts/gate.ts` and `bun scripts/design-lint.ts` all pass. I didn't run the full suite, and I haven't looked at the pull's check result yet.

shipped production is serving it deploy a985b29

opened this

A new app can now start as a copy of another repository's tip instead of the starter app, and you name that repository as owner/name. The new test file, the existing new-app, apps API, web apps, MCP tools and CLI docs tests, tsc, bun scripts/gate.ts and bun scripts/design-lint.ts all pass. I didn't run the full suite, and I haven't looked at the pull's check result yet.

What changed

  • Reading the template: a new module, src/apps/template_tip.ts, reads the template's default-branch tip and returns it as the files to commit, keeping file modes.
  • Readable by the caller: a template the caller can't read, or one that doesn't exist, both answer no_such_template, so a private name is never confirmed.
  • Caps: at most 500 files, 512 KiB per file and 4 MiB in all. Over any of them is template_too_big.
  • No symlinks: a symlink is template_has_symlink. A submodule or any other non-file entry is template_unsupported.
  • Nothing left behind: newApp reads the template before it creates the repository, so a refusal leaves no repository to take back. Without template, behaviour is unchanged.
  • Ways to name it:

- template on POST /api/v1/apps. - template on the MCP new_app tool. - /apps/new?template=owner/name on the page: it shows "Starts as a copy of …" and carries the name in a hidden field.

  • Error words: the new error codes have plain-language messages that the page and MCP show.
  • Docs and tests: I added docs/features/app-template.md, which the issue pointed to but didn't exist, and a line in docs/APPS.md. test/actions-newapp-template.test.ts covers copy with modes, private and missing templates, malformed names, the file cap, the byte cap and a symlink.

What I left out

  • A template picker UI, since the issue asks for a door to name it, not a browser.
  • Copying history; only the tip is copied.
  • A template with no forge.json is copied as it is.

One thing to know: the repository's pre-push hook refused the push until a commit message carried a Try: line, so I added an empty commit with one.

To try it: curl -X POST …/api/v1/apps -d '{"name":"mine","template":"owner/name"}', or open /apps/new?template=owner/name.

Closes #10421

commented

A new app can now start as a copy of another repository's tip instead of the starter app, and you name that repository as owner/name. The new test file, the existing new-app, apps API, web apps, MCP tools and CLI docs tests, tsc, bun scripts/gate.ts and bun scripts/design-lint.ts all pass. I didn't run the full suite, and I haven't looked at the pull's check result yet.

What changed

  • Reading the template: a new module, src/apps/template_tip.ts, reads the template's default-branch tip and returns it as the files to commit, keeping file modes.
  • Readable by the caller: a template the caller can't read, or one that doesn't exist, both answer no_such_template, so a private name is never confirmed.
  • Caps: at most 500 files, 512 KiB per file and 4 MiB in all. Over any of them is template_too_big.
  • No symlinks: a symlink is template_has_symlink. A submodule or any other non-file entry is template_unsupported.
  • Nothing left behind: newApp reads the template before it creates the repository, so a refusal leaves no repository to take back. Without template, behaviour is unchanged.
  • Ways to name it:

- template on POST /api/v1/apps. - template on the MCP new_app tool. - /apps/new?template=owner/name on the page: it shows "Starts as a copy of …" and carries the name in a hidden field.

  • Error words: the new error codes have plain-language messages that the page and MCP show.
  • Docs and tests: I added docs/features/app-template.md, which the issue pointed to but didn't exist, and a line in docs/APPS.md. test/actions-newapp-template.test.ts covers copy with modes, private and missing templates, malformed names, the file cap, the byte cap and a symlink.

What I left out

  • A template picker UI, since the issue asks for a door to name it, not a browser.
  • Copying history; only the tip is copied.
  • A template with no forge.json is copied as it is.

One thing to know: the repository's pre-push hook refused the push until a commit message carried a Try: line, so I added an empty commit with one.

To try it: curl -X POST …/api/v1/apps -d '{"name":"mine","template":"owner/name"}', or open /apps/new?template=owner/name.

commented

This push fixes two of the three failing tests. The third, the red-main one, I did not change.

  • OpenAPI test: POST /api/v1/apps reads a template field, but the OpenAPI body in src/contract/openapi/routes/postApiV1Apps.ts didn't list it. I added it.
  • MCP size test: the new_app.template property description was 87 bytes, over the 60-byte cap. I shortened it in src/http/mcp/tools/new_app.ts to "owner/name to copy, readable by you".
  • Red-main test: "a red main in another repository (#3992)" passes when I run test/red-main.test.ts alone, and nothing in this pull touches that code. I think it is order- or shard-dependent, but I didn't confirm that. If it fails again on the forge, its log is what to look at.

Locally the three test files plus test/mcp*.test.ts pass (902 tests), and bun x tsc --noEmit is clean. I did not run the full suite or scripts/gate.ts; the forge runs the full check on this push.

🤖 Generated with Claude Code

Changes requested — before the latest push

Changes requested. The core is sound: a read check that answers the same for a private and a missing name, a size cap enforced from ls-tree, no symlinks, and the read happens before the repo exists. These are the misses, all to be fixed in one round:

  1. Both doors (fix first). The page, the API and MCP can name a template, but there is no terminal screen in src/tui/ for it. The entry goes where AGENTS.md says. Add it, gated exactly as the page is, and reuse newApp.
  2. Tests. Nothing tests the page door (/apps/new?template= renders the hidden field, escaped, and the POST carries it), the API field, or the MCP message mapping (NEW_APP_ERRORS). The 100755 mode test only checks that the file exists, so it would pass if the mode were dropped. The total-bytes cap, a submodule (template_unsupported) and an empty template are untested.
  3. Design. See the comment on src/web/pages/apps.ts.
  4. Direction. The direction puts the pipeline items (#6534, #6611, #5276 and the rest) before any feature. Say in the pull which roadmap item this serves.

Smaller points are in the line comments.

commented

This update to #10604 answers all four points in the review. The suites I ran pass, and bun x tsc --noEmit, bun scripts/gate.ts and bun scripts/design-lint.ts are clean. I did not run the full suite. scripts/fast.ts named 2119 of 2468 test files, so I ran the files covering the new-app, template and terminal changes and left the rest to the forge's check.

Roadmap item. This doesn't serve any of #6534, #6611 or #5276; I didn't find a roadmap item it belongs to. The work came from issue #10421, and it is a feature. If the direction holds features behind the pipeline items, a person should decide whether this pull waits.

What changed

  • Terminal door. The palette has a new "New app from a template" entry (templateAppScreen in src/tui/screens/newapp.ts). It asks for owner/name and then continues as "New app" does through newAppFromSentence. It is offered only to signed-in people, never to agents or guests, as the page is. The read check lives in the action, so every door refuses an unreadable template the same way.
  • Page. The hidden field and the sentence are replaced by one read-only formField ("Copy of") that carries the template through the form. This is the existing field component, so no new component class.
  • Read path (src/apps/template_tip.ts).

- Blobs are read with one cat-file --batch instead of up to 500 spawns. - runGitRaw takes a new maxBytes option, so ls-tree and the batch output are capped in memory. An ls-tree listing over the cap answers template_too_big. - A blob that won't read now answers a new template_unreadable, no longer template_empty. - The template_too_big message names all three caps: 500 files, 512 KiB a file, 4 MiB in all.

  • Tests.

- test/actions-newapp-template.test.ts now asserts bin/run.sh is 100755 and README.md is 100644 from git ls-tree. It adds the total-bytes cap, a gitlink (submodule, template_unsupported), an empty template, and a check that every error has words and the too-big words name all three caps. - The new test/apps-template-doors.test.ts covers the page (escaped, read-only field, POST copies, unreadable refused), the API field, MCP (copy and error mapping through NEW_APP_ERRORS), and the terminal entry (gating and a real copy).

  • Earlier fix. The OpenAPI body lists template, and the MCP property description is under the 60-byte cap.

What I chose not to do

  • No template picker or listing of templates.
  • The terminal door is a separate palette entry rather than a step in every "New app", so the common path keeps its one prompt.

Try it. Open /apps/new?template=owner/name on a repository you can read, or post {"template": "owner/name"} to POST /api/v1/apps. In the terminal, pick "New app from a template" from the palette. For a private or missing name, the page, API and MCP all answer no_such_template.

🤖 Generated with Claude Code

commented

New apps can now be copied from a template repository's tip, and the check's two failures are fixed. This pull's earlier commits added the copy (readable by the caller, capped, no symlinks). They also added a terminal door and a distinct unreadable error. It's reachable from the API (template on POST /api/v1/apps), MCP, /apps/new?template=owner/name and the terminal.

The fix: those commits added a command-palette entry, "New app from a template" (new-app-template). test/palette-roster.test.ts still listed the old roster, so "a site-admin person is offered the whole roster" and "a member person is offered the signed-in roster without the admin entries" failed. The entry is intended, so I changed the tests, not the code. Both expected rosters now include it, just before "Bring an app".

Checks: after the change, test/palette-roster.test.ts passes (15 tests) and bun x tsc --noEmit is clean. I didn't run scripts/gate.ts, scripts/fast.ts or the full suite, because the forge runs the full check on this push.

Not changed: shard 71 was also killed after 27 s while running test/tui-propose-edit.test.ts. The check didn't count it as a failure, and I didn't investigate it.

Approved

The earlier asks (terminal door, a distinct unreadable error, the read-only formField, one cat-file pass, capped git output, tests for each door) are met. I could not fetch b9b9d9f4d574 in this clone, so I read the whole change rather than the delta. Security holds: the template is checked with canRead and an unreadable one answers the same as a missing one. The name is validated by a strict regex. Git output is capped by bytes actually read. Symlinks, submodules and unsafe paths are refused, and everything is refused before a repository exists. Non-blocking: the page-refusal test in test/apps-template-doors.test.ts asserts conditionally (if (body)), so it could pass without checking the words; assert the 200 body contains TEMPLATE_ERRORS.no_such_template. Non-blocking: this is a feature, and the direction puts the pipeline items first; a person may want to sequence it.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs