savepoint 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.github/workflows/ci.yml +20 -0
  2. package/.savepoint/Design.md +4 -3
  3. package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Audit.md +272 -0
  4. package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Detail.md +25 -8
  5. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T001-benchmarks.md +11 -11
  6. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T002-fuzz-targets.md +15 -9
  7. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T003-debug-flag.md +11 -11
  8. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T004-dist-checksums.md +9 -9
  9. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T005-windows-targets.md +11 -11
  10. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T006-abbreviation-splitting.md +9 -9
  11. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T007-root-test-allowlist.md +15 -10
  12. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T008-ci-and-release-automation.md +46 -0
  13. package/.savepoint/router.md +4 -4
  14. package/AGENTS.md +2 -2
  15. package/Makefile +3 -1
  16. package/agent_skills_test.go +1 -1
  17. package/internal/board/board.go +4 -0
  18. package/internal/board/card_test.go +33 -0
  19. package/internal/board/column.go +43 -14
  20. package/internal/board/column_test.go +71 -0
  21. package/internal/board/debug.go +26 -0
  22. package/internal/board/debug_test.go +108 -0
  23. package/internal/board/detail.go +33 -0
  24. package/internal/board/detail_test.go +48 -0
  25. package/internal/board/epic_panel.go +2 -0
  26. package/internal/board/update.go +19 -0
  27. package/internal/board/update_test.go +27 -0
  28. package/internal/board/view_test.go +62 -0
  29. package/internal/board/watch.go +6 -0
  30. package/internal/buildtool/main.go +44 -6
  31. package/internal/buildtool/main_test.go +178 -0
  32. package/internal/data/fuzz_test.go +75 -0
  33. package/internal/data/parser.go +3 -2
  34. package/internal/data/testdata/fuzz/FuzzSplitFrontmatterBody/68eb66b0fe91e7e3 +2 -0
  35. package/internal/data/write.go +9 -6
  36. package/main.go +24 -5
  37. package/package.json +1 -1
  38. package/savepoint +0 -0
  39. /package/project-audit/{audit_report_opus_4.6 → audit_report_opus_4.6.md} +0 -0
@@ -1,6 +1,7 @@
1
1
  package board
2
2
 
3
3
  import (
4
+ "fmt"
4
5
  "strings"
5
6
  "testing"
6
7
 
@@ -301,6 +302,67 @@ func TestRenderNextActivityLine_truncatesAtNarrowWidth(t *testing.T) {
301
302
  }
302
303
  }
303
304
 
305
+ func BenchmarkCalculateLayout_narrow(b *testing.B) {
306
+ b.ReportAllocs()
307
+ for b.Loop() {
308
+ CalculateLayout(60, 24)
309
+ }
310
+ }
311
+
312
+ func BenchmarkCalculateLayout_standard(b *testing.B) {
313
+ b.ReportAllocs()
314
+ for b.Loop() {
315
+ CalculateLayout(80, 24)
316
+ }
317
+ }
318
+
319
+ func BenchmarkCalculateLayout_wide(b *testing.B) {
320
+ b.ReportAllocs()
321
+ for b.Loop() {
322
+ CalculateLayout(120, 24)
323
+ }
324
+ }
325
+
326
+ func BenchmarkCalculateLayout_extraWide(b *testing.B) {
327
+ b.ReportAllocs()
328
+ for b.Loop() {
329
+ CalculateLayout(220, 50)
330
+ }
331
+ }
332
+
333
+ func BenchmarkView_empty(b *testing.B) {
334
+ m := NewModel(nil, "v1", "E03")
335
+ m.Width = 120
336
+ m.Height = 40
337
+ b.ReportAllocs()
338
+ for b.Loop() {
339
+ m.View()
340
+ }
341
+ }
342
+
343
+ func BenchmarkView_withTasks(b *testing.B) {
344
+ tasks := make([]data.Task, 15)
345
+ stages := []data.ProgressStage{data.StageBuild, data.StageTest, data.StageAudit}
346
+ cols := []data.ColumnType{data.ColumnPlanned, data.ColumnInProgress, data.ColumnDone}
347
+ for i := range tasks {
348
+ tasks[i] = data.Task{
349
+ ID: fmt.Sprintf("E06-layout/T%03d-task-slug", i+1),
350
+ Title: fmt.Sprintf("Task %d with a reasonable title length", i+1),
351
+ Column: cols[i%3],
352
+ Stage: stages[i%3],
353
+ Release: "v1.1",
354
+ Epic: "E06-layout",
355
+ }
356
+ }
357
+ m := NewModel(tasks, "v1.1", "E06")
358
+ m.Width = 120
359
+ m.Height = 40
360
+ b.ReportAllocs()
361
+ for b.Loop() {
362
+ m.View()
363
+ }
364
+ }
365
+
304
366
  func TestView_narrowShowsSingleColumn(t *testing.T) {
305
367
  m := NewModel(nil, "v1", "E03")
306
368
  m.Width = 60
@@ -52,6 +52,7 @@ func watchFiles(w *fsnotify.Watcher) tea.Cmd {
52
52
  if !ok {
53
53
  return nil
54
54
  }
55
+ debugf("watcher: event %s", event)
55
56
  watchCreatedDir(w, event)
56
57
  timer := time.NewTimer(100 * time.Millisecond)
57
58
  drain:
@@ -62,11 +63,13 @@ func watchFiles(w *fsnotify.Watcher) tea.Cmd {
62
63
  timer.Stop()
63
64
  return nil
64
65
  }
66
+ debugf("watcher: event %s", event)
65
67
  watchCreatedDir(w, event)
66
68
  case <-timer.C:
67
69
  break drain
68
70
  }
69
71
  }
