From f5df85ed33a3be2664da9f5ca5d561df820d17ca Mon Sep 17 00:00:00 2001 From: Brian Le Date: Sat, 28 Feb 2026 14:57:34 -0500 Subject: [PATCH] feat(page): add api fallback for archive command --- cmd/page.go | 69 +++++++++++++++++++++++++++--- cmd/page_archive_test.go | 91 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 cmd/page_archive_test.go diff --git a/cmd/page.go b/cmd/page.go index 539fcae..65285c2 100644 --- a/cmd/page.go +++ b/cmd/page.go @@ -13,12 +13,13 @@ import ( ) type PageCmd struct { - List PageListCmd `cmd:"" help:"List pages"` - View PageViewCmd `cmd:"" help:"View a page"` - Create PageCreateCmd `cmd:"" help:"Create a page"` - Upload PageUploadCmd `cmd:"" help:"Upload a markdown file as a page"` - Sync PageSyncCmd `cmd:"" help:"Sync a markdown file to a page (create or update)"` - Edit PageEditCmd `cmd:"" help:"Edit a page"` + List PageListCmd `cmd:"" help:"List pages"` + View PageViewCmd `cmd:"" help:"View a page"` + Create PageCreateCmd `cmd:"" help:"Create a page"` + Upload PageUploadCmd `cmd:"" help:"Upload a markdown file as a page"` + Sync PageSyncCmd `cmd:"" help:"Sync a markdown file to a page (create or update)"` + Edit PageEditCmd `cmd:"" help:"Edit a page"` + Archive PageArchiveCmd `cmd:"" help:"Archive a page"` } type PageListCmd struct { @@ -369,6 +370,62 @@ func runPageEdit(ctx *Context, page, replace, find, replaceWith, appendText stri return nil } +type PageArchiveCmd struct { + Page string `arg:"" help:"Page URL, name, or ID"` +} + +func (c *PageArchiveCmd) Run(ctx *Context) error { + return runPageArchive(ctx, c.Page) +} + +func runPageArchive(ctx *Context, page string) error { + bgCtx := context.Background() + + ref := cli.ParsePageRef(page) + pageID := ref.ID + switch ref.Kind { + case cli.RefName: + client, err := cli.RequireClient() + if err != nil { + return err + } + defer func() { _ = client.Close() }() + + resolved, err := cli.ResolvePageID(bgCtx, client, page) + if err != nil { + output.PrintError(err) + return err + } + pageID = resolved + case cli.RefID: + pageID = ref.ID + case cli.RefURL: + if extractedID, ok := cli.ExtractNotionUUID(page); ok { + pageID = extractedID + break + } + return &output.UserError{Message: fmt.Sprintf("could not extract page ID from URL: %s\nUse the page ID directly instead.", page)} + } + + if err := archivePageViaOfficialAPI(bgCtx, pageID); err != nil { + output.PrintError(err) + return err + } + + output.PrintSuccess("Page archived") + return nil +} + +func archivePageViaOfficialAPI(ctx context.Context, pageID string) error { + client, err := cli.RequireOfficialAPIClient() + if err != nil { + return err + } + return client.PatchPage(ctx, pageID, map[string]any{ + "archived": true, + }) +} + type PageSyncCmd struct { File string `arg:"" help:"Markdown file to sync" type:"existingfile"` Title string `help:"Page title (default: filename or first heading)" short:"t"` diff --git a/cmd/page_archive_test.go b/cmd/page_archive_test.go new file mode 100644 index 0000000..7270e46 --- /dev/null +++ b/cmd/page_archive_test.go @@ -0,0 +1,91 @@ +package cmd + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestRunPageArchiveUsesOfficialAPI(t *testing.T) { + pageID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + + var gotMethod string + var gotPath string + var gotAuth string + var gotBody map[string]any + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotMethod = r.Method + gotPath = r.URL.Path + gotAuth = r.Header.Get("Authorization") + + defer func() { _ = r.Body.Close() }() + if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil { + t.Fatalf("decode request body: %v", err) + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"` + pageID + `","object":"page","archived":true}`)) + })) + defer srv.Close() + + t.Setenv("HOME", t.TempDir()) + t.Setenv("NOTION_API_BASE_URL", srv.URL+"/v1") + t.Setenv("NOTION_API_TOKEN", "test-token") + + if err := runPageArchive(&Context{}, pageID); err != nil { + t.Fatalf("runPageArchive: %v", err) + } + + if gotMethod != http.MethodPatch { + t.Fatalf("method mismatch: got %s", gotMethod) + } + if gotPath != "/v1/pages/"+pageID { + t.Fatalf("path mismatch: got %s", gotPath) + } + if gotAuth != "Bearer test-token" { + t.Fatalf("auth mismatch: got %q", gotAuth) + } + if gotBody["archived"] != true { + t.Fatalf("archived payload mismatch: %#v", gotBody["archived"]) + } +} + +func TestRunPageArchiveSupportsURLInputWithEmbeddedID(t *testing.T) { + pageID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + pageURL := "https://www.notion.so/My-Page-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + var gotPath string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"` + pageID + `","object":"page","archived":true}`)) + })) + defer srv.Close() + + t.Setenv("HOME", t.TempDir()) + t.Setenv("NOTION_API_BASE_URL", srv.URL+"/v1") + t.Setenv("NOTION_API_TOKEN", "test-token") + + if err := runPageArchive(&Context{}, pageURL); err != nil { + t.Fatalf("runPageArchive: %v", err) + } + if gotPath != "/v1/pages/"+pageID { + t.Fatalf("path mismatch: got %s", gotPath) + } +} + +func TestRunPageArchiveRequiresOfficialAPIToken(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + t.Setenv("NOTION_API_BASE_URL", "http://127.0.0.1:65535/v1") + + err := runPageArchive(&Context{}, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + if err == nil { + t.Fatal("expected missing official API token error") + } + if !strings.Contains(err.Error(), "official API token is required") { + t.Fatalf("unexpected error: %v", err) + } +}