Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ jobs:
-X github.com/omniviewdev/omniview/internal/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
-X github.com/omniviewdev/omniview/internal/version.Development=true \
-X github.com/omniviewdev/omniview/internal/telemetry.buildOTLPEndpoint=${{ secrets.TELEMETRY_OTLP_ENDPOINT }} \
-X github.com/omniviewdev/omniview/internal/telemetry.buildPyroscopeEndpoint=${{ secrets.TELEMETRY_PYROSCOPE_ENDPOINT }}" \
-X github.com/omniviewdev/omniview/internal/telemetry.buildPyroscopeEndpoint=${{ secrets.TELEMETRY_PYROSCOPE_ENDPOINT }} \
-X github.com/omniviewdev/omniview/internal/appstate.buildStateDir=~/.omniview-nightly" \
-o bin/Omniview .

- name: Create macOS .app bundle
Expand Down Expand Up @@ -253,7 +254,8 @@ jobs:
-X github.com/omniviewdev/omniview/internal/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
-X github.com/omniviewdev/omniview/internal/version.Development=true \
-X github.com/omniviewdev/omniview/internal/telemetry.buildOTLPEndpoint=${{ secrets.TELEMETRY_OTLP_ENDPOINT }} \
-X github.com/omniviewdev/omniview/internal/telemetry.buildPyroscopeEndpoint=${{ secrets.TELEMETRY_PYROSCOPE_ENDPOINT }}" \
-X github.com/omniviewdev/omniview/internal/telemetry.buildPyroscopeEndpoint=${{ secrets.TELEMETRY_PYROSCOPE_ENDPOINT }} \
-X github.com/omniviewdev/omniview/internal/appstate.buildStateDir=~/.omniview-nightly" \
-o bin/Omniview .

- name: Rename executable
Expand Down Expand Up @@ -325,7 +327,8 @@ jobs:
-X github.com/omniviewdev/omniview/internal/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
-X github.com/omniviewdev/omniview/internal/version.Development=true \
-X github.com/omniviewdev/omniview/internal/telemetry.buildOTLPEndpoint=${{ secrets.TELEMETRY_OTLP_ENDPOINT }} \
-X github.com/omniviewdev/omniview/internal/telemetry.buildPyroscopeEndpoint=${{ secrets.TELEMETRY_PYROSCOPE_ENDPOINT }}" \
-X github.com/omniviewdev/omniview/internal/telemetry.buildPyroscopeEndpoint=${{ secrets.TELEMETRY_PYROSCOPE_ENDPOINT }} \
-X github.com/omniviewdev/omniview/internal/appstate.buildStateDir=~/.omniview-nightly" \
-o bin/Omniview.exe .

- name: Rename executable
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ jobs:
-X github.com/omniviewdev/omniview/internal/version.Version=0.0.0-pr.${{ github.event.pull_request.number }} \
-X github.com/omniviewdev/omniview/internal/version.GitCommit=${{ github.sha }} \
-X github.com/omniviewdev/omniview/internal/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
-X github.com/omniviewdev/omniview/internal/version.Development=true" \
-X github.com/omniviewdev/omniview/internal/version.Development=true \
-X github.com/omniviewdev/omniview/internal/appstate.buildStateDir=~/.omniview-pr" \
-o bin/Omniview .

- name: Create macOS .app bundle
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,6 @@ e2e/playwright-report/

# Task runner
.task
omniview
/omniview
docs/superpowers/
cmd/omniview-plugin-dev/omniview-plugin-dev
25 changes: 25 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Claude Code Guidelines

## Wails Bindings

**Always use the Taskfile task to generate bindings.** Never run `wails3 generate bindings` manually.

```bash
task bindings
```

This ensures bindings are generated with the correct flags, output directory (`packages/omniviewdev-runtime/src/bindings`), and cleanup. The raw `wails3` command generates to the wrong location.

## Build & Test

```bash
GOWORK=off go build . # Build the Go backend
GOWORK=off go test ./... -count=1 # Run all Go tests
task dev # Run the full app in dev mode
```

Use `GOWORK=off` for all Go commands — the module is not in the parent `go.work` file.

## Commits

- Do not add `Co-Authored-By` or any Claude attribution to commits or PRs.
18 changes: 6 additions & 12 deletions backend/clients/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"
"net/url"
"os"
"path"
"path/filepath"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
Expand All @@ -19,27 +19,21 @@ func (lumberjackSink) Sync() error {
return nil
}

func CreateLogger(dev bool) *zap.SugaredLogger {
// CreateLogger creates a zap SugaredLogger that writes to a log file in logDir.
func CreateLogger(dev bool, logDir string) *zap.SugaredLogger {
var level zapcore.Level
if dev {
level = zap.DebugLevel
} else {
level = zap.ErrorLevel
}

// store the logs in the dot directory
baseDir, err := os.UserHomeDir()
if err != nil {
baseDir = os.TempDir()
} else {
baseDir = path.Join(baseDir, ".omniview", "logs")
}

if err := os.MkdirAll(baseDir, 0755); err != nil {
if err := os.MkdirAll(logDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "failed to create log directory %s: %v\n", logDir, err)
return zap.L().Sugar()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

logFile := path.Join(baseDir, "app.log")
logFile := filepath.Join(logDir, "app.log")

encoderConfig := zapcore.EncoderConfig{
TimeKey: "ts",
Expand Down
4 changes: 2 additions & 2 deletions backend/diagnostics/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ type DiagnosticsClient struct {
UI *BackendLogger
}

func NewDiagnosticsClient(dev bool) *DiagnosticsClient {
uiLogger, err := NewBackendLogger("ui", dev)
func NewDiagnosticsClient(dev bool, logDir string) *DiagnosticsClient {
uiLogger, err := NewBackendLogger("ui", dev, logDir)
if err != nil {
log.Fatalf("failed to init UI logger: %v", err)
}
Expand Down
10 changes: 3 additions & 7 deletions backend/diagnostics/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,15 @@ func (b *BackendLogger) ServiceShutdown() error {
}

// NewBackendLogger creates (and binds) a Zap SugaredLogger writing to `<name>.log`.
func NewBackendLogger(name string, dev bool) (*BackendLogger, error) {
// logDir is the directory where log files are stored (e.g. from appstate.Service.Logs().ResolvePath("")).
func NewBackendLogger(name string, dev bool, logDir string) (*BackendLogger, error) {
// determine level
lvl := zapcore.ErrorLevel
if dev {
lvl = zapcore.DebugLevel
}

// prepare log directory
home, err := os.UserHomeDir()
if err != nil {
home = os.TempDir()
}
baseDir := path.Join(home, ".omniview", "logs")
baseDir := logDir
if err := os.MkdirAll(baseDir, 0755); err != nil {
return nil, err
}
Expand Down
20 changes: 20 additions & 0 deletions backend/pkg/plugin/adapters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package plugin

// PluginRefAdapter adapts plugin.Manager to devserver.PluginRef.
type PluginRefAdapter struct{ Mgr Manager }

func (a *PluginRefAdapter) GetDevPluginInfo(pluginID string) (bool, string, error) {
info, err := a.Mgr.GetPlugin(pluginID)
if err != nil {
return false, "", err
}
return info.DevMode, info.DevPath, nil
}

// PluginReloaderAdapter adapts plugin.Manager to devserver.PluginReloader.
type PluginReloaderAdapter struct{ Mgr Manager }

func (a *PluginReloaderAdapter) ReloadPlugin(id string) error {
_, err := a.Mgr.ReloadPlugin(id)
return err
}
70 changes: 17 additions & 53 deletions backend/pkg/plugin/data/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/wailsapp/wails/v3/pkg/application"

"github.com/omniviewdev/omniview/internal/appstate"
logging "github.com/omniviewdev/plugin-sdk/log"
)

// Controller provides a JSON key-value store for plugins to persist arbitrary data.
// Each key is stored as a separate JSON file under ~/.omniview/plugins/{pluginID}/data/.
// Each key is stored as a separate JSON file under <stateRoot>/plugins/{pluginID}/data/.
type Controller interface {
ServiceStartup(ctx context.Context, options application.ServiceOptions) error
ServiceShutdown() error
Expand All @@ -27,13 +27,16 @@ type Controller interface {
var _ Controller = (*controller)(nil)

type controller struct {
logger logging.Logger
logger logging.Logger
pluginDataFn func(pluginID string) (*appstate.ScopedRoot, error)
}

// NewController creates a new data store controller.
func NewController(logger logging.Logger) Controller {
// pluginDataFn returns a ScopedRoot for the given plugin's data directory.
func NewController(logger logging.Logger, pluginDataFn func(string) (*appstate.ScopedRoot, error)) Controller {
return &controller{
logger: logger.Named("DataController"),
logger: logger.Named("DataController"),
pluginDataFn: pluginDataFn,
}
}

Expand All @@ -45,48 +48,15 @@ func (c *controller) ServiceShutdown() error {
return nil
}

// dataDir returns the data directory for a plugin, creating it if necessary.
func (c *controller) dataDir(pluginID string) (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
dir := filepath.Join(homeDir, ".omniview", "plugins", filepath.Clean(pluginID), "data")
// Containment check: ensure the resolved path stays under .omniview/plugins.
pluginsRoot := filepath.Join(homeDir, ".omniview", "plugins")
if !strings.HasPrefix(dir, pluginsRoot+string(filepath.Separator)) {
return "", fmt.Errorf("invalid plugin ID %q: path escapes plugins directory", pluginID)
}
if err := os.MkdirAll(dir, 0700); err != nil {
return "", err
}
return dir, nil
}

// keyPath returns the full file path for a given plugin/key combination.
func (c *controller) keyPath(pluginID, key string) (string, error) {
dir, err := c.dataDir(pluginID)
if err != nil {
return "", err
}
full := filepath.Join(dir, filepath.Clean(key)+".json")
// Containment check: ensure the key doesn't escape the data directory.
if !strings.HasPrefix(full, dir+string(filepath.Separator)) {
return "", fmt.Errorf("invalid key %q: path escapes data directory", key)
}
return full, nil
}

func (c *controller) Get(pluginID, key string) (any, error) {
logger := c.logger.With(logging.Any("pluginID", pluginID), logging.Any("key", key))

path, err := c.keyPath(pluginID, key)
root, err := c.pluginDataFn(pluginID)
if err != nil {
logger.Errorw(context.Background(), "failed to resolve key path", "error", err)
return nil, err
}

data, err := os.ReadFile(path)
data, err := root.ReadFile(key + ".json")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
Expand All @@ -107,19 +77,17 @@ func (c *controller) Get(pluginID, key string) (any, error) {
func (c *controller) Set(pluginID, key string, value any) error {
logger := c.logger.With(logging.Any("pluginID", pluginID), logging.Any("key", key))

path, err := c.keyPath(pluginID, key)
root, err := c.pluginDataFn(pluginID)
if err != nil {
logger.Errorw(context.Background(), "failed to resolve key path", "error", err)
return err
}

data, err := json.MarshalIndent(value, "", " ")
if err != nil {
logger.Errorw(context.Background(), "failed to marshal data", "error", err)
return err
}

if err := os.WriteFile(path, data, 0600); err != nil {
if err := root.WriteFile(key+".json", data, 0600); err != nil {
logger.Errorw(context.Background(), "failed to write data file", "error", err)
return err
}
Expand All @@ -130,13 +98,11 @@ func (c *controller) Set(pluginID, key string, value any) error {
func (c *controller) Delete(pluginID, key string) error {
logger := c.logger.With(logging.Any("pluginID", pluginID), logging.Any("key", key))

path, err := c.keyPath(pluginID, key)
root, err := c.pluginDataFn(pluginID)
if err != nil {
logger.Errorw(context.Background(), "failed to resolve key path", "error", err)
return err
}

if err := os.Remove(path); err != nil {
if err := root.Remove(key + ".json"); err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
Expand All @@ -150,13 +116,11 @@ func (c *controller) Delete(pluginID, key string) error {
func (c *controller) Keys(pluginID string) ([]string, error) {
logger := c.logger.With(logging.Any("pluginID", pluginID))

dir, err := c.dataDir(pluginID)
root, err := c.pluginDataFn(pluginID)
if err != nil {
logger.Errorw(context.Background(), "failed to resolve data dir", "error", err)
return nil, err
}

entries, err := os.ReadDir(dir)
entries, err := root.ReadDir(".")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return []string{}, nil
Expand Down
39 changes: 39 additions & 0 deletions backend/pkg/plugin/data/service_wrapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package data

import (
"context"

"github.com/wailsapp/wails/v3/pkg/application"
)

// ServiceWrapper is an explicit delegation wrapper around data.Controller.
type ServiceWrapper struct {
Ctrl Controller
}

func (s *ServiceWrapper) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
if ss, ok := s.Ctrl.(interface {
ServiceStartup(context.Context, application.ServiceOptions) error
}); ok {
return ss.ServiceStartup(ctx, options)
}
return nil
}
func (s *ServiceWrapper) ServiceShutdown() error {
if ss, ok := s.Ctrl.(interface{ ServiceShutdown() error }); ok {
return ss.ServiceShutdown()
}
return nil
}
func (s *ServiceWrapper) Get(pluginID, key string) (any, error) {
return s.Ctrl.Get(pluginID, key)
}
func (s *ServiceWrapper) Set(pluginID, key string, value any) error {
return s.Ctrl.Set(pluginID, key, value)
}
func (s *ServiceWrapper) Delete(pluginID, key string) error {
return s.Ctrl.Delete(pluginID, key)
}
func (s *ServiceWrapper) Keys(pluginID string) ([]string, error) {
return s.Ctrl.Keys(pluginID)
}
Loading
Loading