diff --git a/.gitignore b/.gitignore index bbd09d9..e15aac6 100644 --- a/.gitignore +++ b/.gitignore @@ -483,5 +483,7 @@ $RECYCLE.BIN/ # Vim temporary swap files *.swp -# Generated schema files +# Generated files *.dbml +.al2dbml/ +.al2dbml/config.local.json diff --git a/src/AL2DBML.CLI/Commands/GenerateCommand.cs b/src/AL2DBML.CLI/Commands/GenerateCommand.cs index 7ed30fb..1bd4f9b 100644 --- a/src/AL2DBML.CLI/Commands/GenerateCommand.cs +++ b/src/AL2DBML.CLI/Commands/GenerateCommand.cs @@ -11,15 +11,15 @@ public class GenerateSettings : CommandSettings { [CommandOption("-i|--input ")] [Description("The path to the AL project folder, AL file, or vscode workspace file of AL projects to parse.")] - public string InputPath { get; init; } = "."; + public string? InputPath { get; init; } [CommandOption("-o|--output ")] [Description("The path to the output directory.")] - public string OutputPath { get; init; } = "."; + public string? OutputPath { get; init; } [CommandOption("-n|--name ")] - [Description("The name of the output file (without extension). 'schema' by default.")] - public string OutputName { get; init; } = "schema"; + [Description("The name of the output file (without extension).")] + public string? OutputName { get; init; } } public class GenerateCommand : AsyncCommand @@ -27,33 +27,42 @@ public class GenerateCommand : AsyncCommand private readonly IAlParser _alParser; private readonly IDBMLWriter _dbmlWriter; private readonly IParsingTracker _tracker; + private readonly IConfigService _configService; - public GenerateCommand(IAlParser alParser, IDBMLWriter dbmlWriter, IParsingTracker tracker) + public GenerateCommand(IAlParser alParser, IDBMLWriter dbmlWriter, IParsingTracker tracker, IConfigService configService) { _alParser = alParser; _dbmlWriter = dbmlWriter; _tracker = tracker; + _configService = configService; } protected override async Task ExecuteAsync(CommandContext context, GenerateSettings settings, CancellationToken cancellationToken) { - var inputType = FileSystemService.GetInputType(settings.InputPath); + var sharedConfig = _configService.LoadSharedConfig(); + var localConfig = _configService.LoadLocalConfig(); + + var inputPath = settings.InputPath ?? localConfig?.Input.Path ?? "."; + var outputPath = settings.OutputPath ?? sharedConfig?.Output.Path ?? "."; + var outputName = settings.OutputName ?? sharedConfig?.Output.Name ?? "schema"; + + var inputType = FileSystemService.GetInputType(inputPath); if (inputType == Enums.InputType.NotSupported) { AnsiConsole.MarkupLine("[red]Error:[/] Unsupported input type. Please provide a valid directory, AL file, or vscode workspace file."); return -1; } - var outputPath = Path.Combine(settings.OutputPath, $"{settings.OutputName}.dbml"); - Directory.CreateDirectory(settings.OutputPath); + var fullOutputPath = Path.Combine(outputPath, $"{outputName}.dbml"); + Directory.CreateDirectory(outputPath); var factory = new InputStrategyFactory(inputType, _alParser, _tracker); - var outputSchema = factory.Strategy.Execute(settings.InputPath); + var outputSchema = factory.Strategy.Execute(inputPath); var dbmlContent = await _dbmlWriter.WriteDBMLAsync(outputSchema); - await File.WriteAllTextAsync(outputPath, dbmlContent, cancellationToken); + await File.WriteAllTextAsync(fullOutputPath, dbmlContent, cancellationToken); - AnsiConsole.MarkupLine($"[green]Done:[/] {_tracker.FileCount} file(s) parsed in {_tracker.Elapsed.TotalSeconds:F2}s → {Markup.Escape(outputPath)}"); + AnsiConsole.MarkupLine($"[green]Done:[/] {_tracker.FileCount} file(s) parsed in {_tracker.Elapsed.TotalSeconds:F2}s → {Markup.Escape(fullOutputPath)}"); return 0; } diff --git a/src/AL2DBML.CLI/Commands/InitCommand.cs b/src/AL2DBML.CLI/Commands/InitCommand.cs new file mode 100644 index 0000000..6d856dd --- /dev/null +++ b/src/AL2DBML.CLI/Commands/InitCommand.cs @@ -0,0 +1,118 @@ +using AL2DBML.CLI.Models; +using AL2DBML.CLI.Services; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace AL2DBML.CLI.Commands; + +public class InitSettings : CommandSettings +{ +} + +public class InitCommand : AsyncCommand +{ + private const string HookStartMarker = "# [al2dbml-start]"; + private const string HookEndMarker = "# [al2dbml-end]"; + private const string HookCommand = "al2dbml generate"; + private const string GitignoreEntry = ".al2dbml/config.local.json"; + + private readonly IConfigService _configService; + + public InitCommand(IConfigService configService) + { + _configService = configService; + } + + protected override Task ExecuteAsync(CommandContext context, InitSettings settings, CancellationToken cancellationToken) + { + var existingShared = _configService.LoadSharedConfig(); + var existingLocal = _configService.LoadLocalConfig(); + + if (_configService.ConfigExists()) + AnsiConsole.MarkupLine("[yellow]Existing config found — pre-filling with current values.[/]"); + + var inputPath = AnsiConsole.Prompt( + new TextPrompt("Input path (you can point to a code-workspace file):") + .DefaultValue(existingLocal?.Input.Path ?? ".")); + + var outputPath = AnsiConsole.Prompt( + new TextPrompt("Output path:") + .DefaultValue(existingShared?.Output.Path ?? "./docs/")); + + var outputName = AnsiConsole.Prompt( + new TextPrompt("Output file name (without extension):") + .DefaultValue(existingShared?.Output.Name ?? "schema")); + + var hookExists = File.Exists(".git/hooks/pre-commit") && + File.ReadAllText(".git/hooks/pre-commit").Contains(HookStartMarker, StringComparison.Ordinal); + var createHook = AnsiConsole.Confirm("Create a pre-commit hook?", hookExists); + + _configService.SaveSharedConfig(new SharedConfig + { + Output = new OutputConfig { Path = outputPath, Name = outputName } + }); + _configService.SaveLocalConfig(new LocalConfig + { + Input = new InputConfig { Path = inputPath } + }); + + EnsureGitignoreEntry(GitignoreEntry); + + if (createHook) + WritePreCommitHook(); + + AnsiConsole.MarkupLine("[green]Done:[/] AL2DBML initialized."); + return Task.FromResult(0); + } + + private static void EnsureGitignoreEntry(string entry) + { + const string gitignorePath = ".gitignore"; + var lines = File.Exists(gitignorePath) + ? File.ReadAllLines(gitignorePath).ToList() + : []; + + if (lines.Contains(entry)) + return; + + lines.Add(entry); + File.WriteAllLines(gitignorePath, lines); + } + + private static void WritePreCommitHook() + { + const string hookPath = ".git/hooks/pre-commit"; + + if (!Directory.Exists(".git/hooks")) + { + AnsiConsole.MarkupLine("[yellow]Warning:[/] .git/hooks directory not found — skipping pre-commit hook creation."); + return; + } + var hookSection = $"{HookStartMarker}\nif command -v al2dbml > /dev/null 2>&1; then\n {HookCommand} || printf \"\\033[33mWarning: al2dbml generate failed, skipping DBML update.\\033[0m\\n\"\nelse\n printf \"\\033[33mWarning: al2dbml not found, skipping DBML update.\\033[0m\\n\"\nfi\n{HookEndMarker}"; + + string content; + if (File.Exists(hookPath)) + { + content = File.ReadAllText(hookPath); + var startIdx = content.IndexOf(HookStartMarker, StringComparison.Ordinal); + var endIdx = content.IndexOf(HookEndMarker, StringComparison.Ordinal); + + if (startIdx >= 0 && endIdx >= 0) + content = content[..startIdx] + hookSection + content[(endIdx + HookEndMarker.Length)..]; + else + content = content.TrimEnd() + $"\n\n{hookSection}\n"; + } + else + { + content = $"#!/bin/sh\n\n{hookSection}\n"; + } + + File.WriteAllText(hookPath, content); + + if (!OperatingSystem.IsWindows()) + File.SetUnixFileMode(hookPath, + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.GroupRead | UnixFileMode.GroupExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherExecute); + } +} diff --git a/src/AL2DBML.CLI/Models/LocalConfig.cs b/src/AL2DBML.CLI/Models/LocalConfig.cs new file mode 100644 index 0000000..86f2d70 --- /dev/null +++ b/src/AL2DBML.CLI/Models/LocalConfig.cs @@ -0,0 +1,11 @@ +namespace AL2DBML.CLI.Models; + +public class LocalConfig +{ + public InputConfig Input { get; set; } = new(); +} + +public class InputConfig +{ + public string Path { get; set; } = "."; +} diff --git a/src/AL2DBML.CLI/Models/SharedConfig.cs b/src/AL2DBML.CLI/Models/SharedConfig.cs new file mode 100644 index 0000000..8c29235 --- /dev/null +++ b/src/AL2DBML.CLI/Models/SharedConfig.cs @@ -0,0 +1,12 @@ +namespace AL2DBML.CLI.Models; + +public class SharedConfig +{ + public OutputConfig Output { get; set; } = new(); +} + +public class OutputConfig +{ + public string Path { get; set; } = "./docs/"; + public string Name { get; set; } = "schema"; +} diff --git a/src/AL2DBML.CLI/Program.cs b/src/AL2DBML.CLI/Program.cs index 1bcdaf0..598714a 100644 --- a/src/AL2DBML.CLI/Program.cs +++ b/src/AL2DBML.CLI/Program.cs @@ -11,7 +11,9 @@ services .AddAL2Dbml() .AddScoped() - .AddScoped(); + .AddScoped() + .AddScoped() + .AddScoped(); var registrar = new TypeRegistrar(services); @@ -20,6 +22,7 @@ app.Configure(config => { config.AddCommand("generate"); + config.AddCommand("init"); }); return await app.RunAsync(args); diff --git a/src/AL2DBML.CLI/Services/ConfigService.cs b/src/AL2DBML.CLI/Services/ConfigService.cs new file mode 100644 index 0000000..62350c1 --- /dev/null +++ b/src/AL2DBML.CLI/Services/ConfigService.cs @@ -0,0 +1,44 @@ +using System.Text.Json; +using AL2DBML.CLI.Models; + +namespace AL2DBML.CLI.Services; + +public class ConfigService : IConfigService +{ + private const string ConfigDir = ".al2dbml"; + private const string SharedConfigFile = "config.json"; + private const string LocalConfigFile = "config.local.json"; + + private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; + + public bool ConfigExists() => Directory.Exists(ConfigDir); + + public SharedConfig? LoadSharedConfig() => Load(Path.Combine(ConfigDir, SharedConfigFile)); + + public LocalConfig? LoadLocalConfig() => Load(Path.Combine(ConfigDir, LocalConfigFile)); + + private static T? Load(string path) + { + if (!File.Exists(path)) return default; + try + { + return JsonSerializer.Deserialize(File.ReadAllText(path), JsonOptions); + } + catch (Exception e) when (e is IOException or JsonException) + { + return default; + } + } + + public void SaveSharedConfig(SharedConfig config) + { + Directory.CreateDirectory(ConfigDir); + File.WriteAllText(Path.Combine(ConfigDir, SharedConfigFile), JsonSerializer.Serialize(config, JsonOptions)); + } + + public void SaveLocalConfig(LocalConfig config) + { + Directory.CreateDirectory(ConfigDir); + File.WriteAllText(Path.Combine(ConfigDir, LocalConfigFile), JsonSerializer.Serialize(config, JsonOptions)); + } +} diff --git a/src/AL2DBML.CLI/Services/IConfigService.cs b/src/AL2DBML.CLI/Services/IConfigService.cs new file mode 100644 index 0000000..b73388c --- /dev/null +++ b/src/AL2DBML.CLI/Services/IConfigService.cs @@ -0,0 +1,12 @@ +using AL2DBML.CLI.Models; + +namespace AL2DBML.CLI.Services; + +public interface IConfigService +{ + bool ConfigExists(); + SharedConfig? LoadSharedConfig(); + LocalConfig? LoadLocalConfig(); + void SaveSharedConfig(SharedConfig config); + void SaveLocalConfig(LocalConfig config); +}