mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 17:45:15 +08:00
feat: add create-svglide shortcut
This commit is contained in:
@@ -9,6 +9,7 @@ import "github.com/larksuite/cli/shortcuts/common"
|
||||
func Shortcuts() []common.Shortcut {
|
||||
return []common.Shortcut{
|
||||
SlidesCreate,
|
||||
SlidesCreateSVGlide,
|
||||
SlidesMediaUpload,
|
||||
SlidesReplaceSlide,
|
||||
SlidesReplacePages,
|
||||
|
||||
114
shortcuts/slides/slides_create_svglide.go
Normal file
114
shortcuts/slides/slides_create_svglide.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package slides
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/svglide"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// SlidesCreateSVGlide manages a local Codex-mediated SVGlide SVG run directory.
|
||||
var SlidesCreateSVGlide = common.Shortcut{
|
||||
Service: "slides",
|
||||
Command: "+create-svglide",
|
||||
Description: "Create and manage a local SVGlide SVG run directory",
|
||||
Risk: "write",
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
Flags: []common.Flag{
|
||||
{Name: "action", Desc: "runtime action: init, status, next, validate, preview", Required: true, Enum: []string{"init", "status", "next", "validate", "preview"}},
|
||||
{Name: "run", Desc: "existing run directory for status/next/validate/preview"},
|
||||
{Name: "title", Desc: "deck title for init"},
|
||||
{Name: "input", Desc: "local source markdown/text path for init"},
|
||||
{Name: "audience", Desc: "final audience for the deck"},
|
||||
{Name: "delivery-mode", Desc: "delivery mode: presented, self_read, dual_mode", Enum: []string{"presented", "self_read", "dual_mode"}},
|
||||
{Name: "pages", Type: "int", Desc: "target page count"},
|
||||
{Name: "out", Desc: "output run directory for init"},
|
||||
{Name: "overwrite", Type: "bool", Desc: "allow init to overwrite an existing run directory"},
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
action := runtime.Str("action")
|
||||
if action == "init" {
|
||||
if strings.TrimSpace(runtime.Str("title")) == "" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--title is required for init").WithParam("--title")
|
||||
}
|
||||
if strings.TrimSpace(runtime.Str("input")) == "" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--input is required for init").WithParam("--input")
|
||||
}
|
||||
if strings.TrimSpace(runtime.Str("out")) == "" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--out is required for init").WithParam("--out")
|
||||
}
|
||||
if stat, err := runtime.FileIO().Stat(runtime.Str("input")); err != nil {
|
||||
return common.WrapInputStatErrorTyped(err, "cannot read --input")
|
||||
} else if !stat.Mode().IsRegular() {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--input must be a regular file").WithParam("--input")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(runtime.Str("run")) == "" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--run is required for %s", action).WithParam("--run")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
action := runtime.Str("action")
|
||||
switch action {
|
||||
case "init":
|
||||
out := runtime.Str("out")
|
||||
if err := svglide.InitRun(out, svglide.InitOptions{
|
||||
Title: runtime.Str("title"),
|
||||
Input: runtime.Str("input"),
|
||||
Audience: runtime.Str("audience"),
|
||||
DeliveryMode: runtime.Str("delivery-mode"),
|
||||
Pages: runtime.Int("pages"),
|
||||
Overwrite: runtime.Bool("overwrite"),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
status, err := svglide.InspectStatus(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(map[string]any{
|
||||
"action": action,
|
||||
"run": out,
|
||||
"next_command": status.NextCommand,
|
||||
}, nil)
|
||||
return nil
|
||||
case "status":
|
||||
report, err := svglide.InspectStatus(runtime.Str("run"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(report, nil)
|
||||
return nil
|
||||
case "next":
|
||||
report, err := svglide.NextTask(runtime.Str("run"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(report, nil)
|
||||
return nil
|
||||
case "validate":
|
||||
report, err := svglide.ValidateRun(runtime.Str("run"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(report, nil)
|
||||
return nil
|
||||
case "preview":
|
||||
report, err := svglide.WritePreview(runtime.Str("run"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(report, nil)
|
||||
return nil
|
||||
default:
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "unsupported --action %q", action).WithParam("--action")
|
||||
}
|
||||
},
|
||||
}
|
||||
254
shortcuts/slides/slides_create_svglide_test.go
Normal file
254
shortcuts/slides/slides_create_svglide_test.go
Normal file
@@ -0,0 +1,254 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package slides
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
)
|
||||
|
||||
func TestSlidesCreateSVGlideInitShortcut(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
withSlidesTestWorkingDir(t, dir)
|
||||
if err := os.WriteFile("source.md", []byte("# Demo"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, slidesTestConfig(t, ""))
|
||||
|
||||
err := runSlidesShortcut(t, f, stdout, SlidesCreateSVGlide, []string{
|
||||
"+create-svglide",
|
||||
"--action", "init",
|
||||
"--title", "Demo",
|
||||
"--input", "source.md",
|
||||
"--audience", "产品负责人",
|
||||
"--delivery-mode", "self_read",
|
||||
"--pages", "8",
|
||||
"--out", "run-demo",
|
||||
"--as", "user",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "run-demo", "run.json")); err != nil {
|
||||
t.Fatalf("missing run.json: %v", err)
|
||||
}
|
||||
data := decodeShortcutData(t, stdout)
|
||||
if data["action"] != "init" {
|
||||
t.Fatalf("action = %v, want init", data["action"])
|
||||
}
|
||||
if data["run"] != "run-demo" {
|
||||
t.Fatalf("run = %v, want run-demo", data["run"])
|
||||
}
|
||||
if !strings.Contains(stringValue(data["next_command"]), "--action next --run run-demo") {
|
||||
t.Fatalf("next_command = %v, want next action", data["next_command"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateSVGlideRejectsPositionalAction(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, slidesTestConfig(t, ""))
|
||||
|
||||
err := runSlidesShortcut(t, f, stdout, SlidesCreateSVGlide, []string{
|
||||
"+create-svglide",
|
||||
"init",
|
||||
"--as", "user",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected positional argument rejection")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "positional arguments are not supported") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateSVGlideStatusAndNextActions(t *testing.T) {
|
||||
dir := initSVGlideShortcutRun(t)
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, slidesTestConfig(t, ""))
|
||||
|
||||
err := runSlidesShortcut(t, f, stdout, SlidesCreateSVGlide, []string{
|
||||
"+create-svglide",
|
||||
"--action", "status",
|
||||
"--run", "run-demo",
|
||||
"--as", "user",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
statusData := decodeShortcutData(t, stdout)
|
||||
if statusData["current_stage"] != "request" {
|
||||
t.Fatalf("current_stage = %v, want request", statusData["current_stage"])
|
||||
}
|
||||
if !strings.Contains(stringValue(statusData["next_command"]), "--action next --run run-demo") {
|
||||
t.Fatalf("next_command = %v, want next action", statusData["next_command"])
|
||||
}
|
||||
|
||||
err = runSlidesShortcut(t, f, stdout, SlidesCreateSVGlide, []string{
|
||||
"+create-svglide",
|
||||
"--action", "next",
|
||||
"--run", "run-demo",
|
||||
"--as", "user",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nextData := decodeShortcutData(t, stdout)
|
||||
if nextData["stage"] != "request" || nextData["prompt_path"] != "prompts/01_request.task.md" {
|
||||
t.Fatalf("next data = %+v, want request prompt", nextData)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "run-demo", "prompts", "01_request.task.md")); err != nil {
|
||||
t.Fatalf("missing prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateSVGlideValidateActionOutputsReport(t *testing.T) {
|
||||
initSVGlideShortcutRunWithDeck(t)
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, slidesTestConfig(t, ""))
|
||||
|
||||
err := runSlidesShortcut(t, f, stdout, SlidesCreateSVGlide, []string{
|
||||
"+create-svglide",
|
||||
"--action", "validate",
|
||||
"--run", "run-demo",
|
||||
"--as", "user",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data := decodeShortcutData(t, stdout)
|
||||
if data["ok"] != true {
|
||||
t.Fatalf("ok = %v, want true; data=%+v", data["ok"], data)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join("run-demo", "receipts", "lint.json")); err != nil {
|
||||
t.Fatalf("missing lint receipt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateSVGlidePreviewActionOutputsReport(t *testing.T) {
|
||||
initSVGlideShortcutRunWithDeck(t)
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, slidesTestConfig(t, ""))
|
||||
|
||||
err := runSlidesShortcut(t, f, stdout, SlidesCreateSVGlide, []string{
|
||||
"+create-svglide",
|
||||
"--action", "preview",
|
||||
"--run", "run-demo",
|
||||
"--as", "user",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data := decodeShortcutData(t, stdout)
|
||||
if data["status"] != "passed" {
|
||||
t.Fatalf("status = %v, want passed; data=%+v", data["status"], data)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join("run-demo", "preview.html")); err != nil {
|
||||
t.Fatalf("missing preview.html: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join("run-demo", "receipts", "preview.json")); err != nil {
|
||||
t.Fatalf("missing preview receipt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateSVGlideValidateActionDoesNotErrorOnValidationFailure(t *testing.T) {
|
||||
initSVGlideShortcutRun(t)
|
||||
writeSVGlideShortcutDeck(t, "slides/missing.svg")
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, slidesTestConfig(t, ""))
|
||||
|
||||
err := runSlidesShortcut(t, f, stdout, SlidesCreateSVGlide, []string{
|
||||
"+create-svglide",
|
||||
"--action", "validate",
|
||||
"--run", "run-demo",
|
||||
"--as", "user",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data := decodeShortcutData(t, stdout)
|
||||
if data["ok"] != false {
|
||||
t.Fatalf("ok = %v, want false; data=%+v", data["ok"], data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateSVGlideRegistered(t *testing.T) {
|
||||
for _, shortcut := range Shortcuts() {
|
||||
if shortcut.Command == "+create-svglide" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("slides +create-svglide shortcut is not registered")
|
||||
}
|
||||
|
||||
func initSVGlideShortcutRunWithDeck(t *testing.T) {
|
||||
initSVGlideShortcutRun(t)
|
||||
writeSVGlideShortcutDeck(t, "slides/01.svg")
|
||||
writeSVGlideShortcutFile(t, filepath.Join("run-demo", "slides", "01.svg"), svglideShortcutVisibleTextSVG())
|
||||
}
|
||||
|
||||
func initSVGlideShortcutRun(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
withSlidesTestWorkingDir(t, dir)
|
||||
if err := os.WriteFile("source.md", []byte("# Demo"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, slidesTestConfig(t, ""))
|
||||
if err := runSlidesShortcut(t, f, stdout, SlidesCreateSVGlide, []string{
|
||||
"+create-svglide",
|
||||
"--action", "init",
|
||||
"--title", "Demo",
|
||||
"--input", "source.md",
|
||||
"--out", "run-demo",
|
||||
"--as", "user",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func writeSVGlideShortcutDeck(t *testing.T, slidePath string) {
|
||||
t.Helper()
|
||||
deck := map[string]any{
|
||||
"title": "Demo",
|
||||
"slides": []map[string]string{{
|
||||
"id": "cover",
|
||||
"title": "Slide",
|
||||
"summary": "Summary",
|
||||
"role": "cover",
|
||||
"key_message": "Message",
|
||||
"path": slidePath,
|
||||
}},
|
||||
}
|
||||
raw, err := json.MarshalIndent(deck, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw = append(raw, '\n')
|
||||
writeSVGlideShortcutFile(t, filepath.Join("run-demo", "outline", "deck.json"), string(raw))
|
||||
}
|
||||
|
||||
func writeSVGlideShortcutFile(t *testing.T, path string, content string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func svglideShortcutVisibleTextSVG() string {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" viewBox="0 0 960 540"><rect width="960" height="540" fill="#fff"/><text x="48" y="80">Hello</text></svg>`
|
||||
}
|
||||
|
||||
func stringValue(value any) string {
|
||||
if text, ok := value.(string); ok {
|
||||
return text
|
||||
}
|
||||
var b bytes.Buffer
|
||||
_ = json.NewEncoder(&b).Encode(value)
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
Reference in New Issue
Block a user