chore: import upstream snapshot with attribution
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
var CreateTasklist = common.Shortcut{
|
||||
Service: "task",
|
||||
Command: "+tasklist-create",
|
||||
Description: "create a tasklist and optionally add tasks",
|
||||
Risk: "write",
|
||||
Scopes: []string{"task:tasklist:write", "task:task:write"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
|
||||
Flags: []common.Flag{
|
||||
{Name: "name", Desc: "tasklist name", Required: true},
|
||||
{Name: "member", Desc: "comma-separated open_ids to add as editors"},
|
||||
{Name: "data", Desc: "JSON array of tasks to create within this tasklist"},
|
||||
},
|
||||
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
body := buildTasklistCreateBody(runtime)
|
||||
|
||||
d := common.NewDryRunAPI().
|
||||
Desc("1. Create Tasklist").
|
||||
POST("/open-apis/task/v2/tasklists").
|
||||
Params(map[string]interface{}{"user_id_type": "open_id"}).
|
||||
Body(body)
|
||||
|
||||
if dataStr := runtime.Str("data"); dataStr != "" {
|
||||
d.Desc("2. Create Tasks within the new tasklist (concurrently)")
|
||||
}
|
||||
|
||||
return d
|
||||
},
|
||||
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
body := buildTasklistCreateBody(runtime)
|
||||
params := map[string]interface{}{"user_id_type": "open_id"}
|
||||
|
||||
// Validate --data (client input) before any remote write, so a malformed
|
||||
// payload fails fast without creating an orphan tasklist.
|
||||
var tasks []map[string]interface{}
|
||||
if dataStr := runtime.Str("data"); dataStr != "" {
|
||||
if err := json.Unmarshal([]byte(dataStr), &tasks); err != nil {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "failed to parse --data as JSON array: %v", err).WithParam("--data")
|
||||
}
|
||||
}
|
||||
|
||||
data, err := callTaskAPITyped(runtime, http.MethodPost, "/open-apis/task/v2/tasklists", params, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tasklist, _ := data["tasklist"].(map[string]interface{})
|
||||
tasklistGuid, _ := tasklist["guid"].(string)
|
||||
tasklistName, _ := tasklist["name"].(string)
|
||||
tasklistUrl, _ := tasklist["url"].(string)
|
||||
tasklistUrl = truncateTaskURL(tasklistUrl)
|
||||
|
||||
var createdTasks []map[string]interface{}
|
||||
var failedTasks []map[string]interface{}
|
||||
|
||||
if len(tasks) > 0 {
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
|
||||
for i, taskDef := range tasks {
|
||||
wg.Add(1)
|
||||
go func(idx int, tDef map[string]interface{}) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "recovered in defer: %v\n", r)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// Add tasklist_guid to the task definition
|
||||
tDef["tasklists"] = []map[string]interface{}{
|
||||
{
|
||||
"tasklist_guid": tasklistGuid,
|
||||
},
|
||||
}
|
||||
|
||||
// If assignee is provided as string, convert it to members
|
||||
if assignee, ok := tDef["assignee"].(string); ok {
|
||||
tDef["members"] = []map[string]interface{}{
|
||||
{
|
||||
"id": assignee,
|
||||
"role": "assignee",
|
||||
"type": "user",
|
||||
},
|
||||
}
|
||||
delete(tDef, "assignee")
|
||||
}
|
||||
|
||||
tData, tErr := callTaskAPITyped(runtime, http.MethodPost, "/open-apis/task/v2/tasks", params, tDef)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if tErr != nil {
|
||||
summary, _ := tDef["summary"].(string)
|
||||
failedTasks = append(failedTasks, buildTaskCreateFailure(idx, summary, tErr))
|
||||
return
|
||||
}
|
||||
|
||||
if t, ok := tData["task"].(map[string]interface{}); ok {
|
||||
guid, _ := t["guid"].(string)
|
||||
urlVal, _ := t["url"].(string)
|
||||
urlVal = truncateTaskURL(urlVal)
|
||||
createdTasks = append(createdTasks, map[string]interface{}{
|
||||
"guid": guid,
|
||||
"url": urlVal,
|
||||
})
|
||||
}
|
||||
}(i, taskDef)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Standardized write output: return resource identifiers
|
||||
outData := map[string]interface{}{
|
||||
"guid": tasklistGuid,
|
||||
"url": tasklistUrl,
|
||||
"created_tasks": createdTasks,
|
||||
"failed_tasks": failedTasks,
|
||||
}
|
||||
|
||||
pretty := func(w io.Writer) {
|
||||
fmt.Fprintf(w, "✅ Tasklist created successfully!\n")
|
||||
fmt.Fprintf(w, "Tasklist Name: %s\n", tasklistName)
|
||||
fmt.Fprintf(w, "Tasklist ID: %s\n", tasklistGuid)
|
||||
if tasklistUrl != "" {
|
||||
fmt.Fprintf(w, "Tasklist URL: %s\n", tasklistUrl)
|
||||
}
|
||||
|
||||
if len(tasks) > 0 {
|
||||
fmt.Fprintln(w, strings.Repeat("-", 20))
|
||||
fmt.Fprintf(w, "Tasks created: %d/%d\n", len(createdTasks), len(tasks))
|
||||
for _, t := range createdTasks {
|
||||
guid, _ := t["guid"].(string)
|
||||
urlVal, _ := t["url"].(string)
|
||||
fmt.Fprintf(w, " - ID: %s", guid)
|
||||
if urlVal != "" {
|
||||
fmt.Fprintf(w, ", URL: %s", urlVal)
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
if len(failedTasks) > 0 {
|
||||
fmt.Fprintf(w, "\nFailed tasks:\n")
|
||||
for _, f := range failedTasks {
|
||||
fmt.Fprintf(w, " - Index %v (%s): %s\n", f["index"], f["summary"], f["message"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sub-task creation failures surface as a non-zero exit. JSON/JQ callers
|
||||
// need an ok:false envelope, while pretty output should preserve the
|
||||
// command-specific human-readable summary.
|
||||
if len(failedTasks) > 0 {
|
||||
if runtime.Format == "pretty" && runtime.JqExpr == "" {
|
||||
runtime.OutFormat(outData, nil, pretty)
|
||||
return output.PartialFailure(output.ExitAPI)
|
||||
}
|
||||
return runtime.OutPartialFailure(outData, nil)
|
||||
}
|
||||
|
||||
runtime.OutFormat(outData, nil, pretty)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func buildTaskCreateFailure(index int, summary string, err error) map[string]interface{} {
|
||||
failDetail := map[string]interface{}{
|
||||
"index": index,
|
||||
"summary": summary,
|
||||
}
|
||||
if p, ok := errs.ProblemOf(err); ok {
|
||||
failDetail["type"] = string(p.Subtype)
|
||||
failDetail["code"] = p.Code
|
||||
failDetail["message"] = p.Message
|
||||
failDetail["hint"] = p.Hint
|
||||
} else {
|
||||
failDetail["type"] = "api_error"
|
||||
failDetail["message"] = err.Error()
|
||||
}
|
||||
return failDetail
|
||||
}
|
||||
|
||||
func buildTasklistCreateBody(runtime *common.RuntimeContext) map[string]interface{} {
|
||||
body := map[string]interface{}{
|
||||
"name": runtime.Str("name"),
|
||||
}
|
||||
|
||||
if memberStr := runtime.Str("member"); memberStr != "" {
|
||||
ids := strings.Split(memberStr, ",")
|
||||
var members []map[string]interface{}
|
||||
for _, id := range ids {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
members = append(members, map[string]interface{}{
|
||||
"id": id,
|
||||
"role": "editor",
|
||||
"type": "user",
|
||||
})
|
||||
}
|
||||
body["members"] = members
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
Reference in New Issue
Block a user