savepoint 1.0.2 → 1.0.3

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 (233) hide show
  1. package/.claude/settings.local.json +12 -1
  2. package/.golangci.yml +11 -0
  3. package/.savepoint/Design.md +37 -36
  4. package/.savepoint/{audit/v1.1/E02-cross-platform-compatibility/proposals.md → releases/v1.1/epics/E02-cross-platform-compatibility/E02-Audit.md} +48 -38
  5. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Audit.md +195 -0
  6. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +14 -1
  7. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +3 -3
  8. package/.savepoint/{audit/v1.1/E04-epic-navigation/proposals.md → releases/v1.1/epics/E04-epic-navigation/E04-Audit.md} +65 -54
  9. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Audit.md +237 -0
  10. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +25 -16
  11. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +17 -6
  12. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +15 -5
  13. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +19 -5
  14. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +11 -1
  15. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +9 -6
  16. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +29 -13
  17. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Audit.md +56 -0
  18. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Detail.md +63 -0
  19. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T005-proposals.md +44 -0
  20. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T007-apply-close.md +35 -0
  21. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T009-integration.md +40 -0
  22. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T010-audit-file-migration.md +45 -0
  23. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T011-model-tab-state.md +26 -0
  24. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T012-epic-audit-render.md +33 -0
  25. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T013-handle-tab-keys.md +34 -0
  26. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T014-tab-indicator.md +33 -0
  27. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Audit.md +336 -0
  28. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Detail.md +61 -0
  29. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T001-cli-entrypoint.md +37 -0
  30. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T002-target-validation.md +28 -0
  31. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T003-scaffold-writer.md +46 -0
  32. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T004-atomic-writes.md +27 -0
  33. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T005-magic-prompt.md +25 -0
  34. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T006-clipboard.md +26 -0
  35. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T007-integration-test.md +26 -0
  36. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Audit.md +333 -0
  37. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Detail.md +68 -0
  38. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T001-cli-entrypoint.md +26 -0
  39. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T002-non-tty-fallback.md +27 -0
  40. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T003-tui-app-shell.md +28 -0
  41. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T004-board-model.md +29 -0
  42. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T005-detail-pane.md +27 -0
  43. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T006-status-transitions.md +29 -0
  44. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T007-theme-fallbacks.md +29 -0
  45. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T008-integration-test.md +27 -0
  46. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Audit.md +207 -0
  47. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Detail.md +65 -0
  48. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T001-cli-entrypoint.md +24 -0
  49. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T002-config-router-validation.md +28 -0
  50. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T003-structure-checks.md +29 -0
  51. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T004-dependency-checks.md +27 -0
  52. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T005-audit-orphan-checks.md +28 -0
  53. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T006-quality-gates-report.md +31 -0
  54. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/E11-Detail.md +36 -0
  55. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T001-debug-logging.md +25 -0
  56. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T002-increase-debounce.md +21 -0
  57. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T003-error-handling.md +22 -0
  58. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T004-test-verify.md +29 -0
  59. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Audit.md +444 -0
  60. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Detail.md +45 -0
  61. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T001-default-phase.md +35 -0
  62. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T002-default-status.md +19 -0
  63. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T003-better-errors.md +29 -0
  64. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T004-validate-on-write.md +25 -0
  65. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T005-tests.md +37 -0
  66. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Audit.md +118 -0
  67. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Detail.md +73 -0
  68. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T001-safe-cleanup.md +66 -0
  69. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T002-bug-fixes.md +35 -0
  70. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T003-centralize-duplication.md +60 -0
  71. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T004-infrastructure.md +33 -0
  72. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T005-decompose-update.md +37 -0
  73. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T006-async-io.md +40 -0
  74. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T007-test-coverage.md +37 -0
  75. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Audit.md +267 -0
  76. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Detail.md +54 -0
  77. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T001-group-model.md +39 -0
  78. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T002-data-interfaces.md +42 -0
  79. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T003-discover-orphans.md +33 -0
  80. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T004-epic-panel-headings.md +35 -0
  81. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T005-shell-tokenization.md +27 -0
  82. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T006-unify-enums.md +29 -0
  83. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T007-testutil-package.md +28 -0
  84. package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Detail.md +43 -0
  85. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T001-benchmarks.md +31 -0
  86. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T002-fuzz-targets.md +28 -0
  87. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T003-debug-flag.md +30 -0
  88. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T004-dist-checksums.md +27 -0
  89. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T005-windows-targets.md +28 -0
  90. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T006-abbreviation-splitting.md +26 -0
  91. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T007-root-test-allowlist.md +28 -0
  92. package/.savepoint/releases/v1.1/epics/_archived/T001-cli-entrypoint.md +25 -0
  93. package/.savepoint/releases/v1.1/epics/_archived/T002-quality-gates.md +27 -0
  94. package/.savepoint/releases/v1.1/epics/_archived/T003-snapshot.md +27 -0
  95. package/.savepoint/releases/v1.1/epics/_archived/T004-ai-reconcile.md +29 -0
  96. package/.savepoint/releases/v1.1/epics/_archived/T006-tui-review.md +31 -0
  97. package/.savepoint/releases/v1.1/epics/_archived/T008-skip-handling.md +34 -0
  98. package/.savepoint/releases/v1.1/v1.1-PRD.md +67 -7
  99. package/.savepoint/router.md +9 -16
  100. package/AGENTS.md +38 -23
  101. package/README.md +0 -1
  102. package/agent-skills/savepoint-audit/SKILL.md +86 -34
  103. package/agent-skills/savepoint-build-task/SKILL.md +7 -2
  104. package/agent-skills/savepoint-create-plan/SKILL.md +7 -2
  105. package/agent-skills/savepoint-create-task/SKILL.md +44 -31
  106. package/agent-skills/savepoint-draft-prd/SKILL.md +7 -2
  107. package/agent-skills/savepoint-system-design/SKILL.md +7 -2
  108. package/agent_skills_test.go +91 -0
  109. package/cmd/board.go +59 -0
  110. package/cmd/board_test.go +137 -0
  111. package/cmd/doctor.go +53 -0
  112. package/cmd/doctor_test.go +146 -0
  113. package/cmd/init.go +63 -0
  114. package/cmd/init_test.go +104 -0
  115. package/internal/board/board.go +40 -36
  116. package/internal/board/board_test.go +27 -82
  117. package/internal/board/card.go +43 -23
  118. package/internal/board/card_test.go +41 -5
  119. package/internal/board/column.go +44 -13
  120. package/internal/board/column_test.go +5 -2
  121. package/internal/board/detail.go +0 -47
  122. package/internal/board/epic_panel.go +118 -22
  123. package/internal/board/epic_panel_test.go +302 -17
  124. package/internal/board/help.go +1 -0
  125. package/internal/board/help_test.go +1 -0
  126. package/internal/board/integration_test.go +266 -0
  127. package/internal/board/interfaces.go +65 -0
  128. package/internal/board/interfaces_test.go +114 -0
  129. package/internal/board/io.go +93 -0
  130. package/internal/board/model.go +79 -118
  131. package/internal/board/plain.go +88 -0
  132. package/internal/board/plain_test.go +117 -0
  133. package/internal/board/release.go +1 -9
  134. package/internal/board/release_test.go +6 -6
  135. package/internal/board/status.go +4 -4
  136. package/internal/board/theme.go +24 -0
  137. package/internal/board/theme_test.go +31 -0
  138. package/internal/board/transitions.go +113 -88
  139. package/internal/board/transitions_test.go +164 -141
  140. package/internal/board/tui.go +32 -0
  141. package/internal/board/update.go +325 -215
  142. package/internal/board/update_test.go +299 -18
  143. package/internal/board/util.go +76 -0
  144. package/internal/board/view.go +31 -28
  145. package/internal/board/view_test.go +12 -2
  146. package/internal/board/watch.go +35 -5
  147. package/internal/buildtool/main.go +2 -10
  148. package/internal/buildtool/main_test.go +46 -0
  149. package/internal/data/config.go +17 -3
  150. package/internal/data/config_test.go +49 -0
  151. package/internal/data/discover.go +26 -0
  152. package/internal/data/discover_test.go +34 -10
  153. package/internal/data/errors.go +4 -0
  154. package/internal/data/lifecycle.go +13 -6
  155. package/internal/data/lifecycle_test.go +14 -11
  156. package/internal/data/parser.go +21 -6
  157. package/internal/data/parser_test.go +31 -7
  158. package/internal/data/task.go +0 -9
  159. package/internal/data/write.go +85 -11
  160. package/internal/data/write_test.go +167 -0
  161. package/internal/doctor/checks.go +567 -0
  162. package/internal/doctor/checks_test.go +716 -0
  163. package/internal/doctor/gates.go +193 -0
  164. package/internal/doctor/gates_test.go +166 -0
  165. package/internal/doctor/interfaces.go +64 -0
  166. package/internal/doctor/interfaces_test.go +104 -0
  167. package/internal/doctor/repairs.go +80 -0
  168. package/internal/doctor/repairs_test.go +81 -0
  169. package/internal/doctor/report.go +157 -0
  170. package/internal/doctor/report_test.go +89 -0
  171. package/internal/init/clipboard.go +146 -0
  172. package/internal/init/clipboard_test.go +74 -0
  173. package/internal/init/install.go +16 -0
  174. package/internal/init/integration_test.go +197 -0
  175. package/internal/init/prompt.go +14 -0
  176. package/internal/init/prompt_test.go +77 -0
  177. package/internal/init/scaffold.go +59 -0
  178. package/internal/init/scaffold_test.go +179 -0
  179. package/internal/init/template_freshness_test.go +56 -0
  180. package/internal/init/validate.go +85 -0
  181. package/internal/init/validate_test.go +141 -0
  182. package/internal/init/write.go +73 -0
  183. package/internal/init/write_test.go +91 -0
  184. package/internal/styles/styles_test.go +133 -0
  185. package/internal/testutil/fixture.go +113 -0
  186. package/internal/testutil/fs.go +26 -0
  187. package/main.go +101 -4
  188. package/package.json +2 -2
  189. package/project-audit/audit_report_glm_5.1.md +411 -0
  190. package/project-audit/audit_report_opus_4.6 +406 -0
  191. package/project-audit/consolidated-audit-report.md +456 -0
  192. package/savepoint +0 -0
  193. package/templates/project/.savepoint/Design.md +2 -2
  194. package/templates/project/.savepoint/router.md +10 -10
  195. package/templates/project/AGENTS.md +33 -21
  196. package/templates/project/agent-skills/savepoint-audit/SKILL.md +87 -0
  197. package/templates/project/agent-skills/savepoint-build-task/SKILL.md +44 -0
  198. package/templates/project/agent-skills/savepoint-create-plan/SKILL.md +33 -0
  199. package/templates/project/agent-skills/savepoint-create-task/SKILL.md +44 -0
  200. package/templates/project/agent-skills/savepoint-draft-prd/SKILL.md +37 -0
  201. package/templates/project/agent-skills/savepoint-system-design/SKILL.md +38 -0
  202. package/templates/prompts/audit-reconciliation.prompt.md +33 -28
  203. package/templates/prompts/design.prompt.md +3 -1
  204. package/.savepoint/audit/v1/E01/proposals.md +0 -168
  205. package/.savepoint/audit/v1/E01/snapshot.md +0 -78
  206. package/.savepoint/audit/v1/E01-go-setup/proposals.md +0 -166
  207. package/.savepoint/audit/v1/E01-go-setup/snapshot.md +0 -71
  208. package/.savepoint/audit/v1/E01-scaffolding/proposals/AGENTS.md +0 -66
  209. package/.savepoint/audit/v1/E01-scaffolding/proposals/Design.md +0 -210
  210. package/.savepoint/audit/v1/E01-scaffolding/proposals/epic-Design.md +0 -117
  211. package/.savepoint/audit/v1/E01-scaffolding/proposals/quality-review.md +0 -101
  212. package/.savepoint/audit/v1/E01-scaffolding/snapshot.md +0 -54
  213. package/.savepoint/audit/v1/E02-data-model/snapshot.md +0 -128
  214. package/.savepoint/audit/v1/E02-data-readers/proposals.md +0 -123
  215. package/.savepoint/audit/v1/E02-data-readers/snapshot.md +0 -54
  216. package/.savepoint/audit/v1/E03-board-tui-core/proposals.md +0 -146
  217. package/.savepoint/audit/v1/E03-board-tui-core/snapshot.md +0 -57
  218. package/.savepoint/audit/v1/E03-cli-foundation/snapshot.md +0 -106
  219. package/.savepoint/audit/v1/E04-board-components/proposals.md +0 -118
  220. package/.savepoint/audit/v1/E04-board-components/snapshot.md +0 -77
  221. package/.savepoint/audit/v1/E04-templates-and-prompts/snapshot.md +0 -115
  222. package/.savepoint/audit/v1/E05-init-command/snapshot.md +0 -125
  223. package/.savepoint/audit/v1/E05-phase-transitions/proposals.md +0 -83
  224. package/.savepoint/audit/v1/E05-phase-transitions/snapshot.md +0 -36
  225. package/.savepoint/audit/v1/E06-atari-noir-layout/proposals.md +0 -130
  226. package/.savepoint/audit/v1/E06-atari-noir-layout/snapshot.md +0 -84
  227. package/.savepoint/audit/v1/E06-tui-board/snapshot.md +0 -64
  228. package/.savepoint/audit/v1/E07-audit-pipeline/snapshot.md +0 -165
  229. package/.savepoint/audit/v1/E08-board-workflow-cleanup/snapshot.md +0 -65
  230. package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/snapshot.md +0 -41
  231. package/.savepoint/audit/v1.1/E04-epic-navigation/snapshot.md +0 -48
  232. package/ink-cli-ui-design.zip +0 -0
  233. package/savepoint.exe +0 -0
