45 Commits

Author SHA1 Message Date
5892210f9d Bump version to 0.0.5 2024-03-11 17:19:03 +01:00
4b40aa64b9 Fix root file link override 2024-03-11 17:05:29 +01:00
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
8d0dfd365e Merge branch 'feature/github-actions' 2022-12-19 20:24:43 +01:00
3a0d5fea9a Add versioning to Info command 2022-12-19 20:24:35 +01:00
218a290e10 Get version from tag 2022-12-19 20:13:39 +01:00
c1c11e9b4f Print environment variables 2022-12-19 20:13:24 +01:00
12e65f2f4e Add version scripts 2022-12-19 20:13:24 +01:00
5ae66d2388 Release test 2022-12-19 20:13:24 +01:00
8f3f55c701 Output name for executable 2022-12-19 20:13:24 +01:00
90ca560ad3 Release depends on build 2022-12-19 20:13:24 +01:00
2e90190a97 Release 2022-12-19 20:13:24 +01:00
525699ef1f Add only executable to the artifact 2022-12-19 20:13:24 +01:00
d31bdbda40 Publish Aot 2022-12-19 20:13:24 +01:00
89438a6935 Fix build path 2022-12-19 20:13:24 +01:00
ad65c61927 Fix dotnet RID 2022-12-19 20:13:24 +01:00
ddb452893d Dotnet publish, publish artifact 2022-12-19 20:13:24 +01:00
4d7d12fe5f CI test 2022-12-19 20:13:24 +01:00
e75f853e6a Use IServiceProvider in Help command 2022-12-19 18:56:39 +01:00
b33568cd67 Dotnet 7, publish scripts 2022-11-16 13:26:17 +01:00
dcdda76488 DevContainer 2022-11-16 13:26:17 +01:00
b23ab518db Windows shell 2022-11-08 22:21:00 +01:00
30c3266e25 Install commands, improvements 2022-11-06 18:36:04 +01:00
Ádám Kovács
8fd6b526f8 Fix indexofoutrange when Path is only ~ 2022-11-03 21:59:40 +01:00
51 changed files with 1380 additions and 212 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]

87
.github/workflows/github-actions.yml vendored Normal file
View File

@@ -0,0 +1,87 @@
name: Alma build
on:
push:
tags:
- "v*"
jobs:
build:
runs-on: ${{ matrix.target.runner }}
strategy:
matrix:
target:
- runtime: win-x64
runner: windows-latest
name: windows
outputname: Alma.exe
version_script: .scripts/versioning.ps1
- runtime: linux-x64
runner: ubuntu-latest
name: linux
outputname: Alma
version_script: .scripts/versioning.sh
- runtime: linux-musl-x64
runner: ubuntu-latest
name: linux-musl
outputname: Alma
version_script: .scripts/versioning.sh
steps:
- uses: actions/checkout@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Patch version
run: ${{ matrix.target.version_script }}
continue-on-error: true
- name: Restore dependencies
run: dotnet restore src/Alma
- name: Build
run: dotnet publish -c Release -p:PublishAot=true -r ${{ matrix.target.runtime }} -o app/ src/Alma
- uses: actions/upload-artifact@v3
with:
name: alma-${{ matrix.target.name }}
path: app/${{ matrix.target.outputname }}
release:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
name: Download Windows artifacts
with:
name: alma-windows
path: app/windows/
- uses: actions/download-artifact@v3
name: Download Linux artifacts
with:
name: alma-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
run: mkdir release
- name: Copy windows executable
run: cp app/windows/Alma.exe release/alma.exe
- name: Copy linux executable
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"
name: Create release
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: latest
prerelease: false
draft: true
files: |
release/alma*

12
.scripts/versioning.ps1 Normal file
View File

@@ -0,0 +1,12 @@
if((Get-Content env:\GITHUB_REF_TYPE) -ne "tag")
{
Return
}
$version = (Get-Content env:\GITHUB_REF).Replace("refs/tags/v", "")
$git_sha = (Get-Content env:\GITHUB_SHA).Substring(0, 8)
Write-Host $version
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

13
.scripts/versioning.sh Executable file
View File

