|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "os" |
| 7 | + "strings" |
| 8 | + |
| 9 | + scriptapi "google.golang.org/api/script/v1" |
| 10 | + |
| 11 | + "github.com/steipete/gogcli/internal/googleapi" |
| 12 | + "github.com/steipete/gogcli/internal/outfmt" |
| 13 | + "github.com/steipete/gogcli/internal/ui" |
| 14 | +) |
| 15 | + |
| 16 | +var newAppScriptService = googleapi.NewAppScript |
| 17 | + |
| 18 | +type AppScriptCmd struct { |
| 19 | + Get AppScriptGetCmd `cmd:"" name:"get" aliases:"info,show" help:"Get Apps Script project metadata"` |
| 20 | + Content AppScriptContentCmd `cmd:"" name:"content" aliases:"cat" help:"Get Apps Script project content"` |
| 21 | + Run AppScriptRunCmd `cmd:"" name:"run" help:"Run a deployed Apps Script function"` |
| 22 | + Create AppScriptCreateCmd `cmd:"" name:"create" aliases:"new" help:"Create an Apps Script project"` |
| 23 | +} |
| 24 | + |
| 25 | +type AppScriptGetCmd struct { |
| 26 | + ScriptID string `arg:"" name:"scriptId" help:"Script ID"` |
| 27 | +} |
| 28 | + |
| 29 | +func (c *AppScriptGetCmd) Run(ctx context.Context, flags *RootFlags) error { |
| 30 | + account, err := requireAccount(flags) |
| 31 | + if err != nil { |
| 32 | + return err |
| 33 | + } |
| 34 | + scriptID := strings.TrimSpace(normalizeGoogleID(c.ScriptID)) |
| 35 | + if scriptID == "" { |
| 36 | + return usage("empty scriptId") |
| 37 | + } |
| 38 | + |
| 39 | + svc, err := newAppScriptService(ctx, account) |
| 40 | + if err != nil { |
| 41 | + return err |
| 42 | + } |
| 43 | + project, err := svc.Projects.Get(scriptID).Context(ctx).Do() |
| 44 | + if err != nil { |
| 45 | + return err |
| 46 | + } |
| 47 | + |
| 48 | + if outfmt.IsJSON(ctx) { |
| 49 | + return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{ |
| 50 | + "project": project, |
| 51 | + "editor_url": appScriptEditURL(scriptID), |
| 52 | + }) |
| 53 | + } |
| 54 | + |
| 55 | + u := ui.FromContext(ctx) |
| 56 | + u.Out().Printf("script_id\t%s", project.ScriptId) |
| 57 | + if project.Title != "" { |
| 58 | + u.Out().Printf("title\t%s", project.Title) |
| 59 | + } |
| 60 | + if project.ParentId != "" { |
| 61 | + u.Out().Printf("parent_id\t%s", project.ParentId) |
| 62 | + } |
| 63 | + if project.CreateTime != "" { |
| 64 | + u.Out().Printf("created\t%s", project.CreateTime) |
| 65 | + } |
| 66 | + if project.UpdateTime != "" { |
| 67 | + u.Out().Printf("updated\t%s", project.UpdateTime) |
| 68 | + } |
| 69 | + u.Out().Printf("editor_url\t%s", appScriptEditURL(scriptID)) |
| 70 | + return nil |
| 71 | +} |
| 72 | + |
| 73 | +type AppScriptContentCmd struct { |
| 74 | + ScriptID string `arg:"" name:"scriptId" help:"Script ID"` |
| 75 | +} |
| 76 | + |
| 77 | +func (c *AppScriptContentCmd) Run(ctx context.Context, flags *RootFlags) error { |
| 78 | + account, err := requireAccount(flags) |
| 79 | + if err != nil { |
| 80 | + return err |
| 81 | + } |
| 82 | + scriptID := strings.TrimSpace(normalizeGoogleID(c.ScriptID)) |
| 83 | + if scriptID == "" { |
| 84 | + return usage("empty scriptId") |
| 85 | + } |
| 86 | + |
| 87 | + svc, err := newAppScriptService(ctx, account) |
| 88 | + if err != nil { |
| 89 | + return err |
| 90 | + } |
| 91 | + content, err := svc.Projects.GetContent(scriptID).Context(ctx).Do() |
| 92 | + if err != nil { |
| 93 | + return err |
| 94 | + } |
| 95 | + |
| 96 | + if outfmt.IsJSON(ctx) { |
| 97 | + return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{ |
| 98 | + "content": content, |
| 99 | + }) |
| 100 | + } |
| 101 | + |
| 102 | + u := ui.FromContext(ctx) |
| 103 | + u.Out().Printf("script_id\t%s", content.ScriptId) |
| 104 | + u.Out().Printf("files\t%d", len(content.Files)) |
| 105 | + for _, file := range content.Files { |
| 106 | + if file == nil { |
| 107 | + continue |
| 108 | + } |
| 109 | + u.Out().Printf("file\t%s\t%s", file.Name, file.Type) |
| 110 | + } |
| 111 | + return nil |
| 112 | +} |
| 113 | + |
| 114 | +type AppScriptRunCmd struct { |
| 115 | + ScriptID string `arg:"" name:"scriptId" help:"Script ID"` |
| 116 | + Function string `arg:"" name:"function" help:"Function name to run"` |
| 117 | + Params string `name:"params" help:"JSON array of function parameters" default:"[]"` |
| 118 | + DevMode bool `name:"dev-mode" help:"Run latest saved code if you own the script"` |
| 119 | +} |
| 120 | + |
| 121 | +func (c *AppScriptRunCmd) Run(ctx context.Context, flags *RootFlags) error { |
| 122 | + account, err := requireAccount(flags) |
| 123 | + if err != nil { |
| 124 | + return err |
| 125 | + } |
| 126 | + scriptID := strings.TrimSpace(normalizeGoogleID(c.ScriptID)) |
| 127 | + if scriptID == "" { |
| 128 | + return usage("empty scriptId") |
| 129 | + } |
| 130 | + function := strings.TrimSpace(c.Function) |
| 131 | + if function == "" { |
| 132 | + return usage("empty function") |
| 133 | + } |
| 134 | + |
| 135 | + params, err := parseJSONArray(c.Params) |
| 136 | + if err != nil { |
| 137 | + return err |
| 138 | + } |
| 139 | + |
| 140 | + svc, err := newAppScriptService(ctx, account) |
| 141 | + if err != nil { |
| 142 | + return err |
| 143 | + } |
| 144 | + op, err := svc.Scripts.Run(scriptID, &scriptapi.ExecutionRequest{ |
| 145 | + Function: function, |
| 146 | + Parameters: params, |
| 147 | + DevMode: c.DevMode, |
| 148 | + }).Context(ctx).Do() |
| 149 | + if err != nil { |
| 150 | + return err |
| 151 | + } |
| 152 | + |
| 153 | + if outfmt.IsJSON(ctx) { |
| 154 | + return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{ |
| 155 | + "operation": op, |
| 156 | + }) |
| 157 | + } |
| 158 | + |
| 159 | + u := ui.FromContext(ctx) |
| 160 | + u.Out().Printf("done\t%t", op.Done) |
| 161 | + |
| 162 | + if op.Error != nil { |
| 163 | + if op.Error.Code != 0 { |
| 164 | + u.Out().Printf("error_code\t%d", op.Error.Code) |
| 165 | + } |
| 166 | + if op.Error.Message != "" { |
| 167 | + u.Out().Printf("error\t%s", op.Error.Message) |
| 168 | + } |
| 169 | + if detail := parseExecutionError(op.Error); detail != nil { |
| 170 | + if detail.ErrorType != "" { |
| 171 | + u.Out().Printf("error_type\t%s", detail.ErrorType) |
| 172 | + } |
| 173 | + if detail.ErrorMessage != "" { |
| 174 | + u.Out().Printf("error_message\t%s", detail.ErrorMessage) |
| 175 | + } |
| 176 | + } |
| 177 | + return nil |
| 178 | + } |
| 179 | + |
| 180 | + if len(op.Response) > 0 { |
| 181 | + var execResp scriptapi.ExecutionResponse |
| 182 | + if err := json.Unmarshal(op.Response, &execResp); err == nil && execResp.Result != nil { |
| 183 | + if b, marshalErr := json.Marshal(execResp.Result); marshalErr == nil { |
| 184 | + u.Out().Printf("result\t%s", string(b)) |
| 185 | + } |
| 186 | + } |
| 187 | + } |
| 188 | + return nil |
| 189 | +} |
| 190 | + |
| 191 | +type AppScriptCreateCmd struct { |
| 192 | + Title string `name:"title" help:"Project title" required:""` |
| 193 | + ParentID string `name:"parent-id" help:"Optional Drive file ID to bind to"` |
| 194 | +} |
| 195 | + |
| 196 | +func (c *AppScriptCreateCmd) Run(ctx context.Context, flags *RootFlags) error { |
| 197 | + account, err := requireAccount(flags) |
| 198 | + if err != nil { |
| 199 | + return err |
| 200 | + } |
| 201 | + title := strings.TrimSpace(c.Title) |
| 202 | + if title == "" { |
| 203 | + return usage("empty --title") |
| 204 | + } |
| 205 | + parentID := strings.TrimSpace(normalizeGoogleID(c.ParentID)) |
| 206 | + |
| 207 | + if dryRunErr := dryRunExit(ctx, flags, "appscript.create", map[string]any{ |
| 208 | + "title": title, |
| 209 | + "parent_id": parentID, |
| 210 | + }); dryRunErr != nil { |
| 211 | + return dryRunErr |
| 212 | + } |
| 213 | + |
| 214 | + svc, err := newAppScriptService(ctx, account) |
| 215 | + if err != nil { |
| 216 | + return err |
| 217 | + } |
| 218 | + project, err := svc.Projects.Create(&scriptapi.CreateProjectRequest{ |
| 219 | + Title: title, |
| 220 | + ParentId: parentID, |
| 221 | + }).Context(ctx).Do() |
| 222 | + if err != nil { |
| 223 | + return err |
| 224 | + } |
| 225 | + |
| 226 | + if outfmt.IsJSON(ctx) { |
| 227 | + return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{ |
| 228 | + "created": true, |
| 229 | + "project": project, |
| 230 | + "editor_url": appScriptEditURL(project.ScriptId), |
| 231 | + }) |
| 232 | + } |
| 233 | + |
| 234 | + u := ui.FromContext(ctx) |
| 235 | + u.Out().Printf("created\ttrue") |
| 236 | + u.Out().Printf("script_id\t%s", project.ScriptId) |
| 237 | + if project.Title != "" { |
| 238 | + u.Out().Printf("title\t%s", project.Title) |
| 239 | + } |
| 240 | + if project.ParentId != "" { |
| 241 | + u.Out().Printf("parent_id\t%s", project.ParentId) |
| 242 | + } |
| 243 | + u.Out().Printf("editor_url\t%s", appScriptEditURL(project.ScriptId)) |
| 244 | + return nil |
| 245 | +} |
| 246 | + |
| 247 | +func parseJSONArray(raw string) ([]interface{}, error) { |
| 248 | + val := strings.TrimSpace(raw) |
| 249 | + if val == "" { |
| 250 | + return nil, nil |
| 251 | + } |
| 252 | + var out []interface{} |
| 253 | + if err := json.Unmarshal([]byte(val), &out); err != nil { |
| 254 | + return nil, usagef("invalid --params JSON array: %v", err) |
| 255 | + } |
| 256 | + return out, nil |
| 257 | +} |
| 258 | + |
| 259 | +func parseExecutionError(status *scriptapi.Status) *scriptapi.ExecutionError { |
| 260 | + if status == nil || len(status.Details) == 0 { |
| 261 | + return nil |
| 262 | + } |
| 263 | + var detail scriptapi.ExecutionError |
| 264 | + if err := json.Unmarshal(status.Details[0], &detail); err != nil { |
| 265 | + return nil |
| 266 | + } |
| 267 | + return &detail |
| 268 | +} |
| 269 | + |
| 270 | +func appScriptEditURL(scriptID string) string { |
| 271 | + scriptID = strings.TrimSpace(scriptID) |
| 272 | + if scriptID == "" { |
| 273 | + return "" |
| 274 | + } |
| 275 | + return "https://script.google.com/d/" + scriptID + "/edit" |
| 276 | +} |
0 commit comments