k1LoW/deck をmonorepo的に使うための補助CLI
k1LoW/deckを使ったスライド作成で、monorepo的にひとつのワークスペース内で複数のスライドをメンテナンスすることを考えた。以下のような設定ファイルでMarkdownファイルとそれに対応するGoogle SlideのIDをマッピングしておく。
code:deckx.json
{
"decks": {
"deck1.md": "xxxxxxx",
"deck2.md": "xxxxxxx"
}
}
この状態で、次のようなコマンドで deck の呼び出しをラップする deckx CLIを作った。これによりGoogle SlideのIDを毎回渡さなくてよくなるので deck の呼び出しが楽になった。
code:sh
deckx apply --watch ./deck1.md
deckx ls-layouts ./deck1.md
deck の思想的にはおそらくUnixコマンド的にシンプルなインターフェースで、設定ファイルなど持つことは想定してないんじゃないかと思うが、せっかく deck はワンショットではなくメンテナンスしやすい作りになってるのでこういう使い方もしてみたいと思った。
deckxの実装
deck と合わせるだけの理由でGoで書いた。要件をざっとまとめて main() 関数のコメントとして書いた後はGitHub Copilotのエージェントがいい感じにやってくれたので9割がた出来上がっている。コメント含めても160行程度なので大したスクリプトじゃない。
code:deckx.go
/*
* deckx is a wrapper for the deck command line tool.
* It simplifies interaction with the deck CLI through predefined parameters.
* Configuration is stored in a deckx.json file in the same directory,
* which maps deck filenames to their corresponding Google Slide IDs.
*
* Configuration format example:
*
* {
* "decks": {
* "deck1.md": "1a2b3c4d5e6f7g8h9i0j",
* "deck2.md": "1j0i9h8g7f6e5d4c3b2a"
* }
* }
*
* Usage:
* deckx [flags] <command> <deck file>
*
* Available commands:
* - apply: Apply changes to the specified deck
* - ls-layouts: List available layouts for the specified deck
*
* Available flags:
* - --watch: Watch for changes in the specified deck (only for apply command)
*
* Example:
* deckx apply --watch ./deck1.md
*
*/
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Config represents the structure of the deckx.json configuration file
type Config struct {
Decks mapstringstring mapstructure:"decks"
}
var (
cfgFile string
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "deckx flags <command> <deck file>",
Short: "A wrapper for the deck command line tool",
Long: `deckx is a wrapper for the deck command line tool.
It simplifies interaction with the deck CLI through predefined parameters.
The configuration file is deckx.json in the same directory.`,
// We're now handling commands through subcommands
}
// applyCmd represents the apply command
var applyCmd = &cobra.Command{
Use: "apply deck file",
Short: "Apply changes to the specified deck",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
deckFile := args0
watchEnabled, _ := cmd.Flags().GetBool("watch")
return runCommand(deckFile, "apply", watchEnabled)
},
}
// lsLayoutsCmd represents the ls-layouts command
var lsLayoutsCmd = &cobra.Command{
Use: "ls-layouts deck file",
Short: "List available layouts for the specified deck",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
deckFile := args0
return runCommand(deckFile, "ls-layouts", false)
},
}
// runCommand validates the deck key and executes the command
func runCommand(deckKey, command string, watchEnabled bool) error {
// Get the slide ID for the deck
normalizedDeckKey := filepath.Base(deckKey)
slideID := viper.GetString(fmt.Sprintf("decks.%s", normalizedDeckKey))
if slideID == "" {
fmt.Printf("Deck '%s' not found in configuration\n", deckKey)
fmt.Println("Available decks:")
decks := viper.GetStringMapString("decks")
for k := range decks {
fmt.Printf("- %s\n", k)
}
return fmt.Errorf("deck not found")
}
// Build args dynamically based on command type
args := []string{command}
switch command {
case "apply":
if watchEnabled {
args = append(args, "--watch")
}
args = append(args, slideID, deckKey)
case "ls-layouts":
args = append(args, slideID)
}
// Execute the command
cmd := exec.Command("deck", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("error executing command: %v", err)
}
return nil
}
// initConfig reads in config file and ENV variables if set
func initConfig() {
if cfgFile != "" {
// Use config file from the flag
viper.SetConfigFile(cfgFile)
} else {
// Search for config in multiple locations
viper.SetConfigName("deckx")
viper.SetConfigType("json")
viper.AddConfigPath(".")
}
// Read the config file
if err := viper.ReadInConfig(); err != nil {
fmt.Printf("Error reading config file: %v\n", err)
os.Exit(1)
}
}
func init() {
// Initialize configuration before command execution
cobra.OnInitialize(initConfig)
// Add a config flag to root command to specify config file path
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is ./deckx.json)")
// Add the watch flag to the apply command
applyCmd.Flags().BoolP("watch", "w", false, "Watch for changes in the specified deck")
// Add commands to root command
rootCmd.AddCommand(applyCmd)
rootCmd.AddCommand(lsLayoutsCmd)
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
あとは楽に呼び出すために mise.toml に deckx コマンドを追加しておわり。
code:mise.toml
tools
go = "latest"
"go:github.com/k1LoW/deck/cmd/deck" = "0.16.0"
tasks
tasks.deckx
run = "go run ./deckx.go"