diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 8a43f433..04672fef 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -27,6 +27,7 @@ {"id":"Grace-f1u","title":"PAT auth: SDK support","description":"Add SDK wrappers and server URI handling for PAT endpoints.","status":"closed","priority":1,"issue_type":"epic","created_at":"2026-01-01T17:33:44.9629598-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:49:56.514499-08:00","closed_at":"2026-01-01T17:49:56.514499-08:00","close_reason":"Completed"} {"id":"Grace-f1u.1","title":"Add PAT SDK wrappers","description":"Implement Grace.SDK PersonalAccessToken wrappers for create/list/revoke and update project compile list.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T17:35:17.2437547-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:49:41.0114696-08:00","closed_at":"2026-01-01T17:49:41.0114696-08:00","close_reason":"Completed","dependencies":[{"issue_id":"Grace-f1u.1","depends_on_id":"Grace-f1u","type":"parent-child","created_at":"2026-01-01T17:35:17.2831975-08:00","created_by":"daemon"}]} {"id":"Grace-f1u.2","title":"Ensure PAT SDK uses GRACE_SERVER_URI","description":"Make PAT SDK endpoints work without graceconfig.json by using GRACE_SERVER_URI or a helper that bypasses Configuration.Current().","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T17:35:24.0343531-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:49:49.2757481-08:00","closed_at":"2026-01-01T17:49:49.2757481-08:00","close_reason":"Completed","dependencies":[{"issue_id":"Grace-f1u.2","depends_on_id":"Grace-f1u","type":"parent-child","created_at":"2026-01-01T17:35:24.0786964-08:00","created_by":"daemon"}]} +{"id":"Grace-lp2","title":"Defer CLI defaults for help","description":"Implement deferred defaults + help customization to avoid config access during help. See spec in chat.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T21:13:38.8410648-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-02T21:52:40.9246985-08:00","closed_at":"2026-01-02T21:52:40.9246985-08:00","close_reason":"Closed"} {"id":"Grace-o3z","title":"PAT auth: Orleans actor storage","description":"Add PersonalAccessToken grain interface, state, proxy, and implementation.","status":"closed","priority":1,"issue_type":"epic","created_at":"2026-01-01T17:33:33.121442-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:42:57.8202313-08:00","closed_at":"2026-01-01T17:42:57.8202313-08:00","close_reason":"Completed"} {"id":"Grace-o3z.1","title":"Add PAT actor constants and interface","description":"Add PersonalAccessToken actor/state names and IPersonalAccessTokenActor interface in Grace.Actors.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T17:34:29.5312209-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:40:21.9276448-08:00","closed_at":"2026-01-01T17:40:21.9276448-08:00","close_reason":"Completed","dependencies":[{"issue_id":"Grace-o3z.1","depends_on_id":"Grace-o3z","type":"parent-child","created_at":"2026-01-01T17:34:29.5383697-08:00","created_by":"daemon"}]} {"id":"Grace-o3z.2","title":"Add PAT actor proxy helper","description":"Add ActorProxy.PersonalAccessToken.CreateActorProxy helper in Grace.Actors/ActorProxy.Extensions.Actor.fs.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T17:34:36.1715625-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:40:54.6647026-08:00","closed_at":"2026-01-01T17:40:54.6647026-08:00","close_reason":"Completed","dependencies":[{"issue_id":"Grace-o3z.2","depends_on_id":"Grace-o3z","type":"parent-child","created_at":"2026-01-01T17:34:36.1768734-08:00","created_by":"daemon"}]} diff --git a/.beads/metadata.json b/.beads/metadata.json index c787975e..7e132279 100644 --- a/.beads/metadata.json +++ b/.beads/metadata.json @@ -1,4 +1,5 @@ { "database": "beads.db", - "jsonl_export": "issues.jsonl" + "jsonl_export": "issues.jsonl", + "last_bd_version": "0.29.0" } \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 3a9fabd2..9e5eba7a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,13 @@ This project uses **bd** (beads) for issue tracking. The repo is initialized at the root `.beads`, so run `bd` from the repo root. +## Before You Start (MANDATORY) +- Always run `bd ready --json` from repo root before doing anything else. +- You MUST claim or create an issue before editing code: + - If a suitable issue exists: `bd update --status in_progress` + - If none exist: create a new issue, then set it in progress. +- Do not make code changes until an issue is in progress. + ## Quick Reference ```bash diff --git a/src/Grace.CLI.Tests/Grace.CLI.Tests.fsproj b/src/Grace.CLI.Tests/Grace.CLI.Tests.fsproj index fea9810f..206310e0 100644 --- a/src/Grace.CLI.Tests/Grace.CLI.Tests.fsproj +++ b/src/Grace.CLI.Tests/Grace.CLI.Tests.fsproj @@ -11,11 +11,12 @@ --test:ParallelIlxGen - - - - - + + + + + + diff --git a/src/Grace.CLI.Tests/HelpDoesNotReadConfig.Tests.fs b/src/Grace.CLI.Tests/HelpDoesNotReadConfig.Tests.fs new file mode 100644 index 00000000..9002e79c --- /dev/null +++ b/src/Grace.CLI.Tests/HelpDoesNotReadConfig.Tests.fs @@ -0,0 +1,115 @@ +namespace Grace.CLI.Tests + +open FsUnit +open Grace.CLI +open Grace.Shared.Client.Configuration +open Grace.Shared.Utilities +open NUnit.Framework +open System +open System.IO + +[] +module HelpDoesNotReadConfigTests = + + let private withTempDir (action: string -> unit) = + let tempDir = Path.Combine(Path.GetTempPath(), $"grace-cli-tests-{Guid.NewGuid():N}") + Directory.CreateDirectory(tempDir) |> ignore + let originalDir = Environment.CurrentDirectory + + try + Environment.CurrentDirectory <- tempDir + action tempDir + finally + Environment.CurrentDirectory <- originalDir + if Directory.Exists(tempDir) then + try + Directory.Delete(tempDir, true) + with _ -> + () + + let private writeInvalidConfig (root: string) = + let graceDir = Path.Combine(root, ".grace") + Directory.CreateDirectory(graceDir) |> ignore + File.WriteAllText(Path.Combine(graceDir, "graceconfig.json"), "not json") + + let private writeValidConfig (root: string) (ownerId: Guid) (orgId: Guid) (repoId: Guid) (branchId: Guid) = + let graceDir = Path.Combine(root, ".grace") + Directory.CreateDirectory(graceDir) |> ignore + let config = GraceConfiguration() + config.OwnerId <- ownerId + config.OrganizationId <- orgId + config.RepositoryId <- repoId + config.BranchId <- branchId + let json = serialize config + File.WriteAllText(Path.Combine(graceDir, "graceconfig.json"), json) + + [] + let ``help works with invalid config`` () = + withTempDir (fun root -> + writeInvalidConfig root + let exitCode = GraceCommand.main [| "access"; "grant-role"; "-h" |] + exitCode |> should equal 0) + + [] + let ``help works without config`` () = + withTempDir (fun _ -> + let exitCode = GraceCommand.main [| "access"; "grant-role"; "-h" |] + exitCode |> should equal 0) + + [] + let ``help shows symbolic defaults`` () = + withTempDir (fun _ -> + use writer = new StringWriter() + let originalOut = Console.Out + + try + Console.SetOut(writer) + let exitCode = GraceCommand.main [| "access"; "grant-role"; "-h" |] + exitCode |> should equal 0 + finally + Console.SetOut(originalOut) + + let output = writer.ToString() + output |> should contain "[default: current OwnerId]" + output |> should contain "[default: current OrganizationId]" + output |> should contain "[default: current RepositoryId]" + output |> should contain "[default: current BranchId]" + output |> should contain "[default: new NanoId]" + ) + + [] + let ``create help rewrites empty guid defaults`` () = + withTempDir (fun _ -> + use writer = new StringWriter() + let originalOut = Console.Out + + try + Console.SetOut(writer) + let exitCode = GraceCommand.main [| "repository"; "create"; "-h" |] + exitCode |> should equal 0 + finally + Console.SetOut(originalOut) + + let output = writer.ToString() + output |> should contain "[default: current OwnerId]" + output |> should contain "[default: current OrganizationId]" + output |> should contain "[default: new Guid]" + output |> should not' (contain "00000000-0000-0000-0000-000000000000") + ) + + [] + let ``getNormalizedIdsAndNames falls back to config ids`` () = + withTempDir (fun root -> + let ownerId = Guid.NewGuid() + let orgId = Guid.NewGuid() + let repoId = Guid.NewGuid() + let branchId = Guid.NewGuid() + writeValidConfig root ownerId orgId repoId branchId + + let parseResult = GraceCommand.rootCommand.Parse([| "access"; "grant-role" |]) + let graceIds = Services.getNormalizedIdsAndNames parseResult + + graceIds.OwnerId |> should equal ownerId + graceIds.OrganizationId |> should equal orgId + graceIds.RepositoryId |> should equal repoId + graceIds.BranchId |> should equal branchId) diff --git a/src/Grace.CLI/Command/Access.CLI.fs b/src/Grace.CLI/Command/Access.CLI.fs index ac0e0fb2..5fd6a0ad 100644 --- a/src/Grace.CLI/Command/Access.CLI.fs +++ b/src/Grace.CLI/Command/Access.CLI.fs @@ -33,7 +33,7 @@ module Access = Required = false, Description = "The owner ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = (fun _ -> if Current().OwnerId = Guid.Empty then Guid.Empty else Current().OwnerId) + DefaultValueFactory = (fun _ -> OwnerId.Empty) ) let organizationId = @@ -42,12 +42,7 @@ module Access = Required = false, Description = "The organization ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = - (fun _ -> - if Current().OrganizationId = Guid.Empty then - Guid.Empty - else - Current().OrganizationId) + DefaultValueFactory = (fun _ -> OrganizationId.Empty) ) let repositoryId = @@ -57,12 +52,7 @@ module Access = Required = false, Description = "The repository ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = - (fun _ -> - if Current().RepositoryId = Guid.Empty then - Guid.Empty - else - Current().RepositoryId) + DefaultValueFactory = (fun _ -> RepositoryId.Empty) ) let branchId = @@ -71,7 +61,7 @@ module Access = Required = false, Description = "The branch ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = (fun _ -> if Current().BranchId = Guid.Empty then Guid.Empty else Current().BranchId) + DefaultValueFactory = (fun _ -> BranchId.Empty) ) let principalTypeRequired = diff --git a/src/Grace.CLI/Command/Admin.CLI.fs b/src/Grace.CLI/Command/Admin.CLI.fs index 4a040158..4475b2c3 100644 --- a/src/Grace.CLI/Command/Admin.CLI.fs +++ b/src/Grace.CLI/Command/Admin.CLI.fs @@ -37,7 +37,7 @@ module Admin = Required = false, Description = "The repository's owner ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = (fun _ -> if Current().OwnerId = Guid.Empty then Guid.Empty else Current().OwnerId) + DefaultValueFactory = (fun _ -> OwnerId.Empty) ) let ownerName = @@ -54,12 +54,7 @@ module Admin = Required = false, Description = "The repository's organization ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = - (fun _ -> - if Current().OrganizationId = Guid.Empty then - Guid.Empty - else - Current().OrganizationId) + DefaultValueFactory = (fun _ -> OrganizationId.Empty) ) let organizationName = @@ -77,12 +72,7 @@ module Admin = Required = false, Description = "The repository's ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = - (fun _ -> - if Current().RepositoryId = Guid.Empty then - Guid.Empty - else - Current().RepositoryId) + DefaultValueFactory = (fun _ -> RepositoryId.Empty) ) let repositoryName = diff --git a/src/Grace.CLI/Command/Branch.CLI.fs b/src/Grace.CLI/Command/Branch.CLI.fs index a801eb77..db114bc4 100644 --- a/src/Grace.CLI/Command/Branch.CLI.fs +++ b/src/Grace.CLI/Command/Branch.CLI.fs @@ -65,7 +65,7 @@ module Branch = Required = false, Description = "The repository's owner ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = (fun _ -> if Current().OwnerId = Guid.Empty then Guid.NewGuid() else Current().OwnerId) + DefaultValueFactory = (fun _ -> OwnerId.Empty) ) let ownerName = @@ -82,12 +82,7 @@ module Branch = Required = false, Description = "The repository's organization ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = - (fun _ -> - if Current().OrganizationId = Guid.Empty then - Guid.NewGuid() - else - Current().OrganizationId) + DefaultValueFactory = (fun _ -> OrganizationId.Empty) ) let organizationName = @@ -105,12 +100,7 @@ module Branch = Required = false, Description = "The repository's ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = - (fun _ -> - if Current().RepositoryId = Guid.Empty then - Guid.NewGuid() - else - Current().RepositoryId) + DefaultValueFactory = (fun _ -> RepositoryId.Empty) ) let repositoryName = @@ -129,7 +119,7 @@ module Branch = Required = false, Description = "The branch's ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> if Current().BranchId = Guid.Empty then Guid.NewGuid() else Current().BranchId) + DefaultValueFactory = (fun _ -> BranchId.Empty) ) let branchName = @@ -160,7 +150,7 @@ module Branch = Required = false, Description = "The name of the parent branch. [default: current branch]", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> $"{Current().BranchName}") + DefaultValueFactory = (fun _ -> String.Empty) ) let newName = new Option(OptionName.NewName, Required = true, Description = "The new name of the branch.", Arity = ArgumentArity.ExactlyOne) @@ -484,7 +474,27 @@ module Branch = graceIds <- { graceIds with BranchId = branchId; BranchIdString = $"{branchId}" } let parentBranchId = parseResult.GetValue(Options.parentBranchId) - let parentBranchName = parseResult.GetValue(Options.parentBranchName) |> valueOrEmpty + let parentBranchNameResult = parseResult.GetResult(Options.parentBranchName) + let parentBranchIdResult = parseResult.GetResult(Options.parentBranchId) + let parentBranchNameExplicit = + not <| isNull parentBranchNameResult + && not parentBranchNameResult.Implicit + let parentBranchIdExplicit = + not <| isNull parentBranchIdResult + && not parentBranchIdResult.Implicit + + let parentBranchName = + let suppliedParentBranchName = parseResult.GetValue(Options.parentBranchName) |> valueOrEmpty + + if + not parentBranchNameExplicit + && not parentBranchIdExplicit + && suppliedParentBranchName = String.Empty + && parentBranchId = Guid.Empty + then + Current().BranchName + else + suppliedParentBranchName let! parentBranchIdString = task { @@ -2635,7 +2645,7 @@ module Branch = // Write the UpdatesInProgress file to let grace watch know to ignore these changes. // This file is deleted in the finally clause. - do! File.WriteAllTextAsync(updateInProgressFileName, "`grace switch` is in progress.") + do! File.WriteAllTextAsync(updateInProgressFileName (), "`grace switch` is in progress.") // Update working directory based on new GraceStatus.Index do! @@ -2657,7 +2667,7 @@ module Branch = t |> setProgressTaskValue showOutput 100.0 finally // Delete the UpdatesInProgress file. - File.Delete(updateInProgressFileName) + File.Delete(updateInProgressFileName ()) | Error error -> logToAnsiConsole Colors.Verbose $"Failed downloading files from object storage for {directoryVersion.RelativePath}." @@ -2801,13 +2811,13 @@ module Branch = override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task = task { - Directory.CreateDirectory(Path.GetDirectoryName(updateInProgressFileName)) + Directory.CreateDirectory(Path.GetDirectoryName(updateInProgressFileName ())) |> ignore try if parseResult |> verbose then printParseResult parseResult - do! File.WriteAllTextAsync(updateInProgressFileName, "`grace switch` is in progress.") + do! File.WriteAllTextAsync(updateInProgressFileName (), "`grace switch` is in progress.") let graceIds = parseResult |> getNormalizedIdsAndNames @@ -2830,8 +2840,8 @@ module Branch = let! result = switchHandler parseResult switchParameters return result finally - if File.Exists(updateInProgressFileName) then - File.Delete(updateInProgressFileName) + if File.Exists(updateInProgressFileName ()) then + File.Delete(updateInProgressFileName ()) } let rebaseHandler (graceIds: GraceIds) (graceStatus: GraceStatus) = @@ -3224,13 +3234,13 @@ module Branch = override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task = task { - Directory.CreateDirectory(Path.GetDirectoryName(updateInProgressFileName)) + Directory.CreateDirectory(Path.GetDirectoryName(updateInProgressFileName ())) |> ignore try if parseResult |> verbose then printParseResult parseResult - do! File.WriteAllTextAsync(updateInProgressFileName, "`grace rebase` is in progress.") + do! File.WriteAllTextAsync(updateInProgressFileName (), "`grace rebase` is in progress.") let graceIds = parseResult |> getNormalizedIdsAndNames let! graceStatus = readGraceStatusFile () @@ -3238,8 +3248,8 @@ module Branch = let! result = rebaseHandler graceIds graceStatus return result finally - if File.Exists(updateInProgressFileName) then - File.Delete(updateInProgressFileName) + if File.Exists(updateInProgressFileName ()) then + File.Delete(updateInProgressFileName ()) } let private statusHandler (parseResult: ParseResult) = diff --git a/src/Grace.CLI/Command/Common.CLI.fs b/src/Grace.CLI/Command/Common.CLI.fs index 05c5e50a..34f62e4c 100644 --- a/src/Grace.CLI/Command/Common.CLI.fs +++ b/src/Grace.CLI/Command/Common.CLI.fs @@ -55,7 +55,7 @@ module Common = Description = "CorrelationId for end-to-end tracking .", Arity = ArgumentArity.ExactlyOne, Recursive = true, - DefaultValueFactory = (fun _ -> generateCorrelationId ()) + DefaultValueFactory = (fun _ -> CorrelationId.Empty) ) let output = @@ -71,7 +71,7 @@ module Common = .AcceptOnlyFromAmong(listCases ()) /// Gets the correlationId value from the command's ParseResult. - let getCorrelationId (parseResult: ParseResult) = parseResult.GetValue(Options.correlationId) + let getCorrelationId (parseResult: ParseResult) = Services.resolveCorrelationId parseResult module Validations = /// Checks that a given name option is a valid Grace name. If the option is not present, it does not return an error. diff --git a/src/Grace.CLI/Command/Diff.CLI.fs b/src/Grace.CLI/Command/Diff.CLI.fs index 94ac84e0..572950dd 100644 --- a/src/Grace.CLI/Command/Diff.CLI.fs +++ b/src/Grace.CLI/Command/Diff.CLI.fs @@ -44,7 +44,7 @@ module Diff = Required = false, Description = "The repository's owner ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = (fun _ -> Current().OwnerId) + DefaultValueFactory = (fun _ -> OwnerId.Empty) ) let ownerName = @@ -61,7 +61,7 @@ module Diff = Required = false, Description = "The repository's organization ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = (fun _ -> Current().OrganizationId) + DefaultValueFactory = (fun _ -> OrganizationId.Empty) ) let organizationName = @@ -79,7 +79,7 @@ module Diff = Required = false, Description = "The repository's Id .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> Current().RepositoryId) + DefaultValueFactory = (fun _ -> RepositoryId.Empty) ) let repositoryName = @@ -98,7 +98,7 @@ module Diff = Required = false, Description = "The branch's ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> Current().BranchId) + DefaultValueFactory = (fun _ -> BranchId.Empty) ) let branchName = diff --git a/src/Grace.CLI/Command/DirectoryVersion.CLI.fs b/src/Grace.CLI/Command/DirectoryVersion.CLI.fs index 65a3ee45..0def1528 100644 --- a/src/Grace.CLI/Command/DirectoryVersion.CLI.fs +++ b/src/Grace.CLI/Command/DirectoryVersion.CLI.fs @@ -48,7 +48,7 @@ module DirectoryVersion = Required = false, Description = "The repository's owner ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = (fun _ -> Current().OwnerId) + DefaultValueFactory = (fun _ -> OwnerId.Empty) ) let ownerName = @@ -65,7 +65,7 @@ module DirectoryVersion = Required = false, Description = "The repository's organization ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> Current().OrganizationId) + DefaultValueFactory = (fun _ -> OrganizationId.Empty) ) let organizationName = @@ -83,7 +83,7 @@ module DirectoryVersion = Required = false, Description = "The repository's ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> Current().RepositoryId) + DefaultValueFactory = (fun _ -> RepositoryId.Empty) ) let repositoryName = diff --git a/src/Grace.CLI/Command/Maintenance.CLI.fs b/src/Grace.CLI/Command/Maintenance.CLI.fs index fc2f0135..08635d5c 100644 --- a/src/Grace.CLI/Command/Maintenance.CLI.fs +++ b/src/Grace.CLI/Command/Maintenance.CLI.fs @@ -37,7 +37,7 @@ module Maintenance = Required = false, Description = "The repository's owner ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = (fun _ -> Current().OwnerId) + DefaultValueFactory = (fun _ -> OwnerId.Empty) ) let ownerName = @@ -54,7 +54,7 @@ module Maintenance = Required = false, Description = "The repository's organization ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = (fun _ -> Current().OrganizationId) + DefaultValueFactory = (fun _ -> OrganizationId.Empty) ) let organizationName = @@ -72,7 +72,7 @@ module Maintenance = Required = false, Description = "The repository's ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> Current().RepositoryId) + DefaultValueFactory = (fun _ -> RepositoryId.Empty) ) let repositoryName = diff --git a/src/Grace.CLI/Command/Organization.CLI.fs b/src/Grace.CLI/Command/Organization.CLI.fs index 0090e923..5ffb9073 100644 --- a/src/Grace.CLI/Command/Organization.CLI.fs +++ b/src/Grace.CLI/Command/Organization.CLI.fs @@ -39,7 +39,7 @@ module Organization = Required = false, Description = "The organization's owner ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> if Current().OwnerId = Guid.Empty then Guid.NewGuid() else Current().OwnerId) + DefaultValueFactory = (fun _ -> OwnerId.Empty) ) let ownerName = @@ -56,12 +56,7 @@ module Organization = Required = false, Description = "The organization ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = - (fun _ -> - if Current().OrganizationId = Guid.Empty then - Guid.NewGuid() - else - Current().OrganizationId) + DefaultValueFactory = (fun _ -> OrganizationId.Empty) ) let organizationName = diff --git a/src/Grace.CLI/Command/Owner.CLI.fs b/src/Grace.CLI/Command/Owner.CLI.fs index 2caca277..27b9266c 100644 --- a/src/Grace.CLI/Command/Owner.CLI.fs +++ b/src/Grace.CLI/Command/Owner.CLI.fs @@ -36,7 +36,7 @@ module Owner = Required = false, Description = "The Id of the owner .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> if Current().OwnerId = Guid.Empty then Guid.NewGuid() else Current().OwnerId) + DefaultValueFactory = (fun _ -> OwnerId.Empty) ) let ownerName = diff --git a/src/Grace.CLI/Command/Reference.CLI.fs b/src/Grace.CLI/Command/Reference.CLI.fs index 319cf5cd..39053bd9 100644 --- a/src/Grace.CLI/Command/Reference.CLI.fs +++ b/src/Grace.CLI/Command/Reference.CLI.fs @@ -49,7 +49,7 @@ module Reference = Required = false, Description = "The branch's ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> Current().BranchId) + DefaultValueFactory = (fun _ -> BranchId.Empty) ) let branchName = @@ -70,7 +70,7 @@ module Reference = Required = false, Description = "The repository's owner ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = (fun _ -> Current().OwnerId) + DefaultValueFactory = (fun _ -> OwnerId.Empty) ) let ownerName = @@ -87,7 +87,7 @@ module Reference = Required = false, Description = "The repository's organization ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> Current().OrganizationId) + DefaultValueFactory = (fun _ -> OrganizationId.Empty) ) let organizationName = @@ -105,7 +105,7 @@ module Reference = Required = false, Description = "The repository's ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> Current().RepositoryId) + DefaultValueFactory = (fun _ -> RepositoryId.Empty) ) let repositoryName = @@ -133,7 +133,7 @@ module Reference = Required = false, Description = "The name of the parent branch. [default: current branch]", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> $"{Current().BranchName}") + DefaultValueFactory = (fun _ -> String.Empty) ) let newName = new Option(OptionName.NewName, Required = true, Description = "The new name of the branch.", Arity = ArgumentArity.ExactlyOne) diff --git a/src/Grace.CLI/Command/Repository.CLI.fs b/src/Grace.CLI/Command/Repository.CLI.fs index 49ed69ea..369f5024 100644 --- a/src/Grace.CLI/Command/Repository.CLI.fs +++ b/src/Grace.CLI/Command/Repository.CLI.fs @@ -38,7 +38,7 @@ module Repository = Required = false, Description = "The repository's owner ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = (fun _ -> if Current().OwnerId = Guid.Empty then Guid.NewGuid() else Current().OwnerId) + DefaultValueFactory = (fun _ -> OwnerId.Empty) ) let ownerName = @@ -55,12 +55,7 @@ module Repository = Required = false, Description = "The repository's organization ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = - (fun _ -> - if Current().OrganizationId = Guid.Empty then - Guid.NewGuid() - else - Current().OrganizationId) + DefaultValueFactory = (fun _ -> OrganizationId.Empty) ) let organizationName = @@ -78,12 +73,7 @@ module Repository = Required = false, Description = "The repository's ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = - (fun _ -> - if Current().RepositoryId = Guid.Empty then - Guid.NewGuid() - else - Current().RepositoryId) + DefaultValueFactory = (fun _ -> RepositoryId.Empty) ) let repositoryName = diff --git a/src/Grace.CLI/Command/Services.CLI.fs b/src/Grace.CLI/Command/Services.CLI.fs index ea59fb03..2480a276 100644 --- a/src/Grace.CLI/Command/Services.CLI.fs +++ b/src/Grace.CLI/Command/Services.CLI.fs @@ -83,6 +83,9 @@ module Services = let mutable graceWatchStatusUpdateTime = Instant.MinValue let mutable parseResult: ParseResult = null + let mutable private invocationCorrelationId: CorrelationId option = None + + let resetInvocationCorrelationId () = invocationCorrelationId <- None // Extension methods for dealing with local file changes. type DirectoryVersion with @@ -1274,7 +1277,7 @@ module Services = newGraceStatus /// Gets the file name used to indicate to `grace watch` that updates are in progress from another Grace command, and that it should ignore them. - let updateInProgressFileName = + let updateInProgressFileName () = let directory = Path.Combine(Path.GetTempPath(), "Grace", Current().BranchName) Directory.CreateDirectory(directory) |> ignore @@ -1575,19 +1578,54 @@ module Services = else false + let resolveCorrelationId (parseResult: ParseResult) : CorrelationId = + if + isOptionPresent parseResult OptionName.CorrelationId + && not <| isOptionResultImplicit parseResult OptionName.CorrelationId + then + parseResult.GetValue(OptionName.CorrelationId) + else + match invocationCorrelationId with + | Some correlationId -> correlationId + | None -> + let correlationId = generateCorrelationId () + invocationCorrelationId <- Some correlationId + correlationId + /// Adjusts command-line options to account for whether Id's or Name's were explicitly specified by the user, /// or should be taken from default values. let getNormalizedIdsAndNames (parseResult: ParseResult) = - let getNormalizedId (idOption: string) (nameOption: string) = - if - isOptionResultImplicit parseResult idOption - && isOptionPresent parseResult nameOption - && not <| isOptionResultImplicit parseResult nameOption - then + let isExplicitName (nameOption: string) = + isOptionPresent parseResult nameOption + && not <| isOptionResultImplicit parseResult nameOption + && not <| String.IsNullOrWhiteSpace(parseResult.GetValue(nameOption)) + + let needsFallback (idOption: string) (nameOption: string) = + isOptionPresent parseResult idOption + && isOptionResultImplicit parseResult idOption + && parseResult.GetValue(idOption) = Guid.Empty + && not <| isExplicitName nameOption + + let needsConfigFallback = + needsFallback OptionName.OwnerId OptionName.OwnerName + || needsFallback OptionName.OrganizationId OptionName.OrganizationName + || needsFallback OptionName.RepositoryId OptionName.RepositoryName + || needsFallback OptionName.BranchId OptionName.BranchName + + let config = if needsConfigFallback then Some(Current()) else None + + let getNormalizedId (idOption: string) (nameOption: string) (configValue: Guid) = + let isImplicit = isOptionResultImplicit parseResult idOption + let explicitName = isExplicitName nameOption + let idValue = parseResult.GetValue(idOption) + + if isImplicit && explicitName then Guid.Empty + elif isImplicit && idValue = Guid.Empty && not explicitName then + configValue else - parseResult.GetValue(idOption) + idValue // If the name was specified on the command line, but the id wasn't (i.e. the default value was specified, and Implicit = true), // then we should only send the name, and we set the id to Guid.Empty. @@ -1595,13 +1633,19 @@ module Services = let mutable graceIds = GraceIds.Default if isOptionPresent parseResult OptionName.CorrelationId then - graceIds <- { graceIds with CorrelationId = parseResult.GetValue(OptionName.CorrelationId) } + graceIds <- { graceIds with CorrelationId = resolveCorrelationId parseResult } if isOptionPresent parseResult OptionName.OwnerId || isOptionPresent parseResult OptionName.OwnerName then - let ownerId = getNormalizedId OptionName.OwnerId OptionName.OwnerName + let ownerId = + let configValue = + config + |> Option.map (fun current -> current.OwnerId) + |> Option.defaultValue OwnerId.Empty + + getNormalizedId OptionName.OwnerId OptionName.OwnerName configValue graceIds <- { graceIds with @@ -1614,7 +1658,13 @@ module Services = isOptionPresent parseResult OptionName.OrganizationId || isOptionPresent parseResult OptionName.OrganizationName then - let organizationId = getNormalizedId OptionName.OrganizationId OptionName.OrganizationName + let organizationId = + let configValue = + config + |> Option.map (fun current -> current.OrganizationId) + |> Option.defaultValue OrganizationId.Empty + + getNormalizedId OptionName.OrganizationId OptionName.OrganizationName configValue graceIds <- { graceIds with @@ -1627,7 +1677,13 @@ module Services = isOptionPresent parseResult OptionName.RepositoryId || isOptionPresent parseResult OptionName.RepositoryName then - let repositoryId = getNormalizedId OptionName.RepositoryId OptionName.RepositoryName + let repositoryId = + let configValue = + config + |> Option.map (fun current -> current.RepositoryId) + |> Option.defaultValue RepositoryId.Empty + + getNormalizedId OptionName.RepositoryId OptionName.RepositoryName configValue graceIds <- { graceIds with @@ -1640,7 +1696,13 @@ module Services = isOptionPresent parseResult OptionName.BranchId || isOptionPresent parseResult OptionName.BranchName then - let branchId = getNormalizedId OptionName.BranchId OptionName.BranchName + let branchId = + let configValue = + config + |> Option.map (fun current -> current.BranchId) + |> Option.defaultValue BranchId.Empty + + getNormalizedId OptionName.BranchId OptionName.BranchName configValue graceIds <- { graceIds with diff --git a/src/Grace.CLI/Command/Watch.CLI.fs b/src/Grace.CLI/Command/Watch.CLI.fs index fa0d45f6..b9f7e7aa 100644 --- a/src/Grace.CLI/Command/Watch.CLI.fs +++ b/src/Grace.CLI/Command/Watch.CLI.fs @@ -48,7 +48,7 @@ module Watch = Required = false, Description = "The repository's owner ID .", Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = (fun _ -> if Current().OwnerId = Guid.Empty then Guid.NewGuid() else Current().OwnerId) + DefaultValueFactory = (fun _ -> OwnerId.Empty) ) let ownerName = @@ -65,12 +65,7 @@ module Watch = Required = false, Description = "The repository's organization ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = - (fun _ -> - if Current().OrganizationId = Guid.Empty then - Guid.NewGuid() - else - Current().OrganizationId) + DefaultValueFactory = (fun _ -> OrganizationId.Empty) ) let organizationName = @@ -88,12 +83,7 @@ module Watch = Required = false, Description = "The repository's ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = - (fun _ -> - if Current().RepositoryId = Guid.Empty then - Guid.NewGuid() - else - Current().RepositoryId) + DefaultValueFactory = (fun _ -> RepositoryId.Empty) ) let repositoryName = @@ -112,7 +102,7 @@ module Watch = Required = false, Description = "The branch's ID .", Arity = ArgumentArity.ExactlyOne, - DefaultValueFactory = (fun _ -> if Current().BranchId = Guid.Empty then Guid.NewGuid() else Current().BranchId) + DefaultValueFactory = (fun _ -> BranchId.Empty) ) let branchName = @@ -146,7 +136,7 @@ module Watch = let fileDeleted filePath = logToConsole $"In Delete: filePath: {filePath}" let isNotDirectory path = not <| Directory.Exists(path) - let updateInProgress () = File.Exists(updateInProgressFileName) + let updateInProgress () = File.Exists(updateInProgressFileName ()) let updateNotInProgress () = not <| updateInProgress () let OnCreated (args: FileSystemEventArgs) = @@ -204,18 +194,18 @@ module Watch = logToAnsiConsole Colors.Error $"I saw that the FileSystemWatcher threw an exception: {args.GetException().Message}. grace watch should be restarted." let OnGraceUpdateInProgressCreated (args: FileSystemEventArgs) = - if args.FullPath = updateInProgressFileName then + if args.FullPath = updateInProgressFileName () then if updateInProgress () then logToAnsiConsole Colors.Important $"Update is in progress from another Grace instance." else - logToAnsiConsole Colors.Important $"{updateInProgressFileName} should already exist, but it doesn't." + logToAnsiConsole Colors.Important $"{updateInProgressFileName ()} should already exist, but it doesn't." let OnGraceUpdateInProgressDeleted (args: FileSystemEventArgs) = - if args.FullPath = updateInProgressFileName then + if args.FullPath = updateInProgressFileName () then if updateNotInProgress () then logToAnsiConsole Colors.Important $"Update has finished in another Grace instance." else - logToAnsiConsole Colors.Important $"{updateInProgressFileName} should have been deleted, but it hasn't yet." + logToAnsiConsole Colors.Important $"{updateInProgressFileName ()} should have been deleted, but it hasn't yet." /// Creates a FileSystemWatcher for the given path. let createFileSystemWatcher path = @@ -459,10 +449,10 @@ module Watch = use errored = Observable.FromEventPattern(rootDirectoryFileSystemWatcher, "Error").Select(fun e -> e.EventArgs).Subscribe(OnError) // I want all of the errors. - Directory.CreateDirectory(Path.GetDirectoryName(updateInProgressFileName)) + Directory.CreateDirectory(Path.GetDirectoryName(updateInProgressFileName ())) |> ignore - use updateInProgressFileSystemWatcher = createFileSystemWatcher (Path.GetDirectoryName(updateInProgressFileName)) + use updateInProgressFileSystemWatcher = createFileSystemWatcher (Path.GetDirectoryName(updateInProgressFileName ())) use updateInProgressChanged = Observable diff --git a/src/Grace.CLI/Program.CLI.fs b/src/Grace.CLI/Program.CLI.fs index 547588a2..608c5fb0 100644 --- a/src/Grace.CLI/Program.CLI.fs +++ b/src/Grace.CLI/Program.CLI.fs @@ -3,6 +3,7 @@ namespace Grace.CLI open Grace.CLI.Command open Grace.CLI.Common open Grace.CLI.Services +open Grace.CLI.Text open Grace.Shared open Grace.Shared.Client.Configuration open Grace.Shared.Converters @@ -41,7 +42,11 @@ module Configuration = module GraceCommand = - type OptionToUpdate = { optionName: string; command: string; display: string; displayOnCreate: string } + type OptionToUpdate = + { optionAlias: string + display: string + displayOnCreate: string + createParentCommand: string } /// Built-in aliases for Grace commands. let private aliases = @@ -102,6 +107,83 @@ module GraceCommand = else allOptions + let private replaceDefaultValue (line: string) (defaultValueText: string) = + let startIndex = line.IndexOf("[default:", StringComparison.OrdinalIgnoreCase) + + if startIndex >= 0 then + let endIndex = line.IndexOf("]", startIndex) + + if endIndex > startIndex then + $"{line.Substring(0, startIndex)}[default: {defaultValueText}]{line.Substring(endIndex + 1)}" + else + line + else + line + + let private rewriteHelpDefaults (helpText: string) (defaultsByAlias: IDictionary) = + let lines = helpText.Split(Environment.NewLine) + let output = ResizeArray(lines.Length) + let mutable pendingAlias: string option = None + let mutable i = 0 + + while i < lines.Length do + let line = lines[i] + let matchedAlias = defaultsByAlias.Keys |> Seq.tryFind (fun alias -> line.Contains(alias)) + + match matchedAlias with + | Some alias -> pendingAlias <- Some alias + | None -> () + + match pendingAlias with + | Some alias when line.Contains("[default:", StringComparison.OrdinalIgnoreCase) -> + let startIndex = line.IndexOf("[default:", StringComparison.OrdinalIgnoreCase) + let endIndex = line.IndexOf("]", startIndex) + + if endIndex > startIndex then + output.Add(replaceDefaultValue line defaultsByAlias[alias]) + pendingAlias <- None + i <- i + 1 + else + let prefix = line.Substring(0, startIndex) + output.Add($"{prefix}[default: {defaultsByAlias[alias]}]") + pendingAlias <- None + + let mutable j = i + 1 + let mutable foundEnd = false + + while j < lines.Length && not foundEnd do + let continuation = lines[j] + let continuationEndIndex = continuation.IndexOf("]") + + if continuationEndIndex >= 0 then + let suffix = + if continuationEndIndex < continuation.Length - 1 then + continuation.Substring(continuationEndIndex + 1) + else + String.Empty + + if not <| String.IsNullOrWhiteSpace(suffix) then + output.Add(suffix) + + foundEnd <- true + + j <- j + 1 + + i <- j + | Some alias when alias = OptionName.CorrelationId && line.Contains("CorrelationId") -> + if not <| line.Contains("[default:", StringComparison.OrdinalIgnoreCase) then + output.Add($"{line} [default: {defaultsByAlias[alias]}]") + else + output.Add(line) + + pendingAlias <- None + i <- i + 1 + | _ -> + output.Add(line) + i <- i + 1 + + String.Join(Environment.NewLine, output) + let rootCommand = // Create the root of the command tree. let rootCommand = new RootCommand("Grace Version Control System") @@ -261,6 +343,7 @@ module GraceCommand = let main args = let startTime = getCurrentInstant () Auth.configureSdkAuth () + Services.resetInvocationCorrelationId () // Create a MemoryCache instance. //let memoryCacheOptions = MemoryCacheOptions(TrackStatistics = false, TrackLinkedCacheEntries = false) @@ -308,124 +391,117 @@ module GraceCommand = try //let commandLineConfiguration = ParserConfiguration rootCommand - // Right now, in order to handle default values, we need to read the Grace configuration file - // as the commands are being built, and before they're executed. - // For instance, when the command is `grace repo create...`, the `repo` command is built by - // System.CommandLine in-memory, and that causes `graceconfig.json` to be read to get - // default values for things like OwnerId, OrganizationId, etc. - // If grace is being run in a directory where there's no config, that obviously won't work, so, - // first, we check if we have a config file. - // If we do have a config file, great! we can parse it and use it. - // If we don't, we're just going to print an error message and exit. - - if configurationFileExists () then - if args.Length > 0 then - let firstToken = if isCaseInsensitive then args[0].ToLowerInvariant() else args[0] - - // Adjust the casing of any options in the args array for case-insensitive OSes. - let properCasedArgs = - args - |> Array.map (fun arg -> - if isCaseInsensitive && arg.StartsWith("--") then - arg.ToLowerInvariant() - else - arg) - - // Check if the first token is an alias for a command. - if aliases.ContainsKey(firstToken) then - let newArgs = List() - - // Reverse the expanded command so we insert them in the right order. - for token in aliases[firstToken].Reverse() do - newArgs.Insert(0, token) - - for token in properCasedArgs[1..] do - newArgs.Add(token) - - //let newArgsString = String.Join(" ", newArgs) - //logToAnsiConsole Colors.Verbose $"Using alias for command: {firstToken} -> {newArgsString}." - argvNormalized <- newArgs.ToArray() - parseResult <- rootCommand.Parse(newArgs) - else - //let argsString = String.Join(" ", properCasedArgs) - //logToAnsiConsole Colors.Verbose $"No alias; parsing command line arguments: {argsString}." - argvNormalized <- properCasedArgs - parseResult <- rootCommand.Parse(properCasedArgs) + let argvToParse = + if args.Length = 0 then + [| helpOptions[0] |] else - // If we have no tokens, we want to show the help text for the command, - // so we parse the command again with one of the help options to get the help text. - parseResult <- rootCommand.Parse(helpOptions[0]) - - parseSucceeded <- parseResult.Errors.Count = 0 - // Write the ParseResult to Services as global context for the CLI. - Services.parseResult <- parseResult - - match parseResult.Action with - | :? HelpAction as helpAction -> - if - parseResult.Tokens.Count = 0 - || (parseResult.Tokens.Count = 1 - && helpOptions.Contains(parseResult.Tokens[0].Value)) - then - // This is where we configure how help is displayed by Grace. - // We want to show the help text for the command, and then the feedback section at the end. - let graceFiglet = FigletText($"Grace") - graceFiglet.Justification <- Justify.Center - graceFiglet.Color <- Color.Green3_1 - AnsiConsole.Write(graceFiglet) - let graceFiglet = FigletText($"Version Control System") - graceFiglet.Justification <- Justify.Center - graceFiglet.Color <- Color.DarkOrange - AnsiConsole.Write(graceFiglet) - AnsiConsole.WriteLine() - - for i in 0 .. rootCommand.Options.Count - 1 do - match rootCommand.Options[i] with - | :? HelpOption as defaultHelpOption -> defaultHelpOption.Action <- FeedbackSection(defaultHelpOption.Action :?> HelpAction) - | _ -> () - - //helpAction.Builder.CustomizeLayout(fun layoutContext -> - // HelpBuilder.Default - // .GetLayout() - // .Where(fun section -> - // not - // <| section.Method.Name.Contains("Synopsis", StringComparison.InvariantCultureIgnoreCase)) - // .Append(feedbackSection)) - - // We're passing a new List