@@ -0,0 +1,146 @@
1
+ package cmd
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "strings"
7
+ "testing"
8
+ )
9
+
10
+ func TestRunDoctorHelp(t *testing.T) {
11
+ var stdout bytes.Buffer
12
+ called := false
13
+
14
+ code, err := RunDoctor(context.Background(), []string{"--help"}, &stdout, func(DoctorOptions) (int, error) {
15
+ called = true
16
+ return 0, nil
17
+ })
18
+
19
+ if err != nil {
20
+ t.Fatalf("RunDoctor() error = %v", err)
21
+ }
22
+ if called {
23
+ t.Fatal("RunDoctor() called runner for help")
24
+ }
25
+ if code != 0 {
26
+ t.Fatalf("RunDoctor() code = %d, want 0", code)
27
+ }
28
+ if !strings.Contains(stdout.String(), "doctor [--epic <epic>]") {
29
+ t.Fatalf("help output = %q", stdout.String())
30
+ }
31
+ }
32
+
33
+ func TestRunDoctorNoArgs(t *testing.T) {
34
+ got := runDoctorOptions(t, nil)
35
+
36
+ if got.Epic != "" {
37
+ t.Fatalf("Epic = %q, want empty", got.Epic)
38
+ }
39
+ }
40
+
41
+ func TestRunDoctorEpic(t *testing.T) {
42
+ got := runDoctorOptions(t, []string{"--epic", "E03"})
43
+
44
+ if got.Epic != "E03" {
45
+ t.Fatalf("Epic = %q, want E03", got.Epic)
46
+ }
47
+ }
48
+
49
+ func TestRunDoctorEpicMissingValue(t *testing.T) {
50
+ var stdout bytes.Buffer
51
+
52
+ code, err := RunDoctor(context.Background(), []string{"--epic"}, &stdout, func(DoctorOptions) (int, error) {
53
+ return 0, nil
54
+ })
55
+
56
+ if err == nil {
57
+ t.Fatal("RunDoctor() error = nil, want missing value error")
58
+ }
59
+ if !strings.Contains(err.Error(), "--epic requires a value") {
60
+ t.Fatalf("error = %q", err.Error())
61
+ }
62
+ if code != 2 {
63
+ t.Fatalf("code = %d, want 2", code)
64
+ }
65
+ }
66
+
67
+ func TestRunDoctorRejectsUnknownFlag(t *testing.T) {
68
+ var stdout bytes.Buffer
69
+
70
+ code, err := RunDoctor(context.Background(), []string{"--bogus"}, &stdout, func(DoctorOptions) (int, error) {
71
+ return 0, nil
72
+ })
73
+
74
+ if err == nil {
75
+ t.Fatal("RunDoctor() error = nil, want unknown flag error")
76
+ }
77
+ if !strings.Contains(err.Error(), "unknown doctor flag") {
78
+ t.Fatalf("error = %q", err.Error())
79
+ }
80
+ if code != 2 {
81
+ t.Fatalf("code = %d, want 2", code)
82
+ }
83
+ }
84
+
85
+ func TestRunDoctorRejectsPositionalArgs(t *testing.T) {
86
+ var stdout bytes.Buffer
87
+
88
+ code, err := RunDoctor(context.Background(), []string{"extra"}, &stdout, func(DoctorOptions) (int, error) {
89
+ return 0, nil
90
+ })
91
+
92
+ if err == nil {
93
+ t.Fatal("RunDoctor() error = nil, want positional arg error")
94
+ }
95
+ if code != 2 {
96
+ t.Fatalf("code = %d, want 2", code)
97
+ }
98
+ }
99
+
100
+ func TestRunDoctorExitCode1(t *testing.T) {
101
+ var stdout bytes.Buffer
102
+
103
+ code, err := RunDoctor(context.Background(), nil, &stdout, func(DoctorOptions) (int, error) {
104
+ return 1, nil
105
+ })
106
+
107
+ if err != nil {
108
+ t.Fatalf("RunDoctor() error = %v", err)
109
+ }
110
+ if code != 1 {
111
+ t.Fatalf("code = %d, want 1", code)
112
+ }
113
+ }
114
+
115
+ func TestRunDoctorExitCode2(t *testing.T) {
116
+ var stdout bytes.Buffer
117
+
118
+ code, err := RunDoctor(context.Background(), nil, &stdout, func(DoctorOptions) (int, error) {
119
+ return 2, nil
120
+ })
121
+
122
+ if err != nil {
123
+ t.Fatalf("RunDoctor() error = %v", err)
124
+ }
125
+ if code != 2 {
126
+ t.Fatalf("code = %d, want 2", code)
127
+ }
128
+ }
129
+
130
+ func runDoctorOptions(t *testing.T, args []string) DoctorOptions {
131
+ t.Helper()
132
+
133
+ var stdout bytes.Buffer
134
+ var got DoctorOptions
135
+ code, err := RunDoctor(context.Background(), args, &stdout, func(options DoctorOptions) (int, error) {
136
+ got = options
137
+ return 0, nil
138
+ })
139
+ if err != nil {
140
+ t.Fatalf("RunDoctor() error = %v", err)
141
+ }
142
+ if code != 0 {
143
+ t.Fatalf("RunDoctor() code = %d, want 0", code)
144
+ }
145
+ return got
146
+ }
package/cmd/init.go ADDED
@@ -0,0 +1,63 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "io"
8
+ )
9
+
10
+ const initUsage = "Usage: init [dir] [--force] [--install]"
11
+
12
+ type InitOptions struct {
13
+ Dir string
14
+ Force bool
15
+ Install bool
16
+ }
17
+
18
+ type InitRunner func(context.Context, InitOptions) error
19
+
20
+ var ErrInitNotImplemented = errors.New("init scaffold is not implemented yet")
21
+
22
+ func RunInit(ctx context.Context, args []string, stdout io.Writer, runner InitRunner) error {
23
+ options, help, err := ParseInitArgs(args)
24
+ if help {
25
+ _, writeErr := fmt.Fprintln(stdout, initUsage)
26
+ return writeErr
27
+ }
28
+ if err != nil {
29
+ return err
30
+ }
31
+ return runner(ctx, options)
32
+ }
33
+
34
+ func ParseInitArgs(args []string) (InitOptions, bool, error) {
35
+ options := InitOptions{Dir: "."}
36
+ var dirSet bool
37
+
38
+ for _, arg := range args {
39
+ switch arg {
40
+ case "--help":
41
+ return InitOptions{}, true, nil
42
+ case "--force":
43
+ options.Force = true
44
+ case "--install":
45
+ options.Install = true
46
+ default:
47
+ if len(arg) > 0 && arg[0] == '-' {
48
+ return InitOptions{}, false, fmt.Errorf("unknown init flag %q", arg)
49
+ }
50
+ if dirSet {
51
+ return InitOptions{}, false, fmt.Errorf("init accepts at most one directory")
52
+ }
53
+ options.Dir = arg
54
+ dirSet = true
55
+ }
56
+ }
57
+
58
+ return options, false, nil
59
+ }
60
+
61
+ func InitNotImplemented(context.Context, InitOptions) error {
62
+ return ErrInitNotImplemented
63
+ }
@@ -0,0 +1,104 @@
1
+ package cmd
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "errors"
7
+ "strings"
8
+ "testing"
9
+ )
10
+
11
+ func TestRunInitHelp(t *testing.T) {
12
+ var stdout bytes.Buffer
13
+ called := false
14
+
15
+ err := RunInit(context.Background(), []string{"--help"}, &stdout, func(context.Context, InitOptions) error {
16
+ called = true
17
+ return nil
18
+ })
19
+
20
+ if err != nil {
21
+ t.Fatalf("RunInit() error = %v", err)
22
+ }
23
+ if called {
24
+ t.Fatal("RunInit() called runner for help")
25
+ }
26
+ if !strings.Contains(stdout.String(), "Usage: init [dir] [--force] [--install]") {
27
+ t.Fatalf("help output = %q", stdout.String())
28
+ }
29
+ }
30
+
31
+ func TestRunInitDefaultsToCurrentDirectory(t *testing.T) {
32
+ got := runInitOptions(t, nil)
33
+
34
+ if got.Dir != "." {
35
+ t.Fatalf("Dir = %q, want .", got.Dir)
36
+ }
37
+ }
38
+
39
+ func TestRunInitUsesSpecifiedDirectory(t *testing.T) {
40
+ got := runInitOptions(t, []string{"example"})
41
+
42
+ if got.Dir != "example" {
43
+ t.Fatalf("Dir = %q, want example", got.Dir)
44
+ }
45
+ }
46
+
47
+ func TestRunInitParsesForceAndInstall(t *testing.T) {
48
+ got := runInitOptions(t, []string{"example", "--force", "--install"})
49
+
50
+ if !got.Force {
51
+ t.Fatal("Force = false, want true")
52
+ }
53
+ if !got.Install {
54
+ t.Fatal("Install = false, want true")
55
+ }
56
+ }
57
+
58
+ func TestRunInitRejectsUnknownFlags(t *testing.T) {
59
+ var stdout bytes.Buffer
60
+ called := false
61
+
62
+ err := RunInit(context.Background(), []string{"--bogus"}, &stdout, func(context.Context, InitOptions) error {
63
+ called = true
64
+ return nil
65
+ })
66
+
67
+ if err == nil {
68
+ t.Fatal("RunInit() error = nil, want unknown flag error")
69
+ }
70
+ if called {
71
+ t.Fatal("RunInit() called runner after invalid args")
72
+ }
73
+ if !strings.Contains(err.Error(), "unknown init flag") {
74
+ t.Fatalf("error = %q, want unknown flag", err.Error())
75
+ }
76
+ }
77
+
78
+ func TestRunInitReturnsRunnerError(t *testing.T) {
79
+ want := errors.New("runner failed")
80
+ var stdout bytes.Buffer
81
+
82
+ err := RunInit(context.Background(), nil, &stdout, func(context.Context, InitOptions) error {
83
+ return want
84
+ })
85
+
86
+ if !errors.Is(err, want) {
87
+ t.Fatalf("RunInit() error = %v, want %v", err, want)
88
+ }
89
+ }
90
+
91
+ func runInitOptions(t *testing.T, args []string) InitOptions {
92
+ t.Helper()
93
+
94
+ var stdout bytes.Buffer
95
+ var got InitOptions
96
+ err := RunInit(context.Background(), args, &stdout, func(_ context.Context, options InitOptions) error {
97
+ got = options
98
+ return nil
99
+ })
100
+ if err != nil {
101
+ t.Fatalf("RunInit() error = %v", err)
102
+ }
103
+ return got
104
+ }
@@ -5,53 +5,65 @@ import (
5
5
  "os"
6
6
  "path/filepath"
7
7
 
8
- tea "github.com/charmbracelet/bubbletea"
9
- "github.com/charmbracelet/lipgloss"
10
- "github.com/muesli/termenv"
8
+ xterm "github.com/charmbracelet/x/term"
11
9
  "github.com/opencode/savepoint/internal/data"
12
10
  )
13
11
 
14
12
  func Run() error {
15
- lipgloss.SetColorProfile(termenv.ANSI256)
13
+ return RunWithFilters("", "")
14
+ }
16
15
 
17
- model, err := newProjectModel(".")
18
- if err != nil {
19
- return err
16
+ func RunWithFilters(release, epic string) error {
17
+ if !xterm.IsTerminal(os.Stdout.Fd()) {
18
+ return runPlainOutput(release, epic)
20
19
  }
20
+ return RunTUI(release, epic)
21
+ }
21
22
 
22
- p := tea.NewProgram(model, tea.WithAltScreen())
23
- if _, err := p.Run(); err != nil {
24
- fmt.Fprintf(os.Stderr, "Error: %v\n", err)
23
+ func runPlainOutput(release, epic string) error {
24
+ model, err := newProjectModel(".", release, epic)
25
+ if err != nil {
25
26
  return err
26
27
  }
28
+ fmt.Print(RenderPlainTable(model))
27
29
  return nil
28
30
  }
29
31
 
30
- func newProgramModel() Model {
31
- return NewModel(nil, "v1", "E03-board-tui-core")
32
+ func newProjectModel(start, releaseFilter, epicFilter string) (Model, error) {
33
+ return newProjectModelWithDependencies(start, releaseFilter, epicFilter, defaultModelDependencies())
32
34
  }
33
35
 
34
- func newProjectModel(start string) (Model, error) {
35
- d := data.NewDiscover()
36
- root, err := d.FindSavepointRoot(start)
36
+ func newProjectModelWithDependencies(start, releaseFilter, epicFilter string, deps ModelDependencies) (Model, error) {
37
+ deps = modelDependencies([]ModelDependencies{deps})
38
+
39
+ root, err := deps.Discoverer.FindSavepointRoot(start)
37
40
  if err != nil {
38
41
  return Model{}, err
39
42
  }
40
43
 
41
- routerState, err := readRouterState(root)
44
+ routerState, err := readRouterState(root, deps.RouterReader)
42
45
  if err != nil {
43
46
  return Model{}, err
44
47
  }
45
48
 
46
- tasks, releaseIDs, releaseEpics, epicStatuses, err := loadBoardData(root)
49
+ tasks, releaseIDs, releaseEpics, epicStatuses, err := loadBoardData(root, deps.Discoverer, deps.Parser)
47
50
  if err != nil {
48
51
  return Model{}, err
49
52
  }
50
53
 
51
- release := firstKnown(routerState.Release, releaseIDs)
52
- epic := firstKnown(routerState.Epic, releaseEpics[release])
54
+ preferredRelease := routerState.Release
55
+ if releaseFilter != "" {
56
+ preferredRelease = releaseFilter
57
+ }
58
+ preferredEpic := routerState.Epic
59
+ if epicFilter != "" {
60
+ preferredEpic = epicFilter
61
+ }
62
+
63
+ release := firstKnown(preferredRelease, releaseIDs)
64
+ epic := firstKnown(preferredEpic, releaseEpics[release])
53
65
 
54
- model := NewModel(tasks, release, epic)
66
+ model := NewModel(tasks, release, epic, deps)
55
67
  model.Root = root
56
68
  model.RouterTask = routerState.Task
57
69
  model.RouterState = routerState
@@ -70,9 +82,8 @@ func newProjectModel(start string) (Model, error) {
70
82
  return model, nil
71
83
  }
72
84
 
73
- func loadBoardData(root string) ([]data.Task, []string, map[string][]string, map[string]string, error) {
74
- d := data.NewDiscover()
75
- releases, err := d.ListReleases(root)
85
+ func loadBoardData(root string, discoverer taskDiscoverer, parser taskParser) ([]data.Task, []string, map[string][]string, map[string]string, error) {
86
+ releases, err := discoverer.ListReleases(root)
76
87
  if err != nil {
77
88
  return nil, nil, nil, nil, err
78
89
  }
@@ -84,13 +95,13 @@ func loadBoardData(root string) ([]data.Task, []string, map[string][]string, map
84
95
 
85
96
  for _, release := range releases {
86
97
  releaseIDs = append(releaseIDs, release.ID)
87
- epics, err := d.ListEpics(root, release.ID)
98
+ epics, err := discoverer.ListEpics(root, release.ID)
88
99
  if err != nil {
89
100
  return nil, nil, nil, nil, err
90
101
  }
91
102
  for _, epic := range epics {
92
103
  releaseEpics[release.ID] = append(releaseEpics[release.ID], epic.ID)
93
- epicTasks, err := loadEpicTasks(d, root, release.ID, epic.ID)
104
+ epicTasks, err := loadEpicTasks(discoverer, parser, root, release.ID, epic.ID)
94
105
  if err != nil {
95
106
  return nil, nil, nil, nil, err
96
107
  }
@@ -98,7 +109,6 @@ func loadBoardData(root string) ([]data.Task, []string, map[string][]string, map
98
109
 
99
110
  detailPath := filepath.Join(epic.Path, shortID(epic.ID)+"-Detail.md")
100
111
  if raw, err := os.ReadFile(detailPath); err == nil {
101
- parser := data.NewParser()
102
112
  if fm, err := parser.ParseFrontmatter(string(raw)); err == nil {
103
113
  if status, ok := fm["status"].(string); ok {
104
114
  epicStatuses[epic.ID] = status
@@ -111,27 +121,21 @@ func loadBoardData(root string) ([]data.Task, []string, map[string][]string, map
111
121
  return tasks, releaseIDs, releaseEpics, epicStatuses, nil
112
122
  }
113
123
 
114
- func loadAllTasks(root string) ([]data.Task, error) {
115
- tasks, _, _, _, err := loadBoardData(root)
116
- return tasks, err
117
- }
118
-
119
- func readRouterState(root string) (*data.RouterState, error) {
124
+ func readRouterState(root string, reader routerReader) (*data.RouterState, error) {
120
125
  content, err := os.ReadFile(filepath.Join(root, "router.md"))
121
126
  if err != nil {
122
127
  return nil, err
123
128
  }
124
129
 
125
- return data.NewRouterReader().ReadState(string(content))
130
+ return reader.ReadState(string(content))
126
131
  }
127
132
 
128
- func loadEpicTasks(d *data.Discover, root, release, epic string) ([]data.Task, error) {
129
- taskInfos, err := d.ListTasks(root, release, epic)
133
+ func loadEpicTasks(discoverer taskDiscoverer, parser taskParser, root, release, epic string) ([]data.Task, error) {
134
+ taskInfos, err := discoverer.ListTasks(root, release, epic)
130
135
  if err != nil {
131
136
  return nil, err
132
137
  }
133
138
 
134
- parser := data.NewParser()
135
139
  tasks := make([]data.Task, 0, len(taskInfos))
136
140
  for _, taskInfo := range taskInfos {
137
141
  content, err := os.ReadFile(taskInfo.Path)
@@ -1,16 +1,16 @@
1
1
  package board
2
2
 
3
3
  import (
4
- "os"
5
4
  "path/filepath"
6
5
  "strings"
7
6
  "testing"
8
7
 
9
8
  "github.com/opencode/savepoint/internal/data"
9
+ "github.com/opencode/savepoint/internal/testutil"
10
10
  )
11
11
 
12
- func TestNewProgramModelUsesBoardCore(t *testing.T) {
13
- m := newProgramModel()
12
+ func TestNewModelUsesBoardCore(t *testing.T) {
13
+ m := NewModel(nil, "v1", "E03-board-tui-core")
14
14
  m.Width = 100
15
15
 
16
16
  got := m.View()
@@ -27,22 +27,11 @@ func TestNewProgramModelUsesBoardCore(t *testing.T) {
27
27
  func TestNewProjectModelLoadsReleasesEpicsAndTasks(t *testing.T) {
28
28
  projectRoot := t.TempDir()
29
29
  savepointRoot := filepath.Join(projectRoot, ".savepoint")
30
- writeFile(t, filepath.Join(savepointRoot, "router.md"), `# Agent State Machine
31
-
32
- ## Current state
33
-
34
- `+"```"+`yaml
35
- state: task-building
36
- release: v2
37
- epic: E03-live
38
- task: ""
39
- next_action: "test"
40
- `+"```"+`
41
- `)
30
+ testutil.WriteRouter(t, savepointRoot, "task-building", "v2", "E03-live", "", "test")
42
31
  writeTask(t, savepointRoot, "v1", "E01-old", "T001-old", data.ColumnPlanned)
43
32
  writeTask(t, savepointRoot, "v2", "E03-live", "T001-live", data.ColumnInProgress)
44
33
 
45
- model, err := newProjectModel(projectRoot)
34
+ model, err := newProjectModel(projectRoot, "", "")
46
35
  if err != nil {
47
36
  t.Fatalf("newProjectModel() error = %v", err)
48
37
  }
@@ -75,21 +64,10 @@ next_action: "test"
75
64
  func TestNewProjectModelUsesPathReleaseForTaskWithoutReleaseFrontmatter(t *testing.T) {
76
65
  projectRoot := t.TempDir()
77
66
  savepointRoot := filepath.Join(projectRoot, ".savepoint")
78
- writeFile(t, filepath.Join(savepointRoot, "router.md"), `# Agent State Machine
79
-
80
- ## Current state
81
-
82
- `+"```"+`yaml
83
- state: task-building
84
- release: v1.1
85
- epic: E01-tui-optimisation
86
- task: E01-tui-optimisation/T001-border-resize-fix
87
- next_action: "test"
88
- `+"```"+`
89
- `)
67
+ testutil.WriteRouter(t, savepointRoot, "task-building", "v1.1", "E01-tui-optimisation", "E01-tui-optimisation/T001-border-resize-fix", "test")
90
68
  writeTaskWithoutRelease(t, savepointRoot, "v1.1", "E01-tui-optimisation", "T001-border-resize-fix", data.ColumnInProgress)
91
69
 
92
- model, err := newProjectModel(projectRoot)
70
+ model, err := newProjectModel(projectRoot, "", "")
93
71
  if err != nil {
94
72
  t.Fatalf("newProjectModel() error = %v", err)
95
73
  }
@@ -112,22 +90,11 @@ next_action: "test"
112
90
  func TestNewProjectModelResolvesShortRouterEpicToFullEpicID(t *testing.T) {
113
91
  projectRoot := t.TempDir()
114
92
  savepointRoot := filepath.Join(projectRoot, ".savepoint")
115
- writeFile(t, filepath.Join(savepointRoot, "router.md"), `# Agent State Machine
116
-
117
- ## Current state
118
-
119
- `+"```"+`yaml
120
- state: task-building
121
- release: v1.1
122
- epic: E03
123
- task: T001
124
- next_action: "Build v1.1 E03/T001"
125
- `+"```"+`
126
- `)
93
+ testutil.WriteRouter(t, savepointRoot, "task-building", "v1.1", "E03", "T001", "Build v1.1 E03/T001")
127
94
  writeTask(t, savepointRoot, "v1.1", "E01-tui-optimisation", "T007-column-focus-border-stability", data.ColumnInProgress)
128
95
  writeTask(t, savepointRoot, "v1.1", "E03-ui-visual-refinement", "T001-border-resize-fix", data.ColumnInProgress)
129
96
 
130
- model, err := newProjectModel(projectRoot)
97
+ model, err := newProjectModel(projectRoot, "", "")
131
98
  if err != nil {
132
99
  t.Fatalf("newProjectModel() error = %v", err)
133
100
  }
@@ -173,51 +140,29 @@ func TestUpdateReloadMsgRefreshesReleaseEpicIndex(t *testing.T) {
173
140
  }
174
141
  }
175
142
 
176
- func writeTask(t *testing.T, root, release, epic, task string, column data.ColumnType) {
143
+ func writeTask(t *testing.T, root, release, epic, taskSlug string, column data.ColumnType) {
177
144
  t.Helper()
178
- path := filepath.Join(root, "releases", release, "epics", epic, "tasks", task+".md")
179
- phase := ""
180
- if column == data.ColumnInProgress {
181
- phase = "phase: build\n"
182
- }
183
- content := `---
184
- id: ` + epic + `/` + task + `
185
- release: ` + release + `
186
- status: ` + string(column) + `
187
- ` + phase + `objective: "Test task"
188
- depends_on: []
189
- ---
190
-
191
- # Test task
192
- `
193
- writeFile(t, path, content)
194
- }
195
-
196
- func writeTaskWithoutRelease(t *testing.T, root, release, epic, task string, column data.ColumnType) {
197
- t.Helper()
198
- path := filepath.Join(root, "releases", release, "epics", epic, "tasks", task+".md")
199
- phase := ""
145
+ tf := testutil.TaskFixture{
146
+ Slug: taskSlug,
147
+ Release: release,
148
+ Status: string(column),
149
+ Objective: "Test task",
150
+ }
200
151
  if column == data.ColumnInProgress {
201
- phase = "phase: build\n"
202
- }
203
- content := `---
204
- id: ` + epic + `/` + task + `
205
- status: ` + string(column) + `
206
- ` + phase + `objective: "Test task"
207
- depends_on: []
208
- ---
209
-
210
- # Test task
211
- `
212
- writeFile(t, path, content)
152
+ tf.Phase = "build"
153
+ }
154
+ testutil.WriteTask(t, root, release, epic, tf)
213
155
  }
214
156
 
215
- func writeFile(t *testing.T, path, content string) {
157
+ func writeTaskWithoutRelease(t *testing.T, root, release, epic, taskSlug string, column data.ColumnType) {
216
158
  t.Helper()
217
- if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
218
- t.Fatal(err)
159
+ tf := testutil.TaskFixture{
160
+ Slug: taskSlug,
161
+ Status: string(column),
162
+ Objective: "Test task",
219
163
  }
220
- if err := os.WriteFile(path, []byte(content), 0644); err != nil {
221
- t.Fatal(err)
164
+ if column == data.ColumnInProgress {
165
+ tf.Phase = "build"
222
166
  }
167
+ testutil.WriteTask(t, root, release, epic, tf)
223
168
  }