@@ -0,0 +1,13 @@
if [ ${GITHUB_REF_TYPE} != "tag" ]; then
exit 1
fi
version="${GITHUB_REF:11}"
git_hash="${GITHUB_SHA}"
echo $git_hash
git_hash=`expr substr $git_hash 1 8`
echo $version
echo $git_hash
sed -i "s/0.0.0/$version/g;s/development/$git_hash/g" src/Alma/Alma.csproj

4
.vscode/launch.json vendored
View File

@@ -10,8 +10,8 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/Alma/bin/Debug/net7.0/Alma.dll",
"args": [],
"program": "${workspaceFolder}/src/Alma/bin/Debug/net8.0/Alma.dll",
"args": ["link", "dotconfig", "git", "-d"],
"cwd": "${workspaceFolder}/src/Alma",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",

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">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Alma</RootNamespace>

View File

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

View File

@@ -4,11 +4,18 @@ public class ModuleConfiguration
{
public string? Target { get; set; }
public Dictionary<string, string>? Links { get; set; }
public List<string>? Exclude { get; set; }
public bool ExcludeReadme { get; set; } = true;
public ModuleConfiguration(string? target, Dictionary<string, string>? links)
public string? Install { get; set; }
public string? Configure { get; set; }
public ModuleConfiguration(string? target, Dictionary<string, string>? links, string? install, string? configure)
{
Target = target;
Links = links;
Install = install;
Configure = configure;
}
public ModuleConfiguration Merge(ModuleConfiguration merge)
@@ -17,10 +24,12 @@ public class ModuleConfiguration
.Concat(merge.Links ?? new Dictionary<string, string>());
return new ModuleConfiguration(
merge.Target ?? Target,
new Dictionary<string, string>(mergedLinks)
new Dictionary<string, string>(mergedLinks),
merge.Install ?? Install,
merge.Configure ?? Configure
);
}
public static ModuleConfiguration Empty() =>
new(null, new Dictionary<string, string>());
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

@@ -2,6 +2,6 @@ namespace Alma.Services;
public interface IOsInformation
{
string GetOsIdentifier();
bool IsOnPlatform(string platform);
Task<string> GetOsIdentifierAsync();
Task<bool> IsOnPlatformAsync(string platform);
}

View File

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

View File

@@ -0,0 +1,6 @@
namespace Alma.Services;
public interface IShellService
{
Task RunCommandAsync(string command);
}

View File

@@ -0,0 +1,6 @@
namespace Alma.Services;
public interface IVersionService
{
public string GetVersion();
}

View File

@@ -5,8 +5,13 @@
<ProjectReference Include="..\Alma.Logging\Alma.Logging.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="LiteDB" Version="5.0.19" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Alma</RootNamespace>

View File

