Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 8 additions & 12 deletions backend/diagnostics/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"fmt"
"log"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
Expand Down Expand Up @@ -40,25 +40,21 @@ 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
}

// file rotate
logFile := path.Join(baseDir, name+".log")
logFile := filepath.Join(baseDir, name+".log")
log.Println(("set logger to app.log"))

lj := &lumberjack.Logger{
Expand Down Expand Up @@ -180,15 +176,15 @@ func (b *BackendLogger) ListLogFiles(ctx context.Context) ([]string, error) {
}

func (b *BackendLogger) ReadLog(ctx context.Context, name string) (string, error) {
data, err := os.ReadFile(path.Join(b.logDir, name+".log"))
data, err := os.ReadFile(filepath.Join(b.logDir, name+".log"))
if err != nil {
return "", err
}
return string(data), nil
}

func (b *BackendLogger) SearchLog(ctx context.Context, name, pattern string) ([]string, error) {
f, err := os.Open(path.Join(b.logDir, name+".log"))
f, err := os.Open(filepath.Join(b.logDir, name+".log"))
if err != nil {
return nil, err
}
Expand All @@ -214,7 +210,7 @@ func (b *BackendLogger) StartTail(ctx context.Context, name string) error {
if _, ok := b.watchers[name]; ok {
return nil // already tailing
}
t, err := tail.TailFile(path.Join(b.logDir, name+".log"), tail.Config{
t, err := tail.TailFile(filepath.Join(b.logDir, name+".log"), tail.Config{
Follow: true, ReOpen: true, MustExist: true,
})
if err != nil {
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