From fbb4c29d38c532e2648bf4866e1f70c94b5fffee Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Mon, 13 Apr 2026 13:24:07 +0200 Subject: [PATCH] Add document CLI commands Add list, create, view, update, delete, archive, and unarchive subcommands under `prb document`. Add document version subcommands: list-versions, view-version, create-draft, delete-draft, update-version, publish-major, and publish-minor. Signed-off-by: Sacha Al Himdani --- pkg/cmd/document/archive/archive.go | 97 +++++++ pkg/cmd/document/create-draft/create_draft.go | 111 +++++++ pkg/cmd/document/create/create.go | 239 ++++++++++++++++ pkg/cmd/document/delete-draft/delete_draft.go | 102 +++++++ pkg/cmd/document/delete/delete.go | 102 +++++++ pkg/cmd/document/document.go | 58 ++++ .../document/list-versions/list_versions.go | 219 ++++++++++++++ pkg/cmd/document/list/list.go | 270 ++++++++++++++++++ .../document/publish-major/publish_major.go | 115 ++++++++ .../document/publish-minor/publish_minor.go | 115 ++++++++ pkg/cmd/document/unarchive/unarchive.go | 97 +++++++ .../document/update-version/update_version.go | 154 ++++++++++ pkg/cmd/document/update/update.go | 136 +++++++++ pkg/cmd/document/view-version/view_version.go | 158 ++++++++++ pkg/cmd/document/view/view.go | 174 +++++++++++ pkg/cmd/root/root.go | 2 + 16 files changed, 2149 insertions(+) create mode 100644 pkg/cmd/document/archive/archive.go create mode 100644 pkg/cmd/document/create-draft/create_draft.go create mode 100644 pkg/cmd/document/create/create.go create mode 100644 pkg/cmd/document/delete-draft/delete_draft.go create mode 100644 pkg/cmd/document/delete/delete.go create mode 100644 pkg/cmd/document/document.go create mode 100644 pkg/cmd/document/list-versions/list_versions.go create mode 100644 pkg/cmd/document/list/list.go create mode 100644 pkg/cmd/document/publish-major/publish_major.go create mode 100644 pkg/cmd/document/publish-minor/publish_minor.go create mode 100644 pkg/cmd/document/unarchive/unarchive.go create mode 100644 pkg/cmd/document/update-version/update_version.go create mode 100644 pkg/cmd/document/update/update.go create mode 100644 pkg/cmd/document/view-version/view_version.go create mode 100644 pkg/cmd/document/view/view.go diff --git a/pkg/cmd/document/archive/archive.go b/pkg/cmd/document/archive/archive.go new file mode 100644 index 000000000..546a0747f --- /dev/null +++ b/pkg/cmd/document/archive/archive.go @@ -0,0 +1,97 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package archive + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const archiveMutation = ` +mutation($input: ArchiveDocumentInput!) { + archiveDocument(input: $input) { + document { + id + status + } + } +} +` + +type archiveResponse struct { + ArchiveDocument struct { + Document struct { + ID string `json:"id"` + Status string `json:"status"` + } `json:"document"` + } `json:"archiveDocument"` +} + +func NewCmdArchive(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "archive ", + Short: "Archive a document", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + data, err := client.Do( + archiveMutation, + map[string]any{ + "input": map[string]any{ + "documentId": args[0], + }, + }, + ) + if err != nil { + return err + } + + var resp archiveResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Archived document %s\n", + resp.ArchiveDocument.Document.ID, + ) + + return nil + }, + } + + return cmd +} diff --git a/pkg/cmd/document/create-draft/create_draft.go b/pkg/cmd/document/create-draft/create_draft.go new file mode 100644 index 000000000..415c3acf9 --- /dev/null +++ b/pkg/cmd/document/create-draft/create_draft.go @@ -0,0 +1,111 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package createdraft + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const createDraftMutation = ` +mutation($input: CreateDraftDocumentVersionInput!) { + createDraftDocumentVersion(input: $input) { + documentVersionEdge { + node { + id + title + major + minor + status + } + } + } +} +` + +type createDraftResponse struct { + CreateDraftDocumentVersion struct { + DocumentVersionEdge struct { + Node struct { + ID string `json:"id"` + Title string `json:"title"` + Major int `json:"major"` + Minor int `json:"minor"` + Status string `json:"status"` + } `json:"node"` + } `json:"documentVersionEdge"` + } `json:"createDraftDocumentVersion"` +} + +func NewCmdCreateDraft(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "create-draft ", + Short: "Create a new draft version of a document", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + data, err := client.Do( + createDraftMutation, + map[string]any{ + "input": map[string]any{ + "documentID": args[0], + }, + }, + ) + if err != nil { + return err + } + + var resp createDraftResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + v := resp.CreateDraftDocumentVersion.DocumentVersionEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created draft version %s (%s v%d.%d)\n", + v.ID, + v.Title, + v.Major, + v.Minor, + ) + + return nil + }, + } + + return cmd +} diff --git a/pkg/cmd/document/create/create.go b/pkg/cmd/document/create/create.go new file mode 100644 index 000000000..1b4bf02ef --- /dev/null +++ b/pkg/cmd/document/create/create.go @@ -0,0 +1,239 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package create + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const createMutation = ` +mutation($input: CreateDocumentInput!) { + createDocument(input: $input) { + documentEdge { + node { + id + } + } + documentVersionEdge { + node { + title + documentType + classification + } + } + } +} +` + +type createResponse struct { + CreateDocument struct { + DocumentEdge struct { + Node struct { + ID string `json:"id"` + } `json:"node"` + } `json:"documentEdge"` + DocumentVersionEdge struct { + Node struct { + Title string `json:"title"` + DocumentType string `json:"documentType"` + Classification string `json:"classification"` + } `json:"node"` + } `json:"documentVersionEdge"` + } `json:"createDocument"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagTitle string + flagContent string + flagDocumentType string + flagClassification string + flagTrustCenterVisibility string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new document", + Example: ` # Create a policy document + prb document create --title "Information Security Policy" --document-type POLICY --classification INTERNAL + + # Create a document interactively + prb document create`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + if flagOrg == "" { + flagOrg = hc.Organization + } + + if flagOrg == "" { + return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'") + } + + if f.IOStreams.IsInteractive() { + if flagTitle == "" { + err := huh.NewInput(). + Title("Document title"). + Value(&flagTitle). + Run() + if err != nil { + return err + } + } + + if flagDocumentType == "" { + err := huh.NewSelect[string](). + Title("Document type"). + Options( + huh.NewOption("Policy", "POLICY"), + huh.NewOption("Procedure", "PROCEDURE"), + huh.NewOption("Governance", "GOVERNANCE"), + huh.NewOption("Plan", "PLAN"), + huh.NewOption("Register", "REGISTER"), + huh.NewOption("Record", "RECORD"), + huh.NewOption("Report", "REPORT"), + huh.NewOption("Template", "TEMPLATE"), + huh.NewOption("Other", "OTHER"), + ). + Value(&flagDocumentType). + Run() + if err != nil { + return err + } + } + + if flagClassification == "" { + err := huh.NewSelect[string](). + Title("Classification"). + Options( + huh.NewOption("Public", "PUBLIC"), + huh.NewOption("Internal", "INTERNAL"), + huh.NewOption("Confidential", "CONFIDENTIAL"), + huh.NewOption("Secret", "SECRET"), + ). + Value(&flagClassification). + Run() + if err != nil { + return err + } + } + } + + if flagTitle == "" { + return fmt.Errorf("title is required; pass --title or run interactively") + } + if flagDocumentType == "" { + return fmt.Errorf("document type is required; pass --document-type or run interactively") + } + if flagClassification == "" { + return fmt.Errorf("classification is required; pass --classification or run interactively") + } + + if err := cmdutil.ValidateEnum( + "document-type", + flagDocumentType, + []string{"OTHER", "GOVERNANCE", "POLICY", "PROCEDURE", "PLAN", "REGISTER", "RECORD", "REPORT", "TEMPLATE"}, + ); err != nil { + return err + } + + if err := cmdutil.ValidateEnum( + "classification", + flagClassification, + []string{"PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"}, + ); err != nil { + return err + } + + input := map[string]any{ + "organizationId": flagOrg, + "title": flagTitle, + "documentType": flagDocumentType, + "classification": flagClassification, + } + + if flagContent != "" { + input["content"] = flagContent + } + + if flagTrustCenterVisibility != "" { + if err := cmdutil.ValidateEnum( + "trust-center-visibility", + flagTrustCenterVisibility, + []string{"NONE", "PRIVATE", "PUBLIC"}, + ); err != nil { + return err + } + input["trustCenterVisibility"] = flagTrustCenterVisibility + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + doc := resp.CreateDocument.DocumentEdge.Node + ver := resp.CreateDocument.DocumentVersionEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created document %s (%s)\n", + doc.ID, + ver.Title, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagTitle, "title", "", "Document title") + cmd.Flags().StringVar(&flagContent, "content", "", "Document content") + cmd.Flags().StringVar(&flagDocumentType, "document-type", "", "Document type: OTHER, GOVERNANCE, POLICY, PROCEDURE, PLAN, REGISTER, RECORD, REPORT, TEMPLATE") + cmd.Flags().StringVar(&flagClassification, "classification", "", "Classification: PUBLIC, INTERNAL, CONFIDENTIAL, SECRET") + cmd.Flags().StringVar(&flagTrustCenterVisibility, "trust-center-visibility", "", "Trust center visibility: NONE, PRIVATE, PUBLIC") + + return cmd +} diff --git a/pkg/cmd/document/delete-draft/delete_draft.go b/pkg/cmd/document/delete-draft/delete_draft.go new file mode 100644 index 000000000..e2a3ebbae --- /dev/null +++ b/pkg/cmd/document/delete-draft/delete_draft.go @@ -0,0 +1,102 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package deletedraft + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteDraftMutation = ` +mutation($input: DeleteDraftDocumentVersionInput!) { + deleteDraftDocumentVersion(input: $input) { + deletedDocumentVersionId + } +} +` + +func NewCmdDeleteDraft(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete-draft ", + Short: "Delete a draft document version", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete draft: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete draft version %s?", args[0])). + Value(&confirmed). + Run() + if err != nil { + return err + } + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + _, err = client.Do( + deleteDraftMutation, + map[string]any{ + "input": map[string]any{ + "documentVersionId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted draft version %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/document/delete/delete.go b/pkg/cmd/document/delete/delete.go new file mode 100644 index 000000000..c3067a886 --- /dev/null +++ b/pkg/cmd/document/delete/delete.go @@ -0,0 +1,102 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteDocumentInput!) { + deleteDocument(input: $input) { + deletedDocumentId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a document", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete document: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete document %s?", args[0])). + Value(&confirmed). + Run() + if err != nil { + return err + } + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + _, err = client.Do( + deleteMutation, + map[string]any{ + "input": map[string]any{ + "documentId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted document %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/document/document.go b/pkg/cmd/document/document.go new file mode 100644 index 000000000..46bd7caa5 --- /dev/null +++ b/pkg/cmd/document/document.go @@ -0,0 +1,58 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package document + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/document/archive" + "go.probo.inc/probo/pkg/cmd/document/create" + createdraft "go.probo.inc/probo/pkg/cmd/document/create-draft" + "go.probo.inc/probo/pkg/cmd/document/delete" + deletedraft "go.probo.inc/probo/pkg/cmd/document/delete-draft" + "go.probo.inc/probo/pkg/cmd/document/list" + listversions "go.probo.inc/probo/pkg/cmd/document/list-versions" + publishmajor "go.probo.inc/probo/pkg/cmd/document/publish-major" + publishminor "go.probo.inc/probo/pkg/cmd/document/publish-minor" + "go.probo.inc/probo/pkg/cmd/document/unarchive" + "go.probo.inc/probo/pkg/cmd/document/update" + updateversion "go.probo.inc/probo/pkg/cmd/document/update-version" + "go.probo.inc/probo/pkg/cmd/document/view" + viewversion "go.probo.inc/probo/pkg/cmd/document/view-version" +) + +func NewCmdDocument(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "document ", + Short: "Manage documents", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + cmd.AddCommand(archive.NewCmdArchive(f)) + cmd.AddCommand(unarchive.NewCmdUnarchive(f)) + cmd.AddCommand(listversions.NewCmdListVersions(f)) + cmd.AddCommand(viewversion.NewCmdViewVersion(f)) + cmd.AddCommand(createdraft.NewCmdCreateDraft(f)) + cmd.AddCommand(deletedraft.NewCmdDeleteDraft(f)) + cmd.AddCommand(updateversion.NewCmdUpdateVersion(f)) + cmd.AddCommand(publishmajor.NewCmdPublishMajor(f)) + cmd.AddCommand(publishminor.NewCmdPublishMinor(f)) + + return cmd +} diff --git a/pkg/cmd/document/list-versions/list_versions.go b/pkg/cmd/document/list-versions/list_versions.go new file mode 100644 index 000000000..b681502d1 --- /dev/null +++ b/pkg/cmd/document/list-versions/list_versions.go @@ -0,0 +1,219 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package listversions + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey, $orderBy: DocumentVersionOrder, $filter: DocumentVersionFilter) { + node(id: $id) { + __typename + ... on Document { + versions(first: $first, after: $after, orderBy: $orderBy, filter: $filter) { + totalCount + edges { + node { + id + title + major + minor + status + documentType + classification + publishedAt + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type documentVersion struct { + ID string `json:"id"` + Title string `json:"title"` + Major int `json:"major"` + Minor int `json:"minor"` + Status string `json:"status"` + DocumentType string `json:"documentType"` + Classification string `json:"classification"` + PublishedAt *string `json:"publishedAt"` + CreatedAt string `json:"createdAt"` +} + +func NewCmdListVersions(f *cmdutil.Factory) *cobra.Command { + var ( + flagLimit int + flagOrderBy string + flagOrderDir string + flagStatus string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list-versions ", + Short: "List versions of a document", + Example: ` # List all versions of a document + prb document list-versions + + # List only published versions + prb document list-versions --status PUBLISHED`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + variables := map[string]any{ + "id": args[0], + } + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum( + "order-by", + flagOrderBy, + []string{"CREATED_AT"}, + ); err != nil { + return err + } + if err := cmdutil.ValidateEnum( + "order-direction", + flagOrderDir, + []string{"ASC", "DESC"}, + ); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + if flagStatus != "" { + if err := cmdutil.ValidateEnum( + "status", + flagStatus, + []string{"DRAFT", "PENDING_APPROVAL", "PUBLISHED"}, + ); err != nil { + return err + } + variables["filter"] = map[string]any{ + "statuses": []string{flagStatus}, + } + } + + versions, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[documentVersion], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Versions api.Connection[documentVersion] `json:"versions"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + if resp.Node == nil { + return nil, fmt.Errorf("document %s not found", args[0]) + } + if resp.Node.Typename != "Document" { + return nil, fmt.Errorf("expected Document node, got %s", resp.Node.Typename) + } + return &resp.Node.Versions, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, versions) + } + + if len(versions) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No versions found.") + return nil + } + + rows := make([][]string, 0, len(versions)) + for _, v := range versions { + rows = append(rows, []string{ + v.ID, + v.Title, + fmt.Sprintf("%d.%d", v.Major, v.Minor), + v.Status, + v.DocumentType, + v.Classification, + }) + } + + t := cmdutil.NewTable("ID", "TITLE", "VERSION", "STATUS", "TYPE", "CLASSIFICATION").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(versions) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d versions\n", + len(versions), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of versions to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + cmd.Flags().StringVar(&flagStatus, "status", "", "Filter by status (DRAFT, PENDING_APPROVAL, PUBLISHED)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/document/list/list.go b/pkg/cmd/document/list/list.go new file mode 100644 index 000000000..70e78a587 --- /dev/null +++ b/pkg/cmd/document/list/list.go @@ -0,0 +1,270 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey, $orderBy: DocumentOrder, $filter: DocumentFilter) { + node(id: $id) { + __typename + ... on Organization { + documents(first: $first, after: $after, orderBy: $orderBy, filter: $filter) { + totalCount + edges { + node { + id + status + trustCenterVisibility + versions(first: 1) { + edges { + node { + title + documentType + classification + } + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type document struct { + ID string `json:"id"` + Status string `json:"status"` + TrustCenterVisibility string `json:"trustCenterVisibility"` + Versions struct { + Edges []struct { + Node struct { + Title string `json:"title"` + DocumentType string `json:"documentType"` + Classification string `json:"classification"` + } `json:"node"` + } `json:"edges"` + } `json:"versions"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagLimit int + flagOrderBy string + flagOrderDir string + flagQuery string + flagDocumentType string + flagClassification string + flagStatus string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List documents in an organization", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + if flagOrg == "" { + flagOrg = hc.Organization + } + + if flagOrg == "" { + return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'") + } + + variables := map[string]any{ + "id": flagOrg, + } + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum( + "order-by", + flagOrderBy, + []string{"TITLE", "CREATED_AT", "UPDATED_AT", "DOCUMENT_TYPE"}, + ); err != nil { + return err + } + if err := cmdutil.ValidateEnum( + "order-direction", + flagOrderDir, + []string{"ASC", "DESC"}, + ); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + filter := map[string]any{} + if flagQuery != "" { + filter["query"] = flagQuery + } + if flagDocumentType != "" { + if err := cmdutil.ValidateEnum( + "document-type", + flagDocumentType, + []string{"OTHER", "GOVERNANCE", "POLICY", "PROCEDURE", "PLAN", "REGISTER", "RECORD", "REPORT", "TEMPLATE"}, + ); err != nil { + return err + } + filter["documentTypes"] = []string{flagDocumentType} + } + if flagClassification != "" { + if err := cmdutil.ValidateEnum( + "classification", + flagClassification, + []string{"PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"}, + ); err != nil { + return err + } + filter["classifications"] = []string{flagClassification} + } + if flagStatus != "" { + if err := cmdutil.ValidateEnum( + "status", + flagStatus, + []string{"ACTIVE", "ARCHIVED"}, + ); err != nil { + return err + } + filter["status"] = []string{flagStatus} + } + if len(filter) > 0 { + variables["filter"] = filter + } + + documents, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[document], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Documents api.Connection[document] `json:"documents"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + if resp.Node == nil { + return nil, fmt.Errorf("organization %s not found", flagOrg) + } + if resp.Node.Typename != "Organization" { + return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) + } + return &resp.Node.Documents, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, documents) + } + + if len(documents) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No documents found.") + return nil + } + + rows := make([][]string, 0, len(documents)) + for _, doc := range documents { + title := "" + docType := "" + classification := "" + if len(doc.Versions.Edges) > 0 { + v := doc.Versions.Edges[0].Node + title = v.Title + docType = v.DocumentType + classification = v.Classification + } + rows = append(rows, []string{ + doc.ID, + title, + docType, + classification, + doc.Status, + }) + } + + t := cmdutil.NewTable("ID", "TITLE", "TYPE", "CLASSIFICATION", "STATUS").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(documents) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d documents\n", + len(documents), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of documents to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (TITLE, CREATED_AT, UPDATED_AT, DOCUMENT_TYPE)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + cmd.Flags().StringVarP(&flagQuery, "query", "q", "", "Search query") + cmd.Flags().StringVar(&flagDocumentType, "document-type", "", "Filter by document type (OTHER, GOVERNANCE, POLICY, PROCEDURE, PLAN, REGISTER, RECORD, REPORT, TEMPLATE)") + cmd.Flags().StringVar(&flagClassification, "classification", "", "Filter by classification (PUBLIC, INTERNAL, CONFIDENTIAL, SECRET)") + cmd.Flags().StringVar(&flagStatus, "status", "", "Filter by status (ACTIVE, ARCHIVED)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/document/publish-major/publish_major.go b/pkg/cmd/document/publish-major/publish_major.go new file mode 100644 index 000000000..d9058712d --- /dev/null +++ b/pkg/cmd/document/publish-major/publish_major.go @@ -0,0 +1,115 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package publishmajor + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const publishMajorMutation = ` +mutation($input: PublishMajorDocumentVersionInput!) { + publishMajorDocumentVersion(input: $input) { + documentVersion { + id + title + major + minor + status + } + } +} +` + +type publishResponse struct { + PublishMajorDocumentVersion struct { + DocumentVersion struct { + ID string `json:"id"` + Title string `json:"title"` + Major int `json:"major"` + Minor int `json:"minor"` + Status string `json:"status"` + } `json:"documentVersion"` + } `json:"publishMajorDocumentVersion"` +} + +func NewCmdPublishMajor(f *cmdutil.Factory) *cobra.Command { + var flagChangelog string + + cmd := &cobra.Command{ + Use: "publish-major ", + Short: "Publish a major version of a document", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + input := map[string]any{ + "documentId": args[0], + } + + if flagChangelog != "" { + input["changelog"] = flagChangelog + } + + data, err := client.Do( + publishMajorMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp publishResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + v := resp.PublishMajorDocumentVersion.DocumentVersion + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Published major version %s (%s v%d.%d)\n", + v.ID, + v.Title, + v.Major, + v.Minor, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagChangelog, "changelog", "", "Changelog for this version") + + return cmd +} diff --git a/pkg/cmd/document/publish-minor/publish_minor.go b/pkg/cmd/document/publish-minor/publish_minor.go new file mode 100644 index 000000000..20c127802 --- /dev/null +++ b/pkg/cmd/document/publish-minor/publish_minor.go @@ -0,0 +1,115 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package publishminor + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const publishMinorMutation = ` +mutation($input: PublishMinorDocumentVersionInput!) { + publishMinorDocumentVersion(input: $input) { + documentVersion { + id + title + major + minor + status + } + } +} +` + +type publishResponse struct { + PublishMinorDocumentVersion struct { + DocumentVersion struct { + ID string `json:"id"` + Title string `json:"title"` + Major int `json:"major"` + Minor int `json:"minor"` + Status string `json:"status"` + } `json:"documentVersion"` + } `json:"publishMinorDocumentVersion"` +} + +func NewCmdPublishMinor(f *cmdutil.Factory) *cobra.Command { + var flagChangelog string + + cmd := &cobra.Command{ + Use: "publish-minor ", + Short: "Publish a minor version of a document", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + input := map[string]any{ + "documentId": args[0], + } + + if flagChangelog != "" { + input["changelog"] = flagChangelog + } + + data, err := client.Do( + publishMinorMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp publishResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + v := resp.PublishMinorDocumentVersion.DocumentVersion + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Published minor version %s (%s v%d.%d)\n", + v.ID, + v.Title, + v.Major, + v.Minor, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagChangelog, "changelog", "", "Changelog for this version") + + return cmd +} diff --git a/pkg/cmd/document/unarchive/unarchive.go b/pkg/cmd/document/unarchive/unarchive.go new file mode 100644 index 000000000..f620611ac --- /dev/null +++ b/pkg/cmd/document/unarchive/unarchive.go @@ -0,0 +1,97 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package unarchive + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const unarchiveMutation = ` +mutation($input: UnarchiveDocumentInput!) { + unarchiveDocument(input: $input) { + document { + id + status + } + } +} +` + +type unarchiveResponse struct { + UnarchiveDocument struct { + Document struct { + ID string `json:"id"` + Status string `json:"status"` + } `json:"document"` + } `json:"unarchiveDocument"` +} + +func NewCmdUnarchive(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "unarchive ", + Short: "Unarchive a document", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + data, err := client.Do( + unarchiveMutation, + map[string]any{ + "input": map[string]any{ + "documentId": args[0], + }, + }, + ) + if err != nil { + return err + } + + var resp unarchiveResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Unarchived document %s\n", + resp.UnarchiveDocument.Document.ID, + ) + + return nil + }, + } + + return cmd +} diff --git a/pkg/cmd/document/update-version/update_version.go b/pkg/cmd/document/update-version/update_version.go new file mode 100644 index 000000000..3a081606f --- /dev/null +++ b/pkg/cmd/document/update-version/update_version.go @@ -0,0 +1,154 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package updateversion + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const updateMutation = ` +mutation($input: UpdateDocumentVersionInput!) { + updateDocumentVersion(input: $input) { + documentVersion { + id + title + major + minor + status + documentType + classification + } + } +} +` + +type updateResponse struct { + UpdateDocumentVersion struct { + DocumentVersion struct { + ID string `json:"id"` + Title string `json:"title"` + Major int `json:"major"` + Minor int `json:"minor"` + Status string `json:"status"` + DocumentType string `json:"documentType"` + Classification string `json:"classification"` + } `json:"documentVersion"` + } `json:"updateDocumentVersion"` +} + +func NewCmdUpdateVersion(f *cmdutil.Factory) *cobra.Command { + var ( + flagTitle string + flagContent string + flagDocumentType string + flagClassification string + ) + + cmd := &cobra.Command{ + Use: "update-version ", + Short: "Update a document version", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + input := map[string]any{ + "documentVersionId": args[0], + } + + if cmd.Flags().Changed("title") { + input["title"] = flagTitle + } + if cmd.Flags().Changed("content") { + input["content"] = flagContent + } + if cmd.Flags().Changed("document-type") { + if err := cmdutil.ValidateEnum( + "document-type", + flagDocumentType, + []string{"OTHER", "GOVERNANCE", "POLICY", "PROCEDURE", "PLAN", "REGISTER", "RECORD", "REPORT", "TEMPLATE"}, + ); err != nil { + return err + } + input["documentType"] = flagDocumentType + } + if cmd.Flags().Changed("classification") { + if err := cmdutil.ValidateEnum( + "classification", + flagClassification, + []string{"PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"}, + ); err != nil { + return err + } + input["classification"] = flagClassification + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do( + updateMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + v := resp.UpdateDocumentVersion.DocumentVersion + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated version %s (%s v%d.%d)\n", + v.ID, + v.Title, + v.Major, + v.Minor, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagTitle, "title", "", "Version title") + cmd.Flags().StringVar(&flagContent, "content", "", "Version content") + cmd.Flags().StringVar(&flagDocumentType, "document-type", "", "Document type: OTHER, GOVERNANCE, POLICY, PROCEDURE, PLAN, REGISTER, RECORD, REPORT, TEMPLATE") + cmd.Flags().StringVar(&flagClassification, "classification", "", "Classification: PUBLIC, INTERNAL, CONFIDENTIAL, SECRET") + + return cmd +} diff --git a/pkg/cmd/document/update/update.go b/pkg/cmd/document/update/update.go new file mode 100644 index 000000000..a8db779b8 --- /dev/null +++ b/pkg/cmd/document/update/update.go @@ -0,0 +1,136 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package update + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const updateMutation = ` +mutation($input: UpdateDocumentInput!) { + updateDocument(input: $input) { + document { + id + trustCenterVisibility + versions(first: 1) { + edges { + node { + title + } + } + } + } + } +} +` + +type updateResponse struct { + UpdateDocument struct { + Document struct { + ID string `json:"id"` + TrustCenterVisibility string `json:"trustCenterVisibility"` + Versions struct { + Edges []struct { + Node struct { + Title string `json:"title"` + } `json:"node"` + } `json:"edges"` + } `json:"versions"` + } `json:"document"` + } `json:"updateDocument"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var flagTrustCenterVisibility string + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a document", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + input := map[string]any{ + "id": args[0], + } + + if cmd.Flags().Changed("trust-center-visibility") { + if err := cmdutil.ValidateEnum( + "trust-center-visibility", + flagTrustCenterVisibility, + []string{"NONE", "PRIVATE", "PUBLIC"}, + ); err != nil { + return err + } + input["trustCenterVisibility"] = flagTrustCenterVisibility + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do( + updateMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + doc := resp.UpdateDocument.Document + title := doc.ID + if len(doc.Versions.Edges) > 0 { + title = doc.Versions.Edges[0].Node.Title + } + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated document %s (%s)\n", + doc.ID, + title, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagTrustCenterVisibility, "trust-center-visibility", "", "Trust center visibility: NONE, PRIVATE, PUBLIC") + + return cmd +} diff --git a/pkg/cmd/document/view-version/view_version.go b/pkg/cmd/document/view-version/view_version.go new file mode 100644 index 000000000..78ba8f363 --- /dev/null +++ b/pkg/cmd/document/view-version/view_version.go @@ -0,0 +1,158 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package viewversion + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on DocumentVersion { + id + title + major + minor + status + content + changelog + documentType + classification + publishedAt + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Title string `json:"title"` + Major int `json:"major"` + Minor int `json:"minor"` + Status string `json:"status"` + Content string `json:"content"` + Changelog string `json:"changelog"` + DocumentType string `json:"documentType"` + Classification string `json:"classification"` + PublishedAt *string `json:"publishedAt"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdViewVersion(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view-version ", + Short: "View a document version", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("document version %s not found", args[0]) + } + + if resp.Node.Typename != "DocumentVersion" { + return fmt.Errorf("expected DocumentVersion node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + v := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(v.Title)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), v.ID) + _, _ = fmt.Fprintf(out, "%s%d.%d\n", label.Render("Version:"), v.Major, v.Minor) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Status:"), v.Status) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Type:"), v.DocumentType) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Classification:"), v.Classification) + + if v.Changelog != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Changelog:"), v.Changelog) + } + + _, _ = fmt.Fprintln(out) + + if v.PublishedAt != nil { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Published:"), cmdutil.FormatTime(*v.PublishedAt)) + } + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt)) + + if v.Content != "" { + _, _ = fmt.Fprintf(out, "\n%s\n%s\n", bold.Render("Content:"), v.Content) + } + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/document/view/view.go b/pkg/cmd/document/view/view.go new file mode 100644 index 000000000..ce189e5da --- /dev/null +++ b/pkg/cmd/document/view/view.go @@ -0,0 +1,174 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on Document { + id + status + trustCenterVisibility + currentPublishedMajor + currentPublishedMinor + versions(first: 1) { + edges { + node { + title + documentType + classification + major + minor + status + } + } + } + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Status string `json:"status"` + TrustCenterVisibility string `json:"trustCenterVisibility"` + CurrentPublishedMajor *int `json:"currentPublishedMajor"` + CurrentPublishedMinor *int `json:"currentPublishedMinor"` + Versions struct { + Edges []struct { + Node struct { + Title string `json:"title"` + DocumentType string `json:"documentType"` + Classification string `json:"classification"` + Major int `json:"major"` + Minor int `json:"minor"` + Status string `json:"status"` + } `json:"node"` + } `json:"edges"` + } `json:"versions"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a document", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("document %s not found", args[0]) + } + + if resp.Node.Typename != "Document" { + return fmt.Errorf("expected Document node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + doc := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + title := doc.ID + if len(doc.Versions.Edges) > 0 { + title = doc.Versions.Edges[0].Node.Title + } + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(title)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), doc.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Status:"), doc.Status) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Visibility:"), doc.TrustCenterVisibility) + + if len(doc.Versions.Edges) > 0 { + v := doc.Versions.Edges[0].Node + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Type:"), v.DocumentType) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Classification:"), v.Classification) + _, _ = fmt.Fprintf(out, "%s%d.%d (%s)\n", label.Render("Latest Version:"), v.Major, v.Minor, v.Status) + } + + if doc.CurrentPublishedMajor != nil && doc.CurrentPublishedMinor != nil { + _, _ = fmt.Fprintf(out, "%s%d.%d\n", label.Render("Published Version:"), *doc.CurrentPublishedMajor, *doc.CurrentPublishedMinor) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(doc.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(doc.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 05f646acc..7ed8a7dcd 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -26,6 +26,7 @@ import ( cmdconfig "go.probo.inc/probo/pkg/cmd/config" cmdcontext "go.probo.inc/probo/pkg/cmd/context" "go.probo.inc/probo/pkg/cmd/control" + "go.probo.inc/probo/pkg/cmd/document" "go.probo.inc/probo/pkg/cmd/evidence" "go.probo.inc/probo/pkg/cmd/finding" "go.probo.inc/probo/pkg/cmd/framework" @@ -76,6 +77,7 @@ func NewCmdRoot(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(cmdconfig.NewCmdConfig(f)) cmd.AddCommand(cmdcontext.NewCmdContext(f)) cmd.AddCommand(control.NewCmdControl(f)) + cmd.AddCommand(document.NewCmdDocument(f)) cmd.AddCommand(evidence.NewCmdEvidence(f)) cmd.AddCommand(finding.NewCmdFinding(f)) cmd.AddCommand(framework.NewCmdFramework(f))