From e2c1c9074e11c4cf9d224aa3c1a8d54b12fd879d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 12 Sep 2026 14:35:16 -0700 Subject: [PATCH] fix(appscript): keep content filenames in one output field Escape tabs and line breaks in text output while preserving the provider's filename in JSON. Share the existing single-line formatter without changing notes or comment formatting. The proposed push command remains deferred pending a write-ownership contract; the API does not enforce conditional whole-project updates. Co-authored-by: haosdent --- CHANGELOG.md | 1 + internal/cmd/appscript.go | 2 +- internal/cmd/appscript_content_output_test.go | 47 +++++++++++++++++++ internal/cmd/comment_ops.go | 6 +-- internal/cmd/output_helpers.go | 10 ++++ internal/cmd/sheets_notes.go | 9 ---- 6 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 internal/cmd/appscript_content_output_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bb7ace92..b315ed4da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.40.1 - Unreleased +- Apps Script: keep file names within one TSV field in `appscript content`, escaping line breaks instead of splitting output rows. (#1091) — thanks @haosdent. - Search Console: add `searchconsole inspect` for per-URL index status via the URL Inspection API (coverage state, indexing/page-fetch/robots.txt state, canonical, sitemaps, last crawl time), using the existing `webmasters` OAuth scope. (#1094) — thanks @laihenyi. - Search Console: preserve permission-denied exit codes when adding API setup or scope guidance. (#1094) - Dependencies and CI: refresh Google protocol modules and tracking-worker tooling within release-age limits; test the Go 1.26 minimum, check worker types and generated skills, and avoid duplicate pull-request test runs. (#1131) diff --git a/internal/cmd/appscript.go b/internal/cmd/appscript.go index b4e0c0e71..b6727471c 100644 --- a/internal/cmd/appscript.go +++ b/internal/cmd/appscript.go @@ -106,7 +106,7 @@ func (c *AppScriptContentCmd) Run(ctx context.Context, flags *RootFlags) error { if file == nil { continue } - u.Out().Linef("file\t%s\t%s", file.Name, file.Type) + u.Out().Linef("file\t%s\t%s", oneLine(file.Name), oneLine(file.Type)) } return nil } diff --git a/internal/cmd/appscript_content_output_test.go b/internal/cmd/appscript_content_output_test.go new file mode 100644 index 000000000..6b7dbaded --- /dev/null +++ b/internal/cmd/appscript_content_output_test.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestAppScriptContentFileNamesStayInOneField(t *testing.T) { + const name = "Name\tWith\r\nBreaks" + for _, mode := range []string{"text", "plain", "json"} { + t.Run(mode, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "scriptId": "script123", "files": []map[string]any{{"name": name, "type": "SERVER_JS"}}, + }) + })) + defer srv.Close() + args := []string{"--account", "a@b.com", "appscript", "content", "script123"} + if mode != "text" { + args = append([]string{"--" + mode}, args...) + } + result := executeWithAppScriptTestService(t, args, newAppScriptTestService(t, srv)) + if result.err != nil { + t.Fatal(result.err) + } + if mode == "json" { + var output struct { + Content struct { + Files []struct{ Name string } `json:"files"` + } `json:"content"` + } + if err := json.Unmarshal([]byte(result.stdout), &output); err != nil { + t.Fatal(err) + } + if len(output.Content.Files) != 1 || output.Content.Files[0].Name != name { + t.Fatalf("JSON changed the file name: %s", result.stdout) + } + } else if !strings.Contains(result.stdout, "file\tName With\\nBreaks\tSERVER_JS\n") { + t.Fatalf("unsafe file row: %q", result.stdout) + } + }) + } +} diff --git a/internal/cmd/comment_ops.go b/internal/cmd/comment_ops.go index e3efce835..ec37811a2 100644 --- a/internal/cmd/comment_ops.go +++ b/internal/cmd/comment_ops.go @@ -525,11 +525,7 @@ func filterOpenComments(comments []*drive.Comment) []*drive.Comment { } func oneLineTSV(s string) string { - s = strings.ReplaceAll(s, "\r\n", "\n") - s = strings.ReplaceAll(s, "\r", "\n") - s = strings.ReplaceAll(s, "\t", " ") - s = strings.ReplaceAll(s, "\n", "\\n") - return strings.TrimSpace(s) + return strings.TrimSpace(oneLine(s)) } func truncateString(s string, maxLen int) string { diff --git a/internal/cmd/output_helpers.go b/internal/cmd/output_helpers.go index f08c43efc..e7723d068 100644 --- a/internal/cmd/output_helpers.go +++ b/internal/cmd/output_helpers.go @@ -3,6 +3,7 @@ package cmd import ( "context" "io" + "strings" "text/tabwriter" "github.com/openclaw/gogcli/internal/outfmt" @@ -62,3 +63,12 @@ func printNextPageHintWithAll(u *ui.UI, nextPageToken string, allFlag string) { } u.Err().Linef("# More results: use %s to fetch every page, or --page %s for the next page", allFlag, nextPageToken) } + +func oneLine(s string) string { + s = strings.ReplaceAll(s, "\r\n", "\n") + s = strings.ReplaceAll(s, "\r", "\n") + // Keep output parseable in tables/TSV. + s = strings.ReplaceAll(s, "\t", " ") + s = strings.ReplaceAll(s, "\n", "\\n") + return s +} diff --git a/internal/cmd/sheets_notes.go b/internal/cmd/sheets_notes.go index 745d60018..c3ea31f20 100644 --- a/internal/cmd/sheets_notes.go +++ b/internal/cmd/sheets_notes.go @@ -97,12 +97,3 @@ func (c *SheetsNotesCmd) Run(ctx context.Context, flags *RootFlags) error { return outfmt.WriteTable(ctx, stdoutWriter(ctx), notes, sheetsNoteColumns()) } - -func oneLine(s string) string { - s = strings.ReplaceAll(s, "\r\n", "\n") - s = strings.ReplaceAll(s, "\r", "\n") - // Keep output parseable in tables/TSV. - s = strings.ReplaceAll(s, "\t", " ") - s = strings.ReplaceAll(s, "\n", "\\n") - return s -}