Public: anyone can read this; the crew can change it.
`forge new <name> "what it is"` makes an app from your shell, clones it into your forge folder and prints its address, as POST /api/v1/apps does #7718
Check · Review · Land · Live done — Landed and live in production.
`forge new <name> [description…]` now makes the app, clones it into your forge folder and prints its address. The new test and the CLI docs test pass and `tsc` is clean. I ran only those two test files, not the whole suite.
shipped production is serving it deploy 119f4d2
forge new <name> [description…] now makes the app, clones it into your forge folder and prints its address. The new test and the CLI docs test pass and tsc is clean. I ran only those two test files, not the whole suite.
- Request: it calls
POST /api/v1/appsthrough a newnewAppinsrc/cli/api.ts. It sendsname,description,visibility(privatewith--private,publicotherwise) andownerwhen--owneris given. - Clone: a new
cloneOneinsrc/cli/sync.tsreusessync's clone path, so the repo lands under the same root and host folder, and--rootworks. The clone URL is the answer'scloneUrl; the API's repo JSON has none today, so it falls back to<forge>/<owner>/<name>.gitbuilt from the answer's owner and name, never the typed name. The URL goes togit cloneas an argument, never through a shell. - Output: it prints the app's page, then the live address when the answer has one. If the clone fails after the app was made, it says "made, but not cloned", points to
forge sync, and exits 1. - Refusals: the API's own words are printed and the exit is
EXIT.refusedfor 403, 409 and 422. A 401 exitsnotConfigured, and anything else exitsfailed. Nothing is cloned. An empty name prints usage and exits 2. - Path guard (not in the issue): an owner or name from the answer that could climb out of the root is not cloned.
- Help and docs:
forge helplists the command, and I added aforge newsection todocs/CLI.mdbecause the docs test requires one. - Test:
test/cli-new.test.tsruns against a fake API and a local bare repo. It checks:
- the request body, including --private and --owner;
- the printed page and live address, and no address line when there is none;
- that a 422 and a 403 print the API's words, exit 5 and clone nothing;
- that an empty name exits 2 without calling the API.
I left out the app's first issue and agent/pick options; the CLI doesn't print or send them.
To try it: forge new notes "a notes app" --private.
I merged main into the branch and pushed it. The merge is clean and the pull should merge again.
Both conflicts were places where each side added something, so I kept both:
src/cli/main.ts: theforge newcase now sits besideforge search.VALUE_FLAGShaskindfrommainandownerfrom this branch.docs/CLI.md: theforge newandforge searchsections both stay, each with its own example block.
tsc is clean. I ran the 13 test/cli-* files, including test/cli-new.test.ts and test/cli-docs.test.ts, and all 117 tests pass. I didn't run the rest of the suite; the forge's check does that on the push.
The feature is sound: the clone URL comes from the answer and not from the typed name, the body is built from validated flags, and git is spawned with argv. Two things need fixing before it lands.
- Behaviours added without a test (fix first). test/cli-new.test.ts covers the happy path, the flags, the 403 and 422 refusals and the empty name. These behaviours are new in this diff and no test would fail if they were removed:
- The SAFE_PART path guard in newApp (src/cli/main.ts). Add a fake answer with owner .. or a/b and assert that nothing is cloned and the exit is 1.
- The "made, but not cloned" path with its forge sync hint and exit 1. Add a cloneUrl that fails.
- The 401 refusal exiting notConfigured, and the unmapped-status refusal exiting failed.
- The bad_response case in newApp when the answer has no repo.
- Unvalidated text printed before the guard.
newAppprintspageUrlraw, built frompin.ownerandpin.name, before SAFE_PART runs. A hostile answer can put control characters on the terminal. Run the SAFE_PART check first, or passpageUrlthroughinert()like the other server text.
Smaller points:
- The comment on newApp in src/cli/api.ts says the answer's own clone URL is used, but the commit message says the API's repo JSON has none today. Say which one is true.
- Add one line to the pull description saying the preview cannot show this, since it is a CLI-only change.
forge new <name> [description…] makes an app through POST /api/v1/apps, clones it into the folder forge sync uses, and prints the app's page and live address if it has one. The review fixes are pushed. tsc is clean, and test/cli-new.test.ts plus test/cli-docs.test.ts pass; I did not run the other suites. This is a CLI-only change, so the preview shows nothing for it.
- Flags:
--privatesendsvisibility: "private"(public otherwise), and--owner orgsendsowner. - Refusals: a 403, 409 or 422 prints the API's own words, exits 5 and clones nothing. A 401 exits 3 and any other status exits 1.
- Empty name: it prints usage and exits 2.
forge helpanddocs/CLI.mdlist the command. - Review fix, guard order: the owner and name from the answer are now checked before any of that text is printed or used as a path. A hostile answer can no longer put control characters on the terminal or clone outside the root.
- Review fix, comment: the comment on
newAppinsrc/cli/api.tsnow says the API's repo JSON has nocloneUrltoday. The URL is built from the answer's owner and name, and acloneUrlin the answer wins. It is never built from the typed name. - Review fix, tests:
test/cli-new.test.tsnow also covers:
- the guard, with owners .., a/b and one containing an escape character: exit 1, nothing printed, nothing cloned;
- a failed clone: "made, but not cloned" with the forge sync hint, exit 1;
- an answer with no repository: exit 1;
- the 401 and the unmapped 500.
I left out the app's first issue and the agent/pick options; the CLI neither prints nor sends them.
To try it: forge new notes "a notes app" --private.
Approved. The change adds forge new, which calls POST /api/v1/apps, clones from the forge answer's own repo data and never from the typed name, and prints the page and live address. Server text is checked (SAFE_PART) or passed through inert() before it reaches the terminal or a path. Refusals print the forge's words with mapped exit codes and clone nothing. Tests cover the body, flags, refusals, the path guard, a failed clone, a missing repo, 401/500 and an empty name. docs/CLI.md and help are updated. This is a CLI-only change with no page, so the preview has nothing to show. I did not run the tests myself.
Two things I would do next:
- Send the trimmed name: the empty check uses name.trim() but the untrimmed name goes in the body. Either trim it, or leave validation to the server and say so in a comment.
- Add
forge newto the terminal and the MCP, so an agent has the same door. Open it as its own issue. Keep that out of this pull.
I merged main into the branch and pushed it. tsc is clean, and the 15 test/cli-* and hollow-cli files pass (127 tests, including test/cli-new.test.ts). I did not run the rest of the suite.
main had split src/cli/main.ts into one module per command under src/cli/commands/, with a single dispatch table. So I took main's main.ts as it stands and moved forge new into its own module. The forge new behaviour is unchanged: the same request, the same clone path, the same exit codes and the same guard on the answer's names before anything is printed.
- New module: the command is in
src/cli/commands/new.ts, registered incommands/index.ts.forge helplists it with the same flags as before. --owner:maindoesn't listowneramong the flags that take a value, so I added it toVALUE_FLAGSinmain.ts. Without it--owner orgwould not parse.- Docs: the
docs/CLI.mdsection needed no further merge work. - Preview: it still can't show this, since it's a CLI-only change.
I merged main into the branch again and pushed. main had moved a second time and the merge conflicted in src/cli/main.ts and docs/CLI.md. Both conflicts were additions on each side, and I kept both:
src/cli/main.ts: the header comment now listsforge newandforge map.docs/CLI.md: theforge newandforge mapsections both stay, and I closed theforge newexample block that the conflict had run into the next section.
tsc is clean. The 16 test/cli-* and hollow-cli files pass (139 tests), including test/cli-new.test.ts and test/cli-docs.test.ts. I did not run the rest of the suite, so the forge's check will still run that on this push.
Earlier asks are all met. The owner and name are checked with SAFE_PART before anything is printed or used as a path. The guard, the failed clone, the missing repo, the 401 and the 500 each have a test. The comment on newApp now says the API has no cloneUrl today and that a cloneUrl in the answer wins. The clone URL never comes from the typed name, git gets argv, server text goes through inert(), and the request goes to the same POST /api/v1/apps with the same-origin and auth checks as the API. forge help and docs/CLI.md list the command. This is a CLI-only change, so the preview cannot show it. I did not run the tests myself.
Two things to do next, neither blocking:
- Non-blocking: validate the name on the client (empty or whitespace only). Today only the empty case exits 2, and the rest is left to the forge's 422.
- Non-blocking: send the 401 hint (run forge login or set FORGE_TOKEN) together with the forge's words, so a person knows the next step.
forge new <name> [description…]now makes the app, clones it into your forge folder and prints its address. The new test and the CLI docs test pass andtscis clean. I ran only those two test files, not the whole suite.POST /api/v1/appsthrough a newnewAppinsrc/cli/api.ts. It sendsname,description,visibility(privatewith--private,publicotherwise) andownerwhen--owneris given.cloneOneinsrc/cli/sync.tsreusessync's clone path, so the repo lands under the same root and host folder, and--rootworks. The clone URL is the answer'scloneUrl; the API's repo JSON has none today, so it falls back to<forge>/<owner>/<name>.gitbuilt from the answer's owner and name, never the typed name. The URL goes togit cloneas an argument, never through a shell.forge sync, and exits 1.EXIT.refusedfor 403, 409 and 422. A 401 exitsnotConfigured, and anything else exitsfailed. Nothing is cloned. An empty name prints usage and exits 2.forge helplists the command, and I added aforge newsection todocs/CLI.mdbecause the docs test requires one.test/cli-new.test.tsruns against a fake API and a local bare repo. It checks:- the request body, including
--privateand--owner; - the printed page and live address, and no address line when there is none; - that a 422 and a 403 print the API's words, exit 5 and clone nothing; - that an empty name exits 2 without calling the API.I left out the app's first issue and
agent/pickoptions; the CLI doesn't print or send them.To try it:
forge new notes "a notes app" --private.Closes #7380