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
18 changes: 9 additions & 9 deletions cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import (

"github.com/supermodeltools/cli/internal/analyze"
"github.com/supermodeltools/cli/internal/config"
"github.com/supermodeltools/cli/internal/files"
"github.com/supermodeltools/cli/internal/shards"
)

func init() {
var opts analyze.Options
var noFiles bool
var noShards bool

c := &cobra.Command{
Use: "analyze [path]",
Expand All @@ -21,8 +21,8 @@ call graph generation, dependency analysis, and domain classification.
Results are cached locally by content hash. Subsequent commands
(dead-code, blast-radius, graph) reuse the cache automatically.

By default, .graph.* sidecar files are written next to each source file.
Use --no-files to skip writing graph files.`,
By default, .graph.* shard files are written next to each source file.
Use --no-shards to skip writing graph files.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
Expand All @@ -36,19 +36,19 @@ Use --no-files to skip writing graph files.`,
if len(args) > 0 {
dir = args[0]
}
if cfg.FilesEnabled() && !noFiles {
// File mode: Generate handles the full pipeline (API call +
// cache + sidecars) in a single upload. Running analyze.Run
if cfg.ShardsEnabled() && !noShards {
// Shard mode: Generate handles the full pipeline (API call +
// cache + shards) in a single upload. Running analyze.Run
// first would duplicate the API call.
return files.Generate(cmd.Context(), cfg, dir, files.GenerateOptions{Force: opts.Force})
return shards.Generate(cmd.Context(), cfg, dir, shards.GenerateOptions{Force: opts.Force})
}
return analyze.Run(cmd.Context(), cfg, dir, opts)
},
}

c.Flags().BoolVar(&opts.Force, "force", false, "re-analyze even if a cached result exists")
c.Flags().StringVarP(&opts.Output, "output", "o", "", "output format: human|json")
c.Flags().BoolVar(&noFiles, "no-files", false, "skip writing .graph.* sidecar files")
c.Flags().BoolVar(&noShards, "no-shards", false, "skip writing .graph.* shard files")

rootCmd.AddCommand(c)
}
4 changes: 2 additions & 2 deletions cmd/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"github.com/spf13/cobra"

"github.com/supermodeltools/cli/internal/config"
"github.com/supermodeltools/cli/internal/files"
"github.com/supermodeltools/cli/internal/shards"
)

