feat: add create-svglide shortcut

This commit is contained in:
songtianyi.theo
2026-07-03 00:15:10 +08:00
parent ead6362ab6
commit 5b264cf7b2
3 changed files with 369 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ import "github.com/larksuite/cli/shortcuts/common"
func Shortcuts() []common.Shortcut {
return []common.Shortcut{
SlidesCreate,
SlidesCreateSVGlide,
SlidesMediaUpload,
SlidesReplaceSlide,
SlidesReplacePages,

View 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")
}
},
}

View 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())
}