Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d5ca9c3
feat(gnovm): add interactive gno init wizard with template scaffolding
davd-gzl Apr 20, 2026
33005c8
feat(gnovm): accept short-form module paths in gno init
davd-gzl Apr 21, 2026
3cd432a
feat(tm2): add shared interactive prompt library
davd-gzl Apr 21, 2026
98c5df1
refactor(gnovm): use shared prompt library in gno init, add --templat…
davd-gzl Apr 21, 2026
a11fb15
docs: rename and update ADR for PR #5557
davd-gzl Apr 21, 2026
38d7f61
Merge branch 'master' into feat/mod-init-template
davd-gzl Apr 21, 2026
d1b9049
refactor: directory-based templates, remove ErrGoBack, simplify wizard
davd-gzl Apr 21, 2026
2b63443
feat: prompt for run script name, add gnokey maketx run comment
davd-gzl Apr 21, 2026
2b65231
fix: review issues — template registry in .gno path, all files writte…
davd-gzl Apr 21, 2026
37a9a39
docs: update run template with gno run command and block comment
davd-gzl Apr 21, 2026
6e24a5c
refactor: remove unused PromptConfirm (YAGNI)
davd-gzl Apr 21, 2026
af62df0
refactor: PromptChoice uses map, PromptSelect keeps ordered slice
davd-gzl Apr 21, 2026
b6317fc
Merge branch 'master' into feat/mod-init-template
davd-gzl Apr 21, 2026
1981e7d
fix: run template uses ScriptPath, correct gnokey flags, ugnot
davd-gzl Apr 21, 2026
ee9d35b
Merge branch 'feat/mod-init-template' of https://github.com/davd-gzl/…
davd-gzl Apr 21, 2026
4265d14
fix: review issues — non-interactive scaffolding, path traversal, con…
davd-gzl Apr 22, 2026
53bb098
fix(gnovm): regenerate README and prevent orphan gnomod.toml on templ…
davd-gzl Apr 22, 2026
b565236
fix(gnovm): address round-2 review nits and consolidate init paths
davd-gzl Apr 22, 2026
f33b055
fix(gnovm): uppercase default in module-kind prompt
davd-gzl Apr 22, 2026
483b57a
Merge branch 'master' into feat/mod-init-template
davd-gzl Apr 22, 2026
feed85a
refactor(gnovm): simplify gno init to scaffold in CWD, never create d…
davd-gzl Apr 22, 2026
55c7fe5
Merge remote-tracking branch 'fork/feat/mod-init-template' into feat/…
davd-gzl Apr 22, 2026
afd7cec
refactor(gnovm): simplify gno init helpers, doc comments, legacy mod …
davd-gzl Apr 22, 2026
fc14234
refactor(gnovm): simplify gno init control flow
davd-gzl Apr 22, 2026
28c1b6f
chore: retrigger CI (unrelated flake in gno.land/pkg/integration)
davd-gzl Apr 22, 2026
7da91e3
docs: simplify comments
davd-gzl Apr 22, 2026
81b20e7
refactor(gnovm): split gno init into init.go and address review feedback
davd-gzl Apr 22, 2026
88ff17a
refactor(gnovm): move newModInitCmd back to mod.go and drop 'legacy' …
davd-gzl Apr 22, 2026
7a4992e
test(gnovm): add regression tests for init review feedback
davd-gzl Apr 22, 2026
f2f1ec2
test(gnovm): convert init CLI regression tests to txtar
davd-gzl Apr 22, 2026
7bd1c5f
test(gnovm): remove duplicate init tests, add missing txtar coverage
davd-gzl Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions gnovm/adr/pr5557_mod_init_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# PR5557: Interactive `gno init` with Template Scaffolding

## Context

`gno mod init` only creates a bare `gnomod.toml` file, and is buried as a subcommand
of `mod`. New developers must then manually create source and test files, look up
realm conventions (e.g. `Render`), and figure out the correct package declaration.
There is also no scaffolding for `gnokey maketx run` scripts. This friction slows
onboarding compared to tools like `cargo init` or `npm init` that scaffold working
starter code.

## Decision

### Make `gno init` a top-level command, interactive when run in a terminal