@@ -1,5 +1,4 @@
using Alma.Command;
using Alma.Command.Help;
using Alma.Logging;
namespace Alma;
@@ -9,9 +8,9 @@ public class Application
private readonly IList<ICommand> _commands;
private readonly ILogger<Application> _logger;
public Application(IEnumerable<ICommand> commands, ILogger<Application> logger, ILogger<HelpCommand> helpCommandLogger)
public Application(IEnumerable<ICommand> commands, ILogger<Application> logger)
{
_commands = commands.Append(new HelpCommand(() => _commands!, helpCommandLogger)).ToList();
_commands = commands.ToList();
_logger = logger;
}
@@ -25,7 +24,7 @@ public class Application
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)
{

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,12 +9,14 @@ public class HelpCommand : ICommand
public string CommandString => "help";
public string[] CommandAliases { get; } = ["--help", "-h"];
public HelpCommand(
Func<IEnumerable<ICommand>> commandsProvider,
IServiceProvider serviceProvider,
ILogger<HelpCommand> logger
)
{
_commandsProvider = commandsProvider;
_commandsProvider = () => (IEnumerable<ICommand>?)serviceProvider.GetService(typeof(IEnumerable<ICommand>)) ?? throw new ApplicationException();
_logger = logger;
}

View File

@@ -0,0 +1,216 @@
using Alma.Configuration.Module;
using Alma.Configuration.Repository;
using Alma.Data;
using Alma.Logging;
using Alma.Services;
namespace Alma.Command.Info;
public class InfoCommand : RepositoryModuleCommandBase
{
public override string CommandString => "info";
public override string[] CommandAliases => Array.Empty<string>();
private readonly IFolderService _folderService;
private readonly IRepositoryConfiguration _repositoryConfiguration;
private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
private readonly ILogger<InfoCommand> _logger;
private readonly IOsInformation _osInformation;
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(
IFolderService folderService,
IRepositoryConfiguration repositoryConfiguration,
IModuleConfigurationResolver moduleConfigurationResolver,
ILogger<InfoCommand> logger,
IOsInformation osInformation,
IVersionService versionService,
IPathHelperService pathHelperService
) : base(repositoryConfiguration, pathHelperService, moduleConfigurationResolver)
{
_folderService = folderService;
_repositoryConfiguration = repositoryConfiguration;
_moduleConfigurationResolver = moduleConfigurationResolver;
_logger = logger;
_osInformation = osInformation;
_versionService = versionService;
_pathHelperService = pathHelperService;
}
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()
{
_logger.LogInformation("Alma " + _versionService.GetVersion());
_logger.LogInformation("");
_logger.LogInformation("AppData folder: " + _folderService.AppData);
if (_folderService.ConfigRoot is { } configRoot)
{
_logger.LogInformation("Configuration folder: " + configRoot);
}
else
{
_logger.LogInformation("Configuration folder not exists.");
_logger.LogInformation("Preferred configuration folder is: " + Path.Combine(_folderService.GetPreferredConfigurationFolder(), _folderService.ApplicationSubfolderName));
}
_logger.LogInformation("");
_logger.LogInformation($"Platform is '{await _osInformation.GetOsIdentifierAsync()}'");
_logger.LogInformation("");
if (_repositoryConfiguration.Configuration.Repositories is { Count: > 0 } repositories)
{
_logger.LogInformation("Repositories:");
foreach (var repository in repositories)
{
Console.Write(repository.Name);
if (repository.RepositoryPath is not null && !Directory.Exists(_pathHelperService.ResolvePath(repository.RepositoryPath)))
{
Console.Write($" (containing folder not exists {repository.RepositoryPath})");
}
_logger.LogInformation("");
}
}
else
{
_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,64 +0,0 @@
using Alma.Configuration.Repository;
using Alma.Logging;
using Alma.Services;
namespace Alma.Command.Info;
public class ModuleInfoCommand : ICommand
{
public string CommandString => "info";
private readonly IFolderService _folderService;
private readonly IRepositoryConfiguration _repositoryConfiguration;
private readonly ILogger<ModuleInfoCommand> _logger;
public ModuleInfoCommand(
IFolderService folderService,
IRepositoryConfiguration repositoryConfiguration,
ILogger<ModuleInfoCommand> logger
)
{
_folderService = folderService;
_repositoryConfiguration = repositoryConfiguration;
_logger = logger;
}
public Task Run(List<string> parameters)
{
//Add info REPO
//Add info REPO MODULE
_logger.LogInformation("AppData folder: " + _folderService.AppData);
if (_folderService.ConfigRoot is string configRoot)
{
_logger.LogInformation("Configuration folder: " + configRoot);
}
else
{
_logger.LogInformation("Configuration folder not exists.");
_logger.LogInformation("Preffered configuration folder is: " + Path.Combine(_folderService.GetPreferredConfigurationFolder(), _folderService.ApplicationSubfolderName));
}
_logger.LogInformation("");
if (_repositoryConfiguration.Configuration.Repositories is var repositores && repositores?.Count > 0)
{
_logger.LogInformation("Repositories:");
foreach (var repository in repositores)
{
Console.Write(repository.Name);
if (repository.RepositoryPath is not null && !Directory.Exists(repository.RepositoryPath))
{
Console.Write($" (containing folder not exists {repository.RepositoryPath})");
}
_logger.LogInformation("");
}
}
else
{
_logger.LogInformation("No repositories found");
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,62 @@
using Alma.Configuration.Repository;
using Alma.Logging;
using Alma.Services;
namespace Alma.Command.Install;
public class InstallCommand : RepositoryModuleCommandBase
{
private readonly ILogger<InstallCommand> _logger;
private readonly IShellService _shellService;
public override string CommandString => "install";
public override string[] CommandAliases => Array.Empty<string>();
public InstallCommand(
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 installLines = moduleConfiguration.Install?.Split(Environment.NewLine);
if (installLines is null)
{
_logger.LogInformation("No install command is found");
return;
}
_logger.LogInformation($"Install command: {string.Join("\n", installLines)}");
if (installLines.Length == 1)
{
_logger.LogInformation("Running install command '" + installLines[0] + "'");
await _shellService.RunCommandAsync(installLines[0]);
}
else
{
_logger.LogError("Multi line scripts are not currently supported");
}
}
}

View File

@@ -3,51 +3,65 @@ using Alma.Configuration.Module;
using Alma.Configuration.Repository;
using Alma.Data;
using Alma.Logging;
using Alma.Models;
using Alma.Services;
namespace Alma.Command.Link;
public class LinkCommand : ICommand
public class LinkCommand : RepositoryModuleCommandBase
{
private readonly IRepositoryConfiguration _repositoryConfiguration;
private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
private readonly IMetadataHandler _metadataHandler;
private readonly IPathHelperService _pathHelperService;
private readonly ILogger<LinkCommand> _logger;
public string CommandString => "link";
public override string CommandString => "link";
public override string[] CommandAliases => Array.Empty<string>();
public LinkCommand(
IRepositoryConfiguration repositoryConfiguration,
IModuleConfigurationResolver moduleConfigurationResolver,
IMetadataHandler metadataHandler,
IPathHelperService pathHelperService,
ILogger<LinkCommand> logger)
: base(repositoryConfiguration, pathHelperService, moduleConfigurationResolver)
{
_repositoryConfiguration = repositoryConfiguration;
_moduleConfigurationResolver = moduleConfigurationResolver;
_metadataHandler = metadataHandler;
_pathHelperService = pathHelperService;
_logger = logger;
}
public async Task Run(List<string> parameters)
public override async Task Run(List<string> parameters)
{
if (parameters.Count == 0)
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);
if (moduleName is null)
{
_logger.LogInformation("No module specified");
return;
}
string moduleName = parameters[0];
string sourceDirectory = Path.Combine(Environment.CurrentDirectory);
string targetDirectory = Path.Combine(Environment.CurrentDirectory, "..");
var repoName = GetRepositoryName(parameters);
if (repoName is not null
&& _repositoryConfiguration.Configuration.Repositories.FirstOrDefault(r => r.Name == repoName) is { } repoConfig)
{
sourceDirectory = repoConfig.RepositoryPath ?? sourceDirectory;
targetDirectory = repoConfig.LinkPath ?? targetDirectory;
}
var (sourceDirectory, targetDirectory) = GetRepositorySourceAndTargetDirectory(repoName);
if (!Directory.Exists(sourceDirectory))
{
@@ -69,7 +83,7 @@ public class LinkCommand : ICommand
if (moduleConfiguration?.Target is string moduleTargetDir)
{
targetDirectory = ResolvePath(moduleTargetDir, targetDirectory);
targetDirectory = _pathHelperService.ResolvePath(moduleTargetDir, targetDirectory);
}
if (!Directory.Exists(targetDirectory))
@@ -87,14 +101,46 @@ public class LinkCommand : ICommand
moduleDir,
currentTargetDirectory,
moduleConfiguration)).ToList();
// Exclude
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);
}
private List<ItemToLink> CreateLinks(List<ItemToLink> itemsToLink)
private List<ItemToLink> CreateLinks(List<ItemToLink> itemsToLink, bool dryRun)
{
var successfulLinks = new List<ItemToLink>();
@@ -113,6 +159,8 @@ public class LinkCommand : ICommand
_logger.LogInformation($"Linking: '{itemToLink.SourcePath}' '{itemToLink.TargetPath}'");
if (!dryRun)
{
if (sourceFileExists)
{
File.CreateSymbolicLink(itemToLink.TargetPath, itemToLink.SourcePath);
@@ -126,6 +174,7 @@ public class LinkCommand : ICommand
_logger.LogInformation("Source not exists: " + itemToLink.SourcePath);
continue;
}
}
successfulLinks.Add(itemToLink);
}
@@ -135,7 +184,7 @@ public class LinkCommand : ICommand
_logger.LogInformation("An error occured while creating links: " + e.Message);
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.");
}
}
@@ -152,7 +201,14 @@ public class LinkCommand : ICommand
var filesToLink = new List<ItemToLink>();
foreach (var file in currentDirectory.GetFiles())
{
filesToLink.Add(new ItemToLink(Path.Combine(currentDirectory.FullName, file.Name), Path.Combine(currentTargetDirectory.FullName, file.Name)));
if (moduleConfiguration?.Links?.ContainsKey(file.Name) ?? false)
{
filesToLink.Add(new ItemToLink(file.FullName, _pathHelperService.ResolvePath(moduleConfiguration.Links[file.Name], targetDirectory.FullName)));
}
else
{
filesToLink.Add(new ItemToLink(file.FullName, Path.Combine(currentTargetDirectory.FullName, file.Name)));
}
}
var subDirLinksToAdd = Enumerable.Empty<ItemToLink>();
@@ -162,7 +218,7 @@ public class LinkCommand : ICommand
var relativePath = GetRelativePath(subDir.FullName, moduleDirectory.FullName);
if (moduleConfiguration?.Links?.ContainsKey(relativePath) ?? false)
{
filesToLink.Add(new ItemToLink(subDir.FullName, ResolvePath(moduleConfiguration.Links[relativePath], targetDirectory.FullName)));
filesToLink.Add(new ItemToLink(subDir.FullName, _pathHelperService.ResolvePath(moduleConfiguration.Links[relativePath], targetDirectory.FullName)));
}
else
{
@@ -180,24 +236,5 @@ public class LinkCommand : ICommand
return filesToLink.Concat(subDirLinksToAdd);
}
private static string? GetRepositoryName(List<string> parameters)
{
//TODO: handle parameters
if (parameters.Count < 2) return null;
return parameters[1];
}
private static string ResolvePath(string path, string currentDirectory)
{
if (path.StartsWith("~"))
{
path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), path.Substring(2));
}
//TODO: more special character
return Path.Combine(currentDirectory, path);
}
private static string GetRelativePath(string full, string parent) => full.Substring(parent.Length).TrimStart(Path.DirectorySeparatorChar);
private static string GetRelativePath(string full, string parent) => full[parent.Length..].TrimStart(Path.DirectorySeparatorChar);
}

