feat(partnerctl-init): add safe Partner Home selection with overwrite/cancel flow

This commit is contained in:
2026-05-05 17:21:02 +08:00
parent 19f87c93e3
commit 1bf02f543e

View File

@@ -52,8 +52,67 @@ class InitCommand : Runnable {
}
private fun initHome(prompt: Prompt) {
prompt.section("Initialize Partner Home")
fun resolveDefaultHome(): Path {
home = choosePartnerHome(prompt)
Files.createDirectories(home)
Files.createDirectories(home.resolve("resource"))
Files.createDirectories(home.resolve("config"))
prompt.success("Partner Home initialized at $home")
}
private fun choosePartnerHome(prompt: Prompt): Path {
val defaultHome = resolveDefaultHome()
while (true) {
val selectedHome = prompt.askPath(
label = "Partner Home",
defaultValue = defaultHome,
required = true,
directoryOnly = true,
)
if (!hasHomeContent(selectedHome)) {
return selectedHome
}
prompt.warn("Partner Home already contains files: $selectedHome")
when (prompt.select(
label = "Partner Home already contains files. Choose how to continue",
choices = listOf(
Choice(
"Use another Partner Home",
HomeDuplicateChoice.ANOTHER,
"Choose a different directory",
),
Choice(
"Overwrite current Partner Home",
HomeDuplicateChoice.OVERWRITE,
"Delete existing contents and continue",
),
Choice("Cancel init", HomeDuplicateChoice.EXIT),
),
defaultIndex = 0,
)) {
HomeDuplicateChoice.ANOTHER -> continue
HomeDuplicateChoice.OVERWRITE -> {
validateSafeHomeOverwrite(selectedHome)
if (!prompt.confirm("Delete all files under $selectedHome?", false)) {
continue
}
clearHomeDirectory(selectedHome)
return selectedHome
}
HomeDuplicateChoice.EXIT -> throw PromptCancelledException()
}
}
}
private fun resolveDefaultHome(): Path {
val envHome = System.getenv("PARTNER_HOME")?.trim()
return if (!envHome.isNullOrEmpty()) {
Paths.get(envHome).toAbsolutePath().normalize()
@@ -62,21 +121,45 @@ class InitCommand : Runnable {
}
}
prompt.section("Initialize Partner Home")
private fun hasHomeContent(path: Path): Boolean {
if (!Files.exists(path)) return false
if (Files.isRegularFile(path)) return true
if (!Files.isDirectory(path)) return false
val defaultHome = resolveDefaultHome()
return Files.walk(path).use { stream ->
stream.anyMatch { Files.isRegularFile(it) }
}
}
home = prompt.askPath(
label = "Partner Home",
defaultValue = defaultHome,
required = true,
directoryOnly = true,
)
Files.createDirectories(home)
Files.createDirectories(home.resolve("resource"))
Files.createDirectories(home.resolve("config"))
private fun clearHomeDirectory(path: Path) {
if (!Files.exists(path)) return
if (!Files.isDirectory(path)) {
throw CommandInterrupted("Partner Home is not a directory: $path")
}
prompt.success("Partner Home initialized at $home")
Files.walk(path).use { stream ->
stream
.sorted(Comparator.reverseOrder())
.filter { it != path }
.forEach { Files.deleteIfExists(it) }
}
}
private fun validateSafeHomeOverwrite(path: Path) {
val normalized = path.toAbsolutePath().normalize()
val userHome = Paths.get(System.getProperty("user.home")).toAbsolutePath().normalize()
if (normalized == normalized.root) {
throw CommandInterrupted("Refuse to overwrite filesystem root: $normalized")
}
if (normalized == userHome) {
throw CommandInterrupted("Refuse to overwrite user home directory: $normalized")
}
if (normalized.nameCount < 2) {
throw CommandInterrupted("Refuse to overwrite suspiciously broad directory: $normalized")
}
}
private fun installPartner(prompt: Prompt) {
@@ -266,4 +349,10 @@ class InitCommand : Runnable {
SKIP("Skip")
}
private enum class HomeDuplicateChoice {
ANOTHER,
OVERWRITE,
EXIT
}
}