Skip to content

Commit 3a8a45f

Browse files
greynewellclaude
andcommitted
feat: surface privacy policy and auto-gitignore shard files
- updateGitignore now adds both .supermodel/ and *.graph.* to .gitignore automatically on first run, so shard files never clutter the repo - Show a zero-data-retention note + privacy URL before every upload in Generate (the sync step users care most about) - Surface the same privacy statement in the setup wizard's Shard Mode step, alongside an explanation that .graph files are gitignored automatically Addresses user feedback: shard files cluttering repo, and lack of visible trust signal during code upload. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent df5b9a0 commit 3a8a45f

3 files changed

Lines changed: 66 additions & 15 deletions

File tree

internal/setup/wizard.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ func Run(ctx context.Context, cfg *config.Config) error {
141141
fmt.Printf(" %sAgents read them automatically via grep and cat — no extra%s\n", dWhite, reset)
142142
fmt.Printf(" %sprompt changes, no new tools to learn.%s\n", dWhite, reset)
143143
fmt.Println()
144+
fmt.Printf(" %s🔒 Privacy: your code is uploaded for analysis and immediately%s\n", dWhite, reset)
145+
fmt.Printf(" %sdeleted from our servers. Zero data retained. No training.%s\n", dWhite, reset)
146+
fmt.Printf(" %s supermodeltools.com/privacy%s\n", dWhite, reset)
147+
fmt.Println()
148+
fmt.Printf(" %s.graph files are generated output — they will be added to%s\n", dWhite, reset)
149+
fmt.Printf(" %s.gitignore automatically so they don't clutter your repo.%s\n", dWhite, reset)
150+
fmt.Println()
144151
fmt.Printf(" %sDisable at any time with:%s %ssupermodel clean%s\n", dWhite, reset, bWhite, reset)
145152
fmt.Println()
146153

internal/shards/handler.go

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ func Generate(ctx context.Context, cfg *config.Config, dir string, opts Generate
147147
client := api.New(cfg)
148148
idemKey := newUUID()
149149

150+
fmt.Fprintf(os.Stderr, " %s🔒 Your code is analyzed server-side and immediately deleted — zero data retained.%s\n", ansiDim, ansiReset)
151+
fmt.Fprintf(os.Stderr, " %ssupermodeltools.com/privacy%s\n\n", ansiDim, ansiReset)
152+
150153
spin = ui.Start("Uploading and analyzing repository…")
151154
ir, err := client.AnalyzeShards(ctx, zipPath, "shards-"+idemKey[:8], prevDomains)
152155
spin.Stop()
@@ -448,22 +451,39 @@ func Render(dir string, opts RenderOptions) error {
448451
return nil
449452
}
450453

451-
// updateGitignore ensures .supermodel/ is in the repo's .gitignore.
454+
// updateGitignore ensures .supermodel/ and *.graph.* are in the repo's .gitignore.
455+
// Both entries are added silently the first time Generate runs — the shard files
456+
// are generated output and should not be committed.
452457
func updateGitignore(repoDir string) error {
453458
gitignorePath := filepath.Join(repoDir, ".gitignore")
454-
const entry = ".supermodel/"
459+
460+
entries := []string{".supermodel/", "*.graph.*"}
455461

456462
data, err := os.ReadFile(gitignorePath)
457463
if err != nil && !os.IsNotExist(err) {
458464
return nil // can't read, skip silently
459465
}
460466

461467
content := string(data)
468+
existing := make(map[string]bool)
462469
for _, line := range strings.Split(content, "\n") {
463-
if strings.TrimSpace(line) == entry || strings.TrimSpace(line) == ".supermodel" {
464-
return nil // already present
470+
t := strings.TrimSpace(line)
471+
existing[t] = true
472+
}
473+
// Treat bare ".supermodel" as equivalent to ".supermodel/"
474+
if existing[".supermodel"] {
475+
existing[".supermodel/"] = true
476+
}
477+
478+
var missing []string
479+
for _, e := range entries {
480+
if !existing[e] {
481+
missing = append(missing, e)
465482
}
466483
}
484+
if len(missing) == 0 {
485+
return nil // all entries already present
486+
}
467487

468488
f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) //nolint:gosec // .gitignore is a standard repo file; 0o600 satisfies gosec while remaining functional
469489
if err != nil {
@@ -474,6 +494,8 @@ func updateGitignore(repoDir string) error {
474494
if content != "" && !strings.HasSuffix(content, "\n") {
475495
fmt.Fprintln(f)
476496
}
477-
fmt.Fprintln(f, entry)
497+
for _, e := range missing {
498+
fmt.Fprintln(f, e)
499+
}
478500
return nil
479501
}

internal/shards/render_test.go

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -518,33 +518,55 @@ func TestUpdateGitignore_AddsEntry(t *testing.T) {
518518
if err != nil {
519519
t.Fatal(err)
520520
}
521-
if !strings.Contains(string(data), ".supermodel/") {
522-
t.Errorf("expected .supermodel/ in gitignore: %s", data)
521+
content := string(data)
522+
if !strings.Contains(content, ".supermodel/") {
523+
t.Errorf("expected .supermodel/ in gitignore: %s", content)
524+
}
525+
if !strings.Contains(content, "*.graph.*") {
526+
t.Errorf("expected *.graph.* in gitignore: %s", content)
523527
}
524528
}
525529

526530
func TestUpdateGitignore_DoesNotDuplicate(t *testing.T) {
527531
dir := t.TempDir()
528-
// Call twice; the entry should appear exactly once.
532+
// Call twice; each entry should appear exactly once.
529533
updateGitignore(dir) //nolint:errcheck
530534
updateGitignore(dir) //nolint:errcheck
531535
data, _ := os.ReadFile(dir + "/.gitignore")
532536
content := string(data)
533-
first := strings.Index(content, ".supermodel/")
534-
last := strings.LastIndex(content, ".supermodel/")
535-
if first != last {
536-
t.Errorf(".supermodel/ appears more than once in gitignore:\n%s", content)
537+
for _, entry := range []string{".supermodel/", "*.graph.*"} {
538+
if strings.Count(content, entry) != 1 {
539+
t.Errorf("%q appears more than once in gitignore:\n%s", entry, content)
540+
}
537541
}
538542
}
539543

540544
func TestUpdateGitignore_ExistingEntrySkipped(t *testing.T) {
541545
dir := t.TempDir()
542-
// Pre-populate with the entry
546+
// Pre-populate with both entries.
547+
os.WriteFile(dir+"/.gitignore", []byte(".supermodel/\n*.graph.*\n"), 0o600) //nolint:errcheck
548+
updateGitignore(dir) //nolint:errcheck
549+
data, _ := os.ReadFile(dir + "/.gitignore")
550+
content := string(data)
551+
for _, entry := range []string{".supermodel/", "*.graph.*"} {
552+
if strings.Count(content, entry) != 1 {
553+
t.Errorf("%q should not be duplicated: %s", entry, content)
554+
}
555+
}
556+
}
557+
558+
func TestUpdateGitignore_AddsOnlyMissingEntries(t *testing.T) {
559+
dir := t.TempDir()
560+
// Pre-populate with only .supermodel/ — *.graph.* should be added.
543561
os.WriteFile(dir+"/.gitignore", []byte(".supermodel/\n"), 0o600) //nolint:errcheck
544562
updateGitignore(dir) //nolint:errcheck
545563
data, _ := os.ReadFile(dir + "/.gitignore")
546-
if strings.Count(string(data), ".supermodel/") != 1 {
547-
t.Errorf("should not add duplicate: %s", data)
564+
content := string(data)
565+
if strings.Count(content, ".supermodel/") != 1 {
566+
t.Errorf(".supermodel/ should appear exactly once: %s", content)
567+
}
568+
if !strings.Contains(content, "*.graph.*") {
569+
t.Errorf("*.graph.* should have been added: %s", content)
548570
}
549571
}
550572

0 commit comments

Comments
 (0)