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
1 change: 0 additions & 1 deletion funURL.CLI.Tests/UrlOperationsTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using funURL.CLI.Core;
using LanguageExt;

namespace funURL.CLI.Tests;

Expand Down
8 changes: 2 additions & 6 deletions funURL.CLI/Commands/DecodeCommand.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.CommandLine;
using funURL.CLI.Core;
using LanguageExt;
using static LanguageExt.Prelude;
using funURL.CLI.Functional;

namespace funURL.CLI.Commands;

Expand All @@ -11,10 +10,7 @@ namespace funURL.CLI.Commands;
public class DecodeCommand : Command
{
private readonly Argument<string> inputArgument = new("input") { Description = "String to decode" };
private readonly System.CommandLine.Option<bool> queryOption = new("--query", "-c")
{
Description = "Decode as query component (handles + as spaces)",
};
private readonly Option<bool> queryOption = new("--query", "-c") { Description = "Decode as query component (handles + as spaces)" };

private DecodeCommand()
: base("decode", "URL-decode a string")
Expand Down
3 changes: 1 addition & 2 deletions funURL.CLI/Commands/DedupeCommand.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using System.CommandLine;
using System.Text.RegularExpressions;
using funURL.CLI.Core;
using LanguageExt;
using static LanguageExt.Prelude;
using funURL.CLI.Functional;

namespace funURL.CLI.Commands;

Expand Down
8 changes: 2 additions & 6 deletions funURL.CLI/Commands/EncodeCommand.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.CommandLine;
using funURL.CLI.Core;
using LanguageExt;
using static LanguageExt.Prelude;
using funURL.CLI.Functional;

namespace funURL.CLI.Commands;

Expand All @@ -11,10 +10,7 @@ namespace funURL.CLI.Commands;
public class EncodeCommand : Command
{
private readonly Argument<string> inputArgument = new("input") { Description = "String to encode" };
private readonly System.CommandLine.Option<bool> queryOption = new("--query", "-c")
{
Description = "Encode as query component (uses + for spaces)",
};
private readonly Option<bool> queryOption = new("--query", "-c") { Description = "Encode as query component (uses + for spaces)" };

private EncodeCommand()
: base("encode", "URL-encode a string")
Expand Down
11 changes: 5 additions & 6 deletions funURL.CLI/Commands/ModifyCommand.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.CommandLine;
using funURL.CLI.Core;
using LanguageExt;
using static LanguageExt.Prelude;
using funURL.CLI.Functional;

namespace funURL.CLI.Commands;

Expand All @@ -11,10 +10,10 @@ namespace funURL.CLI.Commands;
public class ModifyCommand : Command
{
private readonly Argument<string> urlArgument = new("url") { Description = "URL to modify" };
private readonly System.CommandLine.Option<string?> protocolOption = new("--protocol", "-c") { Description = "Change protocol/scheme" };
private readonly System.CommandLine.Option<string?> pathOption = new("--path", "-p") { Description = "Update path" };
private readonly System.CommandLine.Option<string?> queryOption = new("--query", "-q") { Description = "Change query string" };
private readonly System.CommandLine.Option<string?> fragmentOption = new("--fragment", "-f") { Description = "Update fragment" };
private readonly Option<string?> protocolOption = new("--protocol", "-c") { Description = "Change protocol/scheme" };
private readonly Option<string?> pathOption = new("--path", "-p") { Description = "Update path" };
private readonly Option<string?> queryOption = new("--query", "-q") { Description = "Change query string" };
private readonly Option<string?> fragmentOption = new("--fragment", "-f") { Description = "Update fragment" };

private ModifyCommand()
: base("modify", "Modify components of a URL")
Expand Down
17 changes: 8 additions & 9 deletions funURL.CLI/Commands/ParseCommand.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.CommandLine;
using funURL.CLI.Core;
using LanguageExt;
using static LanguageExt.Prelude;
using funURL.CLI.Functional;

namespace funURL.CLI.Commands;

Expand All @@ -11,13 +10,13 @@ namespace funURL.CLI.Commands;
public class ParseCommand : Command
{
private readonly Argument<string> urlArgument = new("url") { Description = "URL to parse" };
private readonly System.CommandLine.Option<bool> protocolOption = new("--protocol", "-c") { Description = "Extract protocol/scheme" };
private readonly System.CommandLine.Option<bool> subdomainOption = new("--subdomain", "-s") { Description = "Extract subdomain" };
private readonly System.CommandLine.Option<bool> tldOption = new("--tld", "-t") { Description = "Extract top-level domain" };
private readonly System.CommandLine.Option<bool> hostnameOption = new("--hostname", "-n") { Description = "Extract hostname" };
private readonly System.CommandLine.Option<bool> pathOption = new("--path", "-p") { Description = "Extract path" };
private readonly System.CommandLine.Option<bool> queryOption = new("--query", "-q") { Description = "Extract query parameters" };
private readonly System.CommandLine.Option<bool> fragmentOption = new("--fragment", "-f") { Description = "Extract fragment" };
private readonly Option<bool> protocolOption = new("--protocol", "-c") { Description = "Extract protocol/scheme" };
private readonly Option<bool> subdomainOption = new("--subdomain", "-s") { Description = "Extract subdomain" };
private readonly Option<bool> tldOption = new("--tld", "-t") { Description = "Extract top-level domain" };
private readonly Option<bool> hostnameOption = new("--hostname", "-n") { Description = "Extract hostname" };
private readonly Option<bool> pathOption = new("--path", "-p") { Description = "Extract path" };
private readonly Option<bool> queryOption = new("--query", "-q") { Description = "Extract query parameters" };
private readonly Option<bool> fragmentOption = new("--fragment", "-f") { Description = "Extract fragment" };

private ParseCommand()
: base("parse", "Parse and extract components from a URL")
Expand Down
37 changes: 37 additions & 0 deletions funURL.CLI/Commands/RootCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.CommandLine;
using Figgle.Fonts;

namespace funURL.CLI.Commands;

/// <summary>
/// The root command for the funURL CLI.
/// </summary>
public class RootCommand : System.CommandLine.RootCommand
{
private readonly Option<bool> silentOption = new("--silent", "-s") { Description = "Suppress the startup banner", Recursive = true };

private RootCommand()
: base("funURL - A Functional URL Swiss Army Knife")
{
Options.Add(silentOption);
Subcommands.Add(ParseCommand.Create());
Subcommands.Add(ModifyCommand.Create());
Subcommands.Add(EncodeCommand.Create());
Subcommands.Add(DecodeCommand.Create());
Subcommands.Add(DedupeCommand.Create());
}

public async Task<ParseResult> Parse(IReadOnlyList<string> args, CancellationToken cancellationToken)
{
var parseResult = base.Parse(args, null);

if (!parseResult.GetValue(silentOption))
{
await Console.Out.WriteAsync(FiggleFonts.Standard.Render("funURL").AsMemory(), cancellationToken);
}
Comment on lines +28 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fire-and-forget WriteAsync — banner may not appear, exceptions are silently swallowed.

Console.Out.WriteAsync(...) returns a ValueTask that is never awaited. Because Parse is a synchronous method, this is a fire-and-forget: the banner write races against the command's own output (typically lost), and any IOException from the write becomes an unobserved task exception. Use the synchronous overload instead.

🐛 Proposed fix
-        Console.Out.WriteAsync(FiggleFonts.Standard.Render("funURL").AsMemory(), cancellationToken);
+        Console.Out.Write(FiggleFonts.Standard.Render("funURL"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@funURL.CLI/Commands/RootCommand.cs` around lines 28 - 31, The banner write is
using fire-and-forget Console.Out.WriteAsync in RootCommand.cs inside the
parseResult.GetValue(silentOption) check which can lose output and swallow IO
exceptions; replace the async call with the synchronous
Console.Out.Write/WriteLine overload that takes the rendered banner string (use
FiggleFonts.Standard.Render("funURL")) so the write happens inline and
exceptions propagate, and remove the unused cancellationToken from that call.


return parseResult;
}

public static RootCommand Create() => new();
}
4 changes: 1 addition & 3 deletions funURL.CLI/Core/UrlOperations.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using System.Net;
using LanguageExt;
using LanguageExt.Common;
using static LanguageExt.Prelude;
using funURL.CLI.Functional;

namespace funURL.CLI.Core;

Expand Down
8 changes: 8 additions & 0 deletions funURL.CLI/Functional/Error.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace funURL.CLI.Functional;

internal sealed class Error(string message)
{
public string Message { get; } = message;

public static Error New(string message) => new(message);
}
39 changes: 39 additions & 0 deletions funURL.CLI/Functional/Fin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace funURL.CLI.Functional;

internal abstract class Fin<T>
{
public abstract bool IsSucc { get; }
public bool IsFail => !IsSucc;

public static Fin<T> Succ(T value) => new SuccCase(value);

public static implicit operator Fin<T>(Error error) => new FailCase(error);

public abstract Fin<TResult> Map<TResult>(Func<T, TResult> mapper);

public abstract Task Match(Func<T, Task> Succ, Func<Error, Task> Fail);

public abstract T ThrowIfFail();

private sealed class SuccCase(T value) : Fin<T>
{
public override bool IsSucc => true;

public override Fin<TResult> Map<TResult>(Func<T, TResult> mapper) => Fin<TResult>.Succ(mapper(value));

public override Task Match(Func<T, Task> Succ, Func<Error, Task> Fail) => Succ(value);

public override T ThrowIfFail() => value;
}

private sealed class FailCase(Error error) : Fin<T>
{
public override bool IsSucc => false;

public override Fin<TResult> Map<TResult>(Func<T, TResult> mapper) => error;

public override Task Match(Func<T, Task> Succ, Func<Error, Task> Fail) => Fail(error);

public override T ThrowIfFail() => throw new InvalidOperationException(error.Message);
}
}
17 changes: 6 additions & 11 deletions funURL.CLI/Program.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
using System.CommandLine;
using funURL.CLI.Commands;
using funURL.CLI.Commands;

using var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;

Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
cancellationTokenSource.Cancel();
};

var rootCommand = new RootCommand("funURL - A Functional URL Swiss Army Knife 🛠️")
{
ParseCommand.Create(),
ModifyCommand.Create(),
EncodeCommand.Create(),
DecodeCommand.Create(),
DedupeCommand.Create(),
};
var rootCommand = RootCommand.Create();

var parseResult = await rootCommand.Parse(args, cancellationToken);

return await rootCommand.Parse(args).InvokeAsync(cancellationToken: cancellationTokenSource.Token);
return await parseResult.InvokeAsync(cancellationToken: cancellationToken);
3 changes: 2 additions & 1 deletion funURL.CLI/funURL.CLI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@

<ItemGroup>
<InternalsVisibleTo Include="funURL.CLI.Tests" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Figgle" Version="0.6.5" />
<PackageReference Include="Figgle.Fonts" Version="0.6.5" />
Comment on lines +33 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find and examine the project file
find . -name "funURL.CLI.csproj" -type f | head -5

Repository: HappyHackingSpace/funURL

Length of output: 98


🏁 Script executed:

# Query NuGet for Figgle and Figgle.Generator information
curl -s "https://api.nuget.org/v3/registration5-semver1/figgle/index.json" \
  | python3 -c "
import json, sys
try:
    data = json.load(sys.stdin)
    pages = data.get('items', [])
    if pages:
        latest = pages[-1].get('items', [])[-1] if pages[-1].get('items') else {}
        entry = latest.get('catalogEntry', {})
        print('Figgle latest:', entry.get('version', 'N/A'))
        print('Listed:', entry.get('listed', 'N/A'))
except:
    print('Figgle: Error parsing')
"

Repository: HappyHackingSpace/funURL

Length of output: 101


🏁 Script executed:

# Check for Figgle.Generator
curl -s "https://api.nuget.org/v3/registration5-semver1/figgle.generator/index.json" \
  | python3 -c "
import json, sys
try:
    data = json.load(sys.stdin)
    pages = data.get('items', [])
    versions = []
    for page in pages:
        for item in page.get('items', []):
            v = item.get('catalogEntry', {}).get('version', '')
            if v:
                versions.append(v)
    if versions:
        print('Figgle.Generator exists: YES')
        print('Latest versions:', versions[-3:])
    else:
        print('Figgle.Generator: NOT FOUND on NuGet')
except Exception as e:
    print(f'Figgle.Generator: Error - {e}')
"

Repository: HappyHackingSpace/funURL

Length of output: 141


🏁 Script executed:

# Check for Figgle.Fonts
curl -s "https://api.nuget.org/v3/registration5-semver1/figgle.fonts/index.json" \
  | python3 -c "
import json, sys
try:
    data = json.load(sys.stdin)
    pages = data.get('items', [])
    if pages:
        latest = pages[-1].get('items', [])[-1] if pages[-1].get('items') else {}
        entry = latest.get('catalogEntry', {})
        print('Figgle.Fonts latest:', entry.get('version', 'N/A'))
except:
    print('Figgle.Fonts: Error parsing')
"

Repository: HappyHackingSpace/funURL

Length of output: 94


🏁 Script executed:

# Read the csproj file to verify PublishAot and PublishTrimmed settings
cat funURL.CLI/funURL.CLI.csproj

Repository: HappyHackingSpace/funURL

Length of output: 1826


🏁 Script executed:

# Get detailed metadata for Figgle 0.6.5 to check for AOT compatibility
curl -s "https://api.nuget.org/v3/registration5-semver1/figgle/0.6.5.json" \
  | python3 -c "
import json, sys
try:
    data = json.load(sys.stdin)
    entry = data.get('catalogEntry', {})
    print('Figgle 0.6.5 Details:')
    print('  Version:', entry.get('version'))
    print('  Tags:', entry.get('tags', 'N/A'))
    print('  Description:', entry.get('description', 'N/A')[:200] if entry.get('description') else 'N/A')
    # Check for AOT indicator (may not exist in standard metadata)
    print('  IsAotCompatible:', entry.get('isAotCompatible', 'Not explicitly set'))
except Exception as e:
    print(f'Error: {e}')
"

Repository: HappyHackingSpace/funURL

Length of output: 132


🏁 Script executed:

# Get Figgle.Generator metadata to confirm source-generation capability
curl -s "https://api.nuget.org/v3/registration5-semver1/figgle.generator/0.6.5.json" \
  | python3 -c "
import json, sys
try:
    data = json.load(sys.stdin)
    entry = data.get('catalogEntry', {})
    print('Figgle.Generator 0.6.5 Details:')
    print('  Version:', entry.get('version'))
    print('  Tags:', entry.get('tags', 'N/A'))
    print('  Description:', entry.get('description', 'N/A')[:300] if entry.get('description') else 'N/A')
    print('  DependencyGroups:', entry.get('dependencyGroups', []))
except Exception as e:
    print(f'Error: {e}')
"

Repository: HappyHackingSpace/funURL

Length of output: 142


🏁 Script executed:

# Search online documentation or GitHub for Figgle AOT compatibility info
# Look for mentions of Figgle.Generator as an AOT-friendly alternative
curl -s "https://api.github.com/repos/drewnoakes/figgle/contents/README.md" \
  | python3 -c "
import json, sys, base64
try:
    data = json.load(sys.stdin)
    if 'content' in data:
        content = base64.b64decode(data['content']).decode('utf-8')
        # Look for mentions of Generator or AOT
        lines = content.split('\n')
        for i, line in enumerate(lines):
            if 'generator' in line.lower() or 'aot' in line.lower():
                print(f'Line {i}: {line}')
except:
    print('Could not fetch README')
"

Repository: HappyHackingSpace/funURL

Length of output: 1160


Consider using Figgle.Generator source generator instead of Figgle + Figgle.Fonts.

This project publishes with <PublishAot>true</PublishAot> and <PublishTrimmed>true</PublishTrimmed>, but Figgle targets .NET Standard 2.0 and is not AOT-annotated. Unvalidated third-party dependencies can silently break AOT guarantees even when no warnings are reported at publish time.

Figgle.Generator (available on NuGet at the same version 0.6.5) is a source generator that, for statically-known text like "funURL", embeds the rendered text directly into the assembly at compile time. This eliminates the runtime font-loading entirely and removes the need to ship the ~500KB Figgle.Fonts package. Per the official documentation, "if all Figgle text is rendered this way, you don't have to ship any Figgle assembly with your app."

Replace both Figgle and Figgle.Fonts with Figgle.Generator and use the [StaticFiggle] attribute to render the text at compile time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@funURL.CLI/funURL.CLI.csproj` around lines 33 - 34, Replace the runtime
Figgle packages with the Figgle source generator to avoid AOT/runtime font
loading: remove the PackageReference entries for "Figgle" and "Figgle.Fonts" and
add a PackageReference for "Figgle.Generator" (v0.6.5); then update any places
that call Figgle rendering at runtime to use the [StaticFiggle] attribute so
text like "funURL" is rendered at compile time; ensure project still has
<PublishAot>true</PublishAot> and <PublishTrimmed>true</PublishTrimmed> and
verify no runtime Figgle assembly is required after the change.

<PackageReference Include="System.CommandLine" Version="2.0.3" />
</ItemGroup>

Expand Down