`init` is promoted from `gno mod init` to `gno init` for ergonomics — it's the
first command new users reach for and shouldn't be buried under `mod`.

### Three module kinds: realm, package, run script

When stdin is a TTY and `--bare` is not set, the user is prompted for the module
kind if it can't be auto-detected from the path:

```
Module kind: [r]ealm, [p]ackage, or [m]ain (run script)? [r/p/m] (default: p):
```

- **Realm** (`/r/`): creates `gnomod.toml`, `<pkg>.gno` with `Render`, `<pkg>_test.gno`.
- **Package** (`/p/`): creates `gnomod.toml`, `<pkg>.gno`, `<pkg>_test.gno`.
- **Run script** (main): creates `run/<name>.gno` — the user chooses the script name.
No `gnomod.toml` is needed since the keeper auto-generates one for `gnokey maketx run`.

If a module path is provided with `/r/` or `/p/`, the kind is auto-detected.
If the path is missing the letter segment (e.g. `gno.land/myname/foo`), the user
is prompted and the letter is inserted automatically.

### `.gno` argument shorthand for run scripts

If the argument ends in `.gno`, a run script is created at that path without
`gnomod.toml`. For example, `gno init run/create_proposal.gno` creates
`run/create_proposal.gno`. The `--template` flag is respected; `--bare` is rejected
as mutually exclusive.

### Template selection menu

Each module kind has a list of templates (currently one "basic" template each).
When only one template is available, it's auto-selected. When multiple exist,
a numbered menu is shown:

```
Select a template:
[1] basic - minimal realm with a Render function
[2] dao - DAO with proposals and voting
Choice [1-2] (default: 1):
```

Adding a new template requires only: (1) add `.tmpl` files under
`templates/<kind>/<name>/`, (2) add one entry to the registry in
`mod_init_templates.go`. No other Go code changes needed.

### Directory-based templates

Templates are defined by directory, not by listing individual files. `renderTemplateDir`
walks the template directory, processes every `.tmpl` file, and produces a map of
output filename → rendered content. Filenames are also Go templates: `{{.PkgName}}.gno.tmpl`
produces `<pkgName>.gno`. This supports complex multi-file templates (e.g. a DAO
template with state, helpers, and tests) without special-casing source/test files.

```
templates/
realm/
basic/
{{.PkgName}}.gno.tmpl → <pkg>.gno (Render function)
{{.PkgName}}_test.gno.tmpl → <pkg>_test.gno (TestRender)
package/
basic/
{{.PkgName}}.gno.tmpl → <pkg>.gno (bare package declaration)
{{.PkgName}}_test.gno.tmpl → <pkg>_test.gno (placeholder test)
run/
basic/
{{.ScriptName}}.gno.tmpl → <name>.gno (package main + gnokey comment)
```

`templateData` provides `{{.PkgName}}` (from module path) and `{{.ScriptName}}`
(for run script filenames). The run template includes a block comment showing
both `gno run` and `gnokey maketx run` commands with the script path.

### Non-interactive fallback

When stdin is not a TTY (CI, piped input) or `--bare` is passed, behavior is
identical to the original `gno mod init`: only `gnomod.toml` is created, module
path must be provided as argument. This preserves backward compatibility.

### Shared prompt primitives in `tm2/pkg/commands/`

Interactive prompts (string input, single-key choice, numbered select, confirm)
are extracted into `tm2/pkg/commands/prompt.go` as reusable primitives. This
allows other CLI tools (e.g. `gnokey maketx`) to build their own interactive
wizards without duplicating prompt logic or adding external dependencies.

Key functions: `IsInteractive()`, `PromptString()`, `PromptChoice()`,
`PromptSelect()`, `PromptConfirm()`.

The wizard is a linear flow — no back-navigation. Backspace cannot be detected
in line-buffered terminal mode, and alternative go-back keys (like `<` or `b`)
conflict with valid user input in `PromptString`. The user can Ctrl+C and
restart the wizard instead.

### File conflict detection

Before writing any template file, `writeModule` and `execInitRun(Script)` check
whether each output file already exists. If a conflict is found, the init
aborts with an error like `file already exists: myrealm.gno`. This prevents
silent overwrites on accidental re-init.

### TTY detection

