Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

meadow project * commands #560

Merged
merged 2 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading