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 main_test
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+ )
9
+
10
+ func TestBundledSavepointSkillsHaveDiscoveryFrontmatter(t *testing.T) {
11
+ assertSavepointSkillsHaveFrontmatter(t, filepath.Join("agent-skills"))
12
+ assertSavepointSkillsHaveFrontmatter(t, filepath.Join("templates", "project", "agent-skills"))
13
+ }
14
+
15
+ func TestProjectAgentGuideIncludesLocalSkillFallback(t *testing.T) {
16
+ path := filepath.Join("templates", "project", "AGENTS.md")
17
+ content, err := os.ReadFile(path)
18
+ if err != nil {
19
+ t.Fatalf("ReadFile(%q) error = %v", path, err)
20
+ }
21
+
22
+ want := "If the agent says the skill is not found, read `agent-skills/{skill}/SKILL.md` directly"
23
+ if !strings.Contains(string(content), want) {
24
+ t.Fatalf("%s missing local skill fallback instruction", path)
25
+ }
26
+ }
27
+
28
+ func TestScaffoldedSavepointSkillsMatchBundledSkills(t *testing.T) {
29
+ root := filepath.Join("agent-skills")
30
+ entries, err := os.ReadDir(root)
31
+ if err != nil {
32
+ t.Fatalf("ReadDir(%q) error = %v", root, err)
33
+ }
34
+
35
+ for _, entry := range entries {
36
+ if !entry.IsDir() || !strings.HasPrefix(entry.Name(), "savepoint-") {
37
+ continue
38
+ }
39
+
40
+ sourcePath := filepath.Join(root, entry.Name(), "SKILL.md")
41
+ scaffoldPath := filepath.Join("templates", "project", "agent-skills", entry.Name(), "SKILL.md")
42
+ source, err := os.ReadFile(sourcePath)
43
+ if err != nil {
44
+ t.Fatalf("ReadFile(%q) error = %v", sourcePath, err)
45
+ }
46
+ scaffold, err := os.ReadFile(scaffoldPath)
47
+ if err != nil {
48
+ t.Fatalf("ReadFile(%q) error = %v", scaffoldPath, err)
49
+ }
50
+ if string(scaffold) != string(source) {
51
+ t.Fatalf("%s does not match %s", scaffoldPath, sourcePath)
52
+ }
53
+ }
54
+ }
55
+
56
+ func assertSavepointSkillsHaveFrontmatter(t *testing.T, root string) {
57
+ t.Helper()
58
+
59
+ entries, err := os.ReadDir(root)
60
+ if err != nil {
61
+ t.Fatalf("ReadDir(%q) error = %v", root, err)
62
+ }
63
+
64
+ var found int
65
+ for _, entry := range entries {
66
+ if !entry.IsDir() || !strings.HasPrefix(entry.Name(), "savepoint-") {
67
+ continue
68
+ }
69
+ found++
70
+ path := filepath.Join(root, entry.Name(), "SKILL.md")
71
+ content, err := os.ReadFile(path)
72
+ if err != nil {
73
+ t.Fatalf("ReadFile(%q) error = %v", path, err)
74
+ }
75
+
76
+ text := string(content)
77
+ if !strings.HasPrefix(text, "---\n") {
78
+ t.Fatalf("%s missing YAML frontmatter", path)
79
+ }
80
+ if !strings.Contains(text, "name: "+entry.Name()) {
81
+ t.Fatalf("%s frontmatter name does not match directory", path)
82
+ }
83
+ if !strings.Contains(text, "description:") {
84
+ t.Fatalf("%s missing frontmatter description", path)
85
+ }
86
+ }
87
+
88
+ if found == 0 {
89
+ t.Fatalf("%s contains %d savepoint skills, want at least 1", root, found)
90
+ }
91
+ }
package/cmd/board.go ADDED
@@ -0,0 +1,59 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "io"
7
+ )
8
+
9
+ const boardUsage = "Usage: board [--release <release>] [--epic <epic>]"
10
+
11
+ type BoardOptions struct {
12
+ Release string
13
+ Epic string
14
+ }
15
+
16
+ type BoardRunner func(BoardOptions) error
17
+
18
+ func RunBoard(ctx context.Context, args []string, stdout io.Writer, runner BoardRunner) error {
19
+ options, help, err := ParseBoardArgs(args)
20
+ if help {
21
+ _, writeErr := fmt.Fprintln(stdout, boardUsage)
22
+ return writeErr
23
+ }
24
+ if err != nil {
25
+ return err
26
+ }
27
+ return runner(options)
28
+ }
29
+
30
+ func ParseBoardArgs(args []string) (BoardOptions, bool, error) {
31
+ var options BoardOptions
32
+
33
+ for i := 0; i < len(args); i++ {
34
+ arg := args[i]
35
+ switch arg {
36
+ case "--help":
37
+ return BoardOptions{}, true, nil
38
+ case "--release":
39
+ i++
40
+ if i >= len(args) {
41
+ return BoardOptions{}, false, fmt.Errorf("--release requires a value")
42
+ }
43
+ options.Release = args[i]
44
+ case "--epic":
45
+ i++
46
+ if i >= len(args) {
47
+ return BoardOptions{}, false, fmt.Errorf("--epic requires a value")
48
+ }
49
+ options.Epic = args[i]
50
+ default:
51
+ if len(arg) > 0 && arg[0] == '-' {
52
+ return BoardOptions{}, false, fmt.Errorf("unknown board flag %q", arg)
53
+ }
54
+ return BoardOptions{}, false, fmt.Errorf("board takes no positional arguments, got %q", arg)
55
+ }
56
+ }
57
+
58
+ return options, false, nil
59
+ }
@@ -0,0 +1,137 @@
1
+ package cmd
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "errors"
7
+ "strings"
8
+ "testing"
9
+ )
10
+
11
+ func TestRunBoardHelp(t *testing.T) {
12
+ var stdout bytes.Buffer
13
+ called := false
14
+
15
+ err := RunBoard(context.Background(), []string{"--help"}, &stdout, func(BoardOptions) error {
16
+ called = true
17
+ return nil
18
+ })
19
+
20
+ if err != nil {
21
+ t.Fatalf("RunBoard() error = %v", err)
22
+ }
23
+ if called {
24
+ t.Fatal("RunBoard() called runner for help")
25
+ }
26
+ if !strings.Contains(stdout.String(), "board [--release <release>] [--epic <epic>]") {
27
+ t.Fatalf("help output = %q", stdout.String())
28
+ }
29
+ }
30
+
31
+ func TestRunBoardNoArgs(t *testing.T) {
32
+ got := runBoardOptions(t, nil)
33
+
34
+ if got.Release != "" {
35
+ t.Fatalf("Release = %q, want empty", got.Release)
36
+ }
37
+ if got.Epic != "" {
38
+ t.Fatalf("Epic = %q, want empty", got.Epic)
39
+ }
40
+ }
41
+
42
+ func TestRunBoardRelease(t *testing.T) {
43
+ got := runBoardOptions(t, []string{"--release", "v1"})
44
+
45
+ if got.Release != "v1" {
46
+ t.Fatalf("Release = %q, want v1", got.Release)
47
+ }
48
+ }
49
+
50
+ func TestRunBoardEpic(t *testing.T) {
51
+ got := runBoardOptions(t, []string{"--epic", "E03"})
52
+
53
+ if got.Epic != "E03" {
54
+ t.Fatalf("Epic = %q, want E03", got.Epic)
55
+ }
56
+ }
57
+
58
+ func TestRunBoardReleaseAndEpic(t *testing.T) {
59
+ got := runBoardOptions(t, []string{"--release", "v1", "--epic", "E03"})
60
+
61
+ if got.Release != "v1" {
62
+ t.Fatalf("Release = %q, want v1", got.Release)
63
+ }
64
+ if got.Epic != "E03" {
65
+ t.Fatalf("Epic = %q, want E03", got.Epic)
66
+ }
67
+ }
68
+
69
+ func TestRunBoardRejectsUnknownFlag(t *testing.T) {
70
+ var stdout bytes.Buffer
71
+
72
+ err := RunBoard(context.Background(), []string{"--bogus"}, &stdout, func(BoardOptions) error {
73
+ return nil
74
+ })
75
+
76
+ if err == nil {
77
+ t.Fatal("RunBoard() error = nil, want unknown flag error")
78
+ }
79
+ if !strings.Contains(err.Error(), "unknown board flag") {
80
+ t.Fatalf("error = %q, want unknown flag", err.Error())
81
+ }
82
+ }
83
+
84
+ func TestRunBoardRejectsPositionalArgs(t *testing.T) {
85
+ var stdout bytes.Buffer
86
+
87
+ err := RunBoard(context.Background(), []string{"extra"}, &stdout, func(BoardOptions) error {
88
+ return nil
89
+ })
90
+
91
+ if err == nil {
92
+ t.Fatal("RunBoard() error = nil, want positional arg error")
93
+ }
94
+ }
95
+
96
+ func TestRunBoardReleaseMissingValue(t *testing.T) {
97
+ var stdout bytes.Buffer
98
+
99
+ err := RunBoard(context.Background(), []string{"--release"}, &stdout, func(BoardOptions) error {
100
+ return nil
101
+ })
102
+
103
+ if err == nil {
104
+ t.Fatal("RunBoard() error = nil, want missing value error")
105
+ }
106
+ if !strings.Contains(err.Error(), "--release requires a value") {
107
+ t.Fatalf("error = %q", err.Error())
108
+ }
109
+ }
110
+
111
+ func TestRunBoardReturnsRunnerError(t *testing.T) {
112
+ want := errors.New("runner failed")
113
+ var stdout bytes.Buffer
114
+
115
+ err := RunBoard(context.Background(), nil, &stdout, func(BoardOptions) error {
116
+ return want
117
+ })
118
+
119
+ if !errors.Is(err, want) {
120
+ t.Fatalf("RunBoard() error = %v, want %v", err, want)
121
+ }
122
+ }
123
+
124
+ func runBoardOptions(t *testing.T, args []string) BoardOptions {
125
+ t.Helper()
126
+
127
+ var stdout bytes.Buffer
128
+ var got BoardOptions
129
+ err := RunBoard(context.Background(), args, &stdout, func(options BoardOptions) error {
130
+ got = options
131
+ return nil
132
+ })
133
+ if err != nil {
134
+ t.Fatalf("RunBoard() error = %v", err)
135
+ }
136
+ return got
137
+ }
package/cmd/doctor.go ADDED
@@ -0,0 +1,53 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "io"
7
+ )
8
+
9
+ const doctorUsage = "Usage: doctor [--epic <epic>]"
10
+
11
+ type DoctorOptions struct {
12
+ Epic string
13
+ }
14
+
15
+ // DoctorRunner receives parsed options and returns an exit code: 0=clean, 1=problems, 2=internal error.
16
+ type DoctorRunner func(DoctorOptions) (int, error)
17
+
18
+ func RunDoctor(ctx context.Context, args []string, stdout io.Writer, runner DoctorRunner) (int, error) {
19
+ options, help, err := ParseDoctorArgs(args)
20
+ if help {
21
+ _, writeErr := fmt.Fprintln(stdout, doctorUsage)
22
+ return 0, writeErr
23
+ }
24
+ if err != nil {
25
+ return 2, err
26
+ }
27
+ return runner(options)
28
+ }
29
+
30
+ func ParseDoctorArgs(args []string) (DoctorOptions, bool, error) {
31
+ var options DoctorOptions
32
+
33
+ for i := 0; i < len(args); i++ {
34
+ arg := args[i]
35
+ switch arg {
36
+ case "--help":
37
+ return DoctorOptions{}, true, nil
38
+ case "--epic":
39
+ i++
40
+ if i >= len(args) {
41
+ return DoctorOptions{}, false, fmt.Errorf("--epic requires a value")
42
+ }
43
+ options.Epic = args[i]
44
+ default:
45
+ if len(arg) > 0 && arg[0] == '-' {
46
+ return DoctorOptions{}, false, fmt.Errorf("unknown doctor flag %q", arg)
47
+ }
48
+ return DoctorOptions{}, false, fmt.Errorf("doctor takes no positional arguments, got %q", arg)
49
+ }
50
+ }
51
+
52
+ return options, false, nil
53
+ }
@@ -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
+ }