22 Commits

Author SHA1 Message Date
2d4fc2c345 Add musl build for ci 2024-03-02 00:12:33 +01:00
6d39621196 Dev container versioning 2024-03-01 23:05:39 +00:00
20a49c4f94 Process command aliases 2024-03-01 23:43:13 +01:00
7351bf4fd1 Dockerfile upgrades for dotnet8 2024-03-01 23:39:31 +01:00
89f1c37cf6 Link exclusion, readme exclusion 2024-03-01 23:29:43 +01:00
f1b2495fb6 Readme command 2024-03-01 22:59:40 +01:00
8b9702b935 Diag command help 2024-03-01 22:44:30 +01:00
9027f28065 Add -h --help aliases for help 2024-03-01 22:39:02 +01:00
4f0e7262ce Csproj upgrade to net8 2024-03-01 22:18:20 +01:00
986696b7d0 Multiple devcontainer 2024-03-01 22:18:07 +01:00
11598c220e Add LiteDB 2023-08-30 12:13:56 +02:00
954d26f4eb Sort diag commands 2023-07-17 18:21:28 +02:00
d7043e1107 Add docker instructions to README 2023-07-17 17:46:38 +02:00
43c03c4f97 Fix linux module names 2023-07-17 17:46:19 +02:00
5f495c8390 Run from docker 2023-07-17 17:24:25 +02:00
82f5a990f3 Formatting 2023-03-09 21:26:56 +01:00
1f4427644c Windows install information 2022-12-23 16:16:25 +01:00
f5f01dd100 InfoCommand repo & module info 2022-12-23 13:59:07 +01:00
a47f9b0826 Move PathHelper to service, ConigureCommand, %DOCUMENTS% 2022-12-23 11:54:20 +01:00
acbae0d18f 📝 Readme Linux installation 2022-12-19 20:50:29 +01:00
68b00169d1 Remove envvar and file content logging 2022-12-19 20:33:12 +01:00
ca28c26366 Remove "Test" from release name 2022-12-19 20:30:45 +01:00
43 changed files with 913 additions and 125 deletions

View File

@@ -0,0 +1,3 @@
{
"image": "mcr.microsoft.com/dotnet/sdk:8.0-alpine"
}

View File

@@ -0,0 +1,3 @@
{
"image": "mcr.microsoft.com/dotnet/sdk:8.0"
}

View File

@@ -1,3 +0,0 @@
{
"image": "mcr.microsoft.com/dotnet/sdk:7.0-alpine"
}

2
.editorconfig Normal file
View File

@@ -0,0 +1,2 @@
[*.cs]

View File

@@ -21,12 +21,17 @@ jobs:
name: linux name: linux
outputname: Alma outputname: Alma
version_script: .scripts/versioning.sh version_script: .scripts/versioning.sh
- runtime: linux-musl-x64
runner: ubuntu-latest
name: linux-musl
outputname: Alma
version_script: .scripts/versioning.sh
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup dotnet - name: Setup dotnet
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '7.0.x' dotnet-version: '8.0.x'
- name: Patch version - name: Patch version
run: ${{ matrix.target.version_script }} run: ${{ matrix.target.version_script }}
continue-on-error: true continue-on-error: true
@@ -53,6 +58,11 @@ jobs:
with: with:
name: alma-linux name: alma-linux
path: app/linux/ path: app/linux/
- uses: actions/download-artifact@v3
name: Download Linux musl artifacts
with:
name: alma-linux
path: app/linux-musl/
- name: Create release directory - name: Create release directory
run: mkdir release run: mkdir release
@@ -63,10 +73,12 @@ jobs:
- name: Copy linux executable - name: Copy linux executable
run: cp app/linux/Alma release/alma-linux run: cp app/linux/Alma release/alma-linux
- name: Copy linux-musl executable
run: cp app/linux-musl/Alma release/alma-linux-musl
- uses: "marvinpinto/action-automatic-releases@latest" - uses: "marvinpinto/action-automatic-releases@latest"
name: Create release name: Create release
with: with:
title: Test
repo_token: "${{ secrets.GITHUB_TOKEN }}" repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: latest automatic_release_tag: latest
prerelease: false prerelease: false

View File

@@ -1,5 +1,3 @@
gci env:\
if((Get-Content env:\GITHUB_REF_TYPE) -ne "tag") if((Get-Content env:\GITHUB_REF_TYPE) -ne "tag")
{ {
Return Return
@@ -12,4 +10,3 @@ Write-Host $version
Write-Host $git_sha Write-Host $git_sha
(Get-Content src\Alma\Alma.csproj).Replace("0.0.0", $version).Replace("development", $git_sha) | Set-Content src\Alma\Alma.csproj (Get-Content src\Alma\Alma.csproj).Replace("0.0.0", $version).Replace("development", $git_sha) | Set-Content src\Alma\Alma.csproj
Write-Host (Get-Content src\Alma\Alma.csproj)

View File

@@ -1,5 +1,3 @@
printenv
if [ ${GITHUB_REF_TYPE} != "tag" ]; then if [ ${GITHUB_REF_TYPE} != "tag" ]; then
exit 1 exit 1
fi fi
@@ -13,4 +11,3 @@ echo $version
echo $git_hash echo $git_hash
sed -i "s/0.0.0/$version/g;s/development/$git_hash/g" src/Alma/Alma.csproj sed -i "s/0.0.0/$version/g;s/development/$git_hash/g" src/Alma/Alma.csproj
cat src/Alma/Alma.csproj

25
Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine as BUILD
RUN apk add -U clang zlib-dev
WORKDIR /build
COPY . .
RUN dotnet publish src/Alma/Alma.csproj -c Release -r linux-musl-x64 -p:PublishAot=true -o /app
FROM alpine:edge as FINAL
RUN mkdir /data
WORKDIR /data
ENV ALMA_APP_DATA_FALLBACK=/appdata
ENV ALMA_CONFIG_FALLBACK=/appconfig
RUN mkdir /appdata
RUN mkdir /appconfig
RUN apk add -U icu-libs libstdc++
COPY --from=BUILD /app/Alma /alma
ENTRYPOINT ["/alma"]
CMD ["help"]

View File

@@ -0,0 +1,32 @@
# Alma
Alma (aka Advanced Link Manager Application) is another dotfiles management tool.
## Installation
**Linux**
```
sudo wget https://github.com/ADIX7/Alma/releases/download/latest/alma-linux -O /usr/local/bin/alma
sudo chmod +x /usr/local/bin/alma
```
**Windows**
This PowerShell command will download the alma.exe to the current folder. Move it to a folder that is in PATH.
```
Invoke-WebRequest https://github.com/ADIX7/Alma/releases/download/latest/Alma.exe -OutFile alma.exe
```
**As a Docker tool**
You can run it with Docker/Podman. You should mount every directory (source and target too) to the same path as they are on the host.
For example, if you have your dotfiles cloned to your home folder and you have your repository.json in ~/.config/alma/repository.json, then you can run this command and the links will be correct on the host.
```
docker run --rm -it -v /home/myuser:/home/myuser -e "HOME=/home/myuser" adix7/alma:latest ...
```
If you don't have repository.json, you can set the WORKDIR (or ALMA_WORKDIR) env var to your repository. For example, if you cloned your dotfiles to ~/dotfiles, you can use this command.
```
docker run --rm -it -v /home/myuser:/home/myuser -e "WORKDIR=/home/myuser/dotfiles" adix7/alma:latest ...
```

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>Alma</RootNamespace> <RootNamespace>Alma</RootNamespace>

View File

@@ -2,6 +2,7 @@ namespace Alma.Command;
public interface ICommand public interface ICommand
{ {
public string CommandString { get; } string CommandString { get; }
public Task Run(List<string> parameters); string[] CommandAliases { get; }
Task Run(List<string> parameters);
} }

View File

@@ -4,14 +4,18 @@ public class ModuleConfiguration
{ {
public string? Target { get; set; } public string? Target { get; set; }
public Dictionary<string, string>? Links { get; set; } public Dictionary<string, string>? Links { get; set; }
public List<string>? Exclude { get; set; }
public bool ExcludeReadme { get; set; } = true;
public string? Install { get; set; } public string? Install { get; set; }
public string? Configure { get; set; }
public ModuleConfiguration(string? target, Dictionary<string, string>? links, string? install) public ModuleConfiguration(string? target, Dictionary<string, string>? links, string? install, string? configure)
{ {
Target = target; Target = target;
Links = links; Links = links;
Install = install; Install = install;
Configure = configure;
} }
public ModuleConfiguration Merge(ModuleConfiguration merge) public ModuleConfiguration Merge(ModuleConfiguration merge)
@@ -21,10 +25,11 @@ public class ModuleConfiguration
return new ModuleConfiguration( return new ModuleConfiguration(
merge.Target ?? Target, merge.Target ?? Target,
new Dictionary<string, string>(mergedLinks), new Dictionary<string, string>(mergedLinks),
merge.Install ?? Install merge.Install ?? Install,
merge.Configure ?? Configure
); );
} }
public static ModuleConfiguration Empty() => public static ModuleConfiguration Empty() =>
new(null, new Dictionary<string, string>(), null); new(null, new Dictionary<string, string>(), null, null);
} }

