diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f72ed274..1bb44cd80 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- #992: Implement automatic history purge logic
- #973: Enables CORS and JWT configuration for WebApplications in module.xml
+- #1034: Add -clean flag to remove source directory while uninstalling
### Fixed
- #1001: The `unmap` and `enable` commands will now only activate CPF merge once after all namespaces have been configured instead after every namespace
diff --git a/preload/cls/IPM/Installer.cls b/preload/cls/IPM/Installer.cls
index 26970544b..25cfb3070 100644
--- a/preload/cls/IPM/Installer.cls
+++ b/preload/cls/IPM/Installer.cls
@@ -113,6 +113,7 @@ ClassMethod ZPMInit(
$$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("UseStandalonePip", "", 0))
$$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("SemVerPostRelease", 0, 0))
$$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("DefaultLogEntryLimit",20, 0))
+ $$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("CleanOnUninstall",0, 0))
quit $$$OK
}
diff --git a/src/cls/IPM/Lifecycle/Base.cls b/src/cls/IPM/Lifecycle/Base.cls
index 8fef53d56..f5aa38b0e 100644
--- a/src/cls/IPM/Lifecycle/Base.cls
+++ b/src/cls/IPM/Lifecycle/Base.cls
@@ -492,6 +492,26 @@ Method %Clean(ByRef pParams) As %Status
quit
}
}
+ // Remove the source directory while uninstalling the module.
+ if $data(pParams("Clean","clean")) ||(##class(%IPM.Repo.UniversalSettings).GetCleanOnUninstall()) {
+ set verbose = $get(pParams("Clean","Verbose"))
+ set moduleRootDir = ..Module.Root
+ set moduleName = ..Module.DisplayName
+ write !,"["_$namespace_"|"_moduleName_"]",$char(9),"Cleanup: Removing source directory"
+ if ##class(%File).DirectoryExists(moduleRootDir) {
+ if verbose {
+ write !,"["_$namespace_"|"_moduleName_"]",$char(9),"Found source directory: "_moduleRootDir
+ }
+ set tSC = ##class(%IPM.Utils.File).RemoveDirectoryTree(moduleRootDir)
+ if verbose {
+ write !,"["_$namespace_"|"_moduleName_"]",$char(9),"Source directory successfully deleted"
+ }
+ if $$$ISERR(tSC) {
+ write !,"["_$namespace_"|"_moduleName_"]",$char(9),"Source directory delete issue: "_$system.Status.GetErrorText(tSC)
+ $$$ThrowOnError(tSC)
+ }
+ }
+ }
} catch e {
set tSC = e.AsStatus()
}
diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls
index 64f7e2819..e09b46328 100644
--- a/src/cls/IPM/Main.cls
+++ b/src/cls/IPM/Main.cls
@@ -481,6 +481,7 @@ reinstall -env /path/to/env1.json;/path/to/env2.json example-package
+
@@ -2553,6 +2554,12 @@ ClassMethod Uninstall(ByRef pCommandInfo) [ Internal ]
$$$ThrowOnError(..CheckModuleNamespace())
}
set tRecurse = $$$HasModifier(pCommandInfo,"recurse") // Recursively uninstall unneeded dependencies
+ if $data(pCommandInfo("modifiers","clean")) {
+ set tParams("Clean","clean") = pCommandInfo("modifiers","clean")
+ }
+ if $get(pCommandInfo("data","Verbose")) {
+ set tParams("Clean","Verbose") = 1
+ }
$$$ThrowOnError(##class(%IPM.Storage.Module).Uninstall(tModuleName,tForce,tRecurse,.tParams))
}
}
diff --git a/src/cls/IPM/Repo/UniversalSettings.cls b/src/cls/IPM/Repo/UniversalSettings.cls
index e1ff1f3ed..364afe4ff 100644
--- a/src/cls/IPM/Repo/UniversalSettings.cls
+++ b/src/cls/IPM/Repo/UniversalSettings.cls
@@ -45,7 +45,10 @@ Parameter SemVerPostRelease = "SemVerPostRelease";
/// to retain IPM history records before they are eligible for cleanup.
Parameter HistoryRetain = "history_retain";
-Parameter CONFIGURABLE = "trackingId,analytics,ColorScheme,TerminalPrompt,PublishTimeout,PipCaller,UseStandalonePip,SemVerPostRelease,DefaultLogEntryLimit,HistoryRetain";
+/// Determines if the source folder should be physically deleted when a module is uninstalled.
+Parameter CleanOnUninstall = "CleanOnUninstall";
+
+Parameter CONFIGURABLE = "trackingId,analytics,ColorScheme,TerminalPrompt,PublishTimeout,PipCaller,UseStandalonePip,SemVerPostRelease,DefaultLogEntryLimit,HistoryRetain,CleanOnUninstall";
/// Returns configArray, that includes all configurable settings
ClassMethod GetAll(Output configArray) As %Status
@@ -185,9 +188,4 @@ ClassMethod SetAnalyticsAvailable(
return ..SetValue(..#analytics, +val, overwrite)
}
-ClassMethod GetHistoryRetain() As %Integer
-{
- return ..GetValue(..#HistoryRetain)
-}
-
}
diff --git a/tests/unit_tests/Test/PM/Unit/CLI.cls b/tests/unit_tests/Test/PM/Unit/CLI.cls
index 8e9f2329a..bd78479de 100644
--- a/tests/unit_tests/Test/PM/Unit/CLI.cls
+++ b/tests/unit_tests/Test/PM/Unit/CLI.cls
@@ -227,6 +227,7 @@ Method RunCommand(pCommand As %String) As %Status
return status
}
+/// This method returns the %Status for checking the success or failure of executed commands.
Method AssertNoException(pCommand As %String)
{
do ##class(%IPM.Main).ShellInternal(pCommand,.tException)
@@ -352,4 +353,75 @@ Method TestUninstallWithoutModuleName()
do $$$AssertNotTrue(exists, "Module removed successfully.")
}
+Method TestUninstallWithCleanModifier()
+{
+ set module = "objectscript-math"
+ do $$$LogMessage("Testing: Uninstall with -clean flag to remove source directory.")
+
+ //TestRepository method removed the registry configuration. So reconfigure while testing
+ do ##class(%IPM.Main).GetVersion("zpm",.out)
+ if $get(out)="" {
+ do $$$LogMessage("No registry configured. So started configuring https://pm.community.intersystems.com")
+ set status = ..RunCommand("repo -remote -n registry -url https://pm.community.intersystems.com/ -user """" -pass """"")
+ do $$$AssertStatusOK(status, "Registory configured successfully")
+ }
+
+ // 1. Setup: Ensure module is installed
+ if '##class(%IPM.Storage.Module).NameExists(module) {
+ do $$$LogMessage("Module "_module_" not found. Installing...")
+ set status = ..RunCommand("install "_module)
+ do $$$AssertStatusOK(status, "Setup: Module installed successfully.")
+ }
+
+ // 2. Capture the root directory path
+ set moduleObj = ##class(%IPM.Storage.Module).NameOpen(module)
+ set moduleRootDir = moduleObj.Root
+ do $$$AssertNotEquals(moduleRootDir, "", "Verified module root directory path: "_moduleRootDir)
+
+ // 3. Test Standard Uninstall (Should NOT delete directory)
+ do $$$LogMessage("Step 1: Uninstalling "_module_" without -clean flag.")
+ set status = ..RunCommand("uninstall "_module)
+ do $$$AssertStatusOK(status, "Standard uninstall completed.")
+
+ set dirExists = ##class(%File).DirectoryExists(moduleRootDir)
+ do $$$AssertTrue(dirExists, "Verification: Directory still exists after standard uninstall (as expected).")
+
+ // 4. Re-install for Clean Test
+ do $$$LogMessage("Step 2: Re-installing for -clean flag test.")
+ set status = ..RunCommand("install "_module)
+ do $$$AssertStatusOK(status, "Module re-installed successfully.")
+
+ // 5. Test Uninstall with -c (Should delete directory)
+ do $$$LogMessage("Step 3: Uninstalling "_module_" with -clean flag.")
+ set status = ..RunCommand("uninstall "_module_" -c")
+ do $$$AssertStatusOK(status, "Clean uninstall completed.")
+
+ set dirDeleted = '##class(%File).DirectoryExists(moduleRootDir)
+ do $$$AssertTrue(dirDeleted, "Verification: Source directory was physically deleted from the file system.")
+
+ do $$$LogMessage("Repeating this test for the UnvierslSettings 'CleanOnUninsall'")
+ // ZPMInit method configuratio from installer
+ /// bydefualt keep this flag as 0
+ set status = ##class(%IPM.Repo.UniversalSettings).SetValue("CleanOnUninstall",0, 0)
+ do $$$AssertStatusOK(status, "initial setup for CleanOnUninstall done.")
+
+ do $$$LogMessage("Step 4: Re-installing for CleanOnUninstall settings test.")
+ set status = ..RunCommand("install "_module)
+ do $$$AssertStatusOK(status, "Module re-installed successfully.")
+
+ do $$$LogMessage("Step 5: Updating the CleanOnUninstall settings to 1.")
+ set status = ##class(%IPM.Repo.UniversalSettings).UpdateOne("CleanOnUninstall",1)
+ do $$$AssertStatusOK(status, "Updated the CleanOnUninstall to 1.")
+
+ do $$$LogMessage("Step 6: Uninstall the module without -clean flag")
+ set status = ..RunCommand("uninstall "_module)
+ do $$$AssertStatusOK(status, "Clean uninstall completed.")
+
+ set dirDeleted = '##class(%File).DirectoryExists(moduleRootDir)
+ do $$$AssertTrue(dirDeleted, "Verification: Source directory was physically deleted from the file system.")
+
+ do ##class(%IPM.Repo.UniversalSettings).ResetToDefault("CleanOnUninstall")
+ do $$$LogMessage("CleanOnUninstall configuration restored")
+}
+
}