Skip to content
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

This repo builds the Grill CLI and project setup tooling. The generated map-project agent notes live in `templates/AGENTS.md`; keep this root file focused on WurstSetup itself.

## Editing templates/AGENTS.md

- The first line marker must stay in lockstep with `SetupApp.AGENTS_TEMPLATE_VERSION` (a test enforces this); bump both when changing the template.
- Keep the intro line starting with "WurstScript Warcraft III map project notes" — `AGENTS_TEMPLATE_SOURCE_HINT` matches on it to detect template-derived files.
- Grill fetches the template from the raw GitHub URL on master, so pushing template changes publishes them to `grill install` users immediately. Only document language features after a compiler release ships them.
- Keep the template token-lean: it is loaded into every agent session in user projects. Prefer pointers to on-demand docs (stdlib/dependency `AGENTS.md`, the online manual) over inlining more content.

## Current Architecture

- Project config models come from `com.github.wurstscript:wurst-project-config`; do not reintroduce local DAO copies.
Expand Down
63 changes: 31 additions & 32 deletions src/main/kotlin/file/SetupApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ object SetupApp {

private data class WurstProcessResult(val exitCode: Int, val output: List<String>)

internal const val AGENTS_TEMPLATE_VERSION = "2026-06-22"
internal const val AGENTS_TEMPLATE_VERSION = "2026-08-05"
private const val AGENTS_TEMPLATE_MARKER_PREFIX = "<!-- WURST_AGENTS_TEMPLATE_VERSION:"
private const val AGENTS_TEMPLATE_MARKER = "<!-- WURST_AGENTS_TEMPLATE_VERSION: $AGENTS_TEMPLATE_VERSION -->"
private const val AGENTS_TEMPLATE_SOURCE_HINT = "WurstScript Warcraft III map project notes"
Expand Down Expand Up @@ -107,7 +107,7 @@ object SetupApp {

private fun handleCMD() {
// Cold-start lever: only spend network round-trips and a compiler subprocess on commands
// that actually consult the installation. help/generate stay fully offline and fast.
// that actually consult the installation. help/generate avoid installation checks and stay fast.
when {
setup.command == CLICommand.INSTALL && setup.commandArg.equals("wurstscript", ignoreCase = true) -> {
// Needs to know whether a newer compiler is available online.
Expand Down Expand Up @@ -367,6 +367,10 @@ object SetupApp {
}

private fun resolveGenerateGamePath(setup: SetupMain, wc3Patch: String?): Path? {
if (setup.gamePathOptedOut) {
log.info("Warcraft III path: not configured by choice.")
return null
}
val gameRoot = setup.gamePath ?: Wc3ClientDetector.detectGameRoot()
val clientInfo = Wc3ClientDetector.inspectGameRoot(gameRoot)
if (clientInfo == null) {
Expand Down Expand Up @@ -546,10 +550,6 @@ object SetupApp {
internal var installPatchPrompt: ((String, String?) -> String?)? = null

internal fun prepareGenerate(setup: SetupMain): Boolean {
Comment thread
Frotty marked this conversation as resolved.
if (setup.commandArg.isNotBlank()) {
return true
}

val prompt = generatePrompt ?: terminalPrompt()
Comment thread
Frotty marked this conversation as resolved.

while (setup.commandArg.isBlank()) {
Expand All @@ -561,20 +561,16 @@ object SetupApp {
}
}

runWizard(setup, prompt, useInteractiveMenus = generatePrompt == null)
runWizard(setup, prompt, useInteractiveMenus = generatePrompt == null && TerminalMenu.canUseInteractive())
Comment thread
Frotty marked this conversation as resolved.
return true
}

private fun terminalPrompt(): (String, String?) -> String? {
val console = System.console()
if (console == null) {
return prompt@ { message, default ->
if (default == null) {
print("$message: ")
} else {
print("$message [$default]: ")
}
val input = readlnOrNull()?.trim() ?: return@prompt null
// Non-console stdin is script input; keep EOF/default generation quiet.
return prompt@ { _, default ->
val input = readlnOrNull()?.trim() ?: return@prompt default
input.ifEmpty { default }
}
}
Expand All @@ -599,15 +595,16 @@ object SetupApp {
useInteractiveMenus = useInteractiveMenus,
currentPatch = setup.wc3Patch
)
setup.gamePath = selectGamePath(prompt, setup.wc3Patch, setup.gamePath)
setup.gamePathOptedOut = false
setup.gamePath = selectGamePath(setup, prompt, setup.wc3Patch, setup.gamePath)
Comment thread
Frotty marked this conversation as resolved.

val agentsDefault = if (setup.addAgents) "Y" else "N"
val agentsInput = prompt("Add AGENTS.md?", agentsDefault)
setup.addAgents = agentsInput?.lowercase() == "y"
val agentsInput = prompt("Add AGENTS.md?", agentsDefault) ?: agentsDefault
setup.addAgents = agentsInput.lowercase() == "y"

val ciDefault = if (setup.addGithubWorkflow) "Y" else "N"
val ciInput = prompt("Add GitHub Actions CI?", ciDefault)
setup.addGithubWorkflow = ciInput?.lowercase() == "y"
val ciInput = prompt("Add GitHub Actions CI?", ciDefault) ?: ciDefault
setup.addGithubWorkflow = ciInput.lowercase() == "y"

setup.curatedDependencyIds = selectCuratedDependencies(prompt, setup.curatedDependencyIds).toMutableList()
}
Expand All @@ -625,8 +622,8 @@ object SetupApp {
val selected = LinkedHashSet(preselectedIds)
for (dependency in catalog) {
val default = if (selected.contains(dependency.id)) "Y" else "N"
val answer = prompt("Add ${dependency.summary}?", default)
if (answer?.trim()?.lowercase() == "y") {
val answer = prompt("Add ${dependency.summary}?", default) ?: default
if (answer.trim().lowercase() == "y") {
selected.add(dependency.id)
} else {
selected.remove(dependency.id)
Expand All @@ -636,6 +633,7 @@ object SetupApp {
}

private fun selectGamePath(
setup: SetupMain,
prompt: (String, String?) -> String?,
wc3Patch: String?,
currentPath: Path?
Expand All @@ -652,12 +650,14 @@ object SetupApp {
val default = detected?.toAbsolutePath()?.normalize()?.toString() ?: "none"
val answer = prompt("Warcraft III directory (or none)", default)?.trim() ?: return detected
if (answer.equals("none", ignoreCase = true) || answer.equals("skip", ignoreCase = true)) {
setup.gamePathOptedOut = true
return null
}
val selected = Paths.get(answer).toAbsolutePath().normalize()
val selectedInfo = Wc3ClientDetector.inspectGameRoot(selected)
if (selectedInfo == null) {
log.warn("No supported Warcraft III executable found in $selected. You can fix wurst.wc3path later in .vscode/settings.json.")
setup.gamePathOptedOut = true
return null
}
Wc3ClientDetector.mismatchMessage(wc3Patch, selectedInfo)?.let { log.warn(it) }
Expand Down Expand Up @@ -760,47 +760,46 @@ object SetupApp {
useInteractiveMenus: Boolean,
currentPatch: String?
): String {
val versions = CoreJassProvider.fetchJassHistoryVersions()
val recommended = CoreJassProvider.recommendedPatchOptions(versions)
val bundledVersions = CoreJassProvider.supportedPatches
val recommended = CoreJassProvider.recommendedPatchOptions(bundledVersions)
val patchTargets = CoreJassProvider.supportedPatches
val exactVersions by lazy { CoreJassProvider.fetchJassHistoryVersions() }
val normalizedCurrentPatch = currentPatch?.let(CoreJassProvider::normalizePatchInput)
val defaultPatch = when {
normalizedCurrentPatch != null && CoreJassProvider.isSupportedPatch(normalizedCurrentPatch) -> normalizedCurrentPatch
else -> recommended.firstOrNull() ?: CoreJassProvider.DEFAULT_PATCH
}
val browseAll = "__browse_all__"
val visibleRecommended = (listOf(defaultPatch) + recommended).distinct()

if (useInteractiveMenus) {
while (true) {
val choices = recommended.map { TerminalMenu.Choice(it, CoreJassProvider.describePatch(it)) } +
val choices = visibleRecommended.map { TerminalMenu.Choice(it, CoreJassProvider.describePatch(it)) } +
TerminalMenu.Choice(browseAll, "Browse all supported patch targets...") +
TerminalMenu.Choice("__browse_exact__", "Advanced: browse exact jass-history dumps...")
val selection = TerminalMenu.choose(
title = intro,
choices = choices,
defaultIndex = recommended.indexOf(defaultPatch).takeIf { it >= 0 } ?: 0
defaultIndex = visibleRecommended.indexOf(defaultPatch).takeIf { it >= 0 } ?: 0
)
when {
selection == null -> return defaultPatch
selection == browseAll -> browsePatchVersionsInteractive("WC3 patch targets", patchTargets)?.let { return it }
selection == "__browse_exact__" -> browsePatchVersionsInteractive("Exact jass-history dumps", versions)?.let { return it }
selection == "__browse_exact__" -> browsePatchVersionsInteractive("Exact jass-history dumps", exactVersions)?.let { return it }
else -> return selection
}
}
}

log.info(intro)
val visibleRecommended = (listOf(defaultPatch) + recommended).distinct()
log.info("Recommended patch choices:")
visibleRecommended.forEachIndexed { index, patch ->
log.info(" ${index + 1}. ${CoreJassProvider.describePatch(patch)}")
}
if (patchTargets.isNotEmpty()) {
log.info("Type `more` to browse supported patch targets.")
}
if (versions.isNotEmpty()) {
log.info("Type `exact` to browse raw jass-history dump folders.")
}
log.info("Type `exact` to browse raw jass-history dump folders.")
log.info("Enter a listed number, press Enter for the default, or type `more`.")

while (true) {
Expand All @@ -817,11 +816,11 @@ object SetupApp {
title = "WC3 patch targets",
versions = patchTargets,
prompt = prompt,
exactVersions = versions
exactVersions = exactVersions

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Keep supported patch browsing offline

When the user types more, this argument evaluates the lazy exactVersions before browsePatchVersions can show the bundled supported patch list, so the supported-target browser still performs the raw jass-history GitHub lookup even if the user never asks for exact. In offline or slow-network sessions this stalls/warns before a local list that should be immediately available; pass a lazy provider and fetch only if exact is selected inside the browser.

Useful? React with 👍 / 👎.

)?.let { return it }
"exact", "raw", "dumps" -> browsePatchVersions(
title = "Exact jass-history dumps",
versions = versions,
versions = exactVersions,
prompt = prompt,
exactVersions = emptyList()
)?.let { return it }
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/file/SetupMain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class SetupMain {

var gamePath: Path? = null

var gamePathOptedOut: Boolean = false

var requireConfirmation = false

var noPJass = false
Expand Down
48 changes: 48 additions & 0 deletions src/test/kotlin/GenerateTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,54 @@ class GenerateTests {
Assert.assertTrue(setup.curatedDependencyIds.isEmpty())
}

@Test(priority = 10)
fun testGenerateWithNameUsesWizardPrompt() {
val setup = SetupMain()
setup.parseArgs(listOf("generate", "wizardproject"))
val answers = java.util.ArrayDeque(listOf("jass", "pre1.29", "none", "y", "y", "n"))
val prevPrompt = SetupApp.generatePrompt
try {
SetupApp.generatePrompt = { _, _ -> answers.removeFirst() }
Assert.assertTrue(SetupApp.prepareGenerate(setup))
} finally {
SetupApp.generatePrompt = prevPrompt
}

Assert.assertEquals(setup.commandArg, "wizardproject")
Assert.assertEquals(setup.scriptMode, ScriptMode.JASS)
Assert.assertEquals(setup.wc3Patch, CoreJassProvider.PRE_129_PATCH)
Assert.assertTrue(setup.gamePathOptedOut)
Assert.assertTrue(setup.addAgents)
Assert.assertTrue(setup.addGithubWorkflow)
Assert.assertTrue(setup.curatedDependencyIds.isEmpty())
}

@Test(priority = 10)
fun testGenerateWithNamePreservesCliSelectionsWhenWizardInputIsUnavailable() {
val setup = SetupMain()
setup.parseArgs(
listOf(
"generate",
"wizardproject",
"--with-agents",
"--with-ci",
"--with-dep",
"table-layout"
)
)
val prevPrompt = SetupApp.generatePrompt
try {
SetupApp.generatePrompt = { _, _ -> null }
Assert.assertTrue(SetupApp.prepareGenerate(setup))
} finally {
SetupApp.generatePrompt = prevPrompt
}

Assert.assertTrue(setup.addAgents)
Assert.assertTrue(setup.addGithubWorkflow)
Assert.assertEquals(setup.curatedDependencyIds, listOf("table-layout"))
}

@Test(priority = 10)
fun testGenerateWizardRejectsUnsupportedScriptModeAndPatchInput() {
val setup = SetupMain()
Expand Down
Loading
Loading