gogcli/internal/cmd/appscript.go
2026-02-14 06:24:41 +01:00

277 lines
6.9 KiB
Go

package cmd
import (
"context"
"encoding/json"
"os"
"strings"
scriptapi "google.golang.org/api/script/v1"
"github.com/steipete/gogcli/internal/googleapi"
"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
)
var newAppScriptService = googleapi.NewAppScript
type AppScriptCmd struct {
Get AppScriptGetCmd `cmd:"" name:"get" aliases:"info,show" help:"Get Apps Script project metadata"`
Content AppScriptContentCmd `cmd:"" name:"content" aliases:"cat" help:"Get Apps Script project content"`
Run AppScriptRunCmd `cmd:"" name:"run" help:"Run a deployed Apps Script function"`
Create AppScriptCreateCmd `cmd:"" name:"create" aliases:"new" help:"Create an Apps Script project"`
}
type AppScriptGetCmd struct {
ScriptID string `arg:"" name:"scriptId" help:"Script ID"`
}
func (c *AppScriptGetCmd) Run(ctx context.Context, flags *RootFlags) error {
account, err := requireAccount(flags)
if err != nil {
return err
}
scriptID := strings.TrimSpace(normalizeGoogleID(c.ScriptID))
if scriptID == "" {
return usage("empty scriptId")
}
svc, err := newAppScriptService(ctx, account)
if err != nil {
return err
}
project, err := svc.Projects.Get(scriptID).Context(ctx).Do()
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"project": project,
"editor_url": appScriptEditURL(scriptID),
})
}
u := ui.FromContext(ctx)
u.Out().Printf("script_id\t%s", project.ScriptId)
if project.Title != "" {
u.Out().Printf("title\t%s", project.Title)
}
if project.ParentId != "" {
u.Out().Printf("parent_id\t%s", project.ParentId)
}
if project.CreateTime != "" {
u.Out().Printf("created\t%s", project.CreateTime)
}
if project.UpdateTime != "" {
u.Out().Printf("updated\t%s", project.UpdateTime)
}
u.Out().Printf("editor_url\t%s", appScriptEditURL(scriptID))
return nil
}
type AppScriptContentCmd struct {
ScriptID string `arg:"" name:"scriptId" help:"Script ID"`
}
func (c *AppScriptContentCmd) Run(ctx context.Context, flags *RootFlags) error {
account, err := requireAccount(flags)
if err != nil {
return err
}
scriptID := strings.TrimSpace(normalizeGoogleID(c.ScriptID))
if scriptID == "" {
return usage("empty scriptId")
}
svc, err := newAppScriptService(ctx, account)
if err != nil {
return err
}
content, err := svc.Projects.GetContent(scriptID).Context(ctx).Do()
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"content": content,
})
}
u := ui.FromContext(ctx)
u.Out().Printf("script_id\t%s", content.ScriptId)
u.Out().Printf("files\t%d", len(content.Files))
for _, file := range content.Files {
if file == nil {
continue
}
u.Out().Printf("file\t%s\t%s", file.Name, file.Type)
}
return nil
}
type AppScriptRunCmd struct {
ScriptID string `arg:"" name:"scriptId" help:"Script ID"`
Function string `arg:"" name:"function" help:"Function name to run"`
Params string `name:"params" help:"JSON array of function parameters" default:"[]"`
DevMode bool `name:"dev-mode" help:"Run latest saved code if you own the script"`
}
func (c *AppScriptRunCmd) Run(ctx context.Context, flags *RootFlags) error {
account, err := requireAccount(flags)
if err != nil {
return err
}
scriptID := strings.TrimSpace(normalizeGoogleID(c.ScriptID))
if scriptID == "" {
return usage("empty scriptId")
}
function := strings.TrimSpace(c.Function)
if function == "" {
return usage("empty function")
}
params, err := parseJSONArray(c.Params)
if err != nil {
return err
}
svc, err := newAppScriptService(ctx, account)
if err != nil {
return err
}
op, err := svc.Scripts.Run(scriptID, &scriptapi.ExecutionRequest{
Function: function,
Parameters: params,
DevMode: c.DevMode,
}).Context(ctx).Do()
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"operation": op,
})
}
u := ui.FromContext(ctx)
u.Out().Printf("done\t%t", op.Done)
if op.Error != nil {
if op.Error.Code != 0 {
u.Out().Printf("error_code\t%d", op.Error.Code)
}
if op.Error.Message != "" {
u.Out().Printf("error\t%s", op.Error.Message)
}
if detail := parseExecutionError(op.Error); detail != nil {
if detail.ErrorType != "" {
u.Out().Printf("error_type\t%s", detail.ErrorType)
}
if detail.ErrorMessage != "" {
u.Out().Printf("error_message\t%s", detail.ErrorMessage)
}
}
return nil
}
if len(op.Response) > 0 {
var execResp scriptapi.ExecutionResponse
if err := json.Unmarshal(op.Response, &execResp); err == nil && execResp.Result != nil {
if b, marshalErr := json.Marshal(execResp.Result); marshalErr == nil {
u.Out().Printf("result\t%s", string(b))
}
}
}
return nil
}
type AppScriptCreateCmd struct {
Title string `name:"title" help:"Project title" required:""`
ParentID string `name:"parent-id" help:"Optional Drive file ID to bind to"`
}
func (c *AppScriptCreateCmd) Run(ctx context.Context, flags *RootFlags) error {
account, err := requireAccount(flags)
if err != nil {
return err
}
title := strings.TrimSpace(c.Title)
if title == "" {
return usage("empty --title")
}
parentID := strings.TrimSpace(normalizeGoogleID(c.ParentID))
if dryRunErr := dryRunExit(ctx, flags, "appscript.create", map[string]any{
"title": title,
"parent_id": parentID,
}); dryRunErr != nil {
return dryRunErr
}
svc, err := newAppScriptService(ctx, account)
if err != nil {
return err
}
project, err := svc.Projects.Create(&scriptapi.CreateProjectRequest{
Title: title,
ParentId: parentID,
}).Context(ctx).Do()
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"created": true,
"project": project,
"editor_url": appScriptEditURL(project.ScriptId),
})
}
u := ui.FromContext(ctx)
u.Out().Printf("created\ttrue")
u.Out().Printf("script_id\t%s", project.ScriptId)
if project.Title != "" {
u.Out().Printf("title\t%s", project.Title)
}
if project.ParentId != "" {
u.Out().Printf("parent_id\t%s", project.ParentId)
}
u.Out().Printf("editor_url\t%s", appScriptEditURL(project.ScriptId))
return nil
}
func parseJSONArray(raw string) ([]interface{}, error) {
val := strings.TrimSpace(raw)
if val == "" {
return nil, nil
}
var out []interface{}
if err := json.Unmarshal([]byte(val), &out); err != nil {
return nil, usagef("invalid --params JSON array: %v", err)
}
return out, nil
}
func parseExecutionError(status *scriptapi.Status) *scriptapi.ExecutionError {
if status == nil || len(status.Details) == 0 {
return nil
}
var detail scriptapi.ExecutionError
if err := json.Unmarshal(status.Details[0], &detail); err != nil {
return nil
}
return &detail
}
func appScriptEditURL(scriptID string) string {
scriptID = strings.TrimSpace(scriptID)
if scriptID == "" {
return ""
}
return "https://script.google.com/d/" + scriptID + "/edit"
}