Skip to content
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
5 changes: 5 additions & 0 deletions .netconfig
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,8 @@

etag = ec1645067cc2319c2ce3304900c260eb8ec700d50b6d8d62285914a6c96e01f9
weak
[file "src/dotnet-meai/Sponsors/SponsorManifest.cs"]
url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/SponsorManifest.cs
sha = a755e4be0f7cb73cfde208857e28f7cfeba2dcc3
etag = 55ef89e8441156541c1c74a50675b7f56633b56493031f0ffa877460839e3536
weak
14 changes: 14 additions & 0 deletions Extensions.AI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AI", "src\AI\AI.csproj", "{
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AI.Tests", "src\AI.Tests\AI.Tests.csproj", "{3553B2FB-B06C-4766-93FD-1B7004761080}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-meai", "src\dotnet-meai\dotnet-meai.csproj", "{58FC11CF-7D61-48B9-B95F-0EAA889D6DED}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -41,6 +43,18 @@ Global
{3553B2FB-B06C-4766-93FD-1B7004761080}.Release|x64.Build.0 = Release|Any CPU
{3553B2FB-B06C-4766-93FD-1B7004761080}.Release|x86.ActiveCfg = Release|Any CPU
{3553B2FB-B06C-4766-93FD-1B7004761080}.Release|x86.Build.0 = Release|Any CPU
{58FC11CF-7D61-48B9-B95F-0EAA889D6DED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{58FC11CF-7D61-48B9-B95F-0EAA889D6DED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{58FC11CF-7D61-48B9-B95F-0EAA889D6DED}.Debug|x64.ActiveCfg = Debug|Any CPU
{58FC11CF-7D61-48B9-B95F-0EAA889D6DED}.Debug|x64.Build.0 = Debug|Any CPU
{58FC11CF-7D61-48B9-B95F-0EAA889D6DED}.Debug|x86.ActiveCfg = Debug|Any CPU
{58FC11CF-7D61-48B9-B95F-0EAA889D6DED}.Debug|x86.Build.0 = Debug|Any CPU
{58FC11CF-7D61-48B9-B95F-0EAA889D6DED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{58FC11CF-7D61-48B9-B95F-0EAA889D6DED}.Release|Any CPU.Build.0 = Release|Any CPU
{58FC11CF-7D61-48B9-B95F-0EAA889D6DED}.Release|x64.ActiveCfg = Release|Any CPU
{58FC11CF-7D61-48B9-B95F-0EAA889D6DED}.Release|x64.Build.0 = Release|Any CPU
{58FC11CF-7D61-48B9-B95F-0EAA889D6DED}.Release|x86.ActiveCfg = Release|Any CPU
{58FC11CF-7D61-48B9-B95F-0EAA889D6DED}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
2 changes: 2 additions & 0 deletions src/Directory.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<RootNamespace>Devlooped.Extensions.AI</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>6eb457f9-16bc-49c5-81f2-33399b254e04</UserSecretsId>

<RestoreSources>https://api.nuget.org/v3/index.json;https://pkg.kzu.app/index.json</RestoreSources>
</PropertyGroup>

</Project>
4 changes: 0 additions & 4 deletions src/Directory.targets
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
<Project>

<PropertyGroup>

</PropertyGroup>

</Project>
72 changes: 72 additions & 0 deletions src/dotnet-meai/App.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Diagnostics.CodeAnalysis;
using Devlooped.Sponsors;
using GitCredentialManager;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console;
using Spectre.Console.Cli;

namespace Devlooped.Extensions.AI;

public static class App
{
static readonly CancellationTokenSource shutdownCancellation = new();

static App() => Console.CancelKeyPress += (s, e) => shutdownCancellation.Cancel();

/// <summary>
/// Whether the CLI app is not interactive (i.e. part of a script run,
/// running in CI, or in a non-interactive user session).
/// </summary>
public static bool IsNonInteractive => !Environment.UserInteractive
|| Console.IsInputRedirected
|| Console.IsOutputRedirected;

public static CommandApp Create() => Create(out _);

public static CommandApp Create([NotNull] out ITypeRegistrar registrar)
=> Create(new ServiceCollection(), out registrar);

public static CommandApp Create(IAnsiConsole console) => Create(console, out _);

public static CommandApp Create(IAnsiConsole console, [NotNull] out ITypeRegistrar registrar)
{
#pragma warning disable DDI001
var app = Create(new ServiceCollection().AddSingleton(console), out registrar);
#pragma warning restore DDI001
app.Configure(config => config.ConfigureConsole(console));
return app;
}

static CommandApp Create(IServiceCollection collection, [NotNull] out ITypeRegistrar registrar)
{
var config = new ConfigurationBuilder()
.AddEnvironmentVariables()
#if DEBUG
.AddUserSecrets<TypeRegistrar>()
#endif
.Build();

collection.AddSingleton(config)
.AddSingleton<IConfiguration>(_ => config)
.AddSingleton(_ => CredentialManager.Create(ThisAssembly.Project.PackageId));

Check warning on line 52 in src/dotnet-meai/App.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

The type 'ThisAssembly' in '/home/runner/work/Extensions.AI/Extensions.AI/src/dotnet-meai/obj/Release/net8.0/ThisAssembly.Constants/ThisAssembly.ConstantsGenerator/Project.CI.g.cs' conflicts with the imported type 'ThisAssembly' in 'Devlooped.Sponsors.Commands, Version=42.42.1585.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in '/home/runner/work/Extensions.AI/Extensions.AI/src/dotnet-meai/obj/Release/net8.0/ThisAssembly.Constants/ThisAssembly.ConstantsGenerator/Project.CI.g.cs'.

collection.AddSingleton(shutdownCancellation);
collection.AddServices();
collection.ConfigureSponsors();

registrar = new TypeRegistrar(collection);
var app = new CommandApp(registrar);

app.Configure(config =>
{
// Allows emitting help markdown on build
if (Environment.GetEnvironmentVariables().Contains("NO_COLOR"))
config.Settings.HelpProviderStyles = null;
});

app.UseSponsors();

return app;
}
}
108 changes: 108 additions & 0 deletions src/dotnet-meai/AppExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using DotNetConfig;
using NuGet.Configuration;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
using Spectre.Console;
using Spectre.Console.Cli;

namespace Devlooped.Extensions.AI;

static class AppExtensions
{
/// <summary>
/// Runs the app and update check in paralell, and renders any update messages if available
/// after the command app completes.
/// </summary>
public static async Task<int> RunWithUpdatesAsync(this ICommandApp app, string[] args)
{
var updates = Task.Run(() => GetUpdatesAsync(args));
var exit = await app.RunAsync(args);

if (await updates is { Length: > 0 } messages)
{
foreach (var message in messages)
AnsiConsole.MarkupLine(message);
}

return exit;
}

/// <summary>
/// Checks for updates to the tool and shows them if available.
/// </summary>
public static async Task ShowUpdatesAsync(this ICommandApp app, string[] args)
{
if (await GetUpdatesAsync(args, true) is { Length: > 0 } messages)
{
foreach (var message in messages)
AnsiConsole.MarkupLine(message);
}
}

/// <summary>
/// Shows the app version, build date and release link.
/// </summary>
/// <param name="app"></param>
public static void ShowVersion(this ICommandApp app)
{
AnsiConsole.MarkupLine($"{ThisAssembly.Project.ToolCommandName} version [lime]{ThisAssembly.Project.Version}[/] ({ThisAssembly.Project.BuildDate})");
AnsiConsole.MarkupLine($"[link]{ThisAssembly.Git.Url}/releases/tag/{ThisAssembly.Project.BuildRef}[/]");
}

static async Task<string[]> GetUpdatesAsync(string[] args, bool forced = false)
{
if (args.Contains("-u") || args.Contains("--unattended"))
return [];

var config = Config.Build(ConfigLevel.Global).GetSection(ThisAssembly.Project.ToolCommandName);

// Check once a day max
if (!forced)
{
var lastCheck = config.GetDateTime("checked") ?? DateTime.UtcNow.AddDays(-2);
// if it's been > 24 hours since the last check, we'll check again
if (lastCheck > DateTime.UtcNow.AddDays(-1))
return [];
}

// We check from a different feed in this case.
var civersion = ThisAssembly.Project.VersionPrefix.StartsWith("42.42.");

var providers = Repository.Provider.GetCoreV3();
var repository = new SourceRepository(new PackageSource(
// use CI feed rather than production feed depending on which version we're using
civersion && !string.IsNullOrEmpty(ThisAssembly.Project.SLEET_FEED_URL) ?
ThisAssembly.Project.SLEET_FEED_URL :
"https://api.nuget.org/v3/index.json"), providers);
var resource = await repository.GetResourceAsync<PackageMetadataResource>();
var localVersion = new NuGetVersion(ThisAssembly.Project.Version);
// Only update to stable versions, not pre-releases
var metadata = await resource.GetMetadataAsync(ThisAssembly.Project.PackageId, includePrerelease: false, false,
new SourceCacheContext
{
NoCache = true,
RefreshMemoryCache = true,
},
NuGet.Common.NullLogger.Instance, CancellationToken.None);

var update = metadata
.Select(x => x.Identity)
.Where(x => x.Version > localVersion)
.OrderByDescending(x => x.Version)
.Select(x => x.Version)
.FirstOrDefault();

config.SetDateTime("checked", DateTime.UtcNow);

if (update != null)
{
return [
$"There is a new version of [yellow]{ThisAssembly.Project.PackageId}[/]: [dim]v{localVersion.ToNormalizedString()}[/] -> [lime]v{update.ToNormalizedString()}[/]",
$"Update with: [yellow]dotnet[/] tool update -g {ThisAssembly.Project.PackageId}" +
(civersion ? $" --source {ThisAssembly.Project.SLEET_FEED_URL}" : ""),
];
}

return [];
}
}
93 changes: 93 additions & 0 deletions src/dotnet-meai/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using Devlooped.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console.Cli;

// Some users reported not getting emoji on Windows, so we force UTF-8 encoding.
// This not great, but I couldn't find a better way to do it.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8;

args = [.. ExpandResponseFiles(args)];

// Alias -? to -h for help
if (args.Contains("-?"))
args = [.. args.Select(x => x == "-?" ? "-h" : x)];

if (args.Contains("--debug"))
{
Debugger.Launch();
args = [.. args.Where(x => x != "--debug")];
}

var app = App.Create(out var registrar);

#if DEBUG
app.Configure(c => c.PropagateExceptions());
#else
if (args.Contains("--exceptions"))
{
app.Configure(c => c.PropagateExceptions());
args = [.. args.Where(x => x != "--exceptions")];
}
#endif

app.Configure(config => config.SetApplicationName(ThisAssembly.Project.ToolCommandName));

Check warning on line 37 in src/dotnet-meai/Program.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

The type 'ThisAssembly' in '/home/runner/work/Extensions.AI/Extensions.AI/src/dotnet-meai/obj/Release/net8.0/ThisAssembly.Constants/ThisAssembly.ConstantsGenerator/Project.CI.g.cs' conflicts with the imported type 'ThisAssembly' in 'Devlooped.Sponsors.Commands, Version=42.42.1585.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in '/home/runner/work/Extensions.AI/Extensions.AI/src/dotnet-meai/obj/Release/net8.0/ThisAssembly.Constants/ThisAssembly.ConstantsGenerator/Project.CI.g.cs'.

if (args.Contains("--version"))
{
app.ShowVersion();
await app.ShowUpdatesAsync(args);
return 0;
}

var result = 0;

#if DEBUG
result = await app.RunAsync(args);
#else
result = await app.RunWithUpdatesAsync(args);
#endif

// --quiet does not report sponsor tier on every run.
await ((IServiceProvider)registrar).GetRequiredService<Devlooped.Sponsors.CheckCommand>().ExecuteAsync(
new CommandContext(["--quiet"], RemainingArguments.Empty, "check", null),
new Devlooped.Sponsors.CheckCommand.CheckSettings { Quiet = true });

return result;

static IEnumerable<string> ExpandResponseFiles(IEnumerable<string> args)
{
foreach (var arg in args)
{
if (arg.StartsWith('@'))
{
var filePath = arg[1..];

if (!File.Exists(filePath))
throw new FileNotFoundException($"Response file not found: {filePath}");

foreach (var line in File.ReadAllLines(filePath))
{
yield return line;
}
}
else
{
yield return arg;
}
}
}

class RemainingArguments : IRemainingArguments
{
public static IRemainingArguments Empty { get; } = new RemainingArguments();

RemainingArguments() { }

public ILookup<string, string?> Parsed => Enumerable.Empty<string>().ToLookup(x => x, x => default(string));

public IReadOnlyList<string> Raw => [];
}
1 change: 1 addition & 0 deletions src/dotnet-meai/Properties/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
launchSettings.json
12 changes: 12 additions & 0 deletions src/dotnet-meai/Sponsors/App.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Devlooped.Sponsors;

public static class App
{
/// <summary>
/// Whether the CLI app is not interactive (i.e. part of a script run,
/// running in CI, or in a non-interactive user session).
/// </summary>
public static bool IsNonInteractive => !Environment.UserInteractive
|| Console.IsInputRedirected
|| Console.IsOutputRedirected;
}
Loading
Loading