72
+ debugf("watcher: emitting fileChangeMsg")
70
73
  return fileChangeMsg{}
71
74
  case _, ok := <-w.Errors:
72
75
  if !ok {
@@ -79,10 +82,13 @@ func watchFiles(w *fsnotify.Watcher) tea.Cmd {
79
82
 
80
83
  func reloadTasks(root string, deps ModelDependencies) tea.Cmd {
81
84
  return func() tea.Msg {
85
+ debugf("reload: starting task reload from %q", root)
82
86
  tasks, releases, releaseEpics, epicStatuses, err := loadBoardData(root, deps.Discoverer, deps.Parser)
83
87
  if err != nil {
88
+ debugf("reload: error: %v", err)
84
89
  return errorMsg{message: "reload failed: " + err.Error()}
85
90
  }
91
+ debugf("reload: loaded %d tasks", len(tasks))
86
92
  routerState, _ := readRouterState(root, deps.RouterReader)
87
93
  return reloadMsg{tasks: tasks, releases: releases, releaseEpics: releaseEpics, epicStatuses: epicStatuses, routerState: routerState}
88
94
  }
@@ -3,15 +3,17 @@ package main
3
3
  import (
4
4
  "archive/tar"
5
5
  "compress/gzip"
6
+ "crypto/sha256"
7
+ "encoding/hex"
6
8
  "errors"
7
9
  "flag"
8
10
  "fmt"
9
11
  "io"
10
12
  "os"
11
13
  "os/exec"
12
- "strings"
13
14
  "path/filepath"
14
15
  "runtime"
16
+ "strings"
15
17
  )
16
18
 
17
19
  type target struct {
@@ -24,6 +26,8 @@ var targets = []target{
24
26
  {os: "linux", arch: "arm64"},
25
27
  {os: "darwin", arch: "amd64"},
26
28
  {os: "darwin", arch: "arm64"},
29
+ {os: "windows", arch: "amd64"},
30
+ {os: "windows", arch: "arm64"},
27
31
  }
28
32
 
29
33
  var versionOverride string
@@ -42,7 +46,7 @@ func run(args []string) error {
42
46
  return err
43
47
  }
44
48
  if flags.NArg() != 1 {
45
- return errors.New("usage: go run ./internal/buildtool [-version vX.Y.Z] <build|clean|build-linux|build-darwin|build-all|dist|smoke-test>")
49
+ return errors.New("usage: go run ./internal/buildtool [-version vX.Y.Z] <build|clean|build-linux|build-darwin|build-windows|build-all|dist|smoke-test>")
46
50
  }
47
51
 
48
52
  switch flags.Arg(0) {
@@ -54,6 +58,8 @@ func run(args []string) error {
54
58
  return buildMatching("linux")
55
59
  case "build-darwin":
56
60
  return buildMatching("darwin")
61
+ case "build-windows":
62
+ return buildMatching("windows")
57
63
  case "build-all":
58
64
  return buildAll()
59
65
  case "dist":
@@ -99,8 +105,15 @@ func buildAll() error {
99
105
  return nil
100
106
  }
101
107
 
108
+ func executableName(goos string) string {
109
+ if goos == "windows" {
110
+ return "savepoint.exe"
111
+ }
112
+ return "savepoint"
113
+ }
114
+
102
115
  func buildTarget(target target) error {
103
- output := filepath.Join("dist", target.os+"-"+target.arch, "savepoint")
116
+ output := filepath.Join("dist", target.os+"-"+target.arch, executableName(target.os))
104
117
  return runGoBuild(output, target.os, target.arch)
105
118
  }
106
119
 
@@ -123,13 +136,39 @@ func dist() error {
123
136
  if err := buildAll(); err != nil {
124
137
  return err
125
138
  }
139
+ var archives []string
126
140
  for _, target := range targets {
127
141
  name := "savepoint-" + version() + "-" + target.os + "-" + target.arch + ".tar.gz"
128
- source := filepath.Join("dist", target.os+"-"+target.arch, "savepoint")
142
+ source := filepath.Join("dist", target.os+"-"+target.arch, executableName(target.os))
129
143
  archive := filepath.Join("dist", name)
130
- if err := writeTarGz(archive, source, "savepoint"); err != nil {
144
+ if err := writeTarGz(archive, source, executableName(target.os)); err != nil {
131
145
  return err
132
146
  }
147
+ archives = append(archives, archive)
148
+ }
149
+ return writeChecksums(filepath.Join("dist", "checksums.txt"), archives)
150
+ }
151
+
152
+ func writeChecksums(dest string, archives []string) error {
153
+ var lines strings.Builder
154
+ for _, path := range archives {
155
+ f, err := os.Open(path)
156
+ if err != nil {
157
+ return fmt.Errorf("checksum open %s: %w", path, err)
158
+ }
159
+ h := sha256.New()
160
+ if _, err := io.Copy(h, f); err != nil {
161
+ f.Close()
162
+ return fmt.Errorf("checksum read %s: %w", path, err)
163
+ }
164
+ f.Close()
165
+ lines.WriteString(hex.EncodeToString(h.Sum(nil)))
166
+ lines.WriteString(" ")
167
+ lines.WriteString(filepath.Base(path))
168
+ lines.WriteString("\n")
169
+ }
170
+ if err := os.WriteFile(dest, []byte(lines.String()), 0o644); err != nil {
171
+ return fmt.Errorf("write checksums: %w", err)
133
172
  }
134
173
  return nil
135
174
  }
@@ -208,4 +247,3 @@ func localExecutable() string {
208
247
  }
209
248
  return "savepoint"
210
249
  }
211
-
@@ -1,8 +1,15 @@
1
1
  package main
2
2
 
3
3
  import (
4
+ "archive/tar"
5
+ "compress/gzip"
6
+ "crypto/sha256"
7
+ "encoding/hex"
8
+ "io"
4
9
  "os"
10
+ "path/filepath"
5
11
  "runtime"
12
+ "strings"
6
13
  "testing"
7
14
  )
8
15
 
@@ -32,6 +39,177 @@ func TestVersion_fallback(t *testing.T) {
32
39
  }
33
40
  }
34
41
 
42
+ func TestWriteChecksums(t *testing.T) {
43
+ dir := t.TempDir()
44
+
45
+ content := []byte("fake archive content")
46
+ archive := filepath.Join(dir, "savepoint-v1.0.0-linux-amd64.tar.gz")
47
+ if err := os.WriteFile(archive, content, 0o644); err != nil {
48
+ t.Fatal(err)
49
+ }
50
+
51
+ dest := filepath.Join(dir, "checksums.txt")
52
+ if err := writeChecksums(dest, []string{archive}); err != nil {
53
+ t.Fatalf("writeChecksums: %v", err)
54
+ }
55
+
56
+ got, err := os.ReadFile(dest)
57
+ if err != nil {
58
+ t.Fatal(err)
59
+ }
60
+
61
+ h := sha256.Sum256(content)
62
+ wantHash := hex.EncodeToString(h[:])
63
+ wantLine := wantHash + " savepoint-v1.0.0-linux-amd64.tar.gz"
64
+
65
+ lines := strings.Split(strings.TrimSpace(string(got)), "\n")
66
+ if len(lines) != 1 {
67
+ t.Fatalf("expected 1 line, got %d", len(lines))
68
+ }
69
+ if lines[0] != wantLine {
70
+ t.Errorf("line = %q, want %q", lines[0], wantLine)
71
+ }
72
+ }
73
+
74
+ func TestWriteChecksums_multiple(t *testing.T) {
75
+ dir := t.TempDir()
76
+
77
+ names := []string{"a.tar.gz", "b.tar.gz"}
78
+ var paths []string
79
+ for _, name := range names {
80
+ p := filepath.Join(dir, name)
81
+ if err := os.WriteFile(p, []byte(name), 0o644); err != nil {
82
+ t.Fatal(err)
83
+ }
84
+ paths = append(paths, p)
85
+ }
86
+
87
+ dest := filepath.Join(dir, "checksums.txt")
88
+ if err := writeChecksums(dest, paths); err != nil {
89
+ t.Fatalf("writeChecksums: %v", err)
90
+ }
91
+
92
+ got, err := os.ReadFile(dest)
93
+ if err != nil {
94
+ t.Fatal(err)
95
+ }
96
+ lines := strings.Split(strings.TrimSpace(string(got)), "\n")
97
+ if len(lines) != 2 {
98
+ t.Fatalf("expected 2 lines, got %d: %s", len(lines), got)
99
+ }
100
+ for i, name := range names {
101
+ h := sha256.Sum256([]byte(name))
102
+ want := hex.EncodeToString(h[:]) + " " + name
103
+ if lines[i] != want {
104
+ t.Errorf("line[%d] = %q, want %q", i, lines[i], want)
105
+ }
106
+ }
107
+ }
108
+
109
+ func TestWriteChecksums_missingFile(t *testing.T) {
110
+ dir := t.TempDir()
111
+ dest := filepath.Join(dir, "checksums.txt")
112
+ err := writeChecksums(dest, []string{filepath.Join(dir, "nonexistent.tar.gz")})
113
+ if err == nil {
114
+ t.Error("expected error for missing archive, got nil")
115
+ }
116
+ }
117
+
118
+ func TestTargets_includesWindows(t *testing.T) {
119
+ var gotAMD64, gotARM64 bool
120
+ for _, tgt := range targets {
121
+ if tgt.os != "windows" {
122
+ continue
123
+ }
124
+ switch tgt.arch {
125
+ case "amd64":
126
+ gotAMD64 = true
127
+ case "arm64":
128
+ gotARM64 = true
129
+ }
130
+ }
131
+ if !gotAMD64 {
132
+ t.Error("targets missing windows/amd64")
133
+ }
134
+ if !gotARM64 {
135
+ t.Error("targets missing windows/arm64")
136
+ }
137
+ }
138
+
139
+ func TestTargets_preservesLinuxDarwin(t *testing.T) {
140
+ want := map[string]bool{
141
+ "linux/amd64": false,
142
+ "linux/arm64": false,
143
+ "darwin/amd64": false,
144
+ "darwin/arm64": false,
145
+ }
146
+ for _, tgt := range targets {
147
+ key := tgt.os + "/" + tgt.arch
148
+ if _, ok := want[key]; ok {
149
+ want[key] = true
150
+ }
151
+ }
152
+ for key, found := range want {
153
+ if !found {
154
+ t.Errorf("targets missing %s", key)
155
+ }
156
+ }
157
+ }
158
+
159
+ func TestExecutableName(t *testing.T) {
160
+ if got := executableName("windows"); got != "savepoint.exe" {
161
+ t.Errorf("executableName(windows) = %q, want savepoint.exe", got)
162
+ }
163
+ if got := executableName("linux"); got != "savepoint" {
164
+ t.Errorf("executableName(linux) = %q, want savepoint", got)
165
+ }
166
+ if got := executableName("darwin"); got != "savepoint" {
167
+ t.Errorf("executableName(darwin) = %q, want savepoint", got)
168
+ }
169
+ }
170
+
171
+ func TestWriteTarGzPreservesWindowsExecutableName(t *testing.T) {
172
+ dir := t.TempDir()
173
+ source := filepath.Join(dir, "savepoint.exe")
174
+ if err := os.WriteFile(source, []byte("binary"), 0o755); err != nil {
175
+ t.Fatal(err)
176
+ }
177
+
178
+ archive := filepath.Join(dir, "savepoint-windows-amd64.tar.gz")
179
+ if err := writeTarGz(archive, source, executableName("windows")); err != nil {
180
+ t.Fatalf("writeTarGz: %v", err)
181
+ }
182
+
183
+ f, err := os.Open(archive)
184
+ if err != nil {
185
+ t.Fatal(err)
186
+ }
187
+ defer f.Close()
188
+
189
+ gz, err := gzip.NewReader(f)
190
+ if err != nil {
191
+ t.Fatal(err)
192
+ }
193
+ defer gz.Close()
194
+
195
+ tr := tar.NewReader(gz)
196
+ header, err := tr.Next()
197
+ if err != nil {
198
+ t.Fatal(err)
199
+ }
200
+ if header.Name != "savepoint.exe" {
201
+ t.Fatalf("archive member = %q, want savepoint.exe", header.Name)
202
+ }
203
+
204
+ content, err := io.ReadAll(tr)
205
+ if err != nil {
206
+ t.Fatal(err)
207
+ }
208
+ if string(content) != "binary" {
209
+ t.Fatalf("archive content = %q, want binary", content)
210
+ }
211
+ }
212
+
35
213
  func TestLocalExecutable(t *testing.T) {
36
214
  got := localExecutable()
37
215
  if runtime.GOOS == "windows" {
@@ -0,0 +1,75 @@
1
+ package data
2
+
3
+ import (
4
+ "testing"
5
+ )
6
+
7
+ func FuzzExtractFrontmatter(f *testing.F) {
8
+ seeds := []string{
9
+ "---\nid: E01/T001\nstatus: planned\n---\nbody",
10
+ "---\n---\n",
11
+ "---\n\n---\n",
12
+ "---\nid: test\n---",
13
+ "",
14
+ "# no frontmatter",
15
+ "---\nid: [broken\n---\n",
16
+ "---\nname: héllo wörld\n---\n",
17
+ "---\nid: test\nstatus: in_progress\nphase: build\n---\nbody content",
18
+ "---\r\nid: test\r\n---\r\nbody",
19
+ }
20
+ for _, s := range seeds {
21
+ f.Add(s)
22
+ }
23
+ f.Fuzz(func(t *testing.T, content string) {
24
+ _, _ = extractFrontmatter(content)
25
+ })
26
+ }
27
+
28
+ func FuzzParseFrontmatter(f *testing.F) {
29
+ seeds := []string{
30
+ "---\nid: E01/T001\nstatus: planned\n---\nbody",
31
+ "---\n---\n",
32
+ "---\nid: [broken\n---\n",
33
+ "---\nname: héllo\n---\n",
34
+ "",
35
+ "no frontmatter",
36
+ "---\ntags: [a, b, c]\n---\n",
37
+ "---\nnested:\n key: val\n---\n",
38
+ }
39
+ for _, s := range seeds {
40
+ f.Add(s)
41
+ }
42
+ f.Fuzz(func(t *testing.T, content string) {
43
+ p := NewParser()
44
+ _, _ = p.ParseFrontmatter(content)
45
+ })
46
+ }
47
+
48
+ func FuzzSplitFrontmatterBody(f *testing.F) {
49
+ seeds := []string{
50
+ "---\nid: E01/T001\nstatus: planned\n---\nbody",
51
+ "---\n---\n",
52
+ "---\nkey: value\n---",
53
+ "",
54
+ "# no frontmatter",
55
+ "---\nid: test\nstatus: in_progress\n---\n\n## Section\n\nContent.",
56
+ "---\nid: test\n---\n\nbody with unicode: 日本語",
57
+ }
58
+ for _, s := range seeds {
59
+ f.Add(s)
60
+ }
61
+ f.Fuzz(func(t *testing.T, content string) {
62
+ yamlStr, body, err := SplitFrontmatterBody(content)
63
+ if err != nil {
64
+ return
65
+ }
66
+ reconstructed := "---\n" + yamlStr + "\n---" + body
67
+ _, body2, err2 := SplitFrontmatterBody(reconstructed)
68
+ if err2 != nil {
69
+ t.Errorf("round-trip SplitFrontmatterBody failed on reconstructed: %v", err2)
70
+ }
71
+ if body2 != body {
72
+ t.Errorf("round-trip body mismatch: got %q, want %q", body2, body)
73
+ }
74
+ })
75
+ }
@@ -88,9 +88,10 @@ type taskFrontmatter struct {
88
88
  Progress Progress `yaml:"progress"`
89
89
  }
90
90
 
91
- // normalizeLineEndings replaces Windows line endings with Unix line endings.
91
+ // normalizeLineEndings replaces Windows (CRLF) and legacy Mac (CR) line endings with LF.
92
92
  func normalizeLineEndings(s string) string {
93
- return strings.ReplaceAll(s, "\r\n", "\n")
93
+ s = strings.ReplaceAll(s, "\r\n", "\n")
94
+ return strings.ReplaceAll(s, "\r", "\n")
94
95
  }
95
96
 
96
97
  func extractFrontmatter(content string) (string, error) {
@@ -0,0 +1,2 @@
1
+ go test fuzz v1
2
+ string("---\n\n---\r\r\n")
@@ -40,17 +40,20 @@ func UpdateLastAudited(path, value string) error {
40
40
  // SplitFrontmatterBody splits content into frontmatter YAML and body.
41
41
  func SplitFrontmatterBody(content string) (yamlStr string, body string, err error) {
42
42
  normalized := normalizeLineEndings(content)
43
- raw, err := extractFrontmatter(normalized)
44
- if err != nil {
45
- return "", "", err
43
+ if !strings.HasPrefix(normalized, "---\n") {
44
+ return "", "", ErrNoFrontmatter
45
+ }
46
+ end := strings.Index(normalized[4:], "\n---")
47
+ if end == -1 {
48
+ return "", "", ErrNoClosingFrontmatter
46
49
  }
47
- delimLen := 4
48
- bodyStart := delimLen + len(raw) + delimLen
50
+ yamlStr = strings.TrimSpace(normalized[4 : 4+end])
51
+ bodyStart := 4 + end + 4 // "---\n" + yaml + "\n---"
49
52
  body = ""
50
53
  if bodyStart < len(normalized) {
51
54
  body = normalized[bodyStart:]
52
55
  }
53
- return raw, body, nil
56
+ return yamlStr, body, nil
54
57
  }
55
58
 
56
59
  func updateFrontmatterField(path, key, value string) error {
package/main.go CHANGED
@@ -22,19 +22,24 @@ var projectTemplates embed.FS
22
22
  var version = "dev"
23
23
 
24
24
  func main() {
25
- if len(os.Args) > 1 {
26
- switch os.Args[1] {
25
+ args, debug := stripDebugFlag(os.Args[1:])
26
+ if debug || os.Getenv("SAVEPOINT_DEBUG") != "" {
27
+ board.SetDebug(true)
28
+ }
29
+
30
+ if len(args) > 0 {
31
+ switch args[0] {
27
32
  case "--version":
28
33
  fmt.Println(version)
29
34
  os.Exit(0)
30
35
  case "init":
31
- if err := cmd.RunInit(context.Background(), os.Args[2:], os.Stdout, initRunner); err != nil {
36
+ if err := cmd.RunInit(context.Background(), args[1:], os.Stdout, initRunner); err != nil {
32
37
  fmt.Fprintln(os.Stderr, err)
33
38
  os.Exit(1)
34
39
  }
35
40
  os.Exit(0)
36
41
  case "board":
37
- if err := cmd.RunBoard(context.Background(), os.Args[2:], os.Stdout, func(opts cmd.BoardOptions) error {
42
+ if err := cmd.RunBoard(context.Background(), args[1:], os.Stdout, func(opts cmd.BoardOptions) error {
38
43
  return board.RunWithFilters(opts.Release, opts.Epic)
39
44
  }); err != nil {
40
45
  fmt.Fprintln(os.Stderr, err)
@@ -42,7 +47,7 @@ func main() {
42
47
  }
43
48
  os.Exit(0)
44
49
  case "doctor":
45
- code, err := cmd.RunDoctor(context.Background(), os.Args[2:], os.Stdout, func(opts cmd.DoctorOptions) (int, error) {
50
+ code, err := cmd.RunDoctor(context.Background(), args[1:], os.Stdout, func(opts cmd.DoctorOptions) (int, error) {
46
51
  return runDoctorChecks(opts)
47
52
  })
48
53
  if err != nil {
@@ -56,6 +61,20 @@ func main() {
56
61
  }
57
62
  }
58
63
 
64
+ // stripDebugFlag removes --debug from args and reports whether it was present.
65
+ func stripDebugFlag(args []string) ([]string, bool) {
66
+ out := make([]string, 0, len(args))
67
+ found := false
68
+ for _, a := range args {
69
+ if a == "--debug" {
70
+ found = true
71
+ } else {
72
+ out = append(out, a)
73
+ }
74
+ }
75
+ return out, found
76
+ }
77
+
59
78
  func runDoctorChecks(opts cmd.DoctorOptions) (int, error) {
60
79
  discover := data.NewDiscover()
61
80
  root, err := discover.FindSavepointRoot(".")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "savepoint",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "It’s a simple, file-based state machine and cinematic Terminal UI (TUI) designed to force you—and your agent (Claude, Cursor, Aider, Gemini)—to slow down, write down what you're actually building, and check your work before moving on.",
5
5
  "keywords": [
6
6
  "board",
package/savepoint DELETED
Binary file