func init() {
Expand All @@ -23,7 +23,7 @@ func init() {
if len(args) > 0 {
dir = args[0]
}
return files.Clean(cmd.Context(), cfg, dir, dryRun)
return shards.Clean(cmd.Context(), cfg, dir, dryRun)
},
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package cmd
import (
"github.com/spf13/cobra"

"github.com/supermodeltools/cli/internal/files"
"github.com/supermodeltools/cli/internal/shards"
)

func init() {
Expand All @@ -15,7 +15,7 @@ func init() {
Long: `Reads a Claude Code PostToolUse JSON payload from stdin and forwards the file path to the running watch daemon via UDP. Install as a PostToolUse hook in .claude/settings.json.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return files.Hook(port)
return shards.Hook(port)
},
}

Expand Down
6 changes: 3 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/spf13/cobra"

"github.com/supermodeltools/cli/internal/config"
"github.com/supermodeltools/cli/internal/files"
"github.com/supermodeltools/cli/internal/shards"
)

// noConfigCommands are subcommands that work without a config file.
Expand Down Expand Up @@ -66,14 +66,14 @@ See https://supermodeltools.com for documentation.`,
if len(args) > 0 {
dir = args[0]
}
opts := files.WatchOptions{
opts := shards.WatchOptions{
CacheFile: watchCacheFile,
Debounce: watchDebounce,
NotifyPort: watchNotifyPort,
FSWatch: watchFSWatch,
PollInterval: watchPollInterval,
}
return files.Watch(cmd.Context(), cfg, dir, opts)
return shards.Watch(cmd.Context(), cfg, dir, opts)
},
}

Expand Down
15 changes: 8 additions & 7 deletions internal/analyze/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ func GetGraph(ctx context.Context, cfg *config.Config, dir string, force bool) (

client := api.New(cfg)
spin = ui.Start("Uploading and analyzing repository…")
ir, err := client.AnalyzeSidecars(ctx, zipPath, "analyze-"+hash[:16])
ir, err := client.AnalyzeShards(ctx, zipPath, "analyze-"+hash[:16])
spin.Stop()
if err != nil {
return nil, hash, err
}

g := api.GraphFromSidecarIR(ir)
g := api.GraphFromShardIR(ir)

// Cache under both keys: fingerprint (fast lookup) and zip hash (fallback).
fingerprint, fpErr := cache.RepoFingerprint(dir)
Expand All @@ -93,15 +93,16 @@ func GetGraph(ctx context.Context, cfg *config.Config, dir string, force bool) (
ui.Warn("could not write cache: %v", err)
}

// Also populate the sidecar cache (.supermodel/graph.json) so that
// Also populate the shard cache (.supermodel/graph.json) so that
// files.Generate() called after analyze reuses this result without a
// second API upload.
sidecarCacheFile := filepath.Join(dir, ".supermodel", "graph.json")
absDir, _ := filepath.Abs(dir)
shardCacheFile := filepath.Join(absDir, ".supermodel", "shards.json")
if irJSON, marshalErr := json.MarshalIndent(ir, "", " "); marshalErr == nil {
if mkErr := os.MkdirAll(filepath.Dir(sidecarCacheFile), 0o755); mkErr == nil {
tmp := sidecarCacheFile + ".tmp"
if mkErr := os.MkdirAll(filepath.Dir(shardCacheFile), 0o755); mkErr == nil {
tmp := shardCacheFile + ".tmp"
if writeErr := os.WriteFile(tmp, irJSON, 0o644); writeErr == nil {
_ = os.Rename(tmp, sidecarCacheFile)
_ = os.Rename(tmp, shardCacheFile)
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,15 @@ func (c *Client) pollLoop(ctx context.Context, post func() (*JobResponse, error)
return job, nil
}

// AnalyzeSidecars uploads a repository ZIP and runs the full analysis pipeline,
// returning the complete SidecarIR response with full Node/Relationship data
// AnalyzeShards uploads a repository ZIP and runs the full analysis pipeline,
// returning the complete ShardIR response with full Node/Relationship data
// required for sidecar rendering (IDs, labels, properties preserved).
func (c *Client) AnalyzeSidecars(ctx context.Context, zipPath, idempotencyKey string) (*SidecarIR, error) {
func (c *Client) AnalyzeShards(ctx context.Context, zipPath, idempotencyKey string) (*ShardIR, error) {
job, err := c.pollUntilComplete(ctx, zipPath, idempotencyKey)
if err != nil {
return nil, err
}
var ir SidecarIR
var ir ShardIR
if err := json.Unmarshal(job.Result, &ir); err != nil {
return nil, fmt.Errorf("decode sidecar result: %w", err)
}
Expand Down
30 changes: 15 additions & 15 deletions internal/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,45 +144,45 @@
DescriptionSummary string `json:"descriptionSummary"`
}

// SidecarIR is the full structured response from /v1/graphs/supermodel used
// ShardIR is the full structured response from /v1/graphs/supermodel used
// by the sidecars vertical slice. Unlike SupermodelIR (which uses simplified
// IRNode/IRRelationship stubs), SidecarIR preserves the complete node graph
// IRNode/IRRelationship stubs), ShardIR preserves the complete node graph
// with IDs, labels, and properties required for sidecar rendering.
type SidecarIR struct {
type ShardIR struct {
Repo string `json:"repo"`

Check failure on line 152 in internal/api/types.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not properly formatted (goimports)
Summary map[string]any `json:"summary"`
Metadata IRMetadata `json:"metadata"`
Domains []SidecarDomain `json:"domains"`
Graph SidecarGraph `json:"graph"`
Domains []ShardDomain `json:"domains"`
Graph ShardGraph `json:"graph"`
}

// SidecarGraph is the full node/relationship graph embedded in SidecarIR.
type SidecarGraph struct {
// ShardGraph is the full node/relationship graph embedded in ShardIR.
type ShardGraph struct {
Nodes []Node `json:"nodes"`
Relationships []Relationship `json:"relationships"`
}

// SidecarDomain is a semantic domain from the API with file references.
type SidecarDomain struct {
// ShardDomain is a semantic domain from the API with file references.
type ShardDomain struct {
Name string `json:"name"`
DescriptionSummary string `json:"descriptionSummary"`
KeyFiles []string `json:"keyFiles"`
Responsibilities []string `json:"responsibilities"`
Subdomains []SidecarSubdomain `json:"subdomains"`
Subdomains []ShardSubdomain `json:"subdomains"`
}

// SidecarSubdomain is a named sub-area within a SidecarDomain.
type SidecarSubdomain struct {
// ShardSubdomain is a named sub-area within a ShardDomain.
type ShardSubdomain struct {
Name string `json:"name"`
DescriptionSummary string `json:"descriptionSummary"`
Files []string `json:"files"`
KeyFiles []string `json:"keyFiles"`
}

// GraphFromSidecarIR builds a display Graph from a SidecarIR response.
// SidecarIR uses the same Node/Relationship types, so this is a zero-copy
// GraphFromShardIR builds a display Graph from a ShardIR response.
// ShardIR uses the same Node/Relationship types, so this is a zero-copy
// extraction that also populates the repoId metadata field.
func GraphFromSidecarIR(ir *SidecarIR) *Graph {
func GraphFromShardIR(ir *ShardIR) *Graph {
return &Graph{
Nodes: ir.Graph.Nodes,
Relationships: ir.Graph.Relationships,
Expand Down
6 changes: 5 additions & 1 deletion internal/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ func PutJSON(hash string, v any) error {
if err := os.WriteFile(tmp, data, 0o600); err != nil {
return fmt.Errorf("write cache: %w", err)
}
return os.Rename(tmp, filepath.Join(dir(), hash+".json"))
if err := os.Rename(tmp, filepath.Join(dir(), hash+".json")); err != nil {
_ = os.Remove(tmp)
return err
}
return nil
}

// GetJSON reads the cached JSON for hash and unmarshals it into v.
Expand Down
14 changes: 7 additions & 7 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type Config struct {
APIKey string `yaml:"api_key,omitempty"`
APIBase string `yaml:"api_base,omitempty"`
Output string `yaml:"output,omitempty"` // "human" | "json"
Files *bool `yaml:"files,omitempty"`
Shards *bool `yaml:"shards,omitempty"`
}

// Dir returns the Supermodel config directory (~/.supermodel).
Expand Down Expand Up @@ -71,10 +71,10 @@ func (c *Config) Save() error {
return nil
}

// FilesEnabled reports whether file mode is on. Defaults to true.
func (c *Config) FilesEnabled() bool {
if c.Files != nil {
return *c.Files
// ShardsEnabled reports whether shard mode is on. Defaults to true.
func (c *Config) ShardsEnabled() bool {
if c.Shards != nil {
return *c.Shards
}
return true
}
Expand Down Expand Up @@ -107,8 +107,8 @@ func (c *Config) applyEnv() {
if base := os.Getenv("SUPERMODEL_API_BASE"); base != "" {
c.APIBase = base
}
if os.Getenv("SUPERMODEL_FILES") == "false" {
c.Files = boolPtr(false)
if os.Getenv("SUPERMODEL_SHARDS") == "false" {
c.Shards = boolPtr(false)
}
}

Expand Down
4 changes: 2 additions & 2 deletions internal/deadcode/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func Run(ctx context.Context, cfg *config.Config, dir string, opts *Options) err
// Fast-path: check cache by git fingerprint before creating the zip.
if !opts.Force {
if fp, err := cache.RepoFingerprint(dir); err == nil {
key := cache.AnalysisKey(fp, "dead-code", build.Version)
key := cache.AnalysisKey(fp, fmt.Sprintf("dead-code:%s:%d", opts.MinConfidence, opts.Limit), build.Version)
var cached api.DeadCodeResult
if hit, _ := cache.GetJSON(key, &cached); hit {
ui.Success("Using cached dead-code analysis")
Expand Down Expand Up @@ -63,7 +63,7 @@ func Run(ctx context.Context, cfg *config.Config, dir string, opts *Options) err

// Store result in cache for subsequent calls.
if fp, err := cache.RepoFingerprint(dir); err == nil {
key := cache.AnalysisKey(fp, "dead-code", build.Version)
key := cache.AnalysisKey(fp, fmt.Sprintf("dead-code:%s:%d", opts.MinConfidence, opts.Limit), build.Version)
_ = cache.PutJSON(key, result)
}

Expand Down
21 changes: 0 additions & 21 deletions internal/files/daemon_export_test.go

This file was deleted.

28 changes: 16 additions & 12 deletions internal/setup/wizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

"github.com/supermodeltools/cli/internal/auth"
"github.com/supermodeltools/cli/internal/config"
"github.com/supermodeltools/cli/internal/files"
"github.com/supermodeltools/cli/internal/shards"
)

// ANSI color codes
Expand Down Expand Up @@ -134,8 +134,8 @@ func Run(ctx context.Context, cfg *config.Config) error {
}
fmt.Println()

// ── Step 4: File mode ─────────────────────────────────────────
fmt.Printf(" %s◆%s File mode\n", cyan, reset)
// ── Step 4: Shard mode ─────────────────────────────────────────
fmt.Printf(" %s◆%s Shard mode\n", cyan, reset)
fmt.Println()
fmt.Printf(" %sWrites a .graph file next to each source file in your repo.%s\n", dWhite, reset)
fmt.Printf(" %sAgents read them automatically via grep and cat — no extra%s\n", dWhite, reset)
Expand All @@ -144,18 +144,18 @@ func Run(ctx context.Context, cfg *config.Config) error {
fmt.Printf(" %sDisable at any time with:%s %ssupermodel clean%s\n", dWhite, reset, bWhite, reset)
fmt.Println()

filesEnabled := confirmYN("Enable file mode?", true)
shardsEnabled := confirmYN("Enable shard mode?", true)
fmt.Println()

cfg.Files = boolPtr(filesEnabled)
cfg.Shards = boolPtr(shardsEnabled)
if err := cfg.Save(); err != nil {
fmt.Fprintf(os.Stderr, " %sWarning: could not save config: %v%s\n", yellow, err, reset)
}

if filesEnabled {
fmt.Printf(" %s✓%s File mode enabled\n", green, reset)
if shardsEnabled {
fmt.Printf(" %s✓%s Shard mode enabled\n", green, reset)
} else {
fmt.Printf(" %s–%s File mode disabled\n", dim, reset)
fmt.Printf(" %s–%s Shard mode disabled\n", dim, reset)
}
fmt.Println()

Expand All @@ -165,10 +165,10 @@ func Run(ctx context.Context, cfg *config.Config) error {
fmt.Printf(" %s✓%s Setup complete\n", bGreen, reset)
fmt.Println()
fileModeStr := "disabled"
if filesEnabled {
if shardsEnabled {
fileModeStr = "enabled"
}
fmt.Printf(" %sFile mode%s %s%s%s\n", dim, reset, bWhite, fileModeStr, reset)
fmt.Printf(" %sShard mode%s %s%s%s\n", dim, reset, bWhite, fileModeStr, reset)
if hookNote != "" {
fmt.Printf(" %sHook%s %s%s%s\n", dim, reset, bWhite, hookNote, reset)
}
Expand All @@ -187,7 +187,7 @@ func Run(ctx context.Context, cfg *config.Config) error {
fmt.Printf(" %sRun %ssupermodel watch%s%s to restart at any time.%s\n", dWhite, bWhite, reset, dWhite, reset)
fmt.Println()

return files.Watch(ctx, cfg, repoDir, files.WatchOptions{})
return shards.Watch(ctx, cfg, repoDir, shards.WatchOptions{})
}

func boolPtr(b bool) *bool { return &b }
Expand Down Expand Up @@ -264,8 +264,12 @@ func installHook(repoDir string) (bool, error) {
settings = make(map[string]interface{})
}

// Prefer the installed binary on $PATH (survives symlinks, go run, dev builds),
// fall back to the current executable path, then the bare name.
hookCmd := "supermodel hook"
if exe, err := os.Executable(); err == nil {
if exe, err := exec.LookPath("supermodel"); err == nil {
hookCmd = exe + " hook"
} else if exe, err := os.Executable(); err == nil {
hookCmd = exe + " hook"
}

Expand Down
Loading
Loading