View File

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

@@ -0,0 +1,89 @@
using Alma.Configuration.Repository;
using Alma.Data;
using Alma.Services;
namespace Alma.Command;
public abstract class RepositoryModuleCommandBase : ICommand
{
private readonly IRepositoryConfiguration _repositoryConfiguration;
private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
private readonly IPathHelperService _pathHelperService;
public abstract string CommandString { get; }
public abstract string[] CommandAliases { get; }
public abstract Task Run(List<string> parameters);
protected RepositoryModuleCommandBase(
IRepositoryConfiguration repositoryConfiguration,
IPathHelperService pathHelperService,
IModuleConfigurationResolver moduleConfigurationResolver)
{
_repositoryConfiguration = repositoryConfiguration;
_pathHelperService = pathHelperService;
_moduleConfigurationResolver = moduleConfigurationResolver;
}
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
string? repositoryName = null;
string? moduleName = null;
parameters = parameters.Where(p => !p.StartsWith("-")).ToList();
if (parameters.Count == 1)
{
if (singleParamIsRepo)
{
repositoryName = parameters[0];
}
else
{
moduleName = parameters[0];
}
}
else if (parameters.Count >= 1)
{
repositoryName = parameters[0];
moduleName = parameters[1];
}
return (repositoryName, moduleName);
}
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
&& _repositoryConfiguration.Configuration.Repositories.Find(r => r.Name == repoName) is { } repoConfig)
{
fallbackSourceDirectory =
repoConfig.RepositoryPath is { } repoPath
? _pathHelperService.ResolvePath(repoPath)
: fallbackSourceDirectory;
fallbackTargetDirectory =
repoConfig.LinkPath is { } linkPath
? _pathHelperService.ResolvePath(linkPath)
: fallbackTargetDirectory;
}
return (fallbackSourceDirectory, fallbackTargetDirectory);
}
}