View File

@@ -0,0 +1,8 @@
namespace Alma.Data;
public static class ColorCodes
{
public const string Reset = "\u001b[0m";
public const string RedForeground = "\u001b[38;5;1m";
public const string GreenForeground = "\u001b[38;5;2m";
}

View File

@@ -0,0 +1,5 @@
using Alma.Configuration.Module;
namespace Alma.Data;
public record ModuleConfigurationWithName(string Name, ModuleConfiguration Configuration);

View File

@@ -0,0 +1,3 @@
namespace Alma.Data;
public record SpecialPathResolver(string PathName, Func<string> Resolver, bool? SkipCombiningCurrentDirectory = null);

View File

@@ -0,0 +1,6 @@
namespace Alma.Services;
public interface IPathHelperService
{
string ResolvePath(string path, string? currentDirectory = null);
}

View File

@@ -6,11 +6,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="LiteDB" Version="5.0.19" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" /> <PackageReference Include="System.Linq.Async" Version="6.0.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>Alma</RootNamespace> <RootNamespace>Alma</RootNamespace>

View File

@@ -1,5 +1,4 @@
using Alma.Command; using Alma.Command;
using Alma.Command.Help;
using Alma.Logging; using Alma.Logging;
namespace Alma; namespace Alma;
@@ -25,7 +24,7 @@ public class Application
var commandString = args[0]; var commandString = args[0];
var command = _commands.FirstOrDefault(c => c.CommandString == commandString); var command = _commands.FirstOrDefault(c => c.CommandString == commandString || c.CommandAliases.Contains(commandString));
if (command is null) if (command is null)
{ {

View File

@@ -0,0 +1,63 @@
using Alma.Command.Install;
using Alma.Configuration.Repository;
using Alma.Logging;
using Alma.Services;
namespace Alma.Command.Configure;
public class ConfigureCommand : RepositoryModuleCommandBase
{
private readonly ILogger<InstallCommand> _logger;
private readonly IShellService _shellService;
public override string CommandString => "configure";
public override string[] CommandAliases => Array.Empty<string>();
public ConfigureCommand(
ILogger<InstallCommand> logger,
IRepositoryConfiguration repositoryConfiguration,
IModuleConfigurationResolver moduleConfigurationResolver,
IShellService shellService,
IPathHelperService pathHelperService)
: base(repositoryConfiguration, pathHelperService, moduleConfigurationResolver)
{
_logger = logger;
_shellService = shellService;
}
public override async Task Run(List<string> parameters)
{
var (repoName, moduleName) = GetRepositoryAndModuleName(parameters);
if (moduleName is null)
{
_logger.LogInformation("No module specified");
return;
}
var (moduleConfiguration, _) = await GetModuleConfiguration(repoName, moduleName);
if (moduleConfiguration is null)
{
_logger.LogInformation($"No module configuration found for module '{moduleName}'{(repoName is null ? "" : $" in repository '{repoName}'")}");
return;
}
var configureLines = moduleConfiguration.Configure?.Split(Environment.NewLine);
if (configureLines is null)
{
_logger.LogInformation("No configure command is found");
return;
}
_logger.LogInformation($"Configure command: {string.Join("\n", configureLines)}");
if (configureLines.Length == 1)
{
_logger.LogInformation("Running configure command '" + configureLines[0] + "'");
await _shellService.RunCommandAsync(configureLines[0]);
}
else
{
_logger.LogError("Multi line scripts are not currently supported");
}
}
}

View File

@@ -0,0 +1,131 @@
using System.Reflection;
using Alma.Logging;
namespace Alma.Command.Diag;
public class DiagCommand : ICommand
{
private readonly ILogger<DiagCommand> _logger;
public string CommandString => "diag";
public string[] CommandAliases => Array.Empty<string>();
private readonly Lazy<IReadOnlyList<MethodInfo>> _diagnosticHelpersLazy;
public DiagCommand(ILogger<DiagCommand> logger)
{
_logger = logger;
_diagnosticHelpersLazy = new Lazy<IReadOnlyList<MethodInfo>>(FindDiagnosticHelpers);
}
public Task Run(List<string> parameters)
{
if (parameters.Count < 1)
{
_logger.LogInformation("No diagnostic helper specified.");
return Task.CompletedTask;
}
var command = parameters[0];
if(command == "--help")
{
_logger.LogInformation(
"""
Usage:
diag list
diag <diag-helper>
"""
);
return Task.CompletedTask;
}
if (command == "list")
{
ListDiagnosticHelpers();
return Task.CompletedTask;
}
var diagnosticHelpers = _diagnosticHelpersLazy.Value;
var helper = diagnosticHelpers.FirstOrDefault(
h => GetDiagnosticHelper(h) is { } attr && attr.Command == command
);
if (helper is null)
{
_logger.LogInformation($"Diagnostic helper {command} is not found.");
return Task.CompletedTask;
}
if (!helper.IsStatic)
{
_logger.LogInformation($"Diagnostic helper {helper.Name} is not static.");
return Task.CompletedTask;
}
HandleHelper(helper, parameters.Skip(1));
return Task.CompletedTask;
}
private void ListDiagnosticHelpers()
{
var diagnosticHelpers = _diagnosticHelpersLazy.Value;
var commands = diagnosticHelpers
.Select(h => GetDiagnosticHelper(h)?.Command)
.OfType<string>()
.Order()
.ToList();
_logger.LogInformation("Available diagnostic helpers:");
foreach (var command in commands)
{
_logger.LogInformation(command);
}
}
private void HandleHelper(MethodInfo helper, IEnumerable<string> parameters)
{
var helperParameters = helper.GetParameters();
var helperArguments = new object[helperParameters.Length];
for (var i = 0; i < helperParameters.Length; i++)
{
var parameterType = helperParameters[i].ParameterType;
if (parameterType == typeof(IEnumerable<string>))
{
helperArguments[i] = parameters;
}
else if (parameterType == typeof(ILogger))
{
helperArguments[i] = _logger;
}
else
{
_logger.LogInformation($"Diagnostic helper {helper.Name} has wrong parameter type, could not resolve '{parameterType}'.");
return;
}
}
helper.Invoke(null, helperArguments);
}
private IReadOnlyList<MethodInfo> FindDiagnosticHelpers()
{
var assemblies = new[]
{
Assembly.GetExecutingAssembly(),
};
return assemblies.SelectMany(
a =>
a
.GetTypes()
.SelectMany(t => t.GetMethods())
.Where(t => t.GetCustomAttributes(typeof(DiagnosticHelper)).Any()))
.ToList()
.AsReadOnly();
}
private DiagnosticHelper? GetDiagnosticHelper(MethodInfo o)
=> o.GetCustomAttributes(typeof(DiagnosticHelper)).FirstOrDefault() as DiagnosticHelper;
}

View File

@@ -0,0 +1,12 @@
namespace Alma.Command.Diag;
[AttributeUsage(AttributeTargets.Method)]
public class DiagnosticHelper : Attribute
{
public string Command { get; }
public DiagnosticHelper(string command)
{
Command = command;
}
}

View File

@@ -9,6 +9,8 @@ public class HelpCommand : ICommand
public string CommandString => "help"; public string CommandString => "help";
public string[] CommandAliases { get; } = ["--help", "-h"];
public HelpCommand( public HelpCommand(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ILogger<HelpCommand> logger ILogger<HelpCommand> logger

View File

@@ -1,39 +1,73 @@
using Alma.Configuration.Module;
using Alma.Configuration.Repository; using Alma.Configuration.Repository;
using Alma.Helper; using Alma.Data;
using Alma.Logging; using Alma.Logging;
using Alma.Services; using Alma.Services;
namespace Alma.Command.Info; namespace Alma.Command.Info;
public class InfoCommand : ICommand public class InfoCommand : RepositoryModuleCommandBase
{ {
public string CommandString => "info"; public override string CommandString => "info";
public override string[] CommandAliases => Array.Empty<string>();
private readonly IFolderService _folderService; private readonly IFolderService _folderService;
private readonly IRepositoryConfiguration _repositoryConfiguration; private readonly IRepositoryConfiguration _repositoryConfiguration;
private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
private readonly ILogger<InfoCommand> _logger; private readonly ILogger<InfoCommand> _logger;
private readonly IOsInformation _osInformation; private readonly IOsInformation _osInformation;
private readonly IVersionService _versionService; private readonly IVersionService _versionService;
private readonly IPathHelperService _pathHelperService;
private static readonly List<Func<ModuleConfiguration, string?>> _moduleInfoDetailResolvers = new()
{
(m) =>
{
var linkCount = m.Links?.Count ?? 0;
return linkCount.ToString().PadLeft(3) + $" link{(linkCount > 1 ? "s" : "")}".PadRight(6);
},
(m) => m.Install is not null ? "[Install]" : null,
(m) => m.Configure is not null ? "[Configure]" : null,
};
public InfoCommand( public InfoCommand(
IFolderService folderService, IFolderService folderService,
IRepositoryConfiguration repositoryConfiguration, IRepositoryConfiguration repositoryConfiguration,
IModuleConfigurationResolver moduleConfigurationResolver,
ILogger<InfoCommand> logger, ILogger<InfoCommand> logger,
IOsInformation osInformation, IOsInformation osInformation,
IVersionService versionService IVersionService versionService,
) IPathHelperService pathHelperService
) : base(repositoryConfiguration, pathHelperService, moduleConfigurationResolver)
{ {
_folderService = folderService; _folderService = folderService;
_repositoryConfiguration = repositoryConfiguration; _repositoryConfiguration = repositoryConfiguration;
_moduleConfigurationResolver = moduleConfigurationResolver;
_logger = logger; _logger = logger;
_osInformation = osInformation; _osInformation = osInformation;
_versionService = versionService; _versionService = versionService;
_pathHelperService = pathHelperService;
} }
public async Task Run(List<string> parameters) public override async Task Run(List<string> parameters)
{
var (repoName, moduleName) = GetRepositoryAndModuleName(parameters, true);
if (repoName is not null && moduleName is null)
{
await ProcessRepoInfoAsync(repoName);
}
else if (repoName is not null && moduleName is not null)
{
await ProcessModuleInfoAsync(repoName, moduleName);
}
else
{
await ProcessGeneralInfoAsync();
}
}
private async Task ProcessGeneralInfoAsync()
{ {
//Add info REPO
//Add info REPO MODULE
_logger.LogInformation("Alma " + _versionService.GetVersion()); _logger.LogInformation("Alma " + _versionService.GetVersion());
_logger.LogInformation(""); _logger.LogInformation("");
@@ -59,10 +93,11 @@ public class InfoCommand : ICommand
foreach (var repository in repositories) foreach (var repository in repositories)
{ {
Console.Write(repository.Name); Console.Write(repository.Name);
if (repository.RepositoryPath is not null && !Directory.Exists(PathHelper.ResolvePath(repository.RepositoryPath))) if (repository.RepositoryPath is not null && !Directory.Exists(_pathHelperService.ResolvePath(repository.RepositoryPath)))
{ {
Console.Write($" (containing folder not exists {repository.RepositoryPath})"); Console.Write($" (containing folder not exists {repository.RepositoryPath})");
} }
_logger.LogInformation(""); _logger.LogInformation("");
} }
} }
@@ -71,4 +106,111 @@ public class InfoCommand : ICommand
_logger.LogInformation("No repositories found"); _logger.LogInformation("No repositories found");
} }
} }
private async Task ProcessRepoInfoAsync(string repoName)
{
var (repoSourceDirectory, _) = GetRepositorySourceAndTargetDirectory(repoName);
var repoRoot = new DirectoryInfo(repoSourceDirectory);
var modules = (await TraverseRepoFolder(repoRoot, repoRoot)).OrderBy(e => e.Name).ToList();
var maxNameLength = modules.Max(m => m.Name.Length);
_logger.LogInformation($"Repository '{repoName}' contains {modules.Count} modules:");
_logger.LogInformation("");
foreach (var module in modules)
{
var moduleDetails = _moduleInfoDetailResolvers
.Select(m => m(module.Configuration))
.Where(m => m is not null)
.ToList();
_logger.LogInformation($"{module.Name.PadRight(maxNameLength + 3)} {string.Join(" ", moduleDetails)}");
}
}
async Task<IEnumerable<ModuleConfigurationWithName>> TraverseRepoFolder(DirectoryInfo repoRoot, DirectoryInfo currentDirectory)
{
var modulesFound = Enumerable.Empty<ModuleConfigurationWithName>();
var moduleConfigFileStub = Path.Combine(currentDirectory.FullName, Constants.ModuleConfigFileStub);
var (moduleConfig, _) = await _moduleConfigurationResolver.ResolveModuleConfiguration(moduleConfigFileStub);
if (moduleConfig is not null)
{
var moduleName = currentDirectory.FullName[(repoRoot.FullName.TrimEnd(Path.DirectorySeparatorChar).Length + 1)..].Replace(Path.DirectorySeparatorChar, '/');
modulesFound = modulesFound.Append(new(moduleName, moduleConfig));
}
foreach (var subDir in currentDirectory.GetDirectories())
{
modulesFound = modulesFound.Concat(await TraverseRepoFolder(repoRoot, subDir));
}
return modulesFound;
}
async Task ProcessModuleInfoAsync(string repoName, string moduleName)
{
var (moduleConfiguration, moduleConfigFileName) = await GetModuleConfiguration(repoName, moduleName);
if (moduleConfiguration is null)
{
_logger.LogInformation($"No configuration is found for module '{moduleName}' in repository '{repoName}':");
return;
}
_logger.LogInformation($"Information about module '{moduleName}' in repository '{repoName}':");
_logger.LogInformation("");
var moduleTargetPath = moduleConfiguration.Target is not null
? _pathHelperService.ResolvePath(moduleConfiguration.Target)
: null;
if (moduleTargetPath is not null)
{
_logger.LogInformation($"Target directory is: {moduleTargetPath}");
_logger.LogInformation("");
}
if (moduleConfiguration.Install is not null)
{
_logger.LogInformation("Can be installed.");
}
if (moduleConfiguration.Configure is not null)
{
_logger.LogInformation("Can be configured.");
}
if (moduleConfiguration.Links is { } links && links.Count != 0)
{
var linkCount = links.Count;
_logger.LogInformation("");
_logger.LogInformation($"Has {linkCount} link{(linkCount > 1 ? "s" : "")}:");
_logger.LogInformation("");
foreach (var link in links)
{
var sourcePath = Path.Combine(new FileInfo(moduleConfigFileName!).Directory!.FullName, link.Key);
var sourceExists = File.Exists(sourcePath) || Directory.Exists(sourcePath);
var sourceColor = sourceExists ? ColorCodes.GreenForeground : ColorCodes.RedForeground;
var targetColor = ColorCodes.RedForeground;
if (moduleTargetPath is not null)
{
var targetPath = Path.Combine(moduleTargetPath, link.Key);
var targetExists = File.Exists(targetPath) || Directory.Exists(targetPath);
targetColor = targetExists ? ColorCodes.GreenForeground : ColorCodes.RedForeground;
}
_logger.LogInformation($"{sourceColor}{link.Key}{ColorCodes.Reset} -> {targetColor}{link.Value}");
}
}
else
{
_logger.LogInformation("Has no links.");
}
}
} }

