savepoint 1.0.2 → 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 (242) hide show
  1. package/.claude/settings.local.json +12 -1
  2. package/.github/workflows/ci.yml +20 -0
  3. package/.golangci.yml +11 -0
  4. package/.savepoint/Design.md +40 -38
  5. package/.savepoint/{audit/v1.1/E02-cross-platform-compatibility/proposals.md → releases/v1.1/epics/E02-cross-platform-compatibility/E02-Audit.md} +48 -38
  6. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Audit.md +195 -0
  7. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +14 -1
  8. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +3 -3
  9. package/.savepoint/{audit/v1.1/E04-epic-navigation/proposals.md → releases/v1.1/epics/E04-epic-navigation/E04-Audit.md} +65 -54
  10. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Audit.md +237 -0
  11. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +25 -16
  12. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +17 -6
  13. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +15 -5
  14. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +19 -5
  15. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +11 -1
  16. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +9 -6
  17. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +29 -13
  18. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Audit.md +56 -0
  19. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Detail.md +63 -0
  20. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T005-proposals.md +44 -0
  21. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T007-apply-close.md +35 -0
  22. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T009-integration.md +40 -0
  23. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T010-audit-file-migration.md +45 -0
  24. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T011-model-tab-state.md +26 -0
  25. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T012-epic-audit-render.md +33 -0
  26. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T013-handle-tab-keys.md +34 -0
  27. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T014-tab-indicator.md +33 -0
  28. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Audit.md +336 -0
  29. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Detail.md +61 -0
  30. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T001-cli-entrypoint.md +37 -0
  31. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T002-target-validation.md +28 -0
  32. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T003-scaffold-writer.md +46 -0
  33. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T004-atomic-writes.md +27 -0
  34. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T005-magic-prompt.md +25 -0
  35. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T006-clipboard.md +26 -0
  36. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T007-integration-test.md +26 -0
  37. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Audit.md +333 -0
  38. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Detail.md +68 -0
  39. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T001-cli-entrypoint.md +26 -0
  40. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T002-non-tty-fallback.md +27 -0
  41. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T003-tui-app-shell.md +28 -0
  42. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T004-board-model.md +29 -0
  43. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T005-detail-pane.md +27 -0
  44. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T006-status-transitions.md +29 -0
  45. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T007-theme-fallbacks.md +29 -0
  46. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T008-integration-test.md +27 -0
  47. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Audit.md +207 -0
  48. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Detail.md +65 -0
  49. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T001-cli-entrypoint.md +24 -0
  50. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T002-config-router-validation.md +28 -0
  51. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T003-structure-checks.md +29 -0
  52. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T004-dependency-checks.md +27 -0
  53. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T005-audit-orphan-checks.md +28 -0
  54. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T006-quality-gates-report.md +31 -0
  55. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/E11-Detail.md +36 -0
  56. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T001-debug-logging.md +25 -0
  57. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T002-increase-debounce.md +21 -0
  58. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T003-error-handling.md +22 -0
  59. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T004-test-verify.md +29 -0
  60. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Audit.md +444 -0
  61. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Detail.md +45 -0
  62. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T001-default-phase.md +35 -0
  63. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T002-default-status.md +19 -0
  64. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T003-better-errors.md +29 -0
  65. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T004-validate-on-write.md +25 -0
  66. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T005-tests.md +37 -0
  67. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Audit.md +118 -0
  68. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Detail.md +73 -0
  69. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T001-safe-cleanup.md +66 -0
  70. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T002-bug-fixes.md +35 -0
  71. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T003-centralize-duplication.md +60 -0
  72. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T004-infrastructure.md +33 -0
  73. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T005-decompose-update.md +37 -0
  74. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T006-async-io.md +40 -0
  75. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T007-test-coverage.md +37 -0
  76. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Audit.md +267 -0
  77. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Detail.md +54 -0
  78. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T001-group-model.md +39 -0
  79. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T002-data-interfaces.md +42 -0
  80. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T003-discover-orphans.md +33 -0
  81. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T004-epic-panel-headings.md +35 -0
  82. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T005-shell-tokenization.md +27 -0
  83. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T006-unify-enums.md +29 -0
  84. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T007-testutil-package.md +28 -0
  85. package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Audit.md +272 -0
  86. package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Detail.md +60 -0
  87. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T001-benchmarks.md +31 -0
  88. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T002-fuzz-targets.md +34 -0
  89. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T003-debug-flag.md +30 -0
  90. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T004-dist-checksums.md +27 -0
  91. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T005-windows-targets.md +28 -0
  92. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T006-abbreviation-splitting.md +26 -0
  93. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T007-root-test-allowlist.md +33 -0
  94. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T008-ci-and-release-automation.md +46 -0
  95. package/.savepoint/releases/v1.1/epics/_archived/T001-cli-entrypoint.md +25 -0
  96. package/.savepoint/releases/v1.1/epics/_archived/T002-quality-gates.md +27 -0
  97. package/.savepoint/releases/v1.1/epics/_archived/T003-snapshot.md +27 -0
  98. package/.savepoint/releases/v1.1/epics/_archived/T004-ai-reconcile.md +29 -0
  99. package/.savepoint/releases/v1.1/epics/_archived/T006-tui-review.md +31 -0
  100. package/.savepoint/releases/v1.1/epics/_archived/T008-skip-handling.md +34 -0
  101. package/.savepoint/releases/v1.1/v1.1-PRD.md +67 -7
  102. package/.savepoint/router.md +10 -17
  103. package/AGENTS.md +39 -24
  104. package/Makefile +3 -1
  105. package/README.md +0 -1
  106. package/agent-skills/savepoint-audit/SKILL.md +86 -34
  107. package/agent-skills/savepoint-build-task/SKILL.md +7 -2
  108. package/agent-skills/savepoint-create-plan/SKILL.md +7 -2
  109. package/agent-skills/savepoint-create-task/SKILL.md +44 -31
  110. package/agent-skills/savepoint-draft-prd/SKILL.md +7 -2
  111. package/agent-skills/savepoint-system-design/SKILL.md +7 -2
  112. package/agent_skills_test.go +91 -0
  113. package/cmd/board.go +59 -0
  114. package/cmd/board_test.go +137 -0
  115. package/cmd/doctor.go +53 -0
  116. package/cmd/doctor_test.go +146 -0
  117. package/cmd/init.go +63 -0
  118. package/cmd/init_test.go +104 -0
  119. package/internal/board/board.go +44 -36
  120. package/internal/board/board_test.go +27 -82
  121. package/internal/board/card.go +43 -23
  122. package/internal/board/card_test.go +74 -5
  123. package/internal/board/column.go +75 -15
  124. package/internal/board/column_test.go +76 -2
  125. package/internal/board/debug.go +26 -0
  126. package/internal/board/debug_test.go +108 -0
  127. package/internal/board/detail.go +33 -47
  128. package/internal/board/detail_test.go +48 -0
  129. package/internal/board/epic_panel.go +120 -22
  130. package/internal/board/epic_panel_test.go +302 -17
  131. package/internal/board/help.go +1 -0
  132. package/internal/board/help_test.go +1 -0
  133. package/internal/board/integration_test.go +266 -0
  134. package/internal/board/interfaces.go +65 -0
  135. package/internal/board/interfaces_test.go +114 -0
  136. package/internal/board/io.go +93 -0
  137. package/internal/board/model.go +79 -118
  138. package/internal/board/plain.go +88 -0
  139. package/internal/board/plain_test.go +117 -0
  140. package/internal/board/release.go +1 -9
  141. package/internal/board/release_test.go +6 -6
  142. package/internal/board/status.go +4 -4
  143. package/internal/board/theme.go +24 -0
  144. package/internal/board/theme_test.go +31 -0
  145. package/internal/board/transitions.go +113 -88
  146. package/internal/board/transitions_test.go +164 -141
  147. package/internal/board/tui.go +32 -0
  148. package/internal/board/update.go +344 -215
  149. package/internal/board/update_test.go +326 -18
  150. package/internal/board/util.go +76 -0
  151. package/internal/board/view.go +31 -28
  152. package/internal/board/view_test.go +74 -2
  153. package/internal/board/watch.go +41 -5
  154. package/internal/buildtool/main.go +45 -15
  155. package/internal/buildtool/main_test.go +224 -0
  156. package/internal/data/config.go +17 -3
  157. package/internal/data/config_test.go +49 -0
  158. package/internal/data/discover.go +26 -0
  159. package/internal/data/discover_test.go +34 -10
  160. package/internal/data/errors.go +4 -0
  161. package/internal/data/fuzz_test.go +75 -0
  162. package/internal/data/lifecycle.go +13 -6
  163. package/internal/data/lifecycle_test.go +14 -11
  164. package/internal/data/parser.go +22 -6
  165. package/internal/data/parser_test.go +31 -7
  166. package/internal/data/task.go +0 -9
  167. package/internal/data/testdata/fuzz/FuzzSplitFrontmatterBody/68eb66b0fe91e7e3 +2 -0
  168. package/internal/data/write.go +88 -11
  169. package/internal/data/write_test.go +167 -0
  170. package/internal/doctor/checks.go +567 -0
  171. package/internal/doctor/checks_test.go +716 -0
  172. package/internal/doctor/gates.go +193 -0
  173. package/internal/doctor/gates_test.go +166 -0
  174. package/internal/doctor/interfaces.go +64 -0
  175. package/internal/doctor/interfaces_test.go +104 -0
  176. package/internal/doctor/repairs.go +80 -0
  177. package/internal/doctor/repairs_test.go +81 -0
  178. package/internal/doctor/report.go +157 -0
  179. package/internal/doctor/report_test.go +89 -0
  180. package/internal/init/clipboard.go +146 -0
  181. package/internal/init/clipboard_test.go +74 -0
  182. package/internal/init/install.go +16 -0
  183. package/internal/init/integration_test.go +197 -0
  184. package/internal/init/prompt.go +14 -0
  185. package/internal/init/prompt_test.go +77 -0
  186. package/internal/init/scaffold.go +59 -0
  187. package/internal/init/scaffold_test.go +179 -0
  188. package/internal/init/template_freshness_test.go +56 -0
  189. package/internal/init/validate.go +85 -0
  190. package/internal/init/validate_test.go +141 -0
  191. package/internal/init/write.go +73 -0
  192. package/internal/init/write_test.go +91 -0
  193. package/internal/styles/styles_test.go +133 -0
  194. package/internal/testutil/fixture.go +113 -0
  195. package/internal/testutil/fs.go +26 -0
  196. package/main.go +120 -4
  197. package/package.json +2 -2
  198. package/project-audit/audit_report_glm_5.1.md +411 -0
  199. package/project-audit/audit_report_opus_4.6.md +406 -0
  200. package/project-audit/consolidated-audit-report.md +456 -0
  201. package/templates/project/.savepoint/Design.md +2 -2
  202. package/templates/project/.savepoint/router.md +10 -10
  203. package/templates/project/AGENTS.md +33 -21
  204. package/templates/project/agent-skills/savepoint-audit/SKILL.md +87 -0
  205. package/templates/project/agent-skills/savepoint-build-task/SKILL.md +44 -0
  206. package/templates/project/agent-skills/savepoint-create-plan/SKILL.md +33 -0
  207. package/templates/project/agent-skills/savepoint-create-task/SKILL.md +44 -0
  208. package/templates/project/agent-skills/savepoint-draft-prd/SKILL.md +37 -0
  209. package/templates/project/agent-skills/savepoint-system-design/SKILL.md +38 -0
  210. package/templates/prompts/audit-reconciliation.prompt.md +33 -28
  211. package/templates/prompts/design.prompt.md +3 -1
  212. package/.savepoint/audit/v1/E01/proposals.md +0 -168
  213. package/.savepoint/audit/v1/E01/snapshot.md +0 -78
  214. package/.savepoint/audit/v1/E01-go-setup/proposals.md +0 -166
  215. package/.savepoint/audit/v1/E01-go-setup/snapshot.md +0 -71
  216. package/.savepoint/audit/v1/E01-scaffolding/proposals/AGENTS.md +0 -66
  217. package/.savepoint/audit/v1/E01-scaffolding/proposals/Design.md +0 -210
  218. package/.savepoint/audit/v1/E01-scaffolding/proposals/epic-Design.md +0 -117
  219. package/.savepoint/audit/v1/E01-scaffolding/proposals/quality-review.md +0 -101
  220. package/.savepoint/audit/v1/E01-scaffolding/snapshot.md +0 -54
  221. package/.savepoint/audit/v1/E02-data-model/snapshot.md +0 -128
  222. package/.savepoint/audit/v1/E02-data-readers/proposals.md +0 -123
  223. package/.savepoint/audit/v1/E02-data-readers/snapshot.md +0 -54
  224. package/.savepoint/audit/v1/E03-board-tui-core/proposals.md +0 -146
  225. package/.savepoint/audit/v1/E03-board-tui-core/snapshot.md +0 -57
  226. package/.savepoint/audit/v1/E03-cli-foundation/snapshot.md +0 -106
  227. package/.savepoint/audit/v1/E04-board-components/proposals.md +0 -118
  228. package/.savepoint/audit/v1/E04-board-components/snapshot.md +0 -77
  229. package/.savepoint/audit/v1/E04-templates-and-prompts/snapshot.md +0 -115
  230. package/.savepoint/audit/v1/E05-init-command/snapshot.md +0 -125
  231. package/.savepoint/audit/v1/E05-phase-transitions/proposals.md +0 -83
  232. package/.savepoint/audit/v1/E05-phase-transitions/snapshot.md +0 -36
  233. package/.savepoint/audit/v1/E06-atari-noir-layout/proposals.md +0 -130
  234. package/.savepoint/audit/v1/E06-atari-noir-layout/snapshot.md +0 -84
  235. package/.savepoint/audit/v1/E06-tui-board/snapshot.md +0 -64
  236. package/.savepoint/audit/v1/E07-audit-pipeline/snapshot.md +0 -165
  237. package/.savepoint/audit/v1/E08-board-workflow-cleanup/snapshot.md +0 -65
  238. package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/snapshot.md +0 -41
  239. package/.savepoint/audit/v1.1/E04-epic-navigation/snapshot.md +0 -48
  240. package/ink-cli-ui-design.zip +0 -0
  241. package/savepoint +0 -0
  242. package/savepoint.exe +0 -0