View File

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

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)
{
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);

View File

@@ -1,47 +1,70 @@
using System.Diagnostics.CodeAnalysis;
using Alma.Command.Diag;
using Alma.Logging;
namespace Alma.Services;
public class FolderService : IFolderService
{
private readonly Dictionary<string, Func<string?>> _configHomeProviders;
private readonly Dictionary<string, Func<string?>> _appDataProviders;
public string? ConfigRoot { get; }
public string AppData { get; }
public string ApplicationSubfolderName => "alma";
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FolderService))]
public FolderService()
{
_configHomeProviders = ConfigHomeProviders();
_appDataProviders = AppDataProviders();
ConfigRoot = GetConfigHomePath();
AppData = GetAppDataPath();
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");
private string? GetConfigHomePath()
{
var configHomeProviders = new List<Func<string?>>
{
() => Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"),
() => GetPreferredConfigurationFolder()
};
var configHome = EnumerateProviders(configHomeProviders);
var configHome = EnumerateProviders(_configHomeProviders.Values);
return configHome == null ? null : Path.Combine(configHome, ApplicationSubfolderName);
}
private string GetAppDataPath()
{
var appDataProviders = new List<Func<string?>>
{
() => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
};
var appData = EnumerateProviders(appDataProviders) ?? Environment.CurrentDirectory;
var appData = EnumerateProviders(_appDataProviders.Values) ?? Environment.CurrentDirectory;
return Path.Combine(appData, ApplicationSubfolderName);
}
private static string? EnumerateProviders(List<Func<string?>> providers)
private static string? EnumerateProviders(IEnumerable<Func<string?>> providers)
{
string? result = null;
@@ -53,4 +76,26 @@ public class FolderService : IFolderService
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);
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

@@ -21,11 +21,19 @@ public class ModuleConfigurationResolver : IModuleConfigurationResolver
if (moduleConfigRoot is null) return (null, null);
var validModuleConfigurations = moduleConfigRoot.Where(m => _osInformation.IsOnPlatform(m.Key));
var validModuleConfigurations = await moduleConfigRoot
.ToAsyncEnumerable()
.WhereAwait(async m => await _osInformation.IsOnPlatformAsync(m.Key))
.ToListAsync();
//TODO: priority order
var orderedValidModuleConfigurations = new Dictionary<string, ModuleConfiguration>(validModuleConfigurations);
if (orderedValidModuleConfigurations.Count == 0)
{
return (ModuleConfiguration.Empty(), moduleConfigFileName);
}
var mergedModuleConfig = orderedValidModuleConfigurations
.Select(m => m.Value)
.Aggregate((a, b) => a.Merge(b));

View File

@@ -6,19 +6,52 @@ public class OsInformation : IOsInformation
{
private const string OsIdentifierDefault = "default";
private const string OsIdentifierWin = "windows";
private const string OsIdentifierMac = "macos";
private const string OsIdentifierFreeBsd = "freebsd";
private const string OsIdentifierLinux = "linux";
public string GetOsIdentifier()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return OsIdentifierWin;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return OsIdentifierLinux;
private const string LinuxOsRelease = "/etc/os-release";
public async Task<string> GetOsIdentifierAsync()
{
string? baseOsIdentifier = null;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) baseOsIdentifier = OsIdentifierWin;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) baseOsIdentifier = OsIdentifierMac;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) baseOsIdentifier = OsIdentifierFreeBsd;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
baseOsIdentifier = OsIdentifierLinux;
try
{
if (File.Exists(LinuxOsRelease))
{
var lines = await File.ReadAllLinesAsync(LinuxOsRelease);
var distroName = lines.FirstOrDefault(l => l.StartsWith("id=", StringComparison.InvariantCultureIgnoreCase));
if (distroName is not null)
{
distroName = distroName.ToLower().Substring(distroName.IndexOf("=", StringComparison.Ordinal) + 1);
baseOsIdentifier += "-" + distroName;
}
}
}
catch
{
}
}
if (baseOsIdentifier is null)
return "unknown";
var architecture = RuntimeInformation.ProcessArchitecture.ToString().ToLower();
return baseOsIdentifier + "-" + architecture;
}
public bool IsOnPlatform(string platform)
public async Task<bool> IsOnPlatformAsync(string platform)
{
if (platform == OsIdentifierDefault) return true;
return platform == GetOsIdentifier();
return platform == OsIdentifierDefault
|| (await GetOsIdentifierAsync()).StartsWith(platform, StringComparison.InvariantCultureIgnoreCase);
}
}

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