View File

@@ -1,6 +1,4 @@
using System.Diagnostics;
using Alma.Configuration.Repository; using Alma.Configuration.Repository;
using Alma.Data;
using Alma.Logging; using Alma.Logging;
using Alma.Services; using Alma.Services;
@@ -9,19 +7,19 @@ namespace Alma.Command.Install;
public class InstallCommand : RepositoryModuleCommandBase public class InstallCommand : RepositoryModuleCommandBase
{ {
private readonly ILogger<InstallCommand> _logger; private readonly ILogger<InstallCommand> _logger;
private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
private readonly IShellService _shellService; private readonly IShellService _shellService;
public override string CommandString => "install"; public override string CommandString => "install";
public override string[] CommandAliases => Array.Empty<string>();
public InstallCommand( public InstallCommand(
ILogger<InstallCommand> logger, ILogger<InstallCommand> logger,
IRepositoryConfiguration repositoryConfiguration, IRepositoryConfiguration repositoryConfiguration,
IModuleConfigurationResolver moduleConfigurationResolver, IModuleConfigurationResolver moduleConfigurationResolver,
IShellService shellService) IShellService shellService,
: base(repositoryConfiguration) IPathHelperService pathHelperService)
: base(repositoryConfiguration, pathHelperService, moduleConfigurationResolver)
{ {
_logger = logger; _logger = logger;
_moduleConfigurationResolver = moduleConfigurationResolver;
_shellService = shellService; _shellService = shellService;
} }
@@ -33,17 +31,7 @@ public class InstallCommand : RepositoryModuleCommandBase
_logger.LogInformation("No module specified"); _logger.LogInformation("No module specified");
return; return;
} }
var (moduleConfiguration, _) = await GetModuleConfiguration(repoName, moduleName);
string sourceDirectory = Path.Combine(Environment.CurrentDirectory);
string targetDirectory = Path.Combine(Environment.CurrentDirectory, "..");
string moduleNameAsPath = moduleName.Replace('/', Path.DirectorySeparatorChar);
(sourceDirectory, _) = GetModuleSourceAndTargetDirectory(repoName, sourceDirectory, targetDirectory);
string moduleDirectory = Path.Combine(sourceDirectory, moduleNameAsPath);
var moduleConfigFileStub = Path.Combine(moduleDirectory, Constants.ModuleConfigFileStub);
var (moduleConfiguration, moduleConfigurationFile) = await _moduleConfigurationResolver.ResolveModuleConfiguration(moduleConfigFileStub);
if (moduleConfiguration is null) if (moduleConfiguration is null)
{ {

View File

@@ -2,8 +2,8 @@ using System.Runtime.InteropServices;
using Alma.Configuration.Module; using Alma.Configuration.Module;
using Alma.Configuration.Repository; using Alma.Configuration.Repository;
using Alma.Data; using Alma.Data;
using Alma.Helper;
using Alma.Logging; using Alma.Logging;
using Alma.Models;
using Alma.Services; using Alma.Services;
namespace Alma.Command.Link; namespace Alma.Command.Link;
@@ -13,25 +13,47 @@ public class LinkCommand : RepositoryModuleCommandBase
private readonly IRepositoryConfiguration _repositoryConfiguration; private readonly IRepositoryConfiguration _repositoryConfiguration;
private readonly IModuleConfigurationResolver _moduleConfigurationResolver; private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
private readonly IMetadataHandler _metadataHandler; private readonly IMetadataHandler _metadataHandler;
private readonly IPathHelperService _pathHelperService;
private readonly ILogger<LinkCommand> _logger; private readonly ILogger<LinkCommand> _logger;
public override string CommandString => "link"; public override string CommandString => "link";
public override string[] CommandAliases => Array.Empty<string>();
public LinkCommand( public LinkCommand(
IRepositoryConfiguration repositoryConfiguration, IRepositoryConfiguration repositoryConfiguration,
IModuleConfigurationResolver moduleConfigurationResolver, IModuleConfigurationResolver moduleConfigurationResolver,
IMetadataHandler metadataHandler, IMetadataHandler metadataHandler,
IPathHelperService pathHelperService,
ILogger<LinkCommand> logger) ILogger<LinkCommand> logger)
: base(repositoryConfiguration) : base(repositoryConfiguration, pathHelperService, moduleConfigurationResolver)
{ {
_repositoryConfiguration = repositoryConfiguration; _repositoryConfiguration = repositoryConfiguration;
_moduleConfigurationResolver = moduleConfigurationResolver; _moduleConfigurationResolver = moduleConfigurationResolver;
_metadataHandler = metadataHandler; _metadataHandler = metadataHandler;
_pathHelperService = pathHelperService;
_logger = logger; _logger = logger;
} }
public override async Task Run(List<string> parameters) public override async Task Run(List<string> parameters)
{ {
if (parameters.Contains("--help"))
{
_logger.LogInformation(
"""
Usage:
alma link [module]
alma link [repository] [module]
Options:
--help Show this message
-d, --dry-run Show what would be linked without actually linking
"""
);
return;
}
var dryRun = parameters.Contains("-d") || parameters.Contains("--dry-run");
var (repoName, moduleName) = GetRepositoryAndModuleName(parameters); var (repoName, moduleName) = GetRepositoryAndModuleName(parameters);
if (moduleName is null) if (moduleName is null)
{ {
@@ -39,10 +61,7 @@ public class LinkCommand : RepositoryModuleCommandBase
return; return;
} }
string sourceDirectory = Path.Combine(Environment.CurrentDirectory); var (sourceDirectory, targetDirectory) = GetRepositorySourceAndTargetDirectory(repoName);
string targetDirectory = Path.Combine(Environment.CurrentDirectory, "..");
(sourceDirectory, targetDirectory) = GetModuleSourceAndTargetDirectory(repoName, sourceDirectory, targetDirectory);
if (!Directory.Exists(sourceDirectory)) if (!Directory.Exists(sourceDirectory))
{ {
@@ -64,7 +83,7 @@ public class LinkCommand : RepositoryModuleCommandBase
if (moduleConfiguration?.Target is string moduleTargetDir) if (moduleConfiguration?.Target is string moduleTargetDir)
{ {
targetDirectory = PathHelper.ResolvePath(moduleTargetDir, targetDirectory); targetDirectory = _pathHelperService.ResolvePath(moduleTargetDir, targetDirectory);
} }
if (!Directory.Exists(targetDirectory)) if (!Directory.Exists(targetDirectory))
@@ -82,14 +101,46 @@ public class LinkCommand : RepositoryModuleCommandBase
moduleDir, moduleDir,
currentTargetDirectory, currentTargetDirectory,
moduleConfiguration)).ToList(); moduleConfiguration)).ToList();
// Exclude
if (moduleConfigurationFile is not null) itemsToLink.RemoveAll(i => i.SourcePath == moduleConfigurationFileFullPath); if (moduleConfigurationFile is not null) itemsToLink.RemoveAll(i => i.SourcePath == moduleConfigurationFileFullPath);
var successfulLinks = CreateLinks(itemsToLink); if (moduleConfiguration?.Exclude is { } excludeList)
{
foreach (var itemToExclude in excludeList)
{
var excludePath = Path.Combine(moduleDirectory, Path.Combine(itemToExclude.Split('/')));
itemsToLink.RemoveAll(
i => i.SourcePath == excludePath
|| i.SourcePath.StartsWith(excludePath + Path.DirectorySeparatorChar)
);
}
}
if(moduleConfiguration?.ExcludeReadme ?? false)
{
foreach (var readmeFile in Enum.GetValues<ReadmeFiles>())
{
var readmeFilePath = Path.Combine(moduleDirectory, readmeFile.GetFileName());
itemsToLink.RemoveAll(i => i.SourcePath == readmeFilePath);
}
}
// Linking
if (dryRun)
{
_logger.LogInformation("Dry run. No links will be created. The following links would be created:");
}
var successfulLinks = CreateLinks(itemsToLink, dryRun);
if (dryRun) return;
await _metadataHandler.SaveLinkedItemsAsync(successfulLinks, moduleDir, currentTargetDirectory); await _metadataHandler.SaveLinkedItemsAsync(successfulLinks, moduleDir, currentTargetDirectory);
} }
private List<ItemToLink> CreateLinks(List<ItemToLink> itemsToLink) private List<ItemToLink> CreateLinks(List<ItemToLink> itemsToLink, bool dryRun)
{ {
var successfulLinks = new List<ItemToLink>(); var successfulLinks = new List<ItemToLink>();
@@ -108,6 +159,8 @@ public class LinkCommand : RepositoryModuleCommandBase
_logger.LogInformation($"Linking: '{itemToLink.SourcePath}' '{itemToLink.TargetPath}'"); _logger.LogInformation($"Linking: '{itemToLink.SourcePath}' '{itemToLink.TargetPath}'");
if (!dryRun)
{
if (sourceFileExists) if (sourceFileExists)
{ {
File.CreateSymbolicLink(itemToLink.TargetPath, itemToLink.SourcePath); File.CreateSymbolicLink(itemToLink.TargetPath, itemToLink.SourcePath);
@@ -121,6 +174,7 @@ public class LinkCommand : RepositoryModuleCommandBase
_logger.LogInformation("Source not exists: " + itemToLink.SourcePath); _logger.LogInformation("Source not exists: " + itemToLink.SourcePath);
continue; continue;
} }
}
successfulLinks.Add(itemToLink); successfulLinks.Add(itemToLink);
} }
@@ -130,7 +184,7 @@ public class LinkCommand : RepositoryModuleCommandBase
_logger.LogInformation("An error occured while creating links: " + e.Message); _logger.LogInformation("An error occured while creating links: " + e.Message);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
_logger.LogInformation("On Windows symlinks can be greated only with Administrator privileges."); _logger.LogInformation("On Windows symlinks can be created only with Administrator privileges.");
} }
} }
@@ -157,7 +211,7 @@ public class LinkCommand : RepositoryModuleCommandBase
var relativePath = GetRelativePath(subDir.FullName, moduleDirectory.FullName); var relativePath = GetRelativePath(subDir.FullName, moduleDirectory.FullName);
if (moduleConfiguration?.Links?.ContainsKey(relativePath) ?? false) if (moduleConfiguration?.Links?.ContainsKey(relativePath) ?? false)
{ {
filesToLink.Add(new ItemToLink(subDir.FullName, PathHelper.ResolvePath(moduleConfiguration.Links[relativePath], targetDirectory.FullName))); filesToLink.Add(new ItemToLink(subDir.FullName, _pathHelperService.ResolvePath(moduleConfiguration.Links[relativePath], targetDirectory.FullName)));
} }
else else
{ {

View File

@@ -12,6 +12,7 @@ public class ListCommand : ICommand
private readonly ILogger<ListCommand> _logger; private readonly ILogger<ListCommand> _logger;
public string CommandString => "ls"; public string CommandString => "ls";
public string[] CommandAliases => Array.Empty<string>();
public ListCommand( public ListCommand(
IRepositoryConfiguration repositoryConfiguration, IRepositoryConfiguration repositoryConfiguration,

View File

@@ -0,0 +1,92 @@
using Alma.Command.Install;
using Alma.Configuration.Repository;
using Alma.Logging;
using Alma.Models;
using Alma.Services;
namespace Alma.Command.List;
public class ReadMeCommand : RepositoryModuleCommandBase
{
private ILogger<InstallCommand> _logger;
public override string CommandString => "readme";
public override string[] CommandAliases => Array.Empty<string>();
private readonly Dictionary<ReadmeFiles, Func<string, Task>> _readmeFilePrinters;
public ReadMeCommand(
ILogger<InstallCommand> logger,
IRepositoryConfiguration repositoryConfiguration,
IModuleConfigurationResolver moduleConfigurationResolver,
IPathHelperService pathHelperService)
: base(repositoryConfiguration, pathHelperService, moduleConfigurationResolver)
{
_logger = logger;
_readmeFilePrinters = new Dictionary<ReadmeFiles, Func<string, Task>>
{
{ ReadmeFiles.Markdown, PrintReadMeMd },
{ ReadmeFiles.Text, PrintReadMeText },
{ ReadmeFiles.NoExtension, PrintReadMeText },
};
}
public override async Task Run(List<string> parameters)
{
var (repoName, moduleName) = GetRepositoryAndModuleName(parameters);
if (moduleName is null)
{
_logger.LogInformation("No module specified");
return;
}
var (repoSource, _) = GetRepositorySourceAndTargetDirectory(repoName);
if (repoSource is null)
{
_logger.LogInformation("No repository source found");
return;
}
var fileFound = false;
foreach (var readmeFile in _readmeFilePrinters.Keys)
{
// TODO: make this case insensitive
var readmeFilePath = Path.Combine(repoSource, moduleName, readmeFile.ToString());
if (File.Exists(readmeFilePath))
{
fileFound = true;
await _readmeFilePrinters[readmeFile](readmeFilePath);
break;
}
}
if (!fileFound)
{
_logger.LogInformation("No README file found. Supported formats: README.md, README.txt, README");
}
}
private async Task PrintReadMeMd(string filePath)
{
var content = await File.ReadAllLinesAsync(filePath);
foreach (var line in content)
{
//TODO: Add support for markdown
_logger.LogInformation(line);
}
}
private async Task PrintReadMeText(string filePath)
{
var content = await File.ReadAllLinesAsync(filePath);
foreach (var line in content)
{
_logger.LogInformation(line);
}
}
}

View File

@@ -1,29 +1,59 @@
using Alma.Configuration.Repository; using Alma.Configuration.Repository;
using Alma.Helper; using Alma.Data;
using Alma.Services;
namespace Alma.Command; namespace Alma.Command;
public abstract class RepositoryModuleCommandBase : ICommand public abstract class RepositoryModuleCommandBase : ICommand
{ {
private readonly IRepositoryConfiguration _repositoryConfiguration; private readonly IRepositoryConfiguration _repositoryConfiguration;
private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
private readonly IPathHelperService _pathHelperService;
public abstract string CommandString { get; } public abstract string CommandString { get; }
public abstract string[] CommandAliases { get; }
public abstract Task Run(List<string> parameters); public abstract Task Run(List<string> parameters);
protected RepositoryModuleCommandBase(IRepositoryConfiguration repositoryConfiguration) protected RepositoryModuleCommandBase(
IRepositoryConfiguration repositoryConfiguration,
IPathHelperService pathHelperService,
IModuleConfigurationResolver moduleConfigurationResolver)
{ {
_repositoryConfiguration = repositoryConfiguration; _repositoryConfiguration = repositoryConfiguration;
_pathHelperService = pathHelperService;
_moduleConfigurationResolver = moduleConfigurationResolver;
} }
protected (string?, string?) GetRepositoryAndModuleName(List<string> parameters) protected async Task<(Configuration.Module.ModuleConfiguration? mergedModuleConfig, string? moduleConfigFileName)> GetModuleConfiguration(string? repoName, string moduleName)
{
var (repoSourceDirectory, _) = GetRepositorySourceAndTargetDirectory(repoName);
var moduleNameAsPath = moduleName.Replace('/', Path.DirectorySeparatorChar);
var moduleDirectory = Path.Combine(repoSourceDirectory, moduleNameAsPath);
var moduleConfigFileStub = Path.Combine(moduleDirectory, Constants.ModuleConfigFileStub);
return await _moduleConfigurationResolver.ResolveModuleConfiguration(moduleConfigFileStub);
}
protected (string? repoName, string? moduleName) GetRepositoryAndModuleName(List<string> parameters, bool singleParamIsRepo = false)
{ {
//TODO: handle parameters //TODO: handle parameters
string? repositoryName = null; string? repositoryName = null;
string? moduleName = null; string? moduleName = null;
parameters = parameters.Where(p => !p.StartsWith("-")).ToList();
if (parameters.Count == 1) if (parameters.Count == 1)
{
if (singleParamIsRepo)
{
repositoryName = parameters[0];
}
else
{ {
moduleName = parameters[0]; moduleName = parameters[0];
} }
}
else if (parameters.Count >= 1) else if (parameters.Count >= 1)
{ {
repositoryName = parameters[0]; repositoryName = parameters[0];
@@ -33,18 +63,24 @@ public abstract class RepositoryModuleCommandBase : ICommand
return (repositoryName, moduleName); return (repositoryName, moduleName);
} }
protected (string sourceDirectory, string targetDirectory) GetModuleSourceAndTargetDirectory(string? repoName, string fallbackSourceDirectory, string fallbackTargetDirectory) protected (string repoSourceDirectory, string repoTargetDirectory) GetRepositorySourceAndTargetDirectory(string? repoName)
{
string repoSourceDirectory = Path.Combine(Environment.CurrentDirectory);
string repoTargetDirectory = Path.Combine(Environment.CurrentDirectory, "..");
return GetRepositorySourceAndTargetDirectory(repoName, repoSourceDirectory, repoTargetDirectory);
}
protected (string repoSourceDirectory, string repoTargetDirectory) GetRepositorySourceAndTargetDirectory(string? repoName, string fallbackSourceDirectory, string fallbackTargetDirectory)
{ {
if (repoName is not null if (repoName is not null
&& _repositoryConfiguration.Configuration.Repositories.FirstOrDefault(r => r.Name == repoName) is { } repoConfig) && _repositoryConfiguration.Configuration.Repositories.Find(r => r.Name == repoName) is { } repoConfig)
{ {
fallbackSourceDirectory = fallbackSourceDirectory =
repoConfig.RepositoryPath is { } repoPath repoConfig.RepositoryPath is { } repoPath
? PathHelper.ResolvePath(repoPath) ? _pathHelperService.ResolvePath(repoPath)
: fallbackSourceDirectory; : fallbackSourceDirectory;
fallbackTargetDirectory = fallbackTargetDirectory =
repoConfig.LinkPath is { } linkPath repoConfig.LinkPath is { } linkPath
? PathHelper.ResolvePath(linkPath) ? _pathHelperService.ResolvePath(linkPath)
: fallbackTargetDirectory; : fallbackTargetDirectory;
} }

View File

@@ -3,6 +3,7 @@ namespace Alma.Command.Unlink;
public class UnlinkCommand : ICommand public class UnlinkCommand : ICommand
{ {
public string CommandString => "unlink"; public string CommandString => "unlink";
public string[] CommandAliases => Array.Empty<string>();
public Task Run(List<string> parameters) public Task Run(List<string> parameters)
{ {
throw new NotImplementedException(); throw new NotImplementedException();

View File

@@ -1,21 +0,0 @@
namespace Alma.Helper;
public static class PathHelper
{
public static string ResolvePath(string path, string? currentDirectory = null)
{
var skipCombiningCurrentDirectory = false;
if (path.StartsWith("~"))
{
var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
path = path.Length > 1 ? Path.Combine(userProfile, path[2..]) : userProfile;
skipCombiningCurrentDirectory = true;
}
//TODO: more special character
return currentDirectory is null || skipCombiningCurrentDirectory
? path
: Path.Combine(currentDirectory, path);
}
}

View File

@@ -0,0 +1,20 @@
namespace Alma.Models;
public enum ReadmeFiles
{
Markdown,
Text,
NoExtension
}
public static class ReadmeFileTypeExtensions
{
public static string GetFileName(this ReadmeFiles type)
=> type switch
{
ReadmeFiles.Markdown => "README.md",
ReadmeFiles.Text => "README.txt",
ReadmeFiles.NoExtension => "README",
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}

View File

@@ -19,7 +19,14 @@ public class ConfigurationFileReader
{ {
foreach (var configurationFileReader in _configurationFileReaders) foreach (var configurationFileReader in _configurationFileReaders)
{ {
if (await configurationFileReader.DeserializeAsync<T>(fileNameWithoutExtension, contextGenerator, extension) is { Result: { } } result) return result; if (await configurationFileReader.DeserializeAsync<T>(
fileNameWithoutExtension,
contextGenerator,
extension) is { Result: { } } result
)
{
return result;
}
} }
return (null, null); return (null, null);

View File

@@ -1,47 +1,70 @@
using System.Diagnostics.CodeAnalysis;
using Alma.Command.Diag;
using Alma.Logging;
namespace Alma.Services; namespace Alma.Services;
public class FolderService : IFolderService public class FolderService : IFolderService
{ {
private readonly Dictionary<string, Func<string?>> _configHomeProviders;
private readonly Dictionary<string, Func<string?>> _appDataProviders;
public string? ConfigRoot { get; } public string? ConfigRoot { get; }
public string AppData { get; } public string AppData { get; }
public string ApplicationSubfolderName => "alma"; public string ApplicationSubfolderName => "alma";
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FolderService))]
public FolderService() public FolderService()
{ {
_configHomeProviders = ConfigHomeProviders();
_appDataProviders = AppDataProviders();
ConfigRoot = GetConfigHomePath(); ConfigRoot = GetConfigHomePath();
AppData = GetAppDataPath(); AppData = GetAppDataPath();
if (!Directory.Exists(AppData)) Directory.CreateDirectory(AppData); if (!Directory.Exists(AppData)) Directory.CreateDirectory(AppData);
} }
public string GetPreferredConfigurationFolder() private static Dictionary<string, Func<string?>> ConfigHomeProviders()
{
return new Dictionary<string, Func<string?>>
{
{"ALMA_CONFIG", () => Environment.GetEnvironmentVariable("ALMA_CONFIG")},
{"XDG_CONFIG_HOME", () => Environment.GetEnvironmentVariable("XDG_CONFIG_HOME")},
{"UserProfile", GetPreferredConfigurationFolder},
{"ALMA_CONFIG_FALLBACK", () => Environment.GetEnvironmentVariable("ALMA_CONFIG_FALLBACK")},
};
}
private static Dictionary<string, Func<string?>> AppDataProviders()
{
return new Dictionary<string, Func<string?>>
{
{"ALMA_APP_DATA", () => Environment.GetEnvironmentVariable("ALMA_APP_DATA")},
{"LocalApplicationData", () => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)},
{"ALMA_APP_DATA_FALLBACK", () => Environment.GetEnvironmentVariable("ALMA_APP_DATA_FALLBACK")},
};
}
string IFolderService.GetPreferredConfigurationFolder()
=> GetPreferredConfigurationFolder();
public static string GetPreferredConfigurationFolder()
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
private string? GetConfigHomePath() private string? GetConfigHomePath()
{ {
var configHomeProviders = new List<Func<string?>> var configHome = EnumerateProviders(_configHomeProviders.Values);
{
() => Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"),
() => GetPreferredConfigurationFolder()
};
var configHome = EnumerateProviders(configHomeProviders);
return configHome == null ? null : Path.Combine(configHome, ApplicationSubfolderName); return configHome == null ? null : Path.Combine(configHome, ApplicationSubfolderName);
} }
private string GetAppDataPath() private string GetAppDataPath()
{ {
var appDataProviders = new List<Func<string?>> var appData = EnumerateProviders(_appDataProviders.Values) ?? Environment.CurrentDirectory;
{
() => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
};
var appData = EnumerateProviders(appDataProviders) ?? Environment.CurrentDirectory;
return Path.Combine(appData, ApplicationSubfolderName); return Path.Combine(appData, ApplicationSubfolderName);
} }
private static string? EnumerateProviders(List<Func<string?>> providers) private static string? EnumerateProviders(IEnumerable<Func<string?>> providers)
{ {
string? result = null; string? result = null;
@@ -53,4 +76,26 @@ public class FolderService : IFolderService
return result; return result;
} }
[DiagnosticHelper("config-home-providers")]
public static void ConfigHomeProviderDiag(ILogger logger)
{
var configHomeProviders = ConfigHomeProviders();
logger.LogInformation($"There are {configHomeProviders.Count} config home providers:");
foreach (var configHome in configHomeProviders)
{
logger.LogInformation($"{configHome.Key} => {configHome.Value() ?? "<null>"}");
}
}
[DiagnosticHelper("app-data-providers")]
public static void AppDataProviderDiag(ILogger logger)
{
var appDataProviders = AppDataProviders();
logger.LogInformation($"There are {appDataProviders.Count} app data providers:");
foreach (var appData in appDataProviders)
{
logger.LogInformation($"{appData.Key} => {appData.Value() ?? "<null>"}");
}
}
} }

View File

@@ -17,6 +17,12 @@ public class JsonConfigurationFileReader : IConfigurationFileReader
if (!File.Exists(fileName)) return (null, null); if (!File.Exists(fileName)) return (null, null);
await using FileStream openStream = File.OpenRead(fileName); await using FileStream openStream = File.OpenRead(fileName);
return ((T?)await JsonSerializer.DeserializeAsync(openStream, typeof(T), contextGenerator(new JsonSerializerOptions(DefaultOptions))), fileName); var result =
(T?)await JsonSerializer.DeserializeAsync(
openStream,
typeof(T),
contextGenerator(new JsonSerializerOptions(DefaultOptions))
);
return (result, fileName);
} }
} }

View File

@@ -29,6 +29,11 @@ public class ModuleConfigurationResolver : IModuleConfigurationResolver
//TODO: priority order //TODO: priority order
var orderedValidModuleConfigurations = new Dictionary<string, ModuleConfiguration>(validModuleConfigurations); var orderedValidModuleConfigurations = new Dictionary<string, ModuleConfiguration>(validModuleConfigurations);
if (orderedValidModuleConfigurations.Count == 0)
{
return (ModuleConfiguration.Empty(), moduleConfigFileName);
}
var mergedModuleConfig = orderedValidModuleConfigurations var mergedModuleConfig = orderedValidModuleConfigurations
.Select(m => m.Value) .Select(m => m.Value)
.Aggregate((a, b) => a.Merge(b)); .Aggregate((a, b) => a.Merge(b));

View File

@@ -0,0 +1,47 @@
using System.Diagnostics.CodeAnalysis;
using Alma.Command.Diag;
using Alma.Data;
using Alma.Logging;
namespace Alma.Services;
public class PathHelperService : IPathHelperService
{
private static readonly List<SpecialPathResolver> _specialPathResolvers = new()
{
new("~", () => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), true),
new("%DOCUMENTS%", () => Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)),
};
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(PathHelperService))]
public string ResolvePath(string path, string? currentDirectory = null)
{
var skipCombiningCurrentDirectory = false;
foreach (var specialPathResolver in _specialPathResolvers)
{
if (path.Contains(specialPathResolver.PathName))
{
var parts = path.Split(specialPathResolver.PathName);
path = string.Join(specialPathResolver.Resolver(), parts);
skipCombiningCurrentDirectory = (specialPathResolver.SkipCombiningCurrentDirectory ?? false) || skipCombiningCurrentDirectory;
}
}
path = path.Replace('/', Path.DirectorySeparatorChar);
return currentDirectory is null || skipCombiningCurrentDirectory
? path
: Path.Combine(currentDirectory, path);
}
[DiagnosticHelper("special-path-resolver")]
public static void SpecialPathResolverDiag(ILogger logger)
{
logger.LogInformation($"There are {_specialPathResolvers.Count} special path resolvers:");
foreach (var specialPathResolver in _specialPathResolvers)
{
logger.LogInformation($"{specialPathResolver.PathName} => {specialPathResolver.Resolver()}");
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@@ -1,6 +1,6 @@
namespace Alma.Logging; namespace Alma.Logging;
public interface ILogger<T> public interface ILogger
{ {
LogLevel DefaultLogLevel { get; } LogLevel DefaultLogLevel { get; }
void LogInformation(string logMessage); void LogInformation(string logMessage);
@@ -10,3 +10,8 @@ public interface ILogger<T>
void LogError(string logMessage); void LogError(string logMessage);
void LogCritical(string logMessage); void LogCritical(string logMessage);
} }
public interface ILogger<T> : ILogger
{
}

View File

@@ -3,5 +3,6 @@
public interface ILoggerFactory public interface ILoggerFactory
{ {
ILogger<T> CreateLogger<T>(); ILogger<T> CreateLogger<T>();
ILogger CreateLogger(Type t);
LogLevel DefaultLogLevel { get; } LogLevel DefaultLogLevel { get; }
} }

View File

@@ -1,10 +1,11 @@
namespace Alma.Logging; namespace Alma.Logging;
public class Logger<T> : ILogger<T>
public class Logger : ILogger
{ {
public LogLevel DefaultLogLevel { get; } public LogLevel DefaultLogLevel { get; }
public Logger(LogLevel defaultLogLevel) public Logger(LogLevel defaultLogLevel, string topicName)
{ {
DefaultLogLevel = defaultLogLevel; DefaultLogLevel = defaultLogLevel;
} }
@@ -33,3 +34,11 @@ public class Logger<T> : ILogger<T>
Log(logMessage, LogLevel.Critical); Log(logMessage, LogLevel.Critical);
} }
} }
public class Logger<T> : Logger, ILogger<T>
{
public Logger(LogLevel defaultLogLevel) : base(defaultLogLevel, typeof(T).Name)
{
}
}

View File

@@ -13,4 +13,9 @@ public class LoggerFactory : ILoggerFactory
{ {
return new Logger<T>(DefaultLogLevel); return new Logger<T>(DefaultLogLevel);
} }
public ILogger CreateLogger(Type t)
{
return new Logger(DefaultLogLevel, t.Name);
}
} }

View File

@@ -6,15 +6,15 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jab" Version="0.8.4" /> <PackageReference Include="Jab" Version="0.10.2" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<VersionPrefix>0.0.0</VersionPrefix> <VersionPrefix>0.0.4</VersionPrefix>
<VersionSuffix>development</VersionSuffix> <VersionSuffix>development</VersionSuffix>
</PropertyGroup> </PropertyGroup>

View File

@@ -1,4 +1,6 @@
using Alma.Command; using Alma.Command;
using Alma.Command.Configure;
using Alma.Command.Diag;
using Alma.Command.Help; using Alma.Command.Help;
using Alma.Command.Info; using Alma.Command.Info;
using Alma.Command.Install; using Alma.Command.Install;
@@ -18,6 +20,14 @@ public static class Program
{ {
InitLogging(); InitLogging();
var logger = AlmaLoggerFactory.CreateLogger(typeof(Program));
var workdir = GetWorkdir(logger);
if (workdir != null)
{
Environment.CurrentDirectory = workdir;
}
var services = new AlmaServiceProvider(); var services = new AlmaServiceProvider();
var repositoryConfiguration = services.GetService<IRepositoryConfiguration>(); var repositoryConfiguration = services.GetService<IRepositoryConfiguration>();
@@ -27,6 +37,33 @@ public static class Program
await application.Run(args); await application.Run(args);
} }
private static string? GetWorkdir(ILogger logger)
{
var workdirProviders = new Dictionary<string, Func<string?>>
{
{"ALMA_WORKDIR", () => Environment.GetEnvironmentVariable("ALMA_WORKDIR")},
{"WORKDIR", () => Environment.GetEnvironmentVariable("WORKDIR")},
};
foreach (var workdirProvider in workdirProviders)
{
var workdir = workdirProvider.Value();
if (workdir != null)
{
if (Directory.Exists(workdir))
{
return workdir;
}
else
{
logger.LogInformation($"{workdirProvider.Key} is set to {workdir} but this directory does not exist.");
}
}
}
return null;
}
private static void InitLogging() private static void InitLogging()
{ {
AlmaLoggerFactory = new LoggerFactory(); AlmaLoggerFactory = new LoggerFactory();
@@ -47,10 +84,14 @@ public static class Program
[Singleton(typeof(ICommand), typeof(ListCommand))] [Singleton(typeof(ICommand), typeof(ListCommand))]
[Singleton(typeof(ICommand), typeof(InstallCommand))] [Singleton(typeof(ICommand), typeof(InstallCommand))]
[Singleton(typeof(ICommand), typeof(HelpCommand))] [Singleton(typeof(ICommand), typeof(HelpCommand))]
[Singleton(typeof(ICommand), typeof(ConfigureCommand))]
[Singleton(typeof(ICommand), typeof(DiagCommand))]
[Singleton(typeof(ICommand), typeof(ReadMeCommand))]
[Singleton(typeof(IModuleConfigurationResolver), typeof(ModuleConfigurationResolver))] [Singleton(typeof(IModuleConfigurationResolver), typeof(ModuleConfigurationResolver))]
[Singleton(typeof(IMetadataHandler), typeof(MetadataHandler))] [Singleton(typeof(IMetadataHandler), typeof(MetadataHandler))]
[Singleton(typeof(IShellService), typeof(ShellService))] [Singleton(typeof(IShellService), typeof(ShellService))]
[Singleton(typeof(IVersionService), typeof(VersionService))] [Singleton(typeof(IVersionService), typeof(VersionService))]
[Singleton(typeof(IPathHelperService), typeof(PathHelperService))]
[Singleton(typeof(Application))] [Singleton(typeof(Application))]
[Transient(typeof(ILogger<>), Factory = nameof(CustomLoggerFactory))] [Transient(typeof(ILogger<>), Factory = nameof(CustomLoggerFactory))]
internal partial class AlmaServiceProvider internal partial class AlmaServiceProvider