19 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
36 changed files with 580 additions and 61 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,6 +73,9 @@ 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:

4
.vscode/launch.json vendored
View File

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

@@ -10,3 +10,23 @@ Alma (aka Advanced Link Manager Application) is another dotfiles management tool
sudo wget https://github.com/ADIX7/Alma/releases/download/latest/alma-linux -O /usr/local/bin/alma sudo wget https://github.com/ADIX7/Alma/releases/download/latest/alma-linux -O /usr/local/bin/alma
sudo chmod +x /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,6 +4,8 @@ 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 string? Configure { get; set; }

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

@@ -1,6 +1,5 @@
using Alma.Command.Install; using Alma.Command.Install;
using Alma.Configuration.Repository; using Alma.Configuration.Repository;
using Alma.Data;
using Alma.Logging; using Alma.Logging;
using Alma.Services; using Alma.Services;
@@ -11,6 +10,7 @@ public class ConfigureCommand : RepositoryModuleCommandBase
private readonly ILogger<InstallCommand> _logger; private readonly ILogger<InstallCommand> _logger;
private readonly IShellService _shellService; private readonly IShellService _shellService;
public override string CommandString => "configure"; public override string CommandString => "configure";
public override string[] CommandAliases => Array.Empty<string>();
public ConfigureCommand( public ConfigureCommand(
ILogger<InstallCommand> logger, ILogger<InstallCommand> logger,

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,7 +1,6 @@
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.Services; using Alma.Services;
@@ -10,6 +9,7 @@ namespace Alma.Command.Info;
public class InfoCommand : RepositoryModuleCommandBase public class InfoCommand : RepositoryModuleCommandBase
{ {
public override 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;
@@ -66,7 +66,7 @@ public class InfoCommand : RepositoryModuleCommandBase
} }
} }
async Task ProcessGeneralInfoAsync() private async Task ProcessGeneralInfoAsync()
{ {
_logger.LogInformation("Alma " + _versionService.GetVersion()); _logger.LogInformation("Alma " + _versionService.GetVersion());
_logger.LogInformation(""); _logger.LogInformation("");
@@ -87,7 +87,7 @@ public class InfoCommand : RepositoryModuleCommandBase
_logger.LogInformation($"Platform is '{await _osInformation.GetOsIdentifierAsync()}'"); _logger.LogInformation($"Platform is '{await _osInformation.GetOsIdentifierAsync()}'");
_logger.LogInformation(""); _logger.LogInformation("");
if (_repositoryConfiguration.Configuration.Repositories is {Count: > 0} repositories) if (_repositoryConfiguration.Configuration.Repositories is { Count: > 0 } repositories)
{ {
_logger.LogInformation("Repositories:"); _logger.LogInformation("Repositories:");
foreach (var repository in repositories) foreach (var repository in repositories)
@@ -107,7 +107,7 @@ public class InfoCommand : RepositoryModuleCommandBase
} }
} }
async Task ProcessRepoInfoAsync(string repoName) private async Task ProcessRepoInfoAsync(string repoName)
{ {
var (repoSourceDirectory, _) = GetRepositorySourceAndTargetDirectory(repoName); var (repoSourceDirectory, _) = GetRepositorySourceAndTargetDirectory(repoName);
@@ -138,7 +138,7 @@ public class InfoCommand : RepositoryModuleCommandBase
if (moduleConfig is not null) if (moduleConfig is not null)
{ {
var moduleName = currentDirectory.FullName[(repoRoot.FullName.Length + 1)..].Replace(Path.DirectorySeparatorChar, '/'); var moduleName = currentDirectory.FullName[(repoRoot.FullName.TrimEnd(Path.DirectorySeparatorChar).Length + 1)..].Replace(Path.DirectorySeparatorChar, '/');
modulesFound = modulesFound.Append(new(moduleName, moduleConfig)); modulesFound = modulesFound.Append(new(moduleName, moduleConfig));
} }

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;
@@ -11,6 +9,7 @@ public class InstallCommand : RepositoryModuleCommandBase
private readonly ILogger<InstallCommand> _logger; private readonly ILogger<InstallCommand> _logger;
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,

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;
@@ -17,6 +17,7 @@ public class LinkCommand : RepositoryModuleCommandBase
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,
@@ -35,6 +36,24 @@ public class LinkCommand : RepositoryModuleCommandBase
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)
{ {
@@ -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,18 +159,21 @@ public class LinkCommand : RepositoryModuleCommandBase
_logger.LogInformation($"Linking: '{itemToLink.SourcePath}' '{itemToLink.TargetPath}'"); _logger.LogInformation($"Linking: '{itemToLink.SourcePath}' '{itemToLink.TargetPath}'");
if (sourceFileExists) if (!dryRun)
{ {
File.CreateSymbolicLink(itemToLink.TargetPath, itemToLink.SourcePath); if (sourceFileExists)
} {
else if (sourceDirectoryExists) File.CreateSymbolicLink(itemToLink.TargetPath, itemToLink.SourcePath);
{ }
Directory.CreateSymbolicLink(itemToLink.TargetPath, itemToLink.SourcePath); else if (sourceDirectoryExists)
} {
else Directory.CreateSymbolicLink(itemToLink.TargetPath, itemToLink.SourcePath);
{ }
_logger.LogInformation("Source not exists: " + itemToLink.SourcePath); else
continue; {
_logger.LogInformation("Source not exists: " + itemToLink.SourcePath);
continue;
}
} }
successfulLinks.Add(itemToLink); successfulLinks.Add(itemToLink);
@@ -147,7 +201,14 @@ public class LinkCommand : RepositoryModuleCommandBase
var filesToLink = new List<ItemToLink>(); var filesToLink = new List<ItemToLink>();
foreach (var file in currentDirectory.GetFiles()) 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>(); var subDirLinksToAdd = Enumerable.Empty<ItemToLink>();

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

@@ -10,6 +10,8 @@ public abstract class RepositoryModuleCommandBase : ICommand
private readonly IModuleConfigurationResolver _moduleConfigurationResolver; private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
private readonly IPathHelperService _pathHelperService; 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( protected RepositoryModuleCommandBase(
@@ -39,6 +41,8 @@ public abstract class RepositoryModuleCommandBase : ICommand
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) if (singleParamIsRepo)

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

@@ -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

@@ -1,4 +1,7 @@
using Alma.Data; using System.Diagnostics.CodeAnalysis;
using Alma.Command.Diag;
using Alma.Data;
using Alma.Logging;
namespace Alma.Services; namespace Alma.Services;
@@ -10,6 +13,7 @@ public class PathHelperService : IPathHelperService
new("%DOCUMENTS%", () => Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)), new("%DOCUMENTS%", () => Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)),
}; };
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(PathHelperService))]
public string ResolvePath(string path, string? currentDirectory = null) public string ResolvePath(string path, string? currentDirectory = null)
{ {
var skipCombiningCurrentDirectory = false; var skipCombiningCurrentDirectory = false;
@@ -30,4 +34,14 @@ public class PathHelperService : IPathHelperService
? path ? path
: Path.Combine(currentDirectory, 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.5</VersionPrefix>
<VersionSuffix>development</VersionSuffix> <VersionSuffix>development</VersionSuffix>
</PropertyGroup> </PropertyGroup>

View File

@@ -1,5 +1,6 @@
using Alma.Command; using Alma.Command;
using Alma.Command.Configure; 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;
@@ -19,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>();
@@ -28,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();
@@ -49,6 +85,8 @@ public static class Program
[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(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))]