@@ -0,0 +1,91 @@
1
+ package init
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+
9
+ "github.com/opencode/savepoint/internal/testutil"
10
+ )
11
+
12
+ func TestAtomicWrite_createsFile(t *testing.T) {
13
+ dir := t.TempDir()
14
+ target := filepath.Join(dir, "output.txt")
15
+
16
+ if err := AtomicWrite(target, []byte("hello")); err != nil {
17
+ t.Fatalf("AtomicWrite() error = %v", err)
18
+ }
19
+
20
+ data, err := os.ReadFile(target)
21
+ if err != nil {
22
+ t.Fatal(err)
23
+ }
24
+ if string(data) != "hello" {
25
+ t.Fatalf("got %q, want %q", string(data), "hello")
26
+ }
27
+ }
28
+
29
+ func TestAtomicWrite_replacesExistingFile(t *testing.T) {
30
+ dir := t.TempDir()
31
+ target := filepath.Join(dir, "output.txt")
32
+ testutil.WriteFile(t, target, "old")
33
+
34
+ if err := AtomicWrite(target, []byte("new")); err != nil {
35
+ t.Fatalf("AtomicWrite() error = %v", err)
36
+ }
37
+
38
+ data, err := os.ReadFile(target)
39
+ if err != nil {
40
+ t.Fatal(err)
41
+ }
42
+ if string(data) != "new" {
43
+ t.Fatalf("got %q, want %q", string(data), "new")
44
+ }
45
+
46
+ assertNoWriteArtifacts(t, dir)
47
+ }
48
+
49
+ func TestAtomicWrite_noTempFileLeftBehind(t *testing.T) {
50
+ dir := t.TempDir()
51
+ target := filepath.Join(dir, "output.txt")
52
+
53
+ if err := AtomicWrite(target, []byte("data")); err != nil {
54
+ t.Fatalf("AtomicWrite() error = %v", err)
55
+ }
56
+
57
+ assertNoWriteArtifacts(t, dir)
58
+ }
59
+
60
+ func TestAtomicWrite_handlesNestedDirectories(t *testing.T) {
61
+ dir := t.TempDir()
62
+ target := filepath.Join(dir, "deep", "nested", "output.txt")
63
+
64
+ // Parent directories must exist before calling AtomicWrite
65
+ testutil.MkdirAll(t, filepath.Dir(target))
66
+
67
+ if err := AtomicWrite(target, []byte("nested")); err != nil {
68
+ t.Fatalf("AtomicWrite() error = %v", err)
69
+ }
70
+
71
+ if _, err := os.Stat(target); err != nil {
72
+ t.Errorf("target not created: %v", err)
73
+ }
74
+ }
75
+
76
+ func assertNoWriteArtifacts(t *testing.T, dir string) {
77
+ t.Helper()
78
+
79
+ entries, err := os.ReadDir(dir)
80
+ if err != nil {
81
+ t.Fatal(err)
82
+ }
83
+ for _, e := range entries {
84
+ if strings.HasPrefix(e.Name(), ".tmp-") && strings.HasSuffix(e.Name(), ".write") {
85
+ t.Errorf("temp file %q left behind after successful write", e.Name())
86
+ }
87
+ if strings.HasSuffix(e.Name(), ".savepoint-bak") {
88
+ t.Errorf("backup file %q left behind after successful write", e.Name())
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,133 @@
1
+ package styles
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/charmbracelet/lipgloss"
7
+ )
8
+
9
+ func TestPaletteConstants_present(t *testing.T) {
10
+ // Truecolor tier
11
+ if Background == "" {
12
+ t.Error("Background constant is empty")
13
+ }
14
+ if Surface == "" {
15
+ t.Error("Surface constant is empty")
16
+ }
17
+ if Surface2 == "" {
18
+ t.Error("Surface2 constant is empty")
19
+ }
20
+ if Border == "" {
21
+ t.Error("Border constant is empty")
22
+ }
23
+ if BorderSubtle == "" {
24
+ t.Error("BorderSubtle constant is empty")
25
+ }
26
+ if PrimaryText == "" {
27
+ t.Error("PrimaryText constant is empty")
28
+ }
29
+ if AtariOrange == "" {
30
+ t.Error("AtariOrange constant is empty")
31
+ }
32
+ if NPPGreen == "" {
33
+ t.Error("NPPGreen constant is empty")
34
+ }
35
+ if VibePurple == "" {
36
+ t.Error("VibePurple constant is empty")
37
+ }
38
+ if Dim == "" {
39
+ t.Error("Dim constant is empty")
40
+ }
41
+ }
42
+
43
+ func TestPaletteConstants_256tier(t *testing.T) {
44
+ if Background256 == "" {
45
+ t.Error("Background256 constant is empty")
46
+ }
47
+ if Surface256 == "" {
48
+ t.Error("Surface256 constant is empty")
49
+ }
50
+ if Surface2256 == "" {
51
+ t.Error("Surface2256 constant is empty")
52
+ }
53
+ if Border256 == "" {
54
+ t.Error("Border256 constant is empty")
55
+ }
56
+ if BorderSubtle256 == "" {
57
+ t.Error("BorderSubtle256 constant is empty")
58
+ }
59
+ if PrimaryText256 == "" {
60
+ t.Error("PrimaryText256 constant is empty")
61
+ }
62
+ if AtariOrange256 == "" {
63
+ t.Error("AtariOrange256 constant is empty")
64
+ }
65
+ if NPPGreen256 == "" {
66
+ t.Error("NPPGreen256 constant is empty")
67
+ }
68
+ if VibePurple256 == "" {
69
+ t.Error("VibePurple256 constant is empty")
70
+ }
71
+ if Dim256 == "" {
72
+ t.Error("Dim256 constant is empty")
73
+ }
74
+ }
75
+
76
+ func TestPaletteConstants_16tier(t *testing.T) {
77
+ if Background16 == "" {
78
+ t.Error("Background16 constant is empty")
79
+ }
80
+ if Surface16 == "" {
81
+ t.Error("Surface16 constant is empty")
82
+ }
83
+ if Surface216 == "" {
84
+ t.Error("Surface216 constant is empty")
85
+ }
86
+ if Border16 == "" {
87
+ t.Error("Border16 constant is empty")
88
+ }
89
+ if BorderSubtle16 == "" {
90
+ t.Error("BorderSubtle16 constant is empty")
91
+ }
92
+ if PrimaryText16 == "" {
93
+ t.Error("PrimaryText16 constant is empty")
94
+ }
95
+ if AtariOrange16 == "" {
96
+ t.Error("AtariOrange16 constant is empty")
97
+ }
98
+ if NPPGreen16 == "" {
99
+ t.Error("NPPGreen16 constant is empty")
100
+ }
101
+ if VibePurple16 == "" {
102
+ t.Error("VibePurple16 constant is empty")
103
+ }
104
+ }
105
+
106
+ func TestColor(t *testing.T) {
107
+ c := color("#FF0000", "196", "9")
108
+ expected := lipgloss.CompleteColor{TrueColor: "#FF0000", ANSI256: "196", ANSI: "9"}
109
+ if c != expected {
110
+ t.Errorf("color() = %+v, want %+v", c, expected)
111
+ }
112
+ }
113
+
114
+ func TestColor_usesPaletteConstants(t *testing.T) {
115
+ c := color(AtariOrange, AtariOrange256, AtariOrange16)
116
+ if c.TrueColor != AtariOrange {
117
+ t.Errorf("color TrueColor = %q, want %q", c.TrueColor, AtariOrange)
118
+ }
119
+ if c.ANSI256 != AtariOrange256 {
120
+ t.Errorf("color ANSI256 = %q, want %q", c.ANSI256, AtariOrange256)
121
+ }
122
+ if c.ANSI != AtariOrange16 {
123
+ t.Errorf("color ANSI = %q, want %q", c.ANSI, AtariOrange16)
124
+ }
125
+ }
126
+
127
+ func TestColor_returnsCompleteColor(t *testing.T) {
128
+ var cc lipgloss.CompleteColor
129
+ cc = color(Background, Background256, Background16)
130
+ if cc.TrueColor != Background {
131
+ t.Errorf("expected TrueColor %q, got %q", Background, cc.TrueColor)
132
+ }
133
+ }
@@ -0,0 +1,113 @@
1
+ package testutil
2
+
3
+ import (
4
+ "fmt"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+ )
9
+
10
+ // WriteRouter writes a router.md at root with the given fields.
11
+ // If nextAction is empty, it defaults to "".
12
+ func WriteRouter(t testing.TB, root, state, release, epic, task, nextAction string) {
13
+ t.Helper()
14
+ if nextAction == "" {
15
+ nextAction = `""`
16
+ } else {
17
+ nextAction = `"` + nextAction + `"`
18
+ }
19
+ if task == "" {
20
+ task = `""`
21
+ } else {
22
+ task = `"` + task + `"`
23
+ }
24
+ content := "# Agent State Machine\n\n## Current state\n\n```yaml\nstate: " + state + "\nrelease: " + release + "\nepic: " + epic + "\ntask: " + task + "\nnext_action: " + nextAction + "\n```\n"
25
+ WriteFile(t, filepath.Join(root, "router.md"), content)
26
+ }
27
+
28
+ // WriteReleasePRD writes a minimal release PRD file.
29
+ func WriteReleasePRD(t testing.TB, releasePath string) {
30
+ t.Helper()
31
+ releaseID := filepath.Base(releasePath)
32
+ WriteFile(t, filepath.Join(releasePath, releaseID+"-PRD.md"), "---\ntype: project-prd\nstatus: active\n---\n\n# Release\n")
33
+ }
34
+
35
+ // WriteEpicDetail writes a minimal epic detail file.
36
+ func WriteEpicDetail(t testing.TB, epicPath, prefix string) {
37
+ t.Helper()
38
+ WriteFile(t, filepath.Join(epicPath, prefix+"-Detail.md"), "---\ntype: epic-design\nstatus: planned\n---\n\n# Epic\n")
39
+ }
40
+
41
+ // TaskFixture describes a task file to create.
42
+ type TaskFixture struct {
43
+ Slug string // e.g. "T001-task" — becomes filename
44
+ Release string // optional; omitted if empty
45
+ Status string
46
+ Phase string // optional; omitted if empty
47
+ Objective string
48
+ DependsOn []string // optional; defaults to empty list
49
+ Body string // optional; defaults to minimal body
50
+ Extra map[string]string // optional; extra frontmatter fields
51
+ }
52
+
53
+ // WriteTask writes a task file in the savepoint directory structure.
54
+ func WriteTask(t testing.TB, root, release, epic string, task TaskFixture) {
55
+ t.Helper()
56
+ path := filepath.Join(root, "releases", release, "epics", epic, "tasks", task.Slug+".md")
57
+ id := epic + "/" + task.Slug
58
+
59
+ var b strings.Builder
60
+ b.WriteString("---\n")
61
+ b.WriteString("id: " + id + "\n")
62
+ if task.Release != "" {
63
+ b.WriteString("release: " + task.Release + "\n")
64
+ }
65
+ b.WriteString("status: " + task.Status + "\n")
66
+ if task.Phase != "" {
67
+ b.WriteString("phase: " + task.Phase + "\n")
68
+ }
69
+ b.WriteString("objective: \"" + task.Objective + "\"\n")
70
+
71
+ deps := "[]"
72
+ if len(task.DependsOn) > 0 {
73
+ quoted := make([]string, len(task.DependsOn))
74
+ for i, d := range task.DependsOn {
75
+ quoted[i] = fmt.Sprintf("%q", d)
76
+ }
77
+ deps = "[" + strings.Join(quoted, ", ") + "]"
78
+ }
79
+ b.WriteString("depends_on: " + deps + "\n")
80
+ for k, v := range task.Extra {
81
+ b.WriteString(k + ": " + v + "\n")
82
+ }
83
+ b.WriteString("---\n\n")
84
+
85
+ if task.Body != "" {
86
+ b.WriteString(task.Body)
87
+ } else {
88
+ b.WriteString("# " + task.Slug + "\n\n## Acceptance Criteria\n\n- it works\n")
89
+ }
90
+
91
+ WriteFile(t, path, b.String())
92
+ }
93
+
94
+ // SetupMinimalProject creates a minimal valid savepoint project structure.
95
+ // It creates config.yml, router.md, release PRD, epic detail, and the directory tree.
96
+ func SetupMinimalProject(t testing.TB, root, release, epic string) {
97
+ t.Helper()
98
+ releasePath := filepath.Join(root, "releases", release)
99
+ //nolint:gocritic // assignment to same epicPath variable is fine
100
+ epicPath := filepath.Join(releasePath, "epics", epic)
101
+ tasksPath := filepath.Join(epicPath, "tasks")
102
+ MkdirAll(t, tasksPath)
103
+
104
+ prefix := epic
105
+ if idx := strings.IndexByte(epic, '-'); idx != -1 {
106
+ prefix = epic[:idx]
107
+ }
108
+
109
+ WriteFile(t, filepath.Join(root, "config.yml"), "quality_gates:\n lint: null\n typecheck: null\n test: null\ntheme:\n bg: \"#000\"\n")
110
+ WriteRouter(t, root, "task-building", release, epic, "", "")
111
+ WriteReleasePRD(t, releasePath)
112
+ WriteEpicDetail(t, epicPath, prefix)
113
+ }
@@ -0,0 +1,26 @@
1
+ package testutil
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "testing"
7
+ )
8
+
9
+ // WriteFile writes content to path, creating parent directories if needed.
10
+ func WriteFile(t testing.TB, path, content string) {
11
+ t.Helper()
12
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
13
+ t.Fatal(err)
14
+ }
15
+ if err := os.WriteFile(path, []byte(content), 0644); err != nil {
16
+ t.Fatal(err)
17
+ }
18
+ }
19
+
20
+ // MkdirAll creates directories with mode 0755, fatal on error.
21
+ func MkdirAll(t testing.TB, path string) {
22
+ t.Helper()
23
+ if err := os.MkdirAll(path, 0755); err != nil {
24
+ t.Fatal(err)
25
+ }
26
+ }
package/main.go CHANGED
@@ -1,20 +1,136 @@
1
1
  package main
