Skip to content

Commit fad3000

Browse files
feat(init): dev-friendly prompts — hints, placeholders, prefilled defaults
Make the init flow understandable for first-time devs: - a short intro explaining what init writes and that nothing is locked in; - an inline help message under every prompt (what the choice means and its consequence) — adapters, build packages, mode, matrix, targets, bin name, command, artifacts, tag format, changelog scope, release notes, provider; - placeholders and pre-filled, editable defaults (accept with Enter); - clearer labels (e.g. "cross-compile a binary per platform?" instead of "build across a target matrix?"). Also fixes the generic Rust/matrix command + artifacts defaults, which still emitted stale GitHub `${{ matrix.* }}` expressions instead of the tool's `{triple}`/`{ext}`/`{bin}` placeholders.
1 parent 664deb0 commit fad3000

2 files changed

Lines changed: 114 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ adheres to [Semantic Versioning](https://semver.org/). Work in progress lives un
2222
`download-artifact` in the publish job when only matrix packages feed it.
2323

2424
### Changed
25+
- **init** — Made the setup flow self-explanatory: a short intro, an inline hint under every prompt
26+
(what it means and its consequence), placeholders, and pre-filled defaults you can edit or accept
27+
with Enter. Also corrected the generic Rust/matrix command/artifacts defaults to use the
28+
`{triple}`/`{ext}`/`{bin}` placeholders (they still showed stale GitHub `${{ matrix.* }}` syntax).
2529
- **init** — An npm package is no longer offered the `build-only` mode: its prebuilt binaries ship
2630
inside the npm tarball, so it is always `publish`. `build-only` (standalone binaries on a GitHub
2731
Release) is now only prompted for cargo/generic packages, and its label clarifies that.

crates/core/src/init.rs

Lines changed: 110 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,21 @@ pub trait InitPrompt {
7171

7272
/// Wire up the real prompt and run the generator.
7373
pub fn run(factory: &dyn AdapterFactory, root: &Path, opts: &InitOptions) -> Result<()> {
74+
print_intro();
7475
orchestrate(factory, &StdinInitPrompt, root, opts)
7576
}
7677

78+
/// A short, friendly preamble so a first-time dev knows what `init` will ask and that nothing is
79+
/// locked in — every answer has a default and is editable afterward.
80+
fn print_intro() {
81+
println!("\notf-release init — configure releases for this repo.\n");
82+
println!(
83+
" • Writes release.toml (the editable source of truth) and a GitHub release workflow."
84+
);
85+
println!(" • Press Enter to accept the default shown in (parentheses); a hint sits under each prompt.");
86+
println!(" • Nothing is permanent — re-run init, edit release.toml by hand, or use `otf-release config`.\n");
87+
}
88+
7789
/// The testable core of `init`.
7890
pub fn orchestrate(
7991
factory: &dyn AdapterFactory,
@@ -192,7 +204,10 @@ fn select_targets(prompt: &str) -> Result<Vec<Target>> {
192204
.collect();
193205
let selected = MultiSelect::new(prompt, labels)
194206
.with_default(&defaults)
195-
.with_help_message(MULTI_HELP)
207+
.with_help_message(
208+
"the widely-supported platforms are pre-selected; \
209+
space toggles · enter confirm",
210+
)
196211
.raw_prompt()?;
197212
Ok(selected
198213
.iter()
@@ -915,9 +930,10 @@ fn configure_generic(
915930
&format!(" {name} — mode:"),
916931
vec![
917932
"publish (to registry)",
918-
"build-only (GitHub Release artifacts)",
933+
"build-only (standalone binaries on a GitHub Release)",
919934
],
920935
)
936+
.with_help_message(MODE_HELP)
921937
.raw_prompt()?
922938
.index
923939
{
@@ -926,20 +942,23 @@ fn configure_generic(
926942
};
927943

928944
let matrix = Select::new(
929-
&format!(" {name} — build across a target matrix?"),
945+
&format!(" {name} — cross-compile a binary per platform?"),
930946
vec!["Yes", "No"],
931947
)
948+
.with_help_message(MATRIX_HELP)
932949
.raw_prompt()?
933950
.index
934951
== 0;
935952
let targets = if matrix {
936-
select_targets(" Target triples:")?
953+
select_targets(" Target platforms:")?
937954
} else {
938955
Vec::new()
939956
};
940957

958+
// `otf-release build` runs `rustup target add {triple}` itself and substitutes the placeholders,
959+
// so the commands here use `{triple}`/`{ext}`/`{bin}`, not GitHub `${{ matrix.* }}` expressions.
941960
let default_cmd = match (kind, matrix) {
942-
(Some("Rust / Cargo"), true) => "rustup target add ${{ matrix.rust_target }} && cargo build --release --target ${{ matrix.rust_target }}",
961+
(Some("Rust / Cargo"), true) => "cargo build --release --target {triple}",
943962
(Some("Rust / Cargo"), false) => "cargo build --release",
944963
(Some("Node / npm"), _) => "npm run build",
945964
(Some("Deno / JSR"), _) => "deno task build",
@@ -951,36 +970,44 @@ fn configure_generic(
951970
};
952971
let command = Text::new(&format!(" {name} — build command (optional):"))
953972
.with_default(default_cmd)
973+
.with_help_message(if matrix {
974+
COMMAND_HELP
975+
} else {
976+
"runs in CI before release; leave blank for none"
977+
})
954978
.prompt()?;
955979

956980
let bin_name = if kind == Some("Rust / Cargo") {
957981
let n = Text::new(&format!(" {name} — binary name:"))
958982
.with_default(name)
983+
.with_help_message(BIN_NAME_HELP)
959984
.prompt()?;
960985
Some(n)
961986
} else {
962987
None
963988
};
964989

965990
let default_artifacts = match (kind, matrix) {
966-
(Some("Rust / Cargo"), true) => format!(
967-
"target/${{{{ matrix.target }}}}/release/{}${{{{ matrix.ext }}}}",
968-
bin_name.as_deref().unwrap()
969-
),
991+
(Some("Rust / Cargo"), true) => "target/{triple}/release/{bin}{ext}".to_string(),
970992
(Some("Rust / Cargo"), false) => format!("target/release/{}", bin_name.as_deref().unwrap()),
971993
(Some("Node / npm"), _) => "dist/*".to_string(),
972-
_ => "".to_string(),
994+
_ => String::new(),
973995
};
974996
let artifacts = Text::new(&format!(" {name} — artifacts to stage (optional):"))
975997
.with_default(&default_artifacts)
998+
.with_help_message(if matrix {
999+
ARTIFACTS_HELP
1000+
} else {
1001+
"files to attach/stage on release"
1002+
})
9761003
.prompt()?;
9771004

9781005
let publish = if mode == Mode::Publish {
979-
let cmd = Text::new(&format!(
980-
" {name} — publish command (e.g. npx jsr publish):"
981-
))
982-
.with_default("")
983-
.prompt()?;
1006+
let cmd = Text::new(&format!(" {name} — publish command:"))
1007+
.with_default("")
1008+
.with_placeholder("e.g. npx jsr publish")
1009+
.with_help_message("the command CI runs to push this package to its registry")
1010+
.prompt()?;
9841011
(!cmd.trim().is_empty()).then_some(cmd)
9851012
} else {
9861013
None
@@ -1006,12 +1033,36 @@ fn configure_generic(
10061033
pub struct StdinInitPrompt;
10071034

10081035
const MULTI_HELP: &str = "↑↓ move · space toggle · enter confirm";
1036+
const SELECT_HELP: &str = "↑↓ move · enter select";
1037+
1038+
const BUILD_PKGS_HELP: &str =
1039+
"a build step compiles/bundles before publish (a Rust binary, a TS bundle, …). \
1040+
Packages you don't pick are published as-is. ↑↓ move · space toggle · enter confirm";
1041+
const MODE_HELP: &str =
1042+
"publish → push to the registry · build-only → standalone binaries on a GitHub Release (no registry)";
1043+
const MATRIX_HELP: &str =
1044+
"Yes → cross-compile one binary per OS/arch (Rust, Go, …), staged per platform · No → a single build";
1045+
const BIN_NAME_HELP: &str =
1046+
"the compiled executable's base name; staged at bin/<platform>-<arch>/<name> inside the package";
1047+
const COMMAND_HELP: &str =
1048+
"runs in CI for each target; {triple} {ext} {bin} are substituted per platform";
1049+
const ARTIFACTS_HELP: &str =
1050+
"path to the binary the command produced; {triple} {ext} {bin} expand per target";
1051+
const TAG_FORMAT_HELP: &str =
1052+
"e.g. v{version} (single package) or {name}@{version} (per-package tags in a monorepo)";
1053+
const CHANGELOG_SCOPE_HELP: &str =
1054+
"Root → one shared CHANGELOG.md · Per-package → each package keeps its own (best for monorepos)";
1055+
const NOTES_HELP: &str =
1056+
"how the GitHub Release body is filled: auto (from PRs/commits), your CHANGELOG, or a commit list";
10091057

10101058
impl InitPrompt for StdinInitPrompt {
10111059
fn select_adapters(&self) -> Result<Vec<Ecosystem>> {
10121060
let labels: Vec<&str> = Ecosystem::ALL.iter().map(|e| e.label()).collect();
10131061
let chosen = MultiSelect::new("Adapters to enable:", labels)
1014-
.with_help_message(MULTI_HELP)
1062+
.with_help_message(
1063+
"the ecosystems/registries this repo releases to; pick all that apply. \
1064+
space toggles · enter confirm",
1065+
)
10151066
.raw_prompt()?;
10161067
Ok(chosen.iter().map(|o| Ecosystem::ALL[o.index]).collect())
10171068
}
@@ -1022,7 +1073,7 @@ impl InitPrompt for StdinInitPrompt {
10221073
}
10231074
let labels: Vec<String> = publishable.iter().map(|p| p.name.clone()).collect();
10241075
let chosen = MultiSelect::new("Which packages need a build step before publish?", labels)
1025-
.with_help_message(MULTI_HELP)
1076+
.with_help_message(BUILD_PKGS_HELP)
10261077
.raw_prompt()?;
10271078
Ok(chosen
10281079
.iter()
@@ -1035,7 +1086,9 @@ impl InitPrompt for StdinInitPrompt {
10351086
enabled[0]
10361087
} else {
10371088
let labels: Vec<&str> = enabled.iter().map(|e| e.label()).collect();
1038-
let opt = Select::new(&format!("{pkg_name} — adapter:"), labels).raw_prompt()?;
1089+
let opt = Select::new(&format!("{pkg_name} — adapter:"), labels)
1090+
.with_help_message("which registry/ecosystem this package is released through")
1091+
.raw_prompt()?;
10391092
enabled[opt.index]
10401093
};
10411094

@@ -1053,6 +1106,7 @@ impl InitPrompt for StdinInitPrompt {
10531106
"build-only (standalone binaries on a GitHub Release)",
10541107
],
10551108
)
1109+
.with_help_message(MODE_HELP)
10561110
.raw_prompt()?
10571111
.index
10581112
{
@@ -1062,9 +1116,10 @@ impl InitPrompt for StdinInitPrompt {
10621116
};
10631117

10641118
let matrix = Select::new(
1065-
&format!("{pkg_name} — build across a target matrix?"),
1119+
&format!("{pkg_name} — cross-compile a binary per platform?"),
10661120
vec!["Yes", "No"],
10671121
)
1122+
.with_help_message(MATRIX_HELP)
10681123
.raw_prompt()?
10691124
.index
10701125
== 0;
@@ -1081,6 +1136,7 @@ impl InitPrompt for StdinInitPrompt {
10811136
let (bin_name, compress, default_cmd, default_artifacts) = if matrix {
10821137
let bin = Text::new(&format!("{pkg_name} — binary name:"))
10831138
.with_default(&slug(pkg_name))
1139+
.with_help_message(BIN_NAME_HELP)
10841140
.prompt()?;
10851141
let compress = (adapter == Ecosystem::Npm).then(|| "brotli".to_string());
10861142
let cmd = if adapter == Ecosystem::Generic {
@@ -1104,9 +1160,19 @@ impl InitPrompt for StdinInitPrompt {
11041160
};
11051161
let command = Text::new(&format!("{pkg_name} — build command:"))
11061162
.with_default(&default_cmd)
1163+
.with_help_message(if matrix {
1164+
COMMAND_HELP
1165+
} else {
1166+
"runs in CI before publish (e.g. a bundler). Leave blank if no build is needed."
1167+
})
11071168
.prompt()?;
11081169
let artifacts = Text::new(&format!("{pkg_name} — artifacts to stage:"))
11091170
.with_default(&default_artifacts)
1171+
.with_help_message(if matrix {
1172+
ARTIFACTS_HELP
1173+
} else {
1174+
"files to include when publishing (e.g. dist/**). Optional."
1175+
})
11101176
.prompt()?;
11111177

11121178
Ok(PackageEntry {
@@ -1153,14 +1219,27 @@ impl InitPrompt for StdinInitPrompt {
11531219
} else {
11541220
"Add another package by hand?"
11551221
};
1156-
if Select::new(question, vec!["Yes", "No"]).raw_prompt()?.index == 1 {
1222+
if Select::new(question, vec!["Yes", "No"])
1223+
.with_help_message(SELECT_HELP)
1224+
.raw_prompt()?
1225+
.index
1226+
== 1
1227+
{
11571228
break;
11581229
}
1159-
let name = Text::new(" name:").prompt()?;
1160-
let manifest =
1161-
Text::new(" manifest file holding the version (e.g. deno.json):").prompt()?;
1230+
let name = Text::new(" name:")
1231+
.with_placeholder("@scope/pkg or my-tool")
1232+
.with_help_message("the package name; also used in tags and the changelog")
1233+
.prompt()?;
1234+
let manifest = Text::new(" manifest file holding the version:")
1235+
.with_placeholder("deno.json")
1236+
.with_help_message("the file the version is read from and bumped in")
1237+
.prompt()?;
11621238
let version_field = Text::new(" version field:")
11631239
.with_default(DEFAULT_VERSION_FIELD)
1240+
.with_help_message(
1241+
"key inside the manifest; dot-paths like workspace.package.version work",
1242+
)
11641243
.prompt()?;
11651244
out.push(configure_generic(&name, &manifest, &version_field, None)?);
11661245
}
@@ -1172,14 +1251,18 @@ impl InitPrompt for StdinInitPrompt {
11721251
&format!("{} already exists. Overwrite?", path.display()),
11731252
vec!["No", "Yes"],
11741253
)
1254+
.with_help_message(
1255+
"regenerates this file from your answers; your other files are untouched",
1256+
)
11751257
.raw_prompt()?
11761258
.index
11771259
== 1)
11781260
}
11791261

11801262
fn tag_format(&self) -> Result<String> {
1181-
Ok(Text::new("Git tag format ({version}, optional {name}):")
1263+
Ok(Text::new("Git tag format:")
11821264
.with_default(DEFAULT_TAG_FORMAT)
1265+
.with_help_message(TAG_FORMAT_HELP)
11831266
.prompt()?)
11841267
}
11851268

@@ -1195,6 +1278,7 @@ impl InitPrompt for StdinInitPrompt {
11951278
"Codeberg (Coming Soon)",
11961279
],
11971280
)
1281+
.with_help_message("only GitHub is fully supported today")
11981282
.prompt()?;
11991283

12001284
if ans == "GitHub" {
@@ -1210,6 +1294,7 @@ impl InitPrompt for StdinInitPrompt {
12101294
"Where should release notes be maintained?",
12111295
vec!["Root CHANGELOG.md", "Per-package CHANGELOG.md files"],
12121296
)
1297+
.with_help_message(CHANGELOG_SCOPE_HELP)
12131298
.prompt()?;
12141299

12151300
if ans.starts_with("Root") {
@@ -1228,6 +1313,7 @@ impl InitPrompt for StdinInitPrompt {
12281313
"Semantic-style commit list since the last matching tag",
12291314
],
12301315
)
1316+
.with_help_message(NOTES_HELP)
12311317
.prompt()?;
12321318

12331319
if ans.starts_with("Copy") {

0 commit comments

Comments
 (0)