@@ -0,0 +1,80 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using Alma.Logging;
namespace Alma.Services;
public class ShellService : IShellService
{
private readonly ILogger<ShellService> _logger;
public ShellService(ILogger<ShellService> logger)
{
_logger = logger;
}
public async Task RunCommandAsync(string command)
{
Process? process;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
process = CreateLinuxShell(command);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
process = CreateWindowsShell(command);
}
else
{
_logger.LogError("Platform not supported");
throw new NotSupportedException();
}
if (!process.Start()) return;
var reader = process.StandardOutput;
while (!reader.EndOfStream)
{
var content = await reader.ReadLineAsync();
if (content is not null)
{
_logger.LogInformation(content);
}
}
await process.WaitForExitAsync();
}
private Process CreateLinuxShell(string command)
{
var processStartInfo = new ProcessStartInfo
{
FileName = "sh",
ArgumentList = {"-c", command},
RedirectStandardOutput = true,
RedirectStandardInput = true,
CreateNoWindow = true,
UseShellExecute = false
};
return new Process {StartInfo = processStartInfo};
}
private Process CreateWindowsShell(string command)
{
var processStartInfo = new ProcessStartInfo
{
//TODO: customizable shell
FileName = "pwsh",
ArgumentList = {"-c", command},
RedirectStandardOutput = true,
RedirectStandardInput = true,
CreateNoWindow = true,
UseShellExecute = false
};
return new Process {StartInfo = processStartInfo};
}
}

