Files
larksuite-cli/shortcuts/wiki/wiki_space_create.go
evandance 154ecdb90f feat(wiki): emit typed error envelopes across the wiki domain (#1350)
Emit structured validation, API, network, file, and internal error envelopes for Wiki shortcuts so users and agents can recover from failed wiki workflows using stable type, subtype, param, and code fields.

Add Wiki domain errscontract and golangci guards to prevent legacy envelope and common helper regressions.
2026-06-11 14:02:29 +08:00

121 lines
4.3 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package wiki
import (
"context"
"fmt"
"strings"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/shortcuts/common"
)
// WikiSpaceCreate wraps wiki.spaces.create. The raw API only takes two
// optional string fields, so the shortcut's value is flag ergonomics
// (no hand-written --params JSON), output flattening (data.space.* lifted
// to the top level), and a dry-run preview.
//
// The API only accepts a user access token (no tenant/bot), so AuthTypes is
// user-only — the framework's CheckIdentity rejects --as bot for us.
var WikiSpaceCreate = common.Shortcut{
Service: "wiki",
Command: "+space-create",
Description: "Create a wiki space",
Risk: "write",
// The API accepts wiki:wiki or wiki:space:write_only. The framework's
// scope preflight does exact-string matching (see +space-list), so
// declare the narrowest form the API takes to avoid false-rejecting
// tokens that only carry wiki:space:write_only.
Scopes: []string{"wiki:space:write_only"},
AuthTypes: []string{"user"},
Flags: []common.Flag{
{Name: "name", Desc: "wiki space name", Required: true},
{Name: "description", Desc: "wiki space description"},
},
Tips: []string{
"Only --as user is supported; the create API does not accept a tenant/bot token.",
"The underlying spaces.create API is flagged danger in the schema browser; a space is recoverable via `wiki +delete-space` if created by mistake.",
"--name is required: an unnamed space is almost always an accident and is hard to find later.",
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
_, err := readWikiSpaceCreateSpec(runtime)
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
spec, err := readWikiSpaceCreateSpec(runtime)
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
return common.NewDryRunAPI().
POST(wikiSpacesAPIPath).
Body(spec.RequestBody())
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
spec, err := readWikiSpaceCreateSpec(runtime)
if err != nil {
return err
}
fmt.Fprintf(runtime.IO().ErrOut, "Creating wiki space %q...\n", spec.Name)
data, err := runtime.CallAPITyped("POST", wikiSpacesAPIPath, nil, spec.RequestBody())
if err != nil {
return err
}
raw := common.GetMap(data, "space")
if raw == nil {
return errs.NewInternalError(errs.SubtypeInvalidResponse, "wiki space create returned no space")
}
out := wikiSpaceCreateOutput(raw)
fmt.Fprintf(runtime.IO().ErrOut, "Created wiki space %s\n", common.MaskToken(common.GetString(out, "space_id")))
runtime.Out(out, nil)
return nil
},
}
// wikiSpaceCreateSpec is the normalized CLI input.
type wikiSpaceCreateSpec struct {
Name string
Description string
}
// RequestBody converts the normalized input into the OpenAPI payload. Both
// fields are optional per the API, but Validate enforces a non-empty name,
// so name is always present here.
func (spec wikiSpaceCreateSpec) RequestBody() map[string]interface{} {
body := map[string]interface{}{"name": spec.Name}
if spec.Description != "" {
body["description"] = spec.Description
}
return body
}
func readWikiSpaceCreateSpec(runtime *common.RuntimeContext) (wikiSpaceCreateSpec, error) {
spec := wikiSpaceCreateSpec{
Name: strings.TrimSpace(runtime.Str("name")),
Description: strings.TrimSpace(runtime.Str("description")),
}
if spec.Name == "" {
return wikiSpaceCreateSpec{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "--name is required and cannot be blank").WithParam("--name")
}
return spec, nil
}
// wikiSpaceCreateOutput flattens data.space into the top-level envelope. It
// reads the raw map (rather than parseWikiSpaceRecord) so the description
// the caller just set round-trips back in the output.
func wikiSpaceCreateOutput(raw map[string]interface{}) map[string]interface{} {
return map[string]interface{}{
"space_id": common.GetString(raw, "space_id"),
"name": common.GetString(raw, "name"),
"description": common.GetString(raw, "description"),
"space_type": common.GetString(raw, "space_type"),
"visibility": common.GetString(raw, "visibility"),
"open_sharing": common.GetString(raw, "open_sharing"),
}
}