Skip to content

Commit

Permalink
Merge pull request #560 from WildernessLabs/dominique-TemplateWizard
Browse files Browse the repository at this point in the history
meadow project * commands
  • Loading branch information
adrianstevens authored Sep 25, 2024
2 parents 9f323bd + 2acce33 commit 1edd91b
Show file tree
Hide file tree
Showing 6 changed files with 393 additions and 8 deletions.
41 changes: 40 additions & 1 deletion Source/v2/Meadow.CLI/Commands/Current/App/AppTools.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CliFx.Infrastructure;
using System.Diagnostics;
using CliFx.Infrastructure;
using Meadow.Hcom;
using Meadow.Package;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -143,4 +144,42 @@ internal static string SanitizeMeadowFilename(string fileName)

return meadowFileName!.Replace(Path.DirectorySeparatorChar, '/');
}

internal static async Task<int> RunProcessCommand(string command, string args, Action<string>? handleOutput = null, Action<string>? handleError = null, CancellationToken cancellationToken = default)
{
var processStartInfo = new ProcessStartInfo
{
FileName = command,
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

using (var process = new Process { StartInfo = processStartInfo })
{
process.Start();

var outputCompletion = ReadLinesAsync(process.StandardOutput, handleOutput, cancellationToken);
var errorCompletion = ReadLinesAsync(process.StandardError, handleError, cancellationToken);

await Task.WhenAll(outputCompletion, errorCompletion, process.WaitForExitAsync());

return process.ExitCode;
}
}

private static async Task ReadLinesAsync(StreamReader reader, Action<string>? handleLine, CancellationToken cancellationToken)
{
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync(cancellationToken);
if (!string.IsNullOrWhiteSpace(line)
&& handleLine != null)
{
handleLine(line);
}
}
}
}
14 changes: 14 additions & 0 deletions Source/v2/Meadow.CLI/Commands/Current/Project/MeadowTemplate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Meadow.CLI.Commands.Current.Project
{
internal class MeadowTemplate
{
public string Name;
public string ShortName;

public MeadowTemplate(string longName, string shortName)
{
this.Name = longName;
this.ShortName = shortName;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using CliFx.Attributes;
using Meadow.CLI.Commands.DeviceManagement;
using Microsoft.Extensions.Logging;
using Spectre.Console;

namespace Meadow.CLI.Commands.Current.Project
{
[Command("project install", Description = Strings.ProjectTemplates.InstallCommandDescription)]
public class ProjectInstallCommand : BaseCommand<ProjectInstallCommand>
{
public ProjectInstallCommand(ILoggerFactory loggerFactory)
: base(loggerFactory)
{
}

protected override async ValueTask ExecuteCommand()
{
AnsiConsole.MarkupLine(Strings.ProjectTemplates.InstallTitle);

var templateList = await ProjectNewCommand.GetInstalledTemplates(LoggerFactory, Console, CancellationToken);

if (templateList != null)
{
DisplayInstalledTemplates(templateList);
}
else
{
Logger?.LogError(Strings.ProjectTemplates.ErrorInstallingTemplates);
}
}

private void DisplayInstalledTemplates(List<string> templateList)
{
// Use regex to split each line into segments using two or more spaces as the separator
var regex = new Regex(@"\s{2,}");

var table = new Table();
// Add some columns
table.AddColumn(Strings.ProjectTemplates.ColumnTemplateName);
table.AddColumn(Strings.ProjectTemplates.ColumnLanguages);
foreach (var templatesLine in templateList)
{
// Isolate the long and shortnames, as well as languages
var segments = regex.Split(templatesLine.Trim());
if (segments.Length >= 2)
{
// Add Key Value of Long Name and Short Name
var longName = segments[0].Trim();
var languages = segments[2].Replace("[", string.Empty).Replace("]", string.Empty).Trim();
table.AddRow(longName, languages);
}
}
AnsiConsole.WriteLine(Strings.ProjectTemplates.Installed);

// Render the table to the console
AnsiConsole.Write(table);
}
}
}
240 changes: 240 additions & 0 deletions Source/v2/Meadow.CLI/Commands/Current/Project/ProjectNewCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using CliFx.Attributes;
using Meadow.CLI.Commands.DeviceManagement;
using Microsoft.Extensions.Logging;
using Spectre.Console;

namespace Meadow.CLI.Commands.Current.Project
{
[Command("project new", Description = Strings.ProjectTemplates.NewCommandDescription)]
public class ProjectNewCommand : BaseCommand<ProjectNewCommand>
{
[CommandOption('o', Description = Strings.ProjectTemplates.CommandOptionOutputPathDescription, IsRequired = false)]
public string? OutputPath { get; private set; } = null;

[CommandOption('l', Description = Strings.ProjectTemplates.CommandOptionSupportedLanguagesDescription, IsRequired = false)]
public string Language { get; private set; } = "C#";

public ProjectNewCommand(ILoggerFactory loggerFactory)
: base(loggerFactory)
{
}

protected override async ValueTask ExecuteCommand()
{
AnsiConsole.MarkupLine(Strings.ProjectTemplates.WizardTitle);

var templateList = await GetInstalledTemplates(LoggerFactory, Console, CancellationToken);

if (templateList != null)
{
// Ask some pertinent questions
var projectName = AnsiConsole.Ask<string>(Strings.ProjectTemplates.ProjectName);

if (string.IsNullOrWhiteSpace(OutputPath))
{
OutputPath = projectName;
}

var outputPathArgument = $"--output {OutputPath}";

List<MeadowTemplate> selectedTemplates = GatherTemplateInformationFromUsers(templateList);

if (selectedTemplates.Count > 0)
{
await GenerateProjectsAndSolutionsFromSelectedTemplates(projectName, outputPathArgument, selectedTemplates);
}
else
{
AnsiConsole.MarkupLine($"[yellow]{Strings.ProjectTemplates.NoTemplateSelected}[/]");
}
}
else
{
Logger?.LogError(Strings.ProjectTemplates.ErrorInstallingTemplates);
}
}

private List<MeadowTemplate> GatherTemplateInformationFromUsers(List<string> templateList)
{
var templateNames = new List<MeadowTemplate>();
MeadowTemplate? startKitGroup = null;
List<MeadowTemplate> startKitTemplates = new List<MeadowTemplate>();

startKitGroup = PopulateTemplateNameList(templateList, templateNames, startKitGroup, startKitTemplates);

var multiSelectionPrompt = new MultiSelectionPrompt<MeadowTemplate>()
.Title(Strings.ProjectTemplates.InstalledTemplates)
.PageSize(15)
.NotRequired() // Can be Blank to exit
.MoreChoicesText(string.Format($"[grey]{Strings.ProjectTemplates.MoreChoicesInstructions}[/]"))
.InstructionsText(string.Format($"[grey]{Strings.ProjectTemplates.Instructions}[/]", $"[blue]<{Strings.Space}>[/]", $"[green]<{Strings.Enter}>[/]"))
.UseConverter(x => x.Name);

// I wanted StartKit to appear 1st, if it exists
if (startKitGroup != null)
{
multiSelectionPrompt.AddChoiceGroup(startKitGroup, startKitTemplates);
}

multiSelectionPrompt.AddChoices(templateNames);

var selectedTemplates = AnsiConsole.Prompt(multiSelectionPrompt);
return selectedTemplates;
}

private async Task GenerateProjectsAndSolutionsFromSelectedTemplates(string projectName, string outputPathArgument, List<MeadowTemplate> selectedTemplates)
{
string generatedProjectName = projectName;

var generateSln = AnsiConsole.Confirm(Strings.ProjectTemplates.GenerateSln);

// Create the selected templates
foreach (var selectedTemplate in selectedTemplates)
{
AnsiConsole.MarkupLine($"[green]{Strings.ProjectTemplates.CreatingProject}[/]", selectedTemplate.Name);

var outputPath = string.Empty;
outputPath = Path.Combine(OutputPath!, $"{OutputPath}.{selectedTemplate.ShortName}");
outputPathArgument = "--output " + outputPath;
generatedProjectName = $"{projectName}.{selectedTemplate.ShortName}";

_ = await AppTools.RunProcessCommand("dotnet", $"new {selectedTemplate.ShortName} --name {generatedProjectName} {outputPathArgument} --language {Language} --force", cancellationToken: CancellationToken);
}

if (generateSln)
{
await GenerateSolution(projectName);
}

AnsiConsole.MarkupLine(Strings.ProjectTemplates.GenerationComplete, $"[green]{projectName}[/]");
}

private async Task GenerateSolution(string projectName)
{
AnsiConsole.MarkupLine($"[green]{Strings.ProjectTemplates.CreatingSln}[/]");

// Create the sln
_ = await AppTools.RunProcessCommand("dotnet", $"new sln -n {projectName} -o {OutputPath} --force", cancellationToken: CancellationToken);

//Now add to the new sln
var slnFilePath = Path.Combine(OutputPath!, projectName + ".sln");

string? searchWildCard;
switch (Language)
{
case "C#":
searchWildCard = "*.csproj";
break;
case "F#":
searchWildCard = "*.fsproj";
break;
case "VB":
searchWildCard = "*.vbproj";
break;
default:
searchWildCard = "*.csproj";
break;
}

// get all the project files and add them to the sln
var projectFiles = Directory.EnumerateFiles(OutputPath!, searchWildCard, SearchOption.AllDirectories);
foreach (var projectFile in projectFiles)
{
_ = await AppTools.RunProcessCommand("dotnet", $"sln {slnFilePath} add {projectFile}", cancellationToken: CancellationToken);
}

await OpenSolution(slnFilePath);
}

private MeadowTemplate? PopulateTemplateNameList(List<string> templateList, List<MeadowTemplate> templateNameList, MeadowTemplate? startKitGroup, List<MeadowTemplate> startKitTemplates)
{
// Use regex to split each line into segments using two or more spaces as the separator
var regexTemplateLines = new Regex(@"\s{2,}");

foreach (var templatesLine in templateList)
{
// Isolate the long and short names
var segments = regexTemplateLines.Split(templatesLine.Trim());
if (segments.Length >= 2)
{
// Add Key Value of Long Name and Short Name
var longName = segments[0].Trim();
var shortName = segments[1].Trim();
var languages = segments[2].Replace("[", string.Empty).Replace("]", string.Empty).Trim();

templateNameList.Add(new MeadowTemplate($"{longName} ({languages})", shortName));
}
}

return startKitGroup;
}

internal static async Task<List<string>?> GetInstalledTemplates(ILoggerFactory loggerFactory, CliFx.Infrastructure.IConsole console, CancellationToken cancellationToken)
{
var templateTable = new List<string>();

// Get the list of Meadow project templates
var exitCode = await AppTools.RunProcessCommand("dotnet", "new list Meadow", handleOutput: outputLogLine =>
{
// Ignore empty output
if (!string.IsNullOrWhiteSpace(outputLogLine))
{
templateTable.Add(outputLogLine);
}
}, cancellationToken: cancellationToken);


if (exitCode == 0)
{
if (templateTable.Count == 0)
{
AnsiConsole.MarkupLine($"[yellow]{Strings.ProjectTemplates.NoTemplatesFound}[/]");

// Let's install the templates then
var projectInstallCommand = new ProjectInstallCommand(loggerFactory);
await projectInstallCommand.ExecuteAsync(console);

// Try to populate the templateTable again, after installing the templates
exitCode = await AppTools.RunProcessCommand("dotnet", "new list Meadow", handleOutput: outputLogLine =>
{
// Ignore empty output
if (!string.IsNullOrWhiteSpace(outputLogLine))
{
templateTable.Add(outputLogLine);
}
}, cancellationToken: cancellationToken);
}

// Extract template names from the output
var templateNameList = templateTable
.Skip(4) // Skip the header information
.Where(line => !string.IsNullOrWhiteSpace(line)) // Avoid empty lines
.Select(line => line.Trim()) // Clean whitespace
.ToList();

return templateNameList;
}

return null;
}

private async Task OpenSolution(string solutionPath)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var exitCode = await AppTools.RunProcessCommand("cmd", $"/c start {solutionPath}");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var exitCode = await AppTools.RunProcessCommand("code", Path.GetDirectoryName(solutionPath));
}
else
{
Logger?.LogError(Strings.UnsupportedOperatingSystem);
}
}
}
}
2 changes: 1 addition & 1 deletion Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public async ValueTask ExecuteAsync(IConsole console)
{
if (MeadowTelemetry.Current.ShouldAskForConsent)
{
AnsiConsole.MarkupLine(Strings.Telemetry.ConsentMessage);
AnsiConsole.MarkupLine(Strings.Telemetry.ConsentMessage, "[bold]meadow telemetry [[enable|disable]][/]", $"[bold]{MeadowTelemetry.TelemetryEnvironmentVariable}[/]");

var result = AnsiConsole.Confirm(Strings.Telemetry.AskToParticipate, defaultValue: true);
MeadowTelemetry.Current.SetTelemetryEnabled(result);
Expand Down
Loading

0 comments on commit 1edd91b

Please sign in to comment.