2
2
 
3
3
  import (
4
+ "context"
5
+ "embed"
4
6
  "fmt"
7
+ "io/fs"
5
8
  "os"
6
9
 
10
+ "github.com/opencode/savepoint/cmd"
7
11
  "github.com/opencode/savepoint/internal/board"
12
+ "github.com/opencode/savepoint/internal/data"
13
+ "github.com/opencode/savepoint/internal/doctor"
14
+ savepointinit "github.com/opencode/savepoint/internal/init"
8
15
  )
9
16
 
17
+ //go:embed templates/project
18
+ //go:embed templates/project/.savepoint
19
+ //go:embed templates/prompts
20
+ var projectTemplates embed.FS
21
+
10
22
  var version = "dev"
11
23
 
12
24
  func main() {
13
- if len(os.Args) > 1 && os.Args[1] == "--version" {
14
- fmt.Println(version)
15
- os.Exit(0)
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] {
32
+ case "--version":
33
+ fmt.Println(version)
34
+ os.Exit(0)
35
+ case "init":
36
+ if err := cmd.RunInit(context.Background(), args[1:], os.Stdout, initRunner); err != nil {
37
+ fmt.Fprintln(os.Stderr, err)
38
+ os.Exit(1)
39
+ }
40
+ os.Exit(0)
41
+ case "board":
42
+ if err := cmd.RunBoard(context.Background(), args[1:], os.Stdout, func(opts cmd.BoardOptions) error {
43
+ return board.RunWithFilters(opts.Release, opts.Epic)
44
+ }); err != nil {
45
+ fmt.Fprintln(os.Stderr, err)
46
+ os.Exit(1)
47
+ }
48
+ os.Exit(0)
49
+ case "doctor":
50
+ code, err := cmd.RunDoctor(context.Background(), args[1:], os.Stdout, func(opts cmd.DoctorOptions) (int, error) {
51
+ return runDoctorChecks(opts)
52
+ })
53
+ if err != nil {
54
+ fmt.Fprintln(os.Stderr, err)
55
+ }
56
+ os.Exit(code)
57
+ }
16
58
  }