View File

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

View File

@@ -1,10 +1,17 @@
namespace Alma.Logging;
public interface ILogger<T>
public interface ILogger
{
LogLevel DefaultLogLevel { get; }
void LogInformation(string logMessage);
void LogDebug(string logMessage);
void LogTrace(string logMessage);
void Log(string logMessage, LogLevel logLevel);
void LogError(string logMessage);
void LogCritical(string logMessage);
}
public interface ILogger<T> : ILogger
{
}

View File

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

View File

@@ -2,6 +2,8 @@
public enum LogLevel
{
Critical,
Error,
Information,
Debug,
Trace

View File

@@ -1,10 +1,11 @@
namespace Alma.Logging;
public class Logger<T> : ILogger<T>
public class Logger : ILogger
{
public LogLevel DefaultLogLevel { get; }
public Logger(LogLevel defaultLogLevel)
public Logger(LogLevel defaultLogLevel, string topicName)
{
DefaultLogLevel = defaultLogLevel;
}
@@ -22,4 +23,22 @@ public class Logger<T> : ILogger<T>
Console.WriteLine(s);
}
}
public void LogError(string logMessage)
{
Log(logMessage, LogLevel.Error);
}
public void LogCritical(string logMessage)
{
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);
}
public ILogger CreateLogger(Type t)
{
return new Logger(DefaultLogLevel, t.Name);
}
}

View File

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

View File

