diff --git a/src/TALXIS.CLI.Workspace/TALXIS.CLI.Workspace.csproj b/src/TALXIS.CLI.Workspace/TALXIS.CLI.Workspace.csproj
index e916c87..7ad583f 100644
--- a/src/TALXIS.CLI.Workspace/TALXIS.CLI.Workspace.csproj
+++ b/src/TALXIS.CLI.Workspace/TALXIS.CLI.Workspace.csproj
@@ -15,4 +15,12 @@
+
+
+ PreserveNewest
+ PreserveNewest
+ Upgrade/Templates/%(RecursiveDir)%(Filename)%(Extension)
+
+
+
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Conversion/ProjectUpgrader.cs b/src/TALXIS.CLI.Workspace/Upgrade/Conversion/ProjectUpgrader.cs
new file mode 100644
index 0000000..6cf8476
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Conversion/ProjectUpgrader.cs
@@ -0,0 +1,196 @@
+using Microsoft.Extensions.Logging;
+using TALXIS.CLI.Workspace.Upgrade.Generation;
+using TALXIS.CLI.Workspace.Upgrade.Models;
+using TALXIS.CLI.Workspace.Upgrade.Parsers;
+using TALXIS.CLI.Workspace.Upgrade.Utilities;
+
+namespace TALXIS.CLI.Workspace.Upgrade.Conversion;
+
+public class ProjectUpgrader
+{
+ private readonly CsprojParser _parser;
+ private readonly SdkProjectGenerator _generator;
+ private readonly ILogger _logger;
+
+ public ProjectUpgrader(ILogger logger)
+ {
+ _parser = new CsprojParser();
+ _generator = new SdkProjectGenerator();
+ _logger = logger;
+ }
+
+ public UpgradeResult Upgrade(
+ string targetFilePath,
+ string oldTemplateFilePath,
+ string newTemplateFilePath,
+ bool createBackup = true)
+ {
+ var result = new UpgradeResult();
+
+ try
+ {
+ _logger.LogInformation("Parsing project and templates...");
+ var targetProject = _parser.Parse(targetFilePath);
+ var oldTemplate = _parser.Parse(oldTemplateFilePath);
+ var newTemplate = _parser.Parse(newTemplateFilePath);
+
+ _logger.LogInformation("Filling parameter placeholders from existing project...");
+ var newProject = FillParameterPlaceholders(newTemplate, targetProject, targetFilePath);
+
+ _logger.LogInformation("Extracting additional dependencies not in baseline template...");
+ ExtractAdditionalDependencies(oldTemplate, targetProject, newProject);
+
+ result.CustomPropertiesFound = CountFilledParameters(newTemplate, targetProject);
+ result.PackageReferencesFound = newProject.PackageReferences.Count - newTemplate.PackageReferences.Count;
+ result.ProjectReferencesFound = newProject.ProjectReferences.Count - newTemplate.ProjectReferences.Count;
+ result.AssemblyReferencesFound = newProject.AssemblyReferences.Count - newTemplate.AssemblyReferences.Count;
+
+ if (createBackup)
+ {
+ var backupPath = targetFilePath + ".backup";
+ if (File.Exists(targetFilePath))
+ {
+ File.Copy(targetFilePath, backupPath, overwrite: true);
+ result.BackupCreated = true;
+ result.BackupPath = backupPath;
+ _logger.LogInformation("Backup created at {BackupPath}", backupPath);
+ }
+ }
+
+ if (File.Exists(targetFilePath))
+ {
+ _logger.LogInformation("Removing original file {TargetFile}", targetFilePath);
+ File.Delete(targetFilePath);
+ }
+
+ var newFilePath = Path.Combine(
+ Path.GetDirectoryName(targetFilePath) ?? string.Empty,
+ Path.GetFileNameWithoutExtension(targetFilePath) + ".csproj");
+
+ _logger.LogInformation("Generating new SDK-style project file at {NewFilePath}", newFilePath);
+ var xml = _generator.Generate(newProject);
+ _generator.SaveToFile(xml, newFilePath);
+
+ result.Success = true;
+ result.OutputFilePath = newFilePath;
+ }
+ catch (Exception ex)
+ {
+ result.Success = false;
+ result.ErrorMessage = ex.Message;
+ result.Exception = ex;
+ _logger.LogError(ex, "Upgrade failed for {TargetFile}", targetFilePath);
+ }
+
+ return result;
+ }
+
+ private CsprojProject FillParameterPlaceholders(
+ CsprojProject newTemplate,
+ CsprojProject targetProject,
+ string targetFilePath)
+ {
+ var newProject = new CsprojProject
+ {
+ FilePath = targetFilePath.Replace(".cdsproj", ".csproj"),
+ IsSdkStyle = true,
+ Sdk = newTemplate.Sdk ?? "Microsoft.NET.Sdk"
+ };
+
+ // Copy all properties
+ foreach (var prop in newTemplate.Properties)
+ {
+ // If template property has empty value, it's a parameter placeholder - fill from target
+ if (string.IsNullOrWhiteSpace(prop.Value))
+ {
+ if (targetProject.Properties.TryGetValue(prop.Key, out var targetValue))
+ {
+ newProject.Properties[prop.Key] = targetValue;
+ }
+ else
+ {
+ newProject.Properties[prop.Key] = prop.Value;
+ }
+ }
+ else
+ {
+ newProject.Properties[prop.Key] = prop.Value;
+ }
+ }
+
+ // Copy all other elements
+ newProject.PackageReferences.AddRange(newTemplate.PackageReferences);
+ newProject.ProjectReferences.AddRange(newTemplate.ProjectReferences);
+ newProject.AssemblyReferences.AddRange(newTemplate.AssemblyReferences);
+ newProject.CustomTargets.AddRange(newTemplate.CustomTargets);
+ newProject.CustomImports.AddRange(newTemplate.CustomImports);
+ newProject.CustomItemGroups.AddRange(newTemplate.CustomItemGroups);
+ newProject.CustomPropertyGroups.AddRange(newTemplate.CustomPropertyGroups);
+
+ return newProject;
+ }
+
+ private int CountFilledParameters(CsprojProject newTemplate, CsprojProject targetProject)
+ {
+ int count = 0;
+ foreach (var prop in newTemplate.Properties)
+ {
+ if (string.IsNullOrWhiteSpace(prop.Value) && targetProject.Properties.ContainsKey(prop.Key))
+ {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private void ExtractAdditionalDependencies(
+ CsprojProject oldTemplate,
+ CsprojProject targetProject,
+ CsprojProject newProject)
+ {
+ // Find PackageReferences that are in target but NOT in old template baseline
+ var baselinePackages = new HashSet(
+ oldTemplate.PackageReferences.Select(p => p.Name),
+ StringComparer.OrdinalIgnoreCase
+ );
+
+ foreach (var package in targetProject.PackageReferences)
+ {
+ if (!baselinePackages.Contains(package.Name))
+ {
+ // This is a custom package added by user
+ newProject.PackageReferences.Add(package);
+ }
+ }
+
+ // Find ProjectReferences that are in target but NOT in old template baseline
+ var baselineProjects = new HashSet(
+ oldTemplate.ProjectReferences.Select(p => p.Include),
+ StringComparer.OrdinalIgnoreCase
+ );
+
+ foreach (var projectRef in targetProject.ProjectReferences)
+ {
+ if (!baselineProjects.Contains(projectRef.Include))
+ {
+ // This is a custom project reference added by user
+ newProject.ProjectReferences.Add(projectRef);
+ }
+ }
+
+ // Find AssemblyReferences that are in target but NOT in old template baseline
+ var baselineAssemblies = new HashSet(
+ oldTemplate.AssemblyReferences.Select(a => a.Include),
+ StringComparer.OrdinalIgnoreCase
+ );
+
+ foreach (var assembly in targetProject.AssemblyReferences)
+ {
+ if (!baselineAssemblies.Contains(assembly.Include))
+ {
+ // This is a custom assembly reference added by user
+ newProject.AssemblyReferences.Add(assembly);
+ }
+ }
+ }
+}
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Generation/SdkProjectGenerator.cs b/src/TALXIS.CLI.Workspace/Upgrade/Generation/SdkProjectGenerator.cs
new file mode 100644
index 0000000..2572a10
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Generation/SdkProjectGenerator.cs
@@ -0,0 +1,184 @@
+using System.Xml.Linq;
+using TALXIS.CLI.Workspace.Upgrade.Models;
+
+namespace TALXIS.CLI.Workspace.Upgrade.Generation;
+
+public class SdkProjectGenerator
+{
+ public XDocument Generate(CsprojProject project)
+ {
+ var root = new XElement("Project",
+ new XAttribute("Sdk", project.Sdk ?? "Microsoft.NET.Sdk")
+ );
+
+ AddPropertyGroup(root, project);
+
+ if (project.PackageReferences.Any())
+ {
+ AddPackageReferences(root, project);
+ }
+
+ if (project.ProjectReferences.Any())
+ {
+ AddProjectReferences(root, project);
+ }
+
+ if (project.AssemblyReferences.Any())
+ {
+ AddAssemblyReferences(root, project);
+ }
+
+ foreach (var itemGroup in project.CustomItemGroups)
+ {
+ root.Add(itemGroup);
+ }
+
+ foreach (var import in project.CustomImports)
+ {
+ root.Add(import);
+ }
+
+ foreach (var target in project.CustomTargets)
+ {
+ root.Add(target);
+ }
+
+ return new XDocument(
+ new XDeclaration("1.0", "utf-8", null),
+ root
+ );
+ }
+
+
+ public void SaveToFile(XDocument document, string filePath)
+ {
+ var directory = Path.GetDirectoryName(filePath);
+ if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ document.Save(filePath, SaveOptions.None);
+ }
+
+ private void AddPropertyGroup(XElement root, CsprojProject project)
+ {
+ if (!project.Properties.Any())
+ return;
+
+ var propertyGroup = new XElement("PropertyGroup");
+
+ var orderedProperties = new[]
+ {
+ "TargetFramework", "TargetFrameworks",
+ "OutputType", "AssemblyName", "RootNamespace",
+ "LangVersion", "Nullable", "ImplicitUsings"
+ };
+
+ foreach (var key in orderedProperties)
+ {
+ if (project.Properties.TryGetValue(key, out var value))
+ {
+ propertyGroup.Add(new XElement(key, value));
+ }
+ }
+
+ foreach (var prop in project.Properties)
+ {
+ if (!orderedProperties.Contains(prop.Key))
+ {
+ propertyGroup.Add(new XElement(prop.Key, prop.Value));
+ }
+ }
+
+ root.Add(propertyGroup);
+ }
+
+ private void AddPackageReferences(XElement root, CsprojProject project)
+ {
+ var itemGroup = new XElement("ItemGroup");
+
+ var attributeNames = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "PrivateAssets", "ExcludeAssets", "IncludeAssets"
+ };
+
+ foreach (var package in project.PackageReferences.OrderBy(p => p.Name))
+ {
+ var element = new XElement("PackageReference",
+ new XAttribute("Include", package.Name)
+ );
+
+ if (!string.IsNullOrEmpty(package.Version))
+ {
+ element.Add(new XAttribute("Version", package.Version));
+ }
+
+ // Add metadata
+ foreach (var metadata in package.Metadata)
+ {
+ if (attributeNames.Contains(metadata.Key))
+ {
+ element.Add(new XAttribute(metadata.Key, metadata.Value));
+ }
+ else
+ {
+ element.Add(new XElement(metadata.Key, metadata.Value));
+ }
+ }
+
+ itemGroup.Add(element);
+ }
+
+ root.Add(itemGroup);
+ }
+
+ private void AddProjectReferences(XElement root, CsprojProject project)
+ {
+ var itemGroup = new XElement("ItemGroup");
+
+ foreach (var projectRef in project.ProjectReferences.OrderBy(p => p.Include))
+ {
+ var element = new XElement("ProjectReference",
+ new XAttribute("Include", projectRef.Include)
+ );
+
+ // Add metadata
+ foreach (var metadata in projectRef.Metadata)
+ {
+ element.Add(new XElement(metadata.Key, metadata.Value));
+ }
+
+ itemGroup.Add(element);
+ }
+
+ root.Add(itemGroup);
+ }
+
+ private void AddAssemblyReferences(XElement root, CsprojProject project)
+ {
+ var itemGroup = new XElement("ItemGroup");
+
+ foreach (var assemblyRef in project.AssemblyReferences.OrderBy(a => a.Include))
+ {
+ var element = new XElement("Reference",
+ new XAttribute("Include", assemblyRef.Include)
+ );
+
+ if (!string.IsNullOrEmpty(assemblyRef.HintPath))
+ {
+ element.Add(new XElement("HintPath", assemblyRef.HintPath));
+ }
+
+ // Add metadata
+ foreach (var metadata in assemblyRef.Metadata)
+ {
+ element.Add(new XElement(metadata.Key, metadata.Value));
+ }
+
+ itemGroup.Add(element);
+ }
+
+ root.Add(itemGroup);
+ }
+}
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Models/AssemblyReference.cs b/src/TALXIS.CLI.Workspace/Upgrade/Models/AssemblyReference.cs
new file mode 100644
index 0000000..c055d94
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Models/AssemblyReference.cs
@@ -0,0 +1,10 @@
+namespace TALXIS.CLI.Workspace.Upgrade.Models;
+
+public class AssemblyReference
+{
+ public string Include { get; set; } = string.Empty;
+ public string? HintPath { get; set; }
+ public Dictionary Metadata { get; set; } = new();
+
+ public override string ToString() => Include;
+}
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Models/CsprojProject.cs b/src/TALXIS.CLI.Workspace/Upgrade/Models/CsprojProject.cs
new file mode 100644
index 0000000..71343b9
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Models/CsprojProject.cs
@@ -0,0 +1,27 @@
+using System.Xml.Linq;
+
+namespace TALXIS.CLI.Workspace.Upgrade.Models;
+
+public class CsprojProject
+{
+ public string FilePath { get; set; } = string.Empty;
+ public bool IsSdkStyle { get; set; }
+ public string? Sdk { get; set; }
+
+ // Properties
+ public Dictionary Properties { get; set; } = new();
+
+ // References
+ public List PackageReferences { get; set; } = new();
+ public List ProjectReferences { get; set; } = new();
+ public List AssemblyReferences { get; set; } = new();
+
+ // Custom elements that need to be preserved
+ public List CustomPropertyGroups { get; set; } = new();
+ public List CustomItemGroups { get; set; } = new();
+ public List CustomTargets { get; set; } = new();
+ public List CustomImports { get; set; } = new();
+
+ // Original XML for reference
+ public XDocument? OriginalXml { get; set; }
+}
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Models/PackageReference.cs b/src/TALXIS.CLI.Workspace/Upgrade/Models/PackageReference.cs
new file mode 100644
index 0000000..67a3ad5
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Models/PackageReference.cs
@@ -0,0 +1,10 @@
+namespace TALXIS.CLI.Workspace.Upgrade.Models;
+
+public class PackageReference
+{
+ public string Name { get; set; } = string.Empty;
+ public string? Version { get; set; }
+ public Dictionary Metadata { get; set; } = new();
+
+ public override string ToString() => $"{Name} {Version}";
+}
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Models/ProjectReference.cs b/src/TALXIS.CLI.Workspace/Upgrade/Models/ProjectReference.cs
new file mode 100644
index 0000000..4c880bf
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Models/ProjectReference.cs
@@ -0,0 +1,9 @@
+namespace TALXIS.CLI.Workspace.Upgrade.Models;
+
+public class ProjectReference
+{
+ public string Include { get; set; } = string.Empty;
+ public Dictionary Metadata { get; set; } = new();
+
+ public override string ToString() => Include;
+}
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Models/ProjectType.cs b/src/TALXIS.CLI.Workspace/Upgrade/Models/ProjectType.cs
new file mode 100644
index 0000000..11f4df3
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Models/ProjectType.cs
@@ -0,0 +1,10 @@
+namespace TALXIS.CLI.Workspace.Upgrade.Models;
+
+public enum ProjectType
+{
+ DataverseSolution,
+ ScriptLibrary,
+ Plugin,
+ PDPackage,
+ Unknown
+}
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Models/UpgradeResult.cs b/src/TALXIS.CLI.Workspace/Upgrade/Models/UpgradeResult.cs
new file mode 100644
index 0000000..1f33aff
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Models/UpgradeResult.cs
@@ -0,0 +1,15 @@
+namespace TALXIS.CLI.Workspace.Upgrade.Models;
+
+public class UpgradeResult
+{
+ public bool Success { get; set; }
+ public string? OutputFilePath { get; set; }
+ public string? BackupPath { get; set; }
+ public bool BackupCreated { get; set; }
+ public int PackageReferencesFound { get; set; }
+ public int ProjectReferencesFound { get; set; }
+ public int AssemblyReferencesFound { get; set; }
+ public int CustomPropertiesFound { get; set; }
+ public string? ErrorMessage { get; set; }
+ public Exception? Exception { get; set; }
+}
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Parsers/CsprojParser.cs b/src/TALXIS.CLI.Workspace/Upgrade/Parsers/CsprojParser.cs
new file mode 100644
index 0000000..e66a018
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Parsers/CsprojParser.cs
@@ -0,0 +1,195 @@
+using System.Xml.Linq;
+using TALXIS.CLI.Workspace.Upgrade.Models;
+
+namespace TALXIS.CLI.Workspace.Upgrade.Parsers;
+
+public class CsprojParser
+{
+ private static readonly XNamespace MsbuildNamespace = "http://schemas.microsoft.com/developer/msbuild/2003";
+
+ public CsprojProject Parse(string filePath)
+ {
+ if (!File.Exists(filePath))
+ throw new FileNotFoundException($"Project file not found: {filePath}");
+
+ var xml = XDocument.Load(filePath);
+ var root = xml.Root ?? throw new InvalidOperationException("Invalid project file: no root element");
+
+ var project = new CsprojProject
+ {
+ FilePath = filePath,
+ OriginalXml = xml,
+ IsSdkStyle = root.Attribute("Sdk") != null,
+ Sdk = root.Attribute("Sdk")?.Value
+ };
+
+ ParseProperties(root, project);
+ ParsePackageReferences(root, project);
+ ParseProjectReferences(root, project);
+ ParseAssemblyReferences(root, project);
+ ParseCustomElements(root, project);
+
+ return project;
+ }
+
+ private void ParseProperties(XElement root, CsprojProject project)
+ {
+ // Parse all PropertyGroup elements
+ var propertyGroups = root.Elements(MsbuildNamespace + "PropertyGroup")
+ .Concat(root.Elements("PropertyGroup"));
+
+ foreach (var group in propertyGroups)
+ {
+ foreach (var property in group.Elements())
+ {
+ var name = property.Name.LocalName;
+ var value = property.Value;
+
+ // Store all properties
+ if (!project.Properties.ContainsKey(name))
+ {
+ project.Properties[name] = value;
+ }
+ }
+ }
+ }
+
+ private void ParsePackageReferences(XElement root, CsprojProject project)
+ {
+ var packageRefs = root.Descendants(MsbuildNamespace + "PackageReference")
+ .Concat(root.Descendants("PackageReference"));
+
+ foreach (var packageRef in packageRefs)
+ {
+ var include = packageRef.Attribute("Include")?.Value;
+ if (string.IsNullOrEmpty(include))
+ continue;
+
+ var package = new PackageReference
+ {
+ Name = include,
+ Version = packageRef.Attribute("Version")?.Value
+ ?? packageRef.Element(MsbuildNamespace + "Version")?.Value
+ ?? packageRef.Element("Version")?.Value
+ };
+
+ // Parse attributes as metadata (except Include and Version)
+ foreach (var attribute in packageRef.Attributes())
+ {
+ if (attribute.Name.LocalName != "Include" && attribute.Name.LocalName != "Version")
+ {
+ package.Metadata[attribute.Name.LocalName] = attribute.Value;
+ }
+ }
+
+ // Parse child elements as metadata
+ foreach (var element in packageRef.Elements())
+ {
+ if (element.Name.LocalName != "Version")
+ {
+ package.Metadata[element.Name.LocalName] = element.Value;
+ }
+ }
+
+ project.PackageReferences.Add(package);
+ }
+ }
+
+ private void ParseProjectReferences(XElement root, CsprojProject project)
+ {
+ var projectRefs = root.Descendants(MsbuildNamespace + "ProjectReference")
+ .Concat(root.Descendants("ProjectReference"));
+
+ foreach (var projectRef in projectRefs)
+ {
+ var include = projectRef.Attribute("Include")?.Value;
+ if (string.IsNullOrEmpty(include))
+ continue;
+
+ var reference = new ProjectReference
+ {
+ Include = include
+ };
+
+ // Parse metadata
+ foreach (var element in projectRef.Elements())
+ {
+ reference.Metadata[element.Name.LocalName] = element.Value;
+ }
+
+ project.ProjectReferences.Add(reference);
+ }
+ }
+
+ private void ParseAssemblyReferences(XElement root, CsprojProject project)
+ {
+ var assemblyRefs = root.Descendants(MsbuildNamespace + "Reference")
+ .Concat(root.Descendants("Reference"));
+
+ foreach (var assemblyRef in assemblyRefs)
+ {
+ var include = assemblyRef.Attribute("Include")?.Value;
+ if (string.IsNullOrEmpty(include))
+ continue;
+
+ var reference = new AssemblyReference
+ {
+ Include = include,
+ HintPath = assemblyRef.Element(MsbuildNamespace + "HintPath")?.Value
+ ?? assemblyRef.Element("HintPath")?.Value
+ };
+
+ // Parse metadata
+ foreach (var element in assemblyRef.Elements())
+ {
+ if (element.Name.LocalName != "HintPath")
+ {
+ reference.Metadata[element.Name.LocalName] = element.Value;
+ }
+ }
+
+ project.AssemblyReferences.Add(reference);
+ }
+ }
+
+ private void ParseCustomElements(XElement root, CsprojProject project)
+ {
+ // Collect custom Target elements
+ var targets = root.Descendants(MsbuildNamespace + "Target")
+ .Concat(root.Descendants("Target"));
+ project.CustomTargets.AddRange(targets);
+
+ // Collect custom Import elements
+ var imports = root.Elements(MsbuildNamespace + "Import")
+ .Concat(root.Elements("Import"));
+ project.CustomImports.AddRange(imports);
+
+ // Store ItemGroups only if they contain elements OTHER than PackageReference, ProjectReference, or Reference
+ // Those are already parsed separately and will be added by the generator
+ var itemGroups = root.Elements(MsbuildNamespace + "ItemGroup")
+ .Concat(root.Elements("ItemGroup"));
+
+ foreach (var itemGroup in itemGroups)
+ {
+ var hasStandardReferences = itemGroup.Elements()
+ .Any(e => e.Name.LocalName == "PackageReference" ||
+ e.Name.LocalName == "ProjectReference" ||
+ e.Name.LocalName == "Reference");
+
+ var hasOtherElements = itemGroup.Elements()
+ .Any(e => e.Name.LocalName != "PackageReference" &&
+ e.Name.LocalName != "ProjectReference" &&
+ e.Name.LocalName != "Reference");
+
+
+ if (hasOtherElements && !hasStandardReferences)
+ {
+ project.CustomItemGroups.Add(itemGroup);
+ }
+ }
+
+ var propertyGroups = root.Elements(MsbuildNamespace + "PropertyGroup")
+ .Concat(root.Elements("PropertyGroup"));
+ project.CustomPropertyGroups.AddRange(propertyGroups);
+ }
+}
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/ProjectUpgradeRunner.cs b/src/TALXIS.CLI.Workspace/Upgrade/ProjectUpgradeRunner.cs
new file mode 100644
index 0000000..0be66b9
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/ProjectUpgradeRunner.cs
@@ -0,0 +1,145 @@
+using Microsoft.Extensions.Logging;
+using TALXIS.CLI.Workspace.Upgrade.Conversion;
+using TALXIS.CLI.Workspace.Upgrade.Models;
+using TALXIS.CLI.Workspace.Upgrade.Utilities;
+
+namespace TALXIS.CLI.Workspace.Upgrade;
+
+///
+/// Coordinates upgrading one or more project files.
+///
+public class ProjectUpgradeRunner
+{
+ private readonly ILogger _logger;
+ private readonly ILoggerFactory _loggerFactory;
+ private readonly string _templatesBasePath;
+ private readonly bool _createBackup;
+
+ public ProjectUpgradeRunner(
+ ILoggerFactory loggerFactory,
+ string templatesBasePath,
+ bool createBackup = true)
+ {
+ _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
+ _logger = loggerFactory.CreateLogger();
+ _templatesBasePath = templatesBasePath ?? throw new ArgumentNullException(nameof(templatesBasePath));
+ _createBackup = createBackup;
+ }
+
+ public int Run(string targetPath)
+ {
+ if (string.IsNullOrWhiteSpace(targetPath))
+ {
+ _logger.LogError("Path to project file or directory must be provided.");
+ return 1;
+ }
+
+ targetPath = Path.GetFullPath(targetPath);
+
+ var projectFiles = ResolveTargets(targetPath);
+ if (projectFiles.Count == 0)
+ {
+ _logger.LogError("No .csproj or .cdsproj files found at {TargetPath}", targetPath);
+ return 1;
+ }
+
+ _logger.LogInformation("Found {Count} project file(s) to upgrade.", projectFiles.Count);
+
+ var detector = new ProjectTypeDetector();
+ var templateManager = new TemplateManager(_templatesBasePath);
+
+ int processed = 0, succeeded = 0, failed = 0;
+
+ foreach (var projectFile in projectFiles)
+ {
+ processed++;
+ _logger.LogInformation("[{Index}/{Total}] Processing {File}", processed, projectFiles.Count, projectFile);
+
+ var projectType = detector.DetectProjectType(projectFile);
+ if (projectType == ProjectType.Unknown)
+ {
+ _logger.LogError("Unsupported project type for {File}. Supported: Dataverse Solution, Script Library, Plugin, PDPackage.", projectFile);
+ failed++;
+ continue;
+ }
+
+ var isOldTalxisFormat = projectType == ProjectType.DataverseSolution && detector.IsOldTalxisFormat(projectFile);
+ if (isOldTalxisFormat)
+ {
+ _logger.LogInformation("Detected old TALXIS Dataverse Solution format for {File}", projectFile);
+ }
+
+ string oldTemplatePath;
+ string newTemplatePath;
+ try
+ {
+ oldTemplatePath = templateManager.GetOldFormatTemplatePath(projectType, isOldTalxisFormat);
+ newTemplatePath = templateManager.GetNewFormatTemplatePath(projectType);
+ templateManager.ValidateTemplates(projectType, isOldTalxisFormat);
+ }
+ catch (FileNotFoundException ex)
+ {
+ _logger.LogError(ex, "Template files missing for {ProjectType}", projectType);
+ failed++;
+ continue;
+ }
+
+ _logger.LogInformation("Using templates. Old: {OldTemplate} | New: {NewTemplate}", Path.GetFileName(oldTemplatePath), Path.GetFileName(newTemplatePath));
+
+ var upgraderLogger = _loggerFactory.CreateLogger();
+ var upgrader = new ProjectUpgrader(upgraderLogger);
+ var result = upgrader.Upgrade(projectFile, oldTemplatePath, newTemplatePath, _createBackup);
+
+ if (result.Success)
+ {
+ _logger.LogInformation("Upgrade successful. Output: {Output}", result.OutputFilePath);
+ if (result.BackupCreated)
+ {
+ _logger.LogInformation("Backup saved at {Backup}", result.BackupPath);
+ }
+ _logger.LogInformation("Transferred: {Packages} package refs, {Projects} project refs, {Assemblies} assembly refs, {Props} custom properties.",
+ result.PackageReferencesFound, result.ProjectReferencesFound, result.AssemblyReferencesFound, result.CustomPropertiesFound);
+ succeeded++;
+ }
+ else
+ {
+ _logger.LogError("Upgrade failed for {File}: {Message}", projectFile, result.ErrorMessage);
+ if (result.Exception != null)
+ {
+ _logger.LogDebug(result.Exception, "Details for {File}", projectFile);
+ }
+ if (result.BackupCreated && !string.IsNullOrWhiteSpace(result.BackupPath))
+ {
+ _logger.LogWarning("Original file backed up at {Backup}", result.BackupPath);
+ }
+ failed++;
+ }
+ }
+
+ if (projectFiles.Count > 1)
+ {
+ _logger.LogInformation("Summary: processed {Processed}, successful {Succeeded}, failed {Failed}", processed, succeeded, failed);
+ }
+
+ return failed == 0 ? 0 : 1;
+ }
+
+ private List ResolveTargets(string targetPath)
+ {
+ var projectFiles = new List();
+
+ if (File.Exists(targetPath))
+ {
+ projectFiles.Add(targetPath);
+ return projectFiles;
+ }
+
+ if (Directory.Exists(targetPath))
+ {
+ projectFiles.AddRange(Directory.GetFiles(targetPath, "*.csproj", SearchOption.AllDirectories));
+ projectFiles.AddRange(Directory.GetFiles(targetPath, "*.cdsproj", SearchOption.AllDirectories));
+ }
+
+ return projectFiles;
+ }
+}
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/TALXIS-Plugin-Migration-README.md b/src/TALXIS.CLI.Workspace/Upgrade/TALXIS-Plugin-Migration-README.md
new file mode 100644
index 0000000..8478820
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/TALXIS-Plugin-Migration-README.md
@@ -0,0 +1,144 @@
+# TALXIS: Migrating Power Platform (Dataverse) plugins to the new project format
+
+This README describes the **standard TALXIS process** for migrating plugins from the old project format to the new one.
+
+TALXIS **templates** and TALXIS **build targets** automatically take care of building/packaging. Migration basically comes down to:
+- creating a new plugin project using a TALXIS template
+- moving the source code
+- referencing the plugin project from the solution project via `ProjectReference`
+- bringing the `PluginAssemblies` folder structure to the standard
+
+---
+
+## Goal
+
+Split the old structure (where plugin code could live in the same project/folder tree as the solution) into two projects:
+
+- **Plugin project** (plugins) — created using a **TALXIS `dotnet new` template**
+- **Solution project** (Dataverse solution) — built using **TALXIS build targets**
+
+After that, the projects are linked with a standard `ProjectReference`.
+
+---
+
+## Requirements
+
+- TALXIS.DevKit.SDK is installed
+- TALXIS templates (`dotnet new`) are installed
+- Plugin source code is available in the old structure
+- Power Platform Solution must already be upgraded to the TALXIS.DevKit.SDK format (the upgrade is performed using TALXIS.DevKit.Dataverse.Project.Upgrade.Tool)
+
+---
+
+## Step-by-step migration
+
+### 1) Create a new Plugin project using a TALXIS template
+
+Create a new plugin project with `dotnet new`:
+
+```console
+dotnet new pp-plugin `
+--output "src/Plugins.Project" `
+--PublisherName "tomas" `
+--SigningKeyFilePath "" `
+--Company "NETWORG" `
+--allow-scripts yes
+```
+
+**Result:**
+- `Plugins.Project.csproj` is created
+
+---
+
+### 2) Move the source code into the new Plugin project
+
+Move the source code from the old project into the new plugin project.
+
+**Move:**
+- plugin classes (`*.cs`)
+- helper/shared classes that were used within the same plugin assembly
+- required resources used by plugins (only if they are actually needed): `*.resx`, `*.json`, `*.xml`, etc.
+
+**Result:**
+- the new plugin project contains the full set of sources required to build the plugin assembly.
+
+---
+
+### 3) Reference the Plugin project from the Solution project via `ProjectReference`
+
+Open the solution project `Solution.csproj` (the one built by TALXIS build targets) and add a `ProjectReference`:
+
+```xml
+
+
+
+```
+
+**Result:**
+- the solution project sees the plugin project as a dependency.
+
+---
+
+### 4) Bring `PluginAssemblies` to the standard structure
+
+TALXIS expects the plugin assembly to be stored in a separate subfolder under `PluginAssemblies`.
+
+Required structure:
+
+```text
+PluginAssemblies\-\
+```
+
+Where:
+- `AssemblyName` = the plugin name
+- `AssemblyGuid` = the GUID id (in uppercase) of the plugin assembly
+Example:
+
+```text
+PluginAssemblies\Plugins.Project-571BCBFE-AAF4-4BE5-A8AE-424E878CBDEC\
+```
+
+#### What you need to do
+
+1. Under `PluginAssemblies`, create a subfolder `AssemblyName-AssemblyGuid`.
+2. If your plugin assembly files currently live directly under `PluginAssemblies\`, move them into the created subfolder.
+
+---
+
+### 5) Plugin Assembly behavior (TALXIS)
+
+TALXIS build targets support both scenarios:
+
+1. **Generate Plugin Assembly automatically** (if the assembly does not exist yet).
+2. **Update an existing Plugin Assembly** (if the assembly already exists).
+
+This means the folder:
+
+```text
+PluginAssemblies\-\
+```
+
+can be used in two ways:
+
+- **Empty folder**: TALXIS build targets will generate the required files/metadata during the build.
+- **Folder with files**: you can pre-copy the assembly from a previous version (old repo/branch/release) into this folder — TALXIS build targets will take it as a base and update it to the current state.
+
+---
+
+### 6) Verification
+
+1. Build the solution project using the standard `dotnet build` command.
+2. Verify that:
+ - the solution project has a `ProjectReference` to the plugin project
+ - `PluginAssemblies` contains a subfolder `AssemblyName-AssemblyGuid`
+ - plugin assembly files are located inside that subfolder
+ - TALXIS build targets generate or update the assembly as expected
+
+---
+
+## Summary
+
+- The **plugin project** is created using a TALXIS template → then plugin sources are moved into it.
+- The **solution project** references the plugin project via `ProjectReference`.
+- **PluginAssemblies** matches the standard: `PluginAssemblies\-\`.
+- The assembly can be generated automatically or copied from a previous version and then updated by TALXIS build targets.
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/TALXIS-ScriptLibrary-Migration-README.md b/src/TALXIS.CLI.Workspace/Upgrade/TALXIS-ScriptLibrary-Migration-README.md
new file mode 100644
index 0000000..1b1d118
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/TALXIS-ScriptLibrary-Migration-README.md
@@ -0,0 +1,96 @@
+# TALXIS: Migrate Script Library to the New Project Format
+
+This README describes the **standard TALXIS migration flow** for moving a Script Library from an old structure to the new TALXIS project format.
+
+TALXIS **templates** and TALXIS **build targets** handle build behavior automatically. The migration is mainly:
+- create a new ScriptLibrary project using the **pp-script-library** template
+- move the `TS` source folder into the new project
+- remove legacy configuration from old files
+- update names so build artifacts do **not** contain `publisherPrefix_` (prefix is added during build)
+- reference the ScriptLibrary project from the updated Solution project
+
+---
+
+## Prerequisites
+
+- Power Platform Solution must already be upgraded to **TALXIS.DevKit.SDK** format
+ (the upgrade is performed using **TALXIS.DevKit.Dataverse.Project.Upgrade.Tool**).
+- TALXIS .NET templates installed (`dotnet new`), including **pp-script-library**
+
+---
+
+## Goal
+
+Split the old layout into two projects:
+
+- **ScriptLibrary project** — created by TALXIS **pp-script-library** template and contains script sources (folder `TS`)
+- **Solution project** — upgraded to **TALXIS.DevKit.SDK** and built by TALXIS build targets
+
+Then connect them using a standard `ProjectReference`.
+
+---
+
+## Step-by-step migration
+
+### 1) Create a new ScriptLibrary project using pp-script-library template
+
+Create the project using `dotnet new`:
+
+```console
+dotnet new pp-script-library -n UI.Scripts --LibraryName main
+```
+
+**Result:**
+- A new ScriptLibrary project exists in the target folder
+- The project already follows the TALXIS standard (SDK/targets/structure)
+
+---
+
+### 2) Move script sources (folder `TS`) into the new project
+
+Move the **entire `TS` folder** from the old ScriptLibrary structure into the new ScriptLibrary project.
+
+> The `TS` folder is the only folder you need to migrate. All required build-related structure is already inside `TS`.
+
+**Result:**
+- The new ScriptLibrary project contains the script sources under `TS`.
+
+---
+
+### 3) Remove legacy configuration from old files
+
+After moving the `TS` folder, remove legacy/old-format configuration that belonged to the previous build pipeline.
+
+**Result:**
+- The new project is not tied to the old configuration and relies on TALXIS build targets.
+
+---
+
+### 4) Reference ScriptLibrary project from the upgraded Solution project
+
+Open the upgraded Solution project (`*.csproj` in **TALXIS.DevKit.SDK** format) and add a `ProjectReference`:
+
+```xml
+
+
+
+```
+
+**Result:**
+- The Solution project sees the ScriptLibrary project as a dependency.
+
+---
+
+### 5) Validation
+
+1. Build the Solution project using the standard repository command dotnrt build.
+2. Verify:
+ - the Solution project contains a `ProjectReference` to the ScriptLibrary project
+ - the ScriptLibrary project builds successfully as part of the Solution build
+
+---
+
+## Final structure (summary)
+
+- **ScriptLibrary project** is created via TALXIS **pp-script-library** template → `TS` folder is migrated into it.
+- **Solution project** is upgraded to **TALXIS.DevKit.SDK** and references the ScriptLibrary project via `ProjectReference`.
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template.cdsproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template.cdsproj
new file mode 100644
index 0000000..2610430
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template.cdsproj
@@ -0,0 +1,50 @@
+
+
+
+ $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps
+
+
+
+
+ d376201b-1c2a-422a-b50b-2979e16746fb
+ v4.6.2
+
+ net462
+ PackageReference
+ Declarations
+ FAE04EC0-301F-11D3-BF4B-00C04F79EFBC
+ FAE04EC0-301F-11D3-BF4B-00C04F79EFBC
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template.csproj
new file mode 100644
index 0000000..9e14d86
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template.csproj
@@ -0,0 +1,11 @@
+
+
+ net462
+ Solution
+
+
+
+
+
+
+
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template_talxis.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template_talxis.csproj
new file mode 100644
index 0000000..6ce84bc
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template_talxis.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net462
+ latest
+ PCT21016.Apps.Core.Model
+ PCT21016.Apps.Core.Model
+ false
+ 0.0.20000.0
+ 0.0.20000.0
+ 0.0.20000.0
+
+
+
+
+
+
+
+
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/PDPackage/template.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/PDPackage/template.csproj
new file mode 100644
index 0000000..ee3d11f
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/PDPackage/template.csproj
@@ -0,0 +1,20 @@
+
+
+ net472
+ Package.Main
+ Package.Main
+ Copyright © 2026
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/PDPackage/template_new.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/PDPackage/template_new.csproj
new file mode 100644
index 0000000..9b00912
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/PDPackage/template_new.csproj
@@ -0,0 +1,16 @@
+
+
+ net472
+
+
+ Copyright © 2026
+ PDPackage
+
+
+
+
+
+
+
+
+
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/Plugin/template.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/Plugin/template.csproj
new file mode 100644
index 0000000..c954b45
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/Plugin/template.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net462
+ $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps
+ 1.0.0.0
+ 1.0.0.0
+ {4C25E9B5-9FA6-436c-8E19-B395D2A65FAF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+
+
+
+
+
+
+
+ $(FileVersion)
+
+
+ This is a sample nuget package which contains a Dataverse plugin and its runtime dependencies like Newtonsoft.Json
+
+
+
+
+
+
+
+
+
+
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/Plugin/template_new.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/Plugin/template_new.csproj
new file mode 100644
index 0000000..f415704
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/Plugin/template_new.csproj
@@ -0,0 +1,22 @@
+
+
+ net462
+ true
+ 1.0.0.0
+ 1.0.0.0
+
+ Plugin
+
+
+
+
+ $(FileVersion)
+
+
+ This is a sample nuget package which contains a Dataverse plugin and its runtime dependencies like Newtonsoft.Json
+
+
+
+
+
+
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/ScriptLibrary/template.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/ScriptLibrary/template.csproj
new file mode 100644
index 0000000..7bcb8f3
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/ScriptLibrary/template.csproj
@@ -0,0 +1,28 @@
+
+
+ net462
+ latest
+ false
+ $(MSBuildProjectDirectory)\TS
+ $(TypeScriptDir)\build
+ true
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/ScriptLibrary/template_new.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/ScriptLibrary/template_new.csproj
new file mode 100644
index 0000000..0a183d6
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/ScriptLibrary/template_new.csproj
@@ -0,0 +1,12 @@
+
+
+
+ net462
+ latest
+ false
+ $(MSBuildProjectDirectory)\TS
+ $(TypeScriptDir)\build
+ true
+ ScriptLibrary
+
+
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Utilities/ProjectTypeDetector.cs b/src/TALXIS.CLI.Workspace/Upgrade/Utilities/ProjectTypeDetector.cs
new file mode 100644
index 0000000..a404cce
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Utilities/ProjectTypeDetector.cs
@@ -0,0 +1,255 @@
+using System.Xml.Linq;
+using TALXIS.CLI.Workspace.Upgrade.Models;
+
+namespace TALXIS.CLI.Workspace.Upgrade.Utilities;
+
+public class ProjectTypeDetector
+{
+ private static readonly XNamespace MsbuildNamespace = "http://schemas.microsoft.com/developer/msbuild/2003";
+
+ public ProjectType DetectProjectType(string filePath)
+ {
+ var extension = Path.GetExtension(filePath).ToLowerInvariant();
+
+ if(extension == ".cdsproj") return ProjectType.DataverseSolution;
+
+ if (extension == ".csproj")
+ {
+ return DetectFromContent(filePath);
+ }
+
+ return ProjectType.Unknown;
+ }
+
+ private ProjectType DetectFromContent(string filePath)
+ {
+ try
+ {
+ var xml = XDocument.Load(filePath);
+ var root = xml.Root;
+
+ if (root == null) return ProjectType.Unknown;
+
+ // Check for PDPackage first (most specific)
+ if (IsPDPackage(root))
+ {
+ return ProjectType.PDPackage;
+ }
+
+ // Check for Plugin
+ if (IsPlugin(root))
+ {
+ return ProjectType.Plugin;
+ }
+
+ // Check for Script Library
+ if (IsScriptLibrary(root))
+ {
+ return ProjectType.ScriptLibrary;
+ }
+
+ // Check for Dataverse Solution
+ if (IsDataverseSolution(root))
+ {
+ return ProjectType.DataverseSolution;
+ }
+
+ return ProjectType.Unknown;
+ }
+ catch
+ {
+ // If we can't parse, fall back to Unknown
+ return ProjectType.Unknown;
+ }
+ }
+
+ private bool IsDataverseSolution(XElement root)
+ {
+ // Get all PropertyGroup elements
+ var propertyGroups = root.Elements(MsbuildNamespace + "PropertyGroup")
+ .Concat(root.Elements("PropertyGroup"));
+
+ // Check for ProjectType = "Solution"
+ var projectType = propertyGroups
+ .Elements(MsbuildNamespace + "ProjectType")
+ .Concat(propertyGroups.Elements("ProjectType"))
+ .FirstOrDefault()?.Value;
+
+ if (projectType == "Solution")
+ return true;
+
+ // Check for SolutionRootPath property
+ var hasSolutionRootPath = propertyGroups
+ .Elements(MsbuildNamespace + "SolutionRootPath")
+ .Concat(propertyGroups.Elements("SolutionRootPath"))
+ .Any();
+
+ if (hasSolutionRootPath)
+ return true;
+
+ // Check for Publisher metadata
+ var hasPublisherName = propertyGroups
+ .Elements(MsbuildNamespace + "PublisherName")
+ .Concat(propertyGroups.Elements("PublisherName"))
+ .Any();
+
+ var hasPublisherPrefix = propertyGroups
+ .Elements(MsbuildNamespace + "PublisherPrefix")
+ .Concat(propertyGroups.Elements("PublisherPrefix"))
+ .Any();
+
+ if (hasPublisherName || hasPublisherPrefix)
+ return true;
+
+ // Check for PowerApps imports
+ var imports = root.Elements(MsbuildNamespace + "Import")
+ .Concat(root.Elements("Import"));
+
+ var hasPowerAppsImport = imports.Any(import =>
+ {
+ var project = import.Attribute("Project")?.Value ?? "";
+ return project.Contains("PowerApps", StringComparison.OrdinalIgnoreCase);
+ });
+
+ if (hasPowerAppsImport)
+ return true;
+
+ // Check for old TALXIS format package reference
+ var itemGroups = root.Elements(MsbuildNamespace + "ItemGroup")
+ .Concat(root.Elements("ItemGroup"));
+
+ var hasTalxisSdkReference = itemGroups
+ .Elements(MsbuildNamespace + "PackageReference")
+ .Concat(itemGroups.Elements("PackageReference"))
+ .Any(pkg =>
+ {
+ var include = pkg.Attribute("Include")?.Value ?? "";
+ return include.Contains("TALXIS.SDK.BuildTargets.CDS.Solution", StringComparison.OrdinalIgnoreCase);
+ });
+
+ if (hasTalxisSdkReference)
+ return true;
+
+ return false;
+ }
+
+ private bool IsPDPackage(XElement root)
+ {
+ var itemGroups = root.Elements(MsbuildNamespace + "ItemGroup")
+ .Concat(root.Elements("ItemGroup"));
+
+ var hasPDPackageReference = itemGroups
+ .Elements(MsbuildNamespace + "PackageReference")
+ .Concat(itemGroups.Elements("PackageReference"))
+ .Any(pkg =>
+ {
+ var include = pkg.Attribute("Include")?.Value ?? "";
+ return include.Contains("TALXIS.PowerApps.MSBuild.PDPackage", StringComparison.OrdinalIgnoreCase) ||
+ include.Contains("TALXIS.SDK.BuildTargets.CDS.Package", StringComparison.OrdinalIgnoreCase) ||
+ include.Contains("Microsoft.PowerApps.MSBuild.PDPackage", StringComparison.OrdinalIgnoreCase);
+ });
+
+ return hasPDPackageReference;
+ }
+
+ private bool IsPlugin(XElement root)
+ {
+ var itemGroups = root.Elements(MsbuildNamespace + "ItemGroup")
+ .Concat(root.Elements("ItemGroup"));
+
+ var hasPluginReference = itemGroups
+ .Elements(MsbuildNamespace + "PackageReference")
+ .Concat(itemGroups.Elements("PackageReference"))
+ .Any(pkg =>
+ {
+ var include = pkg.Attribute("Include")?.Value ?? "";
+ return include.Contains("Microsoft.PowerApps.MSBuild.Plugin", StringComparison.OrdinalIgnoreCase);
+ });
+
+ return hasPluginReference;
+ }
+
+ private bool IsScriptLibrary(XElement root)
+ {
+ var propertyGroups = root.Elements(MsbuildNamespace + "PropertyGroup")
+ .Concat(root.Elements("PropertyGroup"));
+
+ // Check for ProjectType = "ScriptLibrary" (new format)
+ var projectType = propertyGroups
+ .Elements(MsbuildNamespace + "ProjectType")
+ .Concat(propertyGroups.Elements("ProjectType"))
+ .FirstOrDefault()?.Value;
+
+ if (projectType == "ScriptLibrary")
+ return true;
+
+ // Check for TypeScript properties
+ var hasTypeScriptDir = propertyGroups
+ .Elements(MsbuildNamespace + "TypeScriptDir")
+ .Concat(propertyGroups.Elements("TypeScriptDir"))
+ .Any();
+
+ var hasTypeScriptBuildDir = propertyGroups
+ .Elements(MsbuildNamespace + "TypeScriptBuildDir")
+ .Concat(propertyGroups.Elements("TypeScriptBuildDir"))
+ .Any();
+
+ if (hasTypeScriptDir || hasTypeScriptBuildDir)
+ return true;
+
+ // Check for BuildTypeScript target
+ var targets = root.Elements(MsbuildNamespace + "Target")
+ .Concat(root.Elements("Target"));
+
+ var hasBuildTypeScriptTarget = targets.Any(target =>
+ {
+ var name = target.Attribute("Name")?.Value ?? "";
+ return name.Equals("BuildTypeScript", StringComparison.OrdinalIgnoreCase);
+ });
+
+ if (hasBuildTypeScriptTarget)
+ return true;
+
+ // Check for npm commands in Exec elements
+ var execs = targets
+ .Elements(MsbuildNamespace + "Exec")
+ .Concat(targets.Elements("Exec"));
+
+ var hasNpmCommand = execs.Any(exec =>
+ {
+ var command = exec.Attribute("Command")?.Value ?? "";
+ return command.Contains("npm", StringComparison.OrdinalIgnoreCase);
+ });
+
+ return hasNpmCommand;
+ }
+
+ public bool IsOldTalxisFormat(string filePath)
+ {
+ try
+ {
+ var xml = XDocument.Load(filePath);
+ var root = xml.Root;
+
+ if (root == null) return false;
+
+ var itemGroups = root.Elements(MsbuildNamespace + "ItemGroup")
+ .Concat(root.Elements("ItemGroup"));
+
+ var hasTalxisSdkReference = itemGroups
+ .Elements(MsbuildNamespace + "PackageReference")
+ .Concat(itemGroups.Elements("PackageReference"))
+ .Any(pkg =>
+ {
+ var include = pkg.Attribute("Include")?.Value ?? "";
+ return include.Contains("TALXIS.SDK.BuildTargets.CDS.Solution", StringComparison.OrdinalIgnoreCase);
+ });
+
+ return hasTalxisSdkReference;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Utilities/TemplateManager.cs b/src/TALXIS.CLI.Workspace/Upgrade/Utilities/TemplateManager.cs
new file mode 100644
index 0000000..5df1d0d
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/Upgrade/Utilities/TemplateManager.cs
@@ -0,0 +1,67 @@
+using TALXIS.CLI.Workspace.Upgrade.Models;
+
+namespace TALXIS.CLI.Workspace.Upgrade.Utilities;
+
+public class TemplateManager
+{
+ private readonly string _templatesBasePath;
+
+ public TemplateManager(string templateBasePath)
+ {
+ _templatesBasePath = templateBasePath;
+ }
+
+ public string GetOldFormatTemplatePath(ProjectType projectType, bool isOldTalxisFormat = false)
+ {
+ return projectType switch
+ {
+ ProjectType.DataverseSolution => isOldTalxisFormat
+ ? Path.Combine(_templatesBasePath, "DataverseSolution", "template_talxis.csproj")
+ : Path.Combine(_templatesBasePath, "DataverseSolution", "template.cdsproj"),
+ ProjectType.ScriptLibrary => Path.Combine(_templatesBasePath, "ScriptLibrary", "template.csproj"),
+ ProjectType.Plugin => Path.Combine(_templatesBasePath, "Plugin", "template.csproj"),
+ ProjectType.PDPackage => Path.Combine(_templatesBasePath, "PDPackage", "template.csproj"),
+ _ => throw new NotSupportedException($"Project type {projectType} is not supported")
+ };
+ }
+
+ public string GetNewFormatTemplatePath(ProjectType projectType)
+ {
+ return projectType switch
+ {
+ ProjectType.DataverseSolution => Path.Combine(_templatesBasePath, "DataverseSolution", "template.csproj"),
+ ProjectType.ScriptLibrary => Path.Combine(_templatesBasePath, "ScriptLibrary", "template_new.csproj"),
+ ProjectType.Plugin => Path.Combine(_templatesBasePath, "Plugin", "template_new.csproj"),
+ ProjectType.PDPackage => Path.Combine(_templatesBasePath, "PDPackage", "template_new.csproj"),
+ _ => throw new NotSupportedException($"Project type {projectType} is not supported")
+ };
+ }
+
+ public bool TemplatesExist(ProjectType projectType, bool isOldTalxisFormat = false)
+ {
+ try
+ {
+ var oldPath = GetOldFormatTemplatePath(projectType, isOldTalxisFormat);
+ var newPath = GetNewFormatTemplatePath(projectType);
+
+ return File.Exists(oldPath) && File.Exists(newPath);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public void ValidateTemplates(ProjectType projectType, bool isOldTalxisFormat = false)
+ {
+ if (!TemplatesExist(projectType, isOldTalxisFormat))
+ {
+ throw new FileNotFoundException(
+ $"Template files for {projectType} not found in {_templatesBasePath}. " +
+ $"Expected files:\n" +
+ $" - {GetOldFormatTemplatePath(projectType, isOldTalxisFormat)}\n" +
+ $" - {GetNewFormatTemplatePath(projectType)}"
+ );
+ }
+ }
+}
diff --git a/src/TALXIS.CLI.Workspace/UpgradeCliCommand.cs b/src/TALXIS.CLI.Workspace/UpgradeCliCommand.cs
new file mode 100644
index 0000000..7786259
--- /dev/null
+++ b/src/TALXIS.CLI.Workspace/UpgradeCliCommand.cs
@@ -0,0 +1,58 @@
+using DotMake.CommandLine;
+using Microsoft.Extensions.Logging;
+using System.IO;
+using TALXIS.CLI.Workspace.Upgrade;
+
+namespace TALXIS.CLI.Workspace;
+
+[CliCommand(
+ Description = "Upgrade project files to SDK-style format while preserving custom references and backups.",
+ Name = "upgrade",
+ Children = new[] { typeof(ProjectUpgradeCliCommand) })]
+public class UpgradeCliCommand
+{
+ [CliArgument(Name = "path", Description = "Path to a .csproj/.cdsproj file or directory to upgrade.")]
+ public required string TargetPath { get; set; }
+
+ [CliOption(Description = "Skip creating .backup files before rewriting.")]
+ public bool NoBackup { get; set; }
+
+ public int Run()
+ {
+ return RunInternal(TargetPath, NoBackup);
+ }
+
+ [CliCommand(
+ Description = "Upgrade project files (explicit project subcommand).",
+ Name = "project")]
+ public class ProjectUpgradeCliCommand
+ {
+ [CliArgument(Name = "path", Description = "Path to a .csproj/.cdsproj file or directory to upgrade.")]
+ public required string TargetPath { get; set; }
+
+ [CliOption(Description = "Skip creating .backup files before rewriting.")]
+ public bool NoBackup { get; set; }
+
+ public int Run()
+ {
+ return RunInternal(TargetPath, NoBackup);
+ }
+ }
+
+ private static int RunInternal(string targetPath, bool noBackup)
+ {
+ using var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.AddSimpleConsole(options =>
+ {
+ options.SingleLine = true;
+ options.TimestampFormat = "HH:mm:ss ";
+ });
+ builder.SetMinimumLevel(LogLevel.Information);
+ });
+
+ var templatesBasePath = System.IO.Path.Combine(AppContext.BaseDirectory, "Upgrade", "Templates");
+ var runner = new ProjectUpgradeRunner(loggerFactory, templatesBasePath, createBackup: !noBackup);
+ return runner.Run(targetPath);
+ }
+}
diff --git a/src/TALXIS.CLI.Workspace/WorkspaceCliCommand.cs b/src/TALXIS.CLI.Workspace/WorkspaceCliCommand.cs
index ad758e0..5ab9470 100644
--- a/src/TALXIS.CLI.Workspace/WorkspaceCliCommand.cs
+++ b/src/TALXIS.CLI.Workspace/WorkspaceCliCommand.cs
@@ -7,7 +7,8 @@ namespace TALXIS.CLI.Workspace;
Alias = "ws",
Children = new[]
{
- typeof(ComponentCliCommand)
+ typeof(ComponentCliCommand),
+ typeof(UpgradeCliCommand)
})]
public class WorkspaceCliCommand
{