Uses `commands.IsInteractive()`, which wraps `golang.org/x/term.IsTerminal()`
on `os.Stdin.Fd()`. Already a dependency via `tm2/pkg/commands/utils.go`.

### Template files are embedded, not hardcoded

Templates live as `.tmpl` files under `gnovm/cmd/gno/templates/` and are compiled
into the binary via `go:embed`. `text/template` renders them with `{{.PkgName}}`
and `{{.ScriptName}}`.

### No README generation

Considered and rejected. Gno realms have `Render()` for user-facing description,
existing examples don't include READMEs, and a placeholder README adds noise.

## Alternatives Considered

1. **Always generate templates (no flag)** — Rejected because it would break
non-interactive callers and existing test expectations.
2. **`--template` flag to opt-in** — Rejected because the common case (human in
a terminal) should scaffold by default. Requiring a flag hurts discoverability.
3. **Separate `gno new` command** — Rejected. `gno init` in the current directory
is sufficient and matches Go conventions. No need for directory creation.
4. **`--main` flag instead of kind prompt** — Rejected in favor of a unified
interactive prompt for all three kinds, which is more consistent and extensible.
5. **Prompt for "include tests?"** — Rejected for v1. Tests should always be
encouraged. Can be added later if users request it.
6. **External TUI library (Bubble Tea, huh, survey)** — Rejected. Every mature
Go prompt library pulls in the Charm stack (~15 transitive deps). The
`commands` package already has `GetString`/`GetPassword`/`GetConfirmation`
and `golang.org/x/term`; building on those keeps the dependency footprint
at zero new imports. The shared primitives in `prompt.go` are ~190 lines
of code, fully testable via `commands.IO`, and sufficient for sequential
wizard flows.
7. **Go-back navigation (`ErrGoBack`)** — Rejected. Backspace/delete key cannot
be detected in line-buffered terminal mode (the terminal handles it before
the application sees input). Alternative go-back keys (`<`, `b`) conflict
with valid user input in `PromptString` (e.g. namespace "b"). A linear
wizard without back-navigation is simpler; the user can Ctrl+C and restart.

## Key Files

| File | Role |
|------|------|
| `tm2/pkg/commands/prompt.go` | Shared prompt primitives (`PromptString`, `PromptChoice`, `PromptSelect`, `PromptConfirm`, `IsInteractive`) |
| `tm2/pkg/commands/prompt_test.go` | Tests for prompt primitives |
| `gnovm/cmd/gno/main.go` | `gno init` registered as top-level command |
| `gnovm/cmd/gno/mod.go` | `newInitCmd`, `execModInit`, `execInitRun`, `execInitRunScript`, `writeModule`, `promptModuleKind`, `selectTemplate`, `insertPathLetter` |
| `gnovm/cmd/gno/mod_init_templates.go` | `go:embed` declarations, `initTemplate` registry, `renderTemplateDir` |
| `gnovm/cmd/gno/templates/{realm,package,run}/basic/*.tmpl` | Template files with `{{.PkgName}}` and `{{.ScriptName}}` in filenames |
| `gnovm/cmd/gno/mod_test.go` | Tests for helpers and init flows |

## Consequences

- New users get a working starter project with `gno init gno.land/r/myname/myrealm`.
- Run scripts can be scaffolded with `gno init run/create_proposal.gno` (shorthand)
or `gno init` → select "main" → choose script name.
- Existing non-interactive usage is unchanged (all existing tests pass unmodified).
- The `--bare` flag provides an explicit escape hatch.
- Template content is minimal and opinionated — may need iteration based on feedback.
- Adding new templates (e.g. dao, grc20) only requires `.tmpl` files + one registry entry.
- Complex multi-file templates are supported — just add more `.tmpl` files in the
template directory.
- The shared prompt primitives in `tm2/pkg/commands/prompt.go` can be reused by
other CLI wizards (e.g. an interactive `gnokey maketx` flow) without code
duplication or new dependencies.
1 change: 1 addition & 0 deletions gnovm/cmd/gno/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func newGnocliCmd(io commands.IO) (*commands.Command, *rootConfig) {
// install
newListCmd(io),
newLintCmd(io),
newInitCmd(io),
newModCmd(io),
// work
newReplCmd(),
Expand Down
Loading
Loading