@@ -1,5 +1,9 @@
using Alma.Command;
using Alma.Command.Configure;
using Alma.Command.Diag;
using Alma.Command.Help;
using Alma.Command.Info;
using Alma.Command.Install;
using Alma.Command.Link;
using Alma.Command.List;
using Alma.Command.Unlink;
@@ -12,46 +16,18 @@ namespace Alma;
public static class Program
{
/*public static async Task Main(string[] args)
{
var services = BuildServices();
var repositoryConfiguration = services.GetRequiredService<IRepositoryConfiguration>();
await repositoryConfiguration.LoadAsync();
var application = services.GetRequiredService<Application>();
await application.Run(args);
static IServiceProvider BuildServices()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IRepositoryConfiguration, RepositoryConfiguration>();
serviceCollection.AddSingleton<IFolderService, FolderService>();
serviceCollection.AddSingleton<ConfigurationFileReader>();
serviceCollection.AddSingleton<IConfigurationFileReader, JsonConfigurationFileReader>();
serviceCollection.AddSingleton<IOsInformation, OsInformation>();
serviceCollection.AddSingleton<ICommand, LinkCommand>();
serviceCollection.AddSingleton<IModuleConfigurationResolver, ModuleConfigurationResolver>();
serviceCollection.AddSingleton<Application>();
typeof(IRepositoryConfiguration), typeof(RepositoryConfiguration)
typeof(IFolderService), typeof(FolderService)
typeof(ConfigurationFileReader)
typeof(IConfigurationFileReader), typeof(JsonConfigurationFileReader)
typeof(IOsInformation), typeof(OsInformation)
typeof(ICommand), typeof(LinkCommand)
typeof(IModuleConfigurationResolver), typeof(ModuleConfigurationResolver)
typeof(Application)
return serviceCollection.BuildServiceProvider();
}
}*/
public static async Task Main(string[] args)
{
InitLogging();
var logger = AlmaLoggerFactory.CreateLogger(typeof(Program));
var workdir = GetWorkdir(logger);
if (workdir != null)
{
Environment.CurrentDirectory = workdir;
}
var services = new AlmaServiceProvider();
var repositoryConfiguration = services.GetService<IRepositoryConfiguration>();
@@ -61,14 +37,39 @@ public static class Program
await application.Run(args);
}
private static ILoggerFactory InitLogging()
private static string? GetWorkdir(ILogger logger)
{
var loggerFactory = new LoggerFactory();
var workdirProviders = new Dictionary<string, Func<string?>>
{
{"ALMA_WORKDIR", () => Environment.GetEnvironmentVariable("ALMA_WORKDIR")},
{"WORKDIR", () => Environment.GetEnvironmentVariable("WORKDIR")},
};
return AlmaLoggerFactory = loggerFactory;
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.");
}
}
}
public static ILoggerFactory AlmaLoggerFactory { get; private set; }
return null;
}
private static void InitLogging()
{
AlmaLoggerFactory = new LoggerFactory();
}
public static ILoggerFactory AlmaLoggerFactory { get; private set; } = null!;
}
[ServiceProvider]
@@ -79,12 +80,18 @@ public static class Program
[Singleton(typeof(IOsInformation), typeof(OsInformation))]
[Singleton(typeof(ICommand), typeof(LinkCommand))]
[Singleton(typeof(ICommand), typeof(UnlinkCommand))]
[Singleton(typeof(ICommand), typeof(ModuleInfoCommand))]
[Singleton(typeof(ICommand), typeof(InfoCommand))]
[Singleton(typeof(ICommand), typeof(ListCommand))]
//Dependency cycle
//[Singleton(typeof(ICommand), typeof(HelpCommand))]
[Singleton(typeof(ICommand), typeof(InstallCommand))]
[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(IMetadataHandler), typeof(MetadataHandler))]
[Singleton(typeof(IShellService), typeof(ShellService))]
[Singleton(typeof(IVersionService), typeof(VersionService))]
[Singleton(typeof(IPathHelperService), typeof(PathHelperService))]
[Singleton(typeof(Application))]
[Transient(typeof(ILogger<>), Factory = nameof(CustomLoggerFactory))]
internal partial class AlmaServiceProvider

View File

@@ -0,0 +1,15 @@
using System.Reflection;
using Alma.Services;
namespace Alma;
public class VersionService : IVersionService
{
public string GetVersion()
{
return
typeof(Program).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? typeof(Program).Assembly.GetName().Version?.ToString()
?? "unknown";
}
}