17
59
  if err := board.Run(); err != nil {
18
60
  panic(err)
19
61
  }
20
- }
62
+ }
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
+
78
+ func runDoctorChecks(opts cmd.DoctorOptions) (int, error) {
79
+ discover := data.NewDiscover()
80
+ root, err := discover.FindSavepointRoot(".")
81
+ if err != nil {
82
+ return 2, fmt.Errorf("savepoint root not found: %w", err)
83
+ }
84
+
85
+ report := doctor.RunAllChecks(root, opts.Epic)
86
+ fmt.Fprint(os.Stdout, report.Format())
87
+
88
+ if report.HasProblems() {
89
+ return 1, nil
90
+ }
91
+ return 0, nil
92
+ }
93
+
94
+ func initRunner(ctx context.Context, opts cmd.InitOptions) error {
95
+ if err := savepointinit.ValidateTarget(opts.Dir, opts.Force); err != nil {
96
+ return err
97
+ }
98
+
99
+ sub, err := fs.Sub(projectTemplates, "templates/project")
100
+ if err != nil {
101
+ return fmt.Errorf("cannot load templates: %w", err)
102
+ }
103
+
104
+ projectName := savepointinit.ProjectNameFromDir(opts.Dir)
105
+ if err := savepointinit.Scaffold(sub, opts.Dir, projectName, opts.Force); err != nil {
106
+ return err
107
+ }
108
+
109
+ promptSub, err := fs.Sub(projectTemplates, "templates/prompts")
110
+ if err != nil {
111
+ return fmt.Errorf("cannot load prompt templates: %w", err)
112
+ }
113
+
114
+ prompt, err := savepointinit.RenderMagicPrompt(promptSub, projectName)
115
+ if err != nil {
116
+ return fmt.Errorf("render magic prompt: %w", err)
117
+ }
118
+
119
+ fmt.Println(prompt)
120
+
121
+ result := savepointinit.CopyToClipboard(prompt)
122
+ switch result.Status {
123
+ case savepointinit.ClipboardCopied:
124
+ fmt.Fprintf(os.Stderr, "prompt copied to clipboard via %s\n", result.Tool)
125
+ case savepointinit.ClipboardFailed:
126
+ fmt.Fprintf(os.Stderr, "warning: clipboard copy failed: %s\n", result.Message)
127
+ }
128
+
129
+ if opts.Install {
130
+ if err := savepointinit.InstallDependencies(opts.Dir); err != nil {
131
+ return err
132
+ }
133
+ }
134
+
135
+ return nil
136
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "savepoint",
3
- "version": "1.0.2",
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",
@@ -21,6 +21,6 @@
21
21
  "savepoint": "./savepoint"
22
22
  },
23
23
  "scripts": {
24
- "test": "savepoint init"
24
+ "test": "echo \"Run 'make test' for Go tests\""
25
25
  }
26
26
  }