From 28745fc48ab17888f04a083ee4551b726cc47a43 Mon Sep 17 00:00:00 2001 From: Aleksey Bakin Date: Mon, 20 Apr 2026 14:51:22 +0300 Subject: [PATCH 1/2] feat: public go package blueprint --- blueprint/zip.go | 121 ++++++++++++++++++ bp/firmware.lua | 1 - bp/manifest.yml | 7 - .../app/enaptercli/cmd_blueprint_upload.go | 4 +- internal/app/enaptercli/execute.go | 115 ----------------- 5 files changed, 124 insertions(+), 124 deletions(-) create mode 100644 blueprint/zip.go delete mode 100644 bp/firmware.lua delete mode 100644 bp/manifest.yml diff --git a/blueprint/zip.go b/blueprint/zip.go new file mode 100644 index 0000000..661e699 --- /dev/null +++ b/blueprint/zip.go @@ -0,0 +1,121 @@ +// Package blueprint provides functions for packaging Enapter blueprints +// into zip archives, with support for .blueprintignore files. +package blueprint + +import ( + "archive/zip" + "bufio" + "bytes" + "errors" + "fmt" + "io" + "io/fs" + "strings" + + "github.com/go-git/go-git/v5/plumbing/format/gitignore" +) + +const ignoreFileName = ".blueprintignore" + +// Zip creates a zip archive from the blueprint directory at the given +// filesystem root. It respects .blueprintignore patterns (gitignore syntax). +// The .blueprintignore file itself is excluded from the archive. +func Zip(fsys fs.FS) ([]byte, error) { + matcher, err := loadIgnoreMatcher(fsys) + if err != nil { + return nil, fmt.Errorf("load %s: %w", ignoreFileName, err) + } + + buf := &bytes.Buffer{} + zw := zip.NewWriter(buf) + + err = fs.WalkDir(fsys, ".", func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + if path == "." { + return nil + } + + if matcher.Match(path, entry.IsDir()) { + if entry.IsDir() { + return fs.SkipDir + } + return nil + } + + if entry.IsDir() { + return nil + } + + f, err := fsys.Open(path) + if err != nil { + return fmt.Errorf("open: %w", err) + } + defer f.Close() + + zf, err := zw.Create(path) + if err != nil { + return fmt.Errorf("create: %w", err) + } + + if _, err = io.Copy(zf, f); err != nil { + return fmt.Errorf("copy: %w", err) + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("walk dir: %w", err) + } + + if err := zw.Close(); err != nil { + return nil, fmt.Errorf("close zip: %w", err) + } + + return buf.Bytes(), nil +} + +type ignoreMatcher struct { + m gitignore.Matcher + noFile bool +} + +func (m *ignoreMatcher) Match(path string, isDir bool) bool { + if m.noFile { + return false + } + + if path == ignoreFileName { + return true + } + + parts := strings.Split(path, "/") + return m.m.Match(parts, isDir) +} + +func loadIgnoreMatcher(fsys fs.FS) (*ignoreMatcher, error) { + f, err := fsys.Open(ignoreFileName) + if errors.Is(err, fs.ErrNotExist) { + return &ignoreMatcher{noFile: true}, nil + } + if err != nil { + return nil, err + } + defer f.Close() + + var patterns []gitignore.Pattern + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + patterns = append(patterns, gitignore.ParsePattern(line, nil)) + } + if err := scanner.Err(); err != nil { + return nil, err + } + + return &ignoreMatcher{m: gitignore.NewMatcher(patterns)}, nil +} diff --git a/bp/firmware.lua b/bp/firmware.lua deleted file mode 100644 index aa6fd65..0000000 --- a/bp/firmware.lua +++ /dev/null @@ -1 +0,0 @@ -enapter.log("Hello from firmware.lua") diff --git a/bp/manifest.yml b/bp/manifest.yml deleted file mode 100644 index fc8f08a..0000000 --- a/bp/manifest.yml +++ /dev/null @@ -1,7 +0,0 @@ -blueprint_spec: device/3.0 -display_name: Simple Lua - -runtime: - type: lua - options: - file: firmware.lua diff --git a/internal/app/enaptercli/cmd_blueprint_upload.go b/internal/app/enaptercli/cmd_blueprint_upload.go index 58b176d..f995c44 100644 --- a/internal/app/enaptercli/cmd_blueprint_upload.go +++ b/internal/app/enaptercli/cmd_blueprint_upload.go @@ -9,6 +9,8 @@ import ( "os" "github.com/urfave/cli/v3" + + "github.com/enapter/enapter-cli/blueprint" ) type cmdBlueprintUpload struct { @@ -82,7 +84,7 @@ func uploadBlueprint( var data []byte if fi.IsDir() { - data, err = zipDir(blueprintPath) + data, err = blueprint.Zip(os.DirFS(blueprintPath)) if err != nil { return fmt.Errorf("zip blueprint directory: %w", err) } diff --git a/internal/app/enaptercli/execute.go b/internal/app/enaptercli/execute.go index 3da99ae..efa60ee 100644 --- a/internal/app/enaptercli/execute.go +++ b/internal/app/enaptercli/execute.go @@ -1,17 +1,6 @@ package enaptercli import ( - "archive/zip" - "bufio" - "bytes" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" - "strings" - - "github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/urfave/cli/v3" ) @@ -44,107 +33,3 @@ func NewApp() *cli.Command { return cmd } - -const blueprintIgnoreFile = ".blueprintignore" - -func zipDir(dir string) ([]byte, error) { - fsys := os.DirFS(dir) - - matcher, err := loadBlueprintIgnore(dir) - if err != nil { - return nil, fmt.Errorf("load %s: %w", blueprintIgnoreFile, err) - } - - buf := &bytes.Buffer{} - zw := zip.NewWriter(buf) - - err = fs.WalkDir(fsys, ".", func(path string, entry fs.DirEntry, err error) error { - if err != nil { - return err - } - if path == "." { - return nil - } - - if matcher.Match(path, entry.IsDir()) { - if entry.IsDir() { - return fs.SkipDir - } - return nil - } - - if entry.IsDir() { - return nil - } - - f, err := fsys.Open(path) - if err != nil { - return fmt.Errorf("open: %w", err) - } - defer f.Close() - - zf, err := zw.Create(path) - if err != nil { - return fmt.Errorf("create: %w", err) - } - - if _, err = io.Copy(zf, f); err != nil { - return fmt.Errorf("copy: %w", err) - } - return nil - }) - if err != nil { - return nil, fmt.Errorf("walk dir: %w", err) - } - - if err := zw.Close(); err != nil { - return nil, fmt.Errorf("close zip: %w", err) - } - - return buf.Bytes(), nil -} - -type blueprintIgnoreMatcher struct { - m gitignore.Matcher - noFile bool -} - -func (m *blueprintIgnoreMatcher) Match(path string, isDir bool) bool { - if m.noFile { - return false - } - - if path == blueprintIgnoreFile { - return true - } - - parts := strings.Split(path, string(filepath.Separator)) - return m.m.Match(parts, isDir) -} - -func loadBlueprintIgnore(dir string) (*blueprintIgnoreMatcher, error) { - f, err := os.Open(filepath.Join(dir, blueprintIgnoreFile)) - if os.IsNotExist(err) { - return &blueprintIgnoreMatcher{noFile: true}, nil - } - if err != nil { - return nil, err - } - defer f.Close() - - var patterns []gitignore.Pattern - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - patterns = append(patterns, gitignore.ParsePattern(line, nil)) - } - if err := scanner.Err(); err != nil { - return nil, err - } - - return &blueprintIgnoreMatcher{m: gitignore.NewMatcher(patterns)}, nil -} From 4434d45143c0ef74989be19137dbb6b9de0895ba Mon Sep 17 00:00:00 2001 From: Aleksey Bakin Date: Mon, 20 Apr 2026 18:24:43 +0300 Subject: [PATCH 2/2] feat: trim spaces in names --- internal/app/cliflags/trim.go | 17 +++++++++++++++++ internal/app/enaptercli/cmd_connection_add.go | 2 ++ .../enaptercli/cmd_device_create_lua_device.go | 7 +++++-- .../enaptercli/cmd_device_create_standalone.go | 7 +++++-- internal/app/enaptercli/cmd_device_update.go | 7 +++++-- .../enaptercli/cmd_rule_engine_rule_create.go | 1 + .../enaptercli/cmd_rule_engine_rule_update.go | 3 +++ 7 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 internal/app/cliflags/trim.go diff --git a/internal/app/cliflags/trim.go b/internal/app/cliflags/trim.go new file mode 100644 index 0000000..151edd0 --- /dev/null +++ b/internal/app/cliflags/trim.go @@ -0,0 +1,17 @@ +package cliflags + +import ( + "context" + "strings" + + "github.com/urfave/cli/v3" +) + +// TrimSpaceAction returns a cli.StringFlag Action that trims leading and +// trailing whitespace from the flag value and stores it in dest. +func TrimSpaceAction(dest *string) func(context.Context, *cli.Command, string) error { + return func(_ context.Context, _ *cli.Command, v string) error { + *dest = strings.TrimSpace(v) + return nil + } +} diff --git a/internal/app/enaptercli/cmd_connection_add.go b/internal/app/enaptercli/cmd_connection_add.go index 64134bc..ff73dba 100644 --- a/internal/app/enaptercli/cmd_connection_add.go +++ b/internal/app/enaptercli/cmd_connection_add.go @@ -11,6 +11,7 @@ import ( "github.com/urfave/cli/v3" + "github.com/enapter/enapter-cli/internal/app/cliflags" "github.com/enapter/enapter-cli/internal/app/configfile" ) @@ -34,6 +35,7 @@ func buildCmdConnectionAdd() *cli.Command { Usage: "Connection name", Destination: &cmd.name, Required: true, + Action: cliflags.TrimSpaceAction(&cmd.name), }, &cli.BoolFlag{ Name: "gateway", diff --git a/internal/app/enaptercli/cmd_device_create_lua_device.go b/internal/app/enaptercli/cmd_device_create_lua_device.go index 3d5a38b..0b58a35 100644 --- a/internal/app/enaptercli/cmd_device_create_lua_device.go +++ b/internal/app/enaptercli/cmd_device_create_lua_device.go @@ -7,9 +7,10 @@ import ( "errors" "fmt" "net/http" - "strings" "github.com/urfave/cli/v3" + + "github.com/enapter/enapter-cli/internal/app/cliflags" ) type cmdDeviceCreateLua struct { @@ -54,10 +55,12 @@ func (c *cmdDeviceCreateLua) Flags() []cli.Flag { Usage: "name for the new Lua device", Destination: &c.deviceName, Required: true, + Action: cliflags.TrimSpaceAction(&c.deviceName), }, &cli.StringFlag{ Name: "device-slug", Usage: "slug for the new Lua device", Destination: &c.deviceSlug, + Action: cliflags.TrimSpaceAction(&c.deviceSlug), }, &cli.StringFlag{ Name: "blueprint-id", Aliases: []string{"b"}, @@ -101,7 +104,7 @@ func (c *cmdDeviceCreateLua) do(ctx context.Context) error { body, err := json.Marshal(map[string]interface{}{ "runtime_id": runtimeID, - "name": strings.TrimSpace(c.deviceName), + "name": c.deviceName, "slug": c.deviceSlug, "blueprint_id": c.blueprintID, }) diff --git a/internal/app/enaptercli/cmd_device_create_standalone.go b/internal/app/enaptercli/cmd_device_create_standalone.go index 8dc23b5..68e4726 100644 --- a/internal/app/enaptercli/cmd_device_create_standalone.go +++ b/internal/app/enaptercli/cmd_device_create_standalone.go @@ -6,9 +6,10 @@ import ( "encoding/json" "fmt" "net/http" - "strings" "github.com/urfave/cli/v3" + + "github.com/enapter/enapter-cli/internal/app/cliflags" ) type cmdDeviceCreateStandalone struct { @@ -45,10 +46,12 @@ func (c *cmdDeviceCreateStandalone) Flags() []cli.Flag { Usage: "Name for the new device", Destination: &c.deviceName, Required: true, + Action: cliflags.TrimSpaceAction(&c.deviceName), }, &cli.StringFlag{ Name: "device-slug", Usage: "Slug for the new standalone device", Destination: &c.deviceSlug, + Action: cliflags.TrimSpaceAction(&c.deviceSlug), }) } @@ -60,7 +63,7 @@ func (c *cmdDeviceCreateStandalone) do(ctx context.Context) error { body, err := json.Marshal(map[string]any{ "site_id": siteID, - "name": strings.TrimSpace(c.deviceName), + "name": c.deviceName, "slug": c.deviceSlug, }) if err != nil { diff --git a/internal/app/enaptercli/cmd_device_update.go b/internal/app/enaptercli/cmd_device_update.go index 853aac7..83bcadd 100644 --- a/internal/app/enaptercli/cmd_device_update.go +++ b/internal/app/enaptercli/cmd_device_update.go @@ -6,9 +6,10 @@ import ( "encoding/json" "fmt" "net/http" - "strings" "github.com/urfave/cli/v3" + + "github.com/enapter/enapter-cli/internal/app/cliflags" ) type cmdDeviceUpdate struct { @@ -46,18 +47,20 @@ func (c *cmdDeviceUpdate) Flags() []cli.Flag { Name: "name", Usage: "Device name", Destination: &c.name, + Action: cliflags.TrimSpaceAction(&c.name), }, &cli.StringFlag{ Name: "slug", Usage: "Device slug", Destination: &c.slug, + Action: cliflags.TrimSpaceAction(&c.slug), }, ) } func (c *cmdDeviceUpdate) do(ctx context.Context) error { payload := map[string]string{ - "name": strings.TrimSpace(c.name), + "name": c.name, "slug": c.slug, } body, err := json.Marshal(payload) diff --git a/internal/app/enaptercli/cmd_rule_engine_rule_create.go b/internal/app/enaptercli/cmd_rule_engine_rule_create.go index f33f6c8..c08f95e 100644 --- a/internal/app/enaptercli/cmd_rule_engine_rule_create.go +++ b/internal/app/enaptercli/cmd_rule_engine_rule_create.go @@ -45,6 +45,7 @@ func (c *cmdRuleEngineRuleCreate) Flags() []cli.Flag { Usage: "Slug for the new rule", Destination: &c.slug, Required: true, + Action: cliflags.TrimSpaceAction(&c.slug), }, &cli.StringFlag{ Name: "script", diff --git a/internal/app/enaptercli/cmd_rule_engine_rule_update.go b/internal/app/enaptercli/cmd_rule_engine_rule_update.go index d2d7974..5039213 100644 --- a/internal/app/enaptercli/cmd_rule_engine_rule_update.go +++ b/internal/app/enaptercli/cmd_rule_engine_rule_update.go @@ -8,6 +8,8 @@ import ( "net/http" "github.com/urfave/cli/v3" + + "github.com/enapter/enapter-cli/internal/app/cliflags" ) type cmdRuleEngineRuleUpdate struct { @@ -42,6 +44,7 @@ func (c *cmdRuleEngineRuleUpdate) Flags() []cli.Flag { Name: "slug", Usage: "A new rule slug", Destination: &c.slug, + Action: cliflags.TrimSpaceAction